usearch 2.13.4 → 2.14.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  <h1 align="center">USearch</h1>
2
2
  <h3 align="center">
3
3
  Smaller & <a href="https://www.unum.cloud/blog/2023-11-07-scaling-vector-search-with-intel">Faster</a> Single-File<br/>
4
- Similarity Search Engine for <a href="https://github.com/ashvardanian/simsimd">Vectors</a> & 🔜 <a href="https://github.com/ashvardanian/stringzilla">Texts</a>
4
+ Similarity Search & Clustering Engine for <a href="https://github.com/ashvardanian/simsimd">Vectors</a> & 🔜 <a href="https://github.com/ashvardanian/stringzilla">Texts</a>
5
5
  </h3>
6
6
  <br/>
7
7
 
@@ -71,13 +71,10 @@ Linux • MacOS • Windows • iOS • WebAssembly •
71
71
 
72
72
  __Technical Insights__ and related articles:
73
73
 
74
- - [Uses Horner's method for polynomial approximations, beating GCC 12 by 119x](https://ashvardanian.com/posts/gcc-12-vs-avx512fp16/).
75
74
  - [Uses Arm SVE and x86 AVX-512's masked loads to eliminate tail `for`-loops](https://ashvardanian.com/posts/simsimd-faster-scipy/#tails-of-the-past-the-significance-of-masked-loads).
76
- - [Uses AVX-512 FP16 for half-precision operations, that few compilers vectorize](https://ashvardanian.com/posts/simsimd-faster-scipy/#the-challenge-of-f16).
77
- - [Substitutes LibC's `sqrt` calls with bithacks using Jan Kadlec's constant](https://ashvardanian.com/posts/simsimd-faster-scipy/#bonus-section-bypassing-sqrt-and-libc-dependencies).
75
+ - [Uses Horner's method for polynomial approximations, beating GCC 12 by 119x](https://ashvardanian.com/posts/gcc-12-vs-avx512fp16/).
78
76
  - [For every language implements a custom separate binding](https://ashvardanian.com/posts/porting-cpp-library-to-ten-languages/).
79
- - [For Python avoids slow PyBind11, and even `PyArg_ParseTuple` for speed](https://ashvardanian.com/posts/pybind11-cpython-tutorial/).
80
- - [For JavaScript uses typed arrays and NAPI for zero-copy calls](https://ashvardanian.com/posts/javascript-ai-vector-search/).
77
+
81
78
 
82
79
  ## Comparison with FAISS
83
80
 
@@ -119,17 +116,15 @@ USearch is compact and broadly compatible without sacrificing performance, prima
119
116
  Base functionality is identical to FAISS, and the interface must be familiar if you have ever investigated Approximate Nearest Neighbors search:
120
117
 
121
118
  ```py
122
- # pip install numpy usearch
119
+ # pip install usearch
123
120
 
124
121
  import numpy as np
125
122
  from usearch.index import Index
126
123
 
127
- index = Index(ndim=3)
128
-
129
- vector = np.array([0.2, 0.6, 0.4])
130
- index.add(42, vector)
131
-
132
- matches = index.search(vector, 10)
124
+ index = Index(ndim=3) # Default settings for 3D vectors
125
+ vector = np.array([0.2, 0.6, 0.4]) # Can be a matrix for batch operations
126
+ index.add(42, vector) # Add one or many vectors in parallel
127
+ matches = index.search(vector, 10) # Find 10 nearest neighbors
133
128
 
134
129
  assert matches[0].key == 42
135
130
  assert matches[0].distance <= 0.001
@@ -137,13 +132,13 @@ assert np.allclose(index[42], vector, atol=0.1) # Ensure high tolerance in mixed
137
132
  ```
138
133
 
139
134
  More settings are always available, and the API is designed to be as flexible as possible.
140
- The default storage/quantization level is hardware-dependant for efficiency, but `f16` is recommended for most modern CPUs.
135
+ The default storage/quantization level is hardware-dependant for efficiency, but `bf16` is recommended for most modern CPUs.
141
136
 
142
137
  ```py
143
138
  index = Index(
144
139
  ndim=3, # Define the number of dimensions in input vectors
145
140
  metric='cos', # Choose 'l2sq', 'ip', 'haversine' or other metric, default = 'cos'
146
- dtype='f16', # Store as 'f64', 'f32', 'f16', 'i8', 'b1'..., default = None
141
+ dtype='bf16', # Store as 'f64', 'f32', 'f16', 'i8', 'b1'..., default = None
147
142
  connectivity=16, # Optional: Limit number of neighbors per graph node
148
143
  expansion_add=128, # Optional: Control the recall of indexing
149
144
  expansion_search=64, # Optional: Control the quality of the search
@@ -254,7 +249,7 @@ assert!(
254
249
  Training a quantization model and dimension-reduction is a common approach to accelerate vector search.
255
250
  Those, however, are only sometimes reliable, can significantly affect the statistical properties of your data, and require regular adjustments if your distribution shifts.
256
251
  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.
252
+ 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
253
  You can use the following command to check, if hardware acceleration is enabled:
259
254
 
260
255
  ```sh
@@ -264,11 +259,19 @@ $ python -c 'from usearch.index import Index; print(Index(ndim=166, metric="tani
264
259
  > ice
265
260
  ```
266
261
 
262
+ In most cases, it's recommended to use half-precision floating-point numbers on modern hardware.
263
+ 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.
264
+ When quantizing to `i8_t` integers, note that it's only valid for cosine-like metrics.
265
+ 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.
266
+ When quantizing to `b1x8_t` single-bit representations, note that it's only valid for binary metrics like Jaccard, Hamming, etc.
267
+ As part of the quantization process, the scalar components greater than zero are set to `true`, and the rest to `false`.
268
+
269
+ ![USearch uint40_t support](https://github.com/unum-cloud/usearch/blob/main/assets/usearch-neighbor-types.png?raw=true)
270
+
267
271
  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
272
  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
273
  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
274
 
271
- ![USearch uint40_t support](https://github.com/unum-cloud/usearch/blob/main/assets/usearch-neighbor-types.png?raw=true)
272
275
 
273
276
  ## `Indexes` for Multi-Index Lookups
274
277
 
@@ -541,7 +544,7 @@ doi = {10.5281/zenodo.7949416},
541
544
  author = {Vardanian, Ash},
542
545
  title = {{USearch by Unum Cloud}},
543
546
  url = {https://github.com/unum-cloud/usearch},
544
- version = {2.13.4},
547
+ version = {2.14.0},
545
548
  year = {2023},
546
549
  month = oct,
547
550
  }
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
  /**
@@ -8,8 +8,8 @@
8
8
  #define UNUM_USEARCH_HPP
9
9
 
10
10
  #define USEARCH_VERSION_MAJOR 2
11
- #define USEARCH_VERSION_MINOR 13
12
- #define USEARCH_VERSION_PATCH 4
11
+ #define USEARCH_VERSION_MINOR 14
12
+ #define USEARCH_VERSION_PATCH 0
13
13
 
14
14
  // Inferring C++ version
15
15
  // https://stackoverflow.com/a/61552074
@@ -2870,16 +2870,16 @@ class index_gt {
2870
2870
  result.computed_distances = context.computed_distances;
2871
2871
  result.visited_members = context.iteration_cycles;
2872
2872
 
2873
- // If we are updating the entry node itself, it won't contain any neighbors,
2874
- // so we should traverse a level down to find the closest match.
2875
- if (updated_node_level == max_level_copy)
2876
- updated_node_level--;
2877
-
2878
2873
  // Go down the level, tracking only the closest match;
2879
2874
  // It may even be equal to the `updated_slot`
2880
- compressed_slot_t closest_slot = search_for_one_( //
2881
- value, metric, prefetch, //
2882
- entry_slot_copy, max_level_copy, updated_node_level, context);
2875
+ compressed_slot_t closest_slot =
2876
+ // If we are updating the entry node itself, it won't contain any neighbors,
2877
+ // so we should traverse a level down to find the closest match.
2878
+ updated_node_level == max_level_copy //
2879
+ ? entry_slot_copy
2880
+ : search_for_one_( //
2881
+ value, metric, prefetch, //
2882
+ entry_slot_copy, max_level_copy, updated_node_level, context);
2883
2883
 
2884
2884
  // From `updated_node_level` down - perform proper extensive search
2885
2885
  for (level_t level = (std::min)(updated_node_level, max_level_copy); level >= 0; --level) {
@@ -2943,7 +2943,7 @@ class index_gt {
2943
2943
  config.expansion = default_expansion_search();
2944
2944
 
2945
2945
  // Using references is cleaner, but would result in UBSan false positives
2946
- context_t* context_ptr = contexts_.data() + config.thread;
2946
+ context_t* context_ptr = contexts_.data() ? contexts_.data() + config.thread : nullptr;
2947
2947
  top_candidates_t* top_ptr = context_ptr ? &context_ptr->top_candidates : nullptr;
2948
2948
  search_result_t result{*this, top_ptr};
2949
2949
  if (!nodes_count_.load(std::memory_order_relaxed))
@@ -3697,6 +3697,7 @@ class index_gt {
3697
3697
  inline neighbors_ref_t neighbors_base_(node_t node) const noexcept { return {node.neighbors_tape()}; }
3698
3698
 
3699
3699
  inline neighbors_ref_t neighbors_non_base_(node_t node, level_t level) const noexcept {
3700
+ usearch_assert_m(level > 0 && level <= node.level(), "Linking to missing level");
3700
3701
  return {node.neighbors_tape() + pre_.neighbors_base_bytes + (level - 1) * pre_.neighbors_bytes};
3701
3702
  }
3702
3703
 
@@ -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
 
@@ -2235,6 +2240,7 @@ class index_dense_gt {
2235
2240
  case scalar_kind_t::f64_k: return make_casts_<f64_t>();
2236
2241
  case scalar_kind_t::f32_k: return make_casts_<f32_t>();
2237
2242
  case scalar_kind_t::f16_k: return make_casts_<f16_t>();
2243
+ case scalar_kind_t::bf16_k: return make_casts_<bf16_t>();
2238
2244
  case scalar_kind_t::i8_k: return make_casts_<i8_t>();
2239
2245
  case scalar_kind_t::b1x8_k: return make_casts_<b1x8_t>();
2240
2246
  default: return {};
@@ -70,20 +70,26 @@ struct uuid_t {
70
70
  };
71
71
 
72
72
  class f16_bits_t;
73
- class i8_converted_t;
73
+ class bf16_bits_t;
74
74
 
75
75
  #if !USEARCH_USE_FP16LIB
76
76
  #if USEARCH_USE_SIMSIMD
77
77
  using f16_native_t = simsimd_f16_t;
78
+ using bf16_native_t = simsimd_bf16_t;
78
79
  #elif defined(USEARCH_DEFINED_ARM)
79
80
  using f16_native_t = __fp16;
81
+ using bf16_native_t = __fp16; // No better choice on most compilers!
80
82
  #else
81
83
  using f16_native_t = _Float16;
84
+ using bf16_native_t = _Float16; // No better choice on most compilers!
82
85
  #endif
83
86
  using f16_t = f16_native_t;
87
+ using bf16_t = bf16_native_t;
84
88
  #else
85
89
  using f16_native_t = void;
90
+ using bf16_native_t = void;
86
91
  using f16_t = f16_bits_t;
92
+ using bf16_t = bf16_bits_t;
87
93
  #endif
88
94
 
89
95
  using f64_t = double;
@@ -99,6 +105,9 @@ using i32_t = std::int32_t;
99
105
  using i16_t = std::int16_t;
100
106
  using i8_t = std::int8_t;
101
107
 
108
+ /**
109
+ * @brief Enumerates the most commonly used distance metrics, mostly for dense vector representations.
110
+ */
102
111
  enum class metric_kind_t : std::uint8_t {
103
112
  unknown_k = 0,
104
113
  // Classics:
@@ -111,19 +120,26 @@ enum class metric_kind_t : std::uint8_t {
111
120
  haversine_k = 'h',
112
121
  divergence_k = 'd',
113
122
 
114
- // Sets:
115
- jaccard_k = 'j',
123
+ // Dense Sets:
116
124
  hamming_k = 'b',
117
125
  tanimoto_k = 't',
118
126
  sorensen_k = 's',
127
+
128
+ // Sparse Sets:
129
+ jaccard_k = 'j',
119
130
  };
120
131
 
132
+ /**
133
+ * @brief Enumerates the most commonly used scalar types, mostly for dense vector representations.
134
+ * Doesn't include logical types, like complex numbers or quaternions.
135
+ */
121
136
  enum class scalar_kind_t : std::uint8_t {
122
137
  unknown_k = 0,
123
138
  // Custom:
124
139
  b1x8_k = 1,
125
140
  u40_k = 2,
126
141
  uuid_k = 3,
142
+ bf16_k = 4,
127
143
  // Common:
128
144
  f64_k = 10,
129
145
  f32_k = 11,
@@ -159,6 +175,8 @@ template <typename scalar_at> scalar_kind_t scalar_kind() noexcept {
159
175
  return scalar_kind_t::f32_k;
160
176
  if (std::is_same<scalar_at, f16_t>())
161
177
  return scalar_kind_t::f16_k;
178
+ if (std::is_same<scalar_at, bf16_t>())
179
+ return scalar_kind_t::bf16_k;
162
180
  if (std::is_same<scalar_at, i8_t>())
163
181
  return scalar_kind_t::i8_k;
164
182
  if (std::is_same<scalar_at, u64_t>())
@@ -198,33 +216,66 @@ inline bool str_equals(char const* begin, std::size_t len, char const* other_beg
198
216
 
199
217
  inline std::size_t bits_per_scalar(scalar_kind_t scalar_kind) noexcept {
200
218
  switch (scalar_kind) {
219
+ case scalar_kind_t::uuid_k: return 128;
220
+ case scalar_kind_t::u40_k: return 40;
221
+ case scalar_kind_t::bf16_k: return 16;
222
+ case scalar_kind_t::b1x8_k: return 1;
223
+ case scalar_kind_t::u64_k: return 64;
224
+ case scalar_kind_t::i64_k: return 64;
201
225
  case scalar_kind_t::f64_k: return 64;
226
+ case scalar_kind_t::u32_k: return 32;
227
+ case scalar_kind_t::i32_k: return 32;
202
228
  case scalar_kind_t::f32_k: return 32;
229
+ case scalar_kind_t::u16_k: return 16;
230
+ case scalar_kind_t::i16_k: return 16;
203
231
  case scalar_kind_t::f16_k: return 16;
232
+ case scalar_kind_t::u8_k: return 8;
204
233
  case scalar_kind_t::i8_k: return 8;
205
- case scalar_kind_t::b1x8_k: return 1;
234
+ case scalar_kind_t::f8_k: return 8;
206
235
  default: return 0;
207
236
  }
208
237
  }
209
238
 
210
239
  inline std::size_t bits_per_scalar_word(scalar_kind_t scalar_kind) noexcept {
211
240
  switch (scalar_kind) {
241
+ case scalar_kind_t::uuid_k: return 128;
242
+ case scalar_kind_t::u40_k: return 40;
243
+ case scalar_kind_t::bf16_k: return 16;
244
+ case scalar_kind_t::b1x8_k: return 8;
245
+ case scalar_kind_t::u64_k: return 64;
246
+ case scalar_kind_t::i64_k: return 64;
212
247
  case scalar_kind_t::f64_k: return 64;
248
+ case scalar_kind_t::u32_k: return 32;
249
+ case scalar_kind_t::i32_k: return 32;
213
250
  case scalar_kind_t::f32_k: return 32;
251
+ case scalar_kind_t::u16_k: return 16;
252
+ case scalar_kind_t::i16_k: return 16;
214
253
  case scalar_kind_t::f16_k: return 16;
254
+ case scalar_kind_t::u8_k: return 8;
215
255
  case scalar_kind_t::i8_k: return 8;
216
- case scalar_kind_t::b1x8_k: return 8;
256
+ case scalar_kind_t::f8_k: return 8;
217
257
  default: return 0;
218
258
  }
219
259
  }
220
260
 
221
261
  inline char const* scalar_kind_name(scalar_kind_t scalar_kind) noexcept {
222
262
  switch (scalar_kind) {
263
+ case scalar_kind_t::uuid_k: return "uuid";
264
+ case scalar_kind_t::u40_k: return "u40";
265
+ case scalar_kind_t::bf16_k: return "bf16";
266
+ case scalar_kind_t::b1x8_k: return "b1x8";
267
+ case scalar_kind_t::u64_k: return "u64";
268
+ case scalar_kind_t::i64_k: return "i64";
269
+ case scalar_kind_t::f64_k: return "f64";
270
+ case scalar_kind_t::u32_k: return "u32";
271
+ case scalar_kind_t::i32_k: return "i32";
223
272
  case scalar_kind_t::f32_k: return "f32";
273
+ case scalar_kind_t::u16_k: return "u16";
274
+ case scalar_kind_t::i16_k: return "i16";
224
275
  case scalar_kind_t::f16_k: return "f16";
225
- case scalar_kind_t::f64_k: return "f64";
276
+ case scalar_kind_t::u8_k: return "u8";
226
277
  case scalar_kind_t::i8_k: return "i8";
227
- case scalar_kind_t::b1x8_k: return "b1x8";
278
+ case scalar_kind_t::f8_k: return "f8";
228
279
  default: return "";
229
280
  }
230
281
  }
@@ -242,8 +293,8 @@ inline char const* metric_kind_name(metric_kind_t metric) noexcept {
242
293
  case metric_kind_t::hamming_k: return "hamming";
243
294
  case metric_kind_t::tanimoto_k: return "tanimoto";
244
295
  case metric_kind_t::sorensen_k: return "sorensen";
296
+ default: return "";
245
297
  }
246
- return "";
247
298
  }
248
299
  inline expected_gt<scalar_kind_t> scalar_kind_from_name(char const* name, std::size_t len) {
249
300
  expected_gt<scalar_kind_t> parsed;
@@ -253,10 +304,14 @@ inline expected_gt<scalar_kind_t> scalar_kind_from_name(char const* name, std::s
253
304
  parsed.result = scalar_kind_t::f64_k;
254
305
  else if (str_equals(name, len, "f16"))
255
306
  parsed.result = scalar_kind_t::f16_k;
307
+ else if (str_equals(name, len, "bf16"))
308
+ parsed.result = scalar_kind_t::bf16_k;
256
309
  else if (str_equals(name, len, "i8"))
257
310
  parsed.result = scalar_kind_t::i8_k;
311
+ else if (str_equals(name, len, "b1"))
312
+ parsed.result = scalar_kind_t::b1x8_k;
258
313
  else
259
- parsed.failed("Unknown type, choose: f32, f16, f64, i8");
314
+ parsed.failed("Unknown type, choose: f64, f32, f16, bf16, i8, b1");
260
315
  return parsed;
261
316
  }
262
317
 
@@ -289,11 +344,13 @@ inline expected_gt<metric_kind_t> metric_from_name(char const* name, std::size_t
289
344
  "tanimoto, sorensen");
290
345
  return parsed;
291
346
  }
292
-
293
347
  inline expected_gt<metric_kind_t> metric_from_name(char const* name) {
294
348
  return metric_from_name(name, std::strlen(name));
295
349
  }
296
350
 
351
+ /**
352
+ * @brief Convenience function to upcast a half-precision floating point number to a single-precision one.
353
+ */
297
354
  inline float f16_to_f32(std::uint16_t u16) noexcept {
298
355
  #if !USEARCH_USE_FP16LIB
299
356
  f16_native_t f16;
@@ -304,6 +361,9 @@ inline float f16_to_f32(std::uint16_t u16) noexcept {
304
361
  #endif
305
362
  }
306
363
 
364
+ /**
365
+ * @brief Convenience function to downcast a single-precision floating point number to a half-precision one.
366
+ */
307
367
  inline std::uint16_t f32_to_f16(float f32) noexcept {
308
368
  #if !USEARCH_USE_FP16LIB
309
369
  f16_native_t f16 = f16_native_t(f32);
@@ -315,6 +375,36 @@ inline std::uint16_t f32_to_f16(float f32) noexcept {
315
375
  #endif
316
376
  }
317
377
 
378
+ /**
379
+ * @brief Convenience function to upcast a brain-floating point number to a single-precision one.
380
+ * https://github.com/ashvardanian/SimSIMD/blob/ff51434d90c66f916e94ff05b24530b127aa4cff/include/simsimd/types.h#L395-L410
381
+ */
382
+ inline float bf16_to_f32(std::uint16_t u16) noexcept {
383
+ union float_or_unsigned_int_t {
384
+ float f;
385
+ unsigned int i;
386
+ };
387
+ union float_or_unsigned_int_t result_union;
388
+ result_union.i = u16 << 16; // Zero extends the mantissa
389
+ return result_union.f;
390
+ }
391
+
392
+ /**
393
+ * @brief Convenience function to downcast a single-precision floating point number to a brain-floating point one.
394
+ * https://github.com/ashvardanian/SimSIMD/blob/ff51434d90c66f916e94ff05b24530b127aa4cff/include/simsimd/types.h#L412-L425
395
+ */
396
+ inline std::uint16_t f32_to_bf16(float f32) noexcept {
397
+ union float_or_unsigned_int_t {
398
+ float f;
399
+ unsigned int i;
400
+ };
401
+ union float_or_unsigned_int_t value;
402
+ value.f = f32;
403
+ value.i >>= 16;
404
+ value.i &= 0xFFFF;
405
+ return (unsigned short)value.i;
406
+ }
407
+
318
408
  /**
319
409
  * @brief Numeric type for the IEEE 754 half-precision floating point.
320
410
  * If hardware support isn't available, falls back to a hardware
@@ -333,8 +423,7 @@ class f16_bits_t {
333
423
  inline operator float() const noexcept { return f16_to_f32(uint16_); }
334
424
  inline explicit operator bool() const noexcept { return f16_to_f32(uint16_) > 0.5f; }
335
425
 
336
- inline f16_bits_t(i8_converted_t) noexcept;
337
- inline f16_bits_t(std::int8_t v) noexcept : uint16_(v) {}
426
+ inline f16_bits_t(std::int8_t v) noexcept : uint16_(f32_to_f16(v)) {}
338
427
  inline f16_bits_t(bool v) noexcept : uint16_(f32_to_f16(v)) {}
339
428
  inline f16_bits_t(float v) noexcept : uint16_(f32_to_f16(v)) {}
340
429
  inline f16_bits_t(double v) noexcept : uint16_(f32_to_f16(static_cast<float>(v))) {}
@@ -345,14 +434,14 @@ class f16_bits_t {
345
434
  inline f16_bits_t operator-(f16_bits_t other) const noexcept { return {float(*this) - float(other)}; }
346
435
  inline f16_bits_t operator*(f16_bits_t other) const noexcept { return {float(*this) * float(other)}; }
347
436
  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}; }
437
+ inline float operator+(float other) const noexcept { return float(*this) + other; }
438
+ inline float operator-(float other) const noexcept { return float(*this) - other; }
439
+ inline float operator*(float other) const noexcept { return float(*this) * other; }
440
+ inline float operator/(float other) const noexcept { return float(*this) / other; }
441
+ inline double operator+(double other) const noexcept { return float(*this) + other; }
442
+ inline double operator-(double other) const noexcept { return float(*this) - other; }
443
+ inline double operator*(double other) const noexcept { return float(*this) * other; }
444
+ inline double operator/(double other) const noexcept { return float(*this) / other; }
356
445
 
357
446
  inline f16_bits_t& operator+=(float v) noexcept {
358
447
  uint16_ = f32_to_f16(v + f16_to_f32(uint16_));
@@ -375,6 +464,65 @@ class f16_bits_t {
375
464
  }
376
465
  };
377
466
 
467
+ /**
468
+ * @brief Numeric type for brain-floating point half-precision floating point.
469
+ * If hardware support isn't available, falls back to a hardware
470
+ * agnostic in-software implementation.
471
+ */
472
+ class bf16_bits_t {
473
+ std::uint16_t uint16_{};
474
+
475
+ public:
476
+ inline bf16_bits_t() noexcept : uint16_(0) {}
477
+ inline bf16_bits_t(bf16_bits_t&&) = default;
478
+ inline bf16_bits_t& operator=(bf16_bits_t&&) = default;
479
+ inline bf16_bits_t(bf16_bits_t const&) = default;
480
+ inline bf16_bits_t& operator=(bf16_bits_t const&) = default;
481
+
482
+ inline operator float() const noexcept { return bf16_to_f32(uint16_); }
483
+ inline explicit operator bool() const noexcept { return bf16_to_f32(uint16_) > 0.5f; }
484
+
485
+ inline bf16_bits_t(std::int8_t v) noexcept : uint16_(f32_to_bf16(v)) {}
486
+ inline bf16_bits_t(bool v) noexcept : uint16_(f32_to_bf16(v)) {}
487
+ inline bf16_bits_t(float v) noexcept : uint16_(f32_to_bf16(v)) {}
488
+ inline bf16_bits_t(double v) noexcept : uint16_(f32_to_bf16(static_cast<float>(v))) {}
489
+
490
+ inline bool operator<(bf16_bits_t const& other) const noexcept { return float(*this) < float(other); }
491
+
492
+ inline bf16_bits_t operator+(bf16_bits_t other) const noexcept { return {float(*this) + float(other)}; }
493
+ inline bf16_bits_t operator-(bf16_bits_t other) const noexcept { return {float(*this) - float(other)}; }
494
+ inline bf16_bits_t operator*(bf16_bits_t other) const noexcept { return {float(*this) * float(other)}; }
495
+ inline bf16_bits_t operator/(bf16_bits_t other) const noexcept { return {float(*this) / float(other)}; }
496
+ inline float operator+(float other) const noexcept { return float(*this) + other; }
497
+ inline float operator-(float other) const noexcept { return float(*this) - other; }
498
+ inline float operator*(float other) const noexcept { return float(*this) * other; }
499
+ inline float operator/(float other) const noexcept { return float(*this) / other; }
500
+ inline double operator+(double other) const noexcept { return float(*this) + other; }
501
+ inline double operator-(double other) const noexcept { return float(*this) - other; }
502
+ inline double operator*(double other) const noexcept { return float(*this) * other; }
503
+ inline double operator/(double other) const noexcept { return float(*this) / other; }
504
+
505
+ inline bf16_bits_t& operator+=(float v) noexcept {
506
+ uint16_ = f32_to_bf16(v + bf16_to_f32(uint16_));
507
+ return *this;
508
+ }
509
+
510
+ inline bf16_bits_t& operator-=(float v) noexcept {
511
+ uint16_ = f32_to_bf16(v - bf16_to_f32(uint16_));
512
+ return *this;
513
+ }
514
+
515
+ inline bf16_bits_t& operator*=(float v) noexcept {
516
+ uint16_ = f32_to_bf16(v * bf16_to_f32(uint16_));
517
+ return *this;
518
+ }
519
+
520
+ inline bf16_bits_t& operator/=(float v) noexcept {
521
+ uint16_ = f32_to_bf16(v / bf16_to_f32(uint16_));
522
+ return *this;
523
+ }
524
+ };
525
+
378
526
  /**
379
527
  * @brief An STL-based executor or a "thread-pool" for parallel execution.
380
528
  * Isn't efficient for small batches, as it recreates the threads on every call.
@@ -907,7 +1055,7 @@ template <> struct cast_gt<b1x8_t, b1x8_t> {
907
1055
  static bool try_(byte_t const*, std::size_t, byte_t*) noexcept { return false; }
908
1056
  };
909
1057
 
910
- template <typename from_scalar_at> struct cast_gt<from_scalar_at, b1x8_t> {
1058
+ template <typename from_scalar_at> struct cast_to_b1x8_gt {
911
1059
  inline static bool try_(byte_t const* input, std::size_t dim, byte_t* output) noexcept {
912
1060
  from_scalar_at const* typed_input = reinterpret_cast<from_scalar_at const*>(input);
913
1061
  unsigned char* typed_output = reinterpret_cast<unsigned char*>(output);
@@ -928,7 +1076,7 @@ template <typename from_scalar_at> struct cast_gt<from_scalar_at, b1x8_t> {
928
1076
  }
929
1077
  };
930
1078
 
931
- template <typename to_scalar_at> struct cast_gt<b1x8_t, to_scalar_at> {
1079
+ template <typename to_scalar_at> struct cast_from_b1x8_gt {
932
1080
  static bool try_(byte_t const* input, std::size_t dim, byte_t* output) noexcept {
933
1081
  unsigned char const* typed_input = reinterpret_cast<unsigned char const*>(input);
934
1082
  to_scalar_at* typed_output = reinterpret_cast<to_scalar_at*>(output);
@@ -940,53 +1088,59 @@ template <typename to_scalar_at> struct cast_gt<b1x8_t, to_scalar_at> {
940
1088
  }
941
1089
  };
942
1090
 
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_{};
1091
+ template <typename from_scalar_at> struct cast_to_i8_gt {
1092
+ inline static bool try_(byte_t const* input, std::size_t dim, byte_t* output) noexcept {
1093
+ from_scalar_at const* typed_input = reinterpret_cast<from_scalar_at const*>(input);
1094
+ std::int8_t* typed_output = reinterpret_cast<std::int8_t*>(output);
1095
+ // Unlike other casting mechanisms, switching to small range integers is a two step procedure.
1096
+ // First we want to estimate the magnitude of the vector to scale into [-1.0, 1.0] interval,
1097
+ // instead of clamping. And then we scale the values into the [-127, 127] range.
1098
+ // ! This makes an assumption, that the distance metric is dot-product-like, which may not
1099
+ // ! be true in many cases, so it's recommended to avoid automatic casting from floats to
1100
+ // ! integers.
1101
+ double magnitude = 0.0;
1102
+ for (std::size_t i = 0; i != dim; ++i)
1103
+ magnitude += (double)typed_input[i] * (double)typed_input[i];
1104
+ magnitude = std::sqrt(magnitude);
1105
+ for (std::size_t i = 0; i != dim; ++i)
1106
+ typed_output[i] =
1107
+ static_cast<std::int8_t>(usearch::clamp<double>(typed_input[i] * 127.0 / magnitude, -127.0, 127.0));
1108
+ return true;
1109
+ }
1110
+ };
949
1111
 
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))) {}
1112
+ template <typename to_scalar_at> struct cast_from_i8_gt {
1113
+ static bool try_(byte_t const* input, std::size_t dim, byte_t* output) noexcept {
1114
+ std::int8_t const* typed_input = reinterpret_cast<std::int8_t const*>(input);
1115
+ to_scalar_at* typed_output = reinterpret_cast<to_scalar_at*>(output);
1116
+ for (std::size_t i = 0; i != dim; ++i)
1117
+ typed_output[i] = static_cast<to_scalar_at>(typed_input[i]) / 127.f;
1118
+ return true;
1119
+ }
979
1120
  };
980
1121
 
981
- f16_bits_t::f16_bits_t(i8_converted_t v) noexcept : uint16_(f32_to_f16(v)) {}
1122
+ template <> struct cast_gt<i8_t, f16_t> : public cast_from_i8_gt<f16_t> {};
1123
+ template <> struct cast_gt<i8_t, bf16_t> : public cast_from_i8_gt<bf16_t> {};
1124
+ template <> struct cast_gt<i8_t, f32_t> : public cast_from_i8_gt<f32_t> {};
1125
+ template <> struct cast_gt<i8_t, f64_t> : public cast_from_i8_gt<f64_t> {};
1126
+
1127
+ template <> struct cast_gt<f16_t, i8_t> : public cast_to_i8_gt<f16_t> {};
1128
+ template <> struct cast_gt<bf16_t, i8_t> : public cast_to_i8_gt<bf16_t> {};
1129
+ template <> struct cast_gt<f32_t, i8_t> : public cast_to_i8_gt<f32_t> {};
1130
+ template <> struct cast_gt<f64_t, i8_t> : public cast_to_i8_gt<f64_t> {};
982
1131
 
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> {};
1132
+ template <> struct cast_gt<b1x8_t, f16_t> : public cast_from_b1x8_gt<f16_t> {};
1133
+ template <> struct cast_gt<b1x8_t, bf16_t> : public cast_from_b1x8_gt<bf16_t> {};
1134
+ template <> struct cast_gt<b1x8_t, f32_t> : public cast_from_b1x8_gt<f32_t> {};
1135
+ template <> struct cast_gt<b1x8_t, f64_t> : public cast_from_b1x8_gt<f64_t> {};
986
1136
 
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> {};
1137
+ template <> struct cast_gt<f16_t, b1x8_t> : public cast_to_b1x8_gt<f16_t> {};
1138
+ template <> struct cast_gt<bf16_t, b1x8_t> : public cast_to_b1x8_gt<bf16_t> {};
1139
+ template <> struct cast_gt<f32_t, b1x8_t> : public cast_to_b1x8_gt<f32_t> {};
1140
+ template <> struct cast_gt<f64_t, b1x8_t> : public cast_to_b1x8_gt<f64_t> {};
1141
+
1142
+ template <> struct cast_gt<b1x8_t, i8_t> : public cast_from_b1x8_gt<i8_t> {};
1143
+ template <> struct cast_gt<i8_t, b1x8_t> : public cast_to_b1x8_gt<i8_t> {};
990
1144
 
991
1145
  /* Don't complain if the vectorization of the inner loops fails:
992
1146
  *
@@ -1563,6 +1717,7 @@ class metric_punned_t {
1563
1717
  case scalar_kind_t::f32_k: datatype = simsimd_datatype_f32_k; break;
1564
1718
  case scalar_kind_t::f64_k: datatype = simsimd_datatype_f64_k; break;
1565
1719
  case scalar_kind_t::f16_k: datatype = simsimd_datatype_f16_k; break;
1720
+ case scalar_kind_t::bf16_k: datatype = simsimd_datatype_bf16_k; break;
1566
1721
  case scalar_kind_t::i8_k: datatype = simsimd_datatype_i8_k; break;
1567
1722
  case scalar_kind_t::b1x8_k: datatype = simsimd_datatype_b8_k; break;
1568
1723
  default: break;
@@ -1615,9 +1770,10 @@ class metric_punned_t {
1615
1770
  switch (metric_kind_) {
1616
1771
  case metric_kind_t::ip_k: {
1617
1772
  switch (scalar_kind_) {
1618
- case scalar_kind_t::f32_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_ip_gt<f32_t>>; break;
1619
- case scalar_kind_t::f16_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_ip_gt<f16_t, f32_t>>; break;
1773
+ case scalar_kind_t::bf16_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_ip_gt<bf16_t, f32_t>>; break;
1620
1774
  case scalar_kind_t::i8_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_ip_gt<i8_t, f32_t>>; break;
1775
+ case scalar_kind_t::f16_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_ip_gt<f16_t, f32_t>>; break;
1776
+ case scalar_kind_t::f32_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_ip_gt<f32_t>>; break;
1621
1777
  case scalar_kind_t::f64_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_ip_gt<f64_t>>; break;
1622
1778
  default: metric_ptr_ = 0; break;
1623
1779
  }
@@ -1625,9 +1781,10 @@ class metric_punned_t {
1625
1781
  }
1626
1782
  case metric_kind_t::cos_k: {
1627
1783
  switch (scalar_kind_) {
1628
- case scalar_kind_t::f32_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_cos_gt<f32_t>>; break;
1629
- case scalar_kind_t::f16_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_cos_gt<f16_t, f32_t>>; break;
1784
+ case scalar_kind_t::bf16_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_cos_gt<bf16_t, f32_t>>; break;
1630
1785
  case scalar_kind_t::i8_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_cos_gt<i8_t, f32_t>>; break;
1786
+ case scalar_kind_t::f16_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_cos_gt<f16_t, f32_t>>; break;
1787
+ case scalar_kind_t::f32_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_cos_gt<f32_t>>; break;
1631
1788
  case scalar_kind_t::f64_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_cos_gt<f64_t>>; break;
1632
1789
  default: metric_ptr_ = 0; break;
1633
1790
  }
@@ -1635,9 +1792,10 @@ class metric_punned_t {
1635
1792
  }
1636
1793
  case metric_kind_t::l2sq_k: {
1637
1794
  switch (scalar_kind_) {
1638
- case scalar_kind_t::f32_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_l2sq_gt<f32_t>>; break;
1639
- case scalar_kind_t::f16_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_l2sq_gt<f16_t, f32_t>>; break;
1795
+ case scalar_kind_t::bf16_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_l2sq_gt<bf16_t, f32_t>>; break;
1640
1796
  case scalar_kind_t::i8_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_l2sq_gt<i8_t, f32_t>>; break;
1797
+ case scalar_kind_t::f16_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_l2sq_gt<f16_t, f32_t>>; break;
1798
+ case scalar_kind_t::f32_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_l2sq_gt<f32_t>>; break;
1641
1799
  case scalar_kind_t::f64_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_l2sq_gt<f64_t>>; break;
1642
1800
  default: metric_ptr_ = 0; break;
1643
1801
  }
@@ -1645,6 +1803,9 @@ class metric_punned_t {
1645
1803
  }
1646
1804
  case metric_kind_t::pearson_k: {
1647
1805
  switch (scalar_kind_) {
1806
+ case scalar_kind_t::bf16_k:
1807
+ metric_ptr_ = (uptr_t)&equidimensional_<metric_pearson_gt<bf16_t, f32_t>>;
1808
+ break;
1648
1809
  case scalar_kind_t::i8_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_pearson_gt<i8_t, f32_t>>; break;
1649
1810
  case scalar_kind_t::f16_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_pearson_gt<f16_t, f32_t>>; break;
1650
1811
  case scalar_kind_t::f32_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_pearson_gt<f32_t>>; break;
@@ -1655,7 +1816,8 @@ class metric_punned_t {
1655
1816
  }
1656
1817
  case metric_kind_t::haversine_k: {
1657
1818
  switch (scalar_kind_) {
1658
- case scalar_kind_t::f16_k: metric_ptr_ = 0; break; //< Having half-precision 2D coordinates is a bit silly.
1819
+ case scalar_kind_t::bf16_k: metric_ptr_ = 0; break; //< Half-precision 2D vectors are silly.
1820
+ case scalar_kind_t::f16_k: metric_ptr_ = 0; break; //< Half-precision 2D vectors are silly.
1659
1821
  case scalar_kind_t::f32_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_haversine_gt<f32_t>>; break;
1660
1822
  case scalar_kind_t::f64_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_haversine_gt<f64_t>>; break;
1661
1823
  default: metric_ptr_ = 0; break;
@@ -1664,6 +1826,9 @@ class metric_punned_t {
1664
1826
  }
1665
1827
  case metric_kind_t::divergence_k: {
1666
1828
  switch (scalar_kind_) {
1829
+ case scalar_kind_t::bf16_k:
1830
+ metric_ptr_ = (uptr_t)&equidimensional_<metric_divergence_gt<bf16_t, f32_t>>;
1831
+ break;
1667
1832
  case scalar_kind_t::f16_k:
1668
1833
  metric_ptr_ = (uptr_t)&equidimensional_<metric_divergence_gt<f16_t, f32_t>>;
1669
1834
  break;
@@ -29,6 +29,7 @@ export declare enum ScalarKind {
29
29
  F32 = "f32",
30
30
  F64 = "f64",
31
31
  F16 = "f16",
32
+ BF16 = "bf16",
32
33
  I8 = "i8",
33
34
  B1 = "b1"
34
35
  }
@@ -74,6 +74,7 @@ var ScalarKind;
74
74
  ScalarKind["F32"] = "f32";
75
75
  ScalarKind["F64"] = "f64";
76
76
  ScalarKind["F16"] = "f16";
77
+ ScalarKind["BF16"] = "bf16";
77
78
  ScalarKind["I8"] = "i8";
78
79
  ScalarKind["B1"] = "b1";
79
80
  })(ScalarKind || (exports.ScalarKind = ScalarKind = {}));
@@ -29,6 +29,7 @@ export declare enum ScalarKind {
29
29
  F32 = "f32",
30
30
  F64 = "f64",
31
31
  F16 = "f16",
32
+ BF16 = "bf16",
32
33
  I8 = "i8",
33
34
  B1 = "b1"
34
35
  }
@@ -33,6 +33,7 @@ export var ScalarKind;
33
33
  ScalarKind["F32"] = "f32";
34
34
  ScalarKind["F64"] = "f64";
35
35
  ScalarKind["F16"] = "f16";
36
+ ScalarKind["BF16"] = "bf16";
36
37
  ScalarKind["I8"] = "i8";
37
38
  ScalarKind["B1"] = "b1";
38
39
  })(ScalarKind || (ScalarKind = {}));
@@ -70,6 +70,7 @@ export enum ScalarKind {
70
70
  F32 = "f32",
71
71
  F64 = "f64",
72
72
  F16 = "f16",
73
+ BF16 = "bf16",
73
74
  I8 = "i8",
74
75
  B1 = "b1",
75
76
  }
@@ -610,12 +611,12 @@ function exactSearch(
610
611
  }
611
612
 
612
613
  const usearch = {
613
- Index,
614
- MetricKind,
615
- ScalarKind,
616
- Matches,
617
- BatchMatches,
618
- exactSearch,
614
+ Index,
615
+ MetricKind,
616
+ ScalarKind,
617
+ Matches,
618
+ BatchMatches,
619
+ exactSearch,
619
620
  };
620
621
  export default usearch;
621
622
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usearch",
3
- "version": "2.13.4",
3
+ "version": "2.14.0",
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