usearch 0.2.0 → 0.2.3

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/src/advanced.hpp CHANGED
@@ -1,8 +1,22 @@
1
1
  #pragma once
2
- #include <functional>
3
- #include <stdexcept> // `std::invalid_argument`
4
- #include <thread>
5
- #include <vector>
2
+
3
+ #if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__)
4
+ #define _USE_MATH_DEFINES
5
+
6
+ #define WINDOWS
7
+ #endif
8
+
9
+ #include <stdlib.h> // `aligned_alloc`
10
+
11
+ #include <functional> // `std::function`
12
+ #include <numeric> // `std::iota`
13
+ #include <stdexcept> // `std::invalid_argument`
14
+ #include <thread> // `std::thread`
15
+ #include <vector> // `std::vector`
16
+
17
+ #if defined(_OPENMP)
18
+ #include <omp.h> // `omp_get_num_threads()`
19
+ #endif
6
20
 
7
21
  #if __linux__
8
22
  #include <sys/auxv.h>
@@ -43,11 +57,15 @@ template <typename at> inline at clamp(at v, at lo, at hi) noexcept {
43
57
  return usearch::clamp(v, lo, hi, std::less<at>{});
44
58
  }
45
59
 
46
- class i8q100_converted_t;
47
- class f16_converted_t;
60
+ class f8_bits_t;
61
+ class f16_bits_t;
48
62
 
49
63
  inline float f16_to_f32(std::uint16_t u16) noexcept {
50
- #if defined(__aarch64__)
64
+ #if defined(__AVX512F__)
65
+ _Float16 f16;
66
+ std::memcpy(&f16, &u16, sizeof(std::uint16_t));
67
+ return float(f16);
68
+ #elif defined(__aarch64__)
51
69
  __fp16 f16;
52
70
  std::memcpy(&f16, &u16, sizeof(std::uint16_t));
53
71
  return float(f16);
@@ -57,7 +75,12 @@ inline float f16_to_f32(std::uint16_t u16) noexcept {
57
75
  }
58
76
 
59
77
  inline std::uint16_t f32_to_f16(float f32) noexcept {
60
- #if defined(__aarch64__)
78
+ #if defined(__AVX512F__)
79
+ _Float16 f16 = _Float16(f32);
80
+ std::uint16_t u16;
81
+ std::memcpy(&u16, &f16, sizeof(std::uint16_t));
82
+ return u16;
83
+ #elif defined(__aarch64__)
61
84
  __fp16 f16 = __fp16(f32);
62
85
  std::uint16_t u16;
63
86
  std::memcpy(&u16, &f16, sizeof(std::uint16_t));
@@ -67,81 +90,89 @@ inline std::uint16_t f32_to_f16(float f32) noexcept {
67
90
  #endif
68
91
  }
69
92
 
70
- class f16_converted_t {
93
+ class f16_bits_t {
71
94
  std::uint16_t uint16_{};
72
95
 
73
96
  public:
74
- inline f16_converted_t() noexcept : uint16_(0) {}
75
- inline f16_converted_t(f16_converted_t&&) = default;
76
- inline f16_converted_t& operator=(f16_converted_t&&) = default;
77
- inline f16_converted_t(f16_converted_t const&) = default;
78
- inline f16_converted_t& operator=(f16_converted_t const&) = default;
97
+ inline f16_bits_t() noexcept : uint16_(0) {}
98
+ inline f16_bits_t(f16_bits_t&&) = default;
99
+ inline f16_bits_t& operator=(f16_bits_t&&) = default;
100
+ inline f16_bits_t(f16_bits_t const&) = default;
101
+ inline f16_bits_t& operator=(f16_bits_t const&) = default;
79
102
 
80
103
  inline operator float() const noexcept { return f16_to_f32(uint16_); }
81
104
 
82
- inline f16_converted_t(i8q100_converted_t) noexcept;
83
- inline f16_converted_t(float v) noexcept : uint16_(f32_to_f16(v)) {}
84
- inline f16_converted_t(double v) noexcept : uint16_(f32_to_f16(v)) {}
85
-
86
- inline f16_converted_t operator+(f16_converted_t other) const noexcept { return {float(*this) + float(other)}; }
87
- inline f16_converted_t operator-(f16_converted_t other) const noexcept { return {float(*this) - float(other)}; }
88
- inline f16_converted_t operator*(f16_converted_t other) const noexcept { return {float(*this) * float(other)}; }
89
- inline f16_converted_t operator/(f16_converted_t other) const noexcept { return {float(*this) / float(other)}; }
90
- inline f16_converted_t operator+(float other) const noexcept { return {float(*this) + other}; }
91
- inline f16_converted_t operator-(float other) const noexcept { return {float(*this) - other}; }
92
- inline f16_converted_t operator*(float other) const noexcept { return {float(*this) * other}; }
93
- inline f16_converted_t operator/(float other) const noexcept { return {float(*this) / other}; }
94
- inline f16_converted_t operator+(double other) const noexcept { return {float(*this) + other}; }
95
- inline f16_converted_t operator-(double other) const noexcept { return {float(*this) - other}; }
96
- inline f16_converted_t operator*(double other) const noexcept { return {float(*this) * other}; }
97
- inline f16_converted_t operator/(double other) const noexcept { return {float(*this) / other}; }
98
-
99
- inline f16_converted_t& operator+=(float v) noexcept {
105
+ inline f16_bits_t(f8_bits_t) noexcept;
106
+ inline f16_bits_t(float v) noexcept : uint16_(f32_to_f16(v)) {}
107
+ inline f16_bits_t(double v) noexcept : uint16_(f32_to_f16(v)) {}
108
+
109
+ inline f16_bits_t operator+(f16_bits_t other) const noexcept { return {float(*this) + float(other)}; }
110
+ inline f16_bits_t operator-(f16_bits_t other) const noexcept { return {float(*this) - float(other)}; }
111
+ inline f16_bits_t operator*(f16_bits_t other) const noexcept { return {float(*this) * float(other)}; }
112
+ inline f16_bits_t operator/(f16_bits_t other) const noexcept { return {float(*this) / float(other)}; }
113
+ inline f16_bits_t operator+(float other) const noexcept { return {float(*this) + other}; }
114
+ inline f16_bits_t operator-(float other) const noexcept { return {float(*this) - other}; }
115
+ inline f16_bits_t operator*(float other) const noexcept { return {float(*this) * other}; }
116
+ inline f16_bits_t operator/(float other) const noexcept { return {float(*this) / other}; }
117
+ inline f16_bits_t operator+(double other) const noexcept { return {float(*this) + other}; }
118
+ inline f16_bits_t operator-(double other) const noexcept { return {float(*this) - other}; }
119
+ inline f16_bits_t operator*(double other) const noexcept { return {float(*this) * other}; }
120
+ inline f16_bits_t operator/(double other) const noexcept { return {float(*this) / other}; }
121
+
122
+ inline f16_bits_t& operator+=(float v) noexcept {
100
123
  uint16_ = f32_to_f16(v + f16_to_f32(uint16_));
101
124
  return *this;
102
125
  }
103
126
 
104
- inline f16_converted_t& operator-=(float v) noexcept {
127
+ inline f16_bits_t& operator-=(float v) noexcept {
105
128
  uint16_ = f32_to_f16(v - f16_to_f32(uint16_));
106
129
  return *this;
107
130
  }
108
131
 
109
- inline f16_converted_t& operator*=(float v) noexcept {
132
+ inline f16_bits_t& operator*=(float v) noexcept {
110
133
  uint16_ = f32_to_f16(v * f16_to_f32(uint16_));
111
134
  return *this;
112
135
  }
113
136
 
114
- inline f16_converted_t& operator/=(float v) noexcept {
137
+ inline f16_bits_t& operator/=(float v) noexcept {
115
138
  uint16_ = f32_to_f16(v / f16_to_f32(uint16_));
116
139
  return *this;
117
140
  }
118
141
  };
119
142
 
120
- class i8q100_converted_t {
143
+ /**
144
+ * @brief Numeric type for uniformly-distributed floating point
145
+ * values within [-1,1] range, quantized to integers [-100,100].
146
+ */
147
+ class f8_bits_t {
121
148
  std::int8_t int8_{};
122
149
 
123
150
  public:
124
- inline i8q100_converted_t() noexcept : int8_(0) {}
125
- inline i8q100_converted_t(i8q100_converted_t&&) = default;
126
- inline i8q100_converted_t& operator=(i8q100_converted_t&&) = default;
127
- inline i8q100_converted_t(i8q100_converted_t const&) = default;
128
- inline i8q100_converted_t& operator=(i8q100_converted_t const&) = default;
129
-
130
- inline operator float() const noexcept { return float(int8_) / 100.f; }
131
- inline operator f16_converted_t() const noexcept { return float(int8_) / 100.f; }
132
- inline operator double() const noexcept { return double(int8_) / 100.0; }
133
- inline operator std::int8_t() const noexcept { return int8_; }
134
- inline operator std::int16_t() const noexcept { return int8_; }
135
- inline operator std::int32_t() const noexcept { return int8_; }
136
- inline operator std::int64_t() const noexcept { return int8_; }
137
-
138
- inline i8q100_converted_t(float v) noexcept
139
- : int8_(usearch::clamp<std::int8_t>(static_cast<std::int8_t>(v * 100.f), -100, 100)) {}
140
- inline i8q100_converted_t(double v) noexcept
141
- : int8_(usearch::clamp<std::int8_t>(static_cast<std::int8_t>(v * 100.0), -100, 100)) {}
151
+ constexpr static float divisor_k = 100.f;
152
+ constexpr static std::int8_t min_k = -100;
153
+ constexpr static std::int8_t max_k = 100;
154
+
155
+ inline f8_bits_t() noexcept : int8_(0) {}
156
+ inline f8_bits_t(f8_bits_t&&) = default;
157
+ inline f8_bits_t& operator=(f8_bits_t&&) = default;
158
+ inline f8_bits_t(f8_bits_t const&) = default;
159
+ inline f8_bits_t& operator=(f8_bits_t const&) = default;
160
+
161
+ inline operator float() const noexcept { return float(int8_) / divisor_k; }
162
+ inline operator f16_bits_t() const noexcept { return float(int8_) / divisor_k; }
163
+ inline operator double() const noexcept { return double(int8_) / divisor_k; }
164
+ inline explicit operator std::int8_t() const noexcept { return int8_; }
165
+ inline explicit operator std::int16_t() const noexcept { return int8_; }
166
+ inline explicit operator std::int32_t() const noexcept { return int8_; }
167
+ inline explicit operator std::int64_t() const noexcept { return int8_; }
168
+
169
+ inline f8_bits_t(float v) noexcept
170
+ : int8_(usearch::clamp<std::int8_t>(static_cast<std::int8_t>(v * divisor_k), min_k, max_k)) {}
171
+ inline f8_bits_t(double v) noexcept
172
+ : int8_(usearch::clamp<std::int8_t>(static_cast<std::int8_t>(v * divisor_k), min_k, max_k)) {}
142
173
  };
143
174
 
144
- inline f16_converted_t::f16_converted_t(i8q100_converted_t v) noexcept : f16_converted_t(float(v)) {}
175
+ inline f16_bits_t::f16_bits_t(f8_bits_t v) noexcept : f16_bits_t(float(v)) {}
145
176
 
146
177
  struct uuid_t {
147
178
  std::uint8_t octets[16];
@@ -152,24 +183,30 @@ void multithreaded(std::size_t threads, std::size_t tasks, callback_at&& callbac
152
183
 
153
184
  if (threads == 0)
154
185
  threads = std::thread::hardware_concurrency();
155
- if (threads == 1) {
186
+ if (threads == 1 || tasks <= 128) {
156
187
  for (std::size_t task_idx = 0; task_idx < tasks; ++task_idx)
157
188
  callback(0, task_idx);
158
189
  return;
159
190
  }
160
191
 
192
+ #if defined(_OPENMP)
193
+ omp_set_num_threads(threads);
194
+ #pragma omp parallel for schedule(dynamic)
195
+ for (std::size_t i = 0; i < tasks; ++i)
196
+ callback(omp_get_thread_num(), i);
197
+ #else
161
198
  std::vector<std::thread> threads_pool;
162
199
  std::size_t tasks_per_thread = (tasks / threads) + ((tasks % threads) != 0);
163
200
  for (std::size_t thread_idx = 0; thread_idx != threads; ++thread_idx) {
164
201
  threads_pool.emplace_back([=]() {
165
202
  for (std::size_t task_idx = thread_idx * tasks_per_thread;
166
- task_idx < std::min(tasks, thread_idx * tasks_per_thread + tasks_per_thread); ++task_idx)
203
+ task_idx < (std::min)(tasks, thread_idx * tasks_per_thread + tasks_per_thread); ++task_idx)
167
204
  callback(thread_idx, task_idx);
168
205
  });
169
206
  }
170
-
171
207
  for (std::size_t thread_idx = 0; thread_idx != threads; ++thread_idx)
172
208
  threads_pool[thread_idx].join();
209
+ #endif
173
210
  }
174
211
 
175
212
  /**
@@ -201,7 +238,7 @@ enum class accuracy_t {
201
238
  f32_k,
202
239
  f16_k,
203
240
  f64_k,
204
- i8q100_k,
241
+ f8_k,
205
242
  };
206
243
 
207
244
  inline char const* accuracy_name(accuracy_t accuracy) noexcept {
@@ -209,7 +246,7 @@ inline char const* accuracy_name(accuracy_t accuracy) noexcept {
209
246
  case accuracy_t::f32_k: return "f32";
210
247
  case accuracy_t::f16_k: return "f16";
211
248
  case accuracy_t::f64_k: return "f64";
212
- case accuracy_t::i8q100_k: return "i8q100";
249
+ case accuracy_t::f8_k: return "f8";
213
250
  default: return "";
214
251
  }
215
252
  }
@@ -227,10 +264,10 @@ inline accuracy_t accuracy_from_name(char const* name, std::size_t len) {
227
264
  accuracy = accuracy_t::f64_k;
228
265
  else if (str_equals(name, len, "f16"))
229
266
  accuracy = accuracy_t::f16_k;
230
- else if (str_equals(name, len, "i8q100"))
231
- accuracy = accuracy_t::i8q100_k;
267
+ else if (str_equals(name, len, "f8"))
268
+ accuracy = accuracy_t::f8_k;
232
269
  else
233
- throw std::invalid_argument("Unknown type, choose: f32, f16, f64, i8q100");
270
+ throw std::invalid_argument("Unknown type, choose: f32, f16, f64, f8");
234
271
  return accuracy;
235
272
  }
236
273
 
@@ -283,7 +320,7 @@ inline std::size_t bytes_per_scalar(accuracy_t accuracy) noexcept {
283
320
  case accuracy_t::f32_k: return 4;
284
321
  case accuracy_t::f16_k: return 2;
285
322
  case accuracy_t::f64_k: return 8;
286
- case accuracy_t::i8q100_k: return 1;
323
+ case accuracy_t::f8_k: return 1;
287
324
  default: return 0;
288
325
  }
289
326
  }
@@ -318,26 +355,10 @@ template <> struct cast_gt<f64_t, f64_t> {
318
355
  bool operator()(byte_t const*, std::size_t, byte_t*) noexcept { return false; }
319
356
  };
320
357
 
321
- template <> struct cast_gt<f16_converted_t, f16_converted_t> {
358
+ template <> struct cast_gt<f16_bits_t, f16_bits_t> {
322
359
  bool operator()(byte_t const*, std::size_t, byte_t*) noexcept { return false; }
323
360
  };
324
361
 
325
- struct ip_i8q100_converted_t {
326
- float operator()(i8q100_converted_t const* a, i8q100_converted_t const* b, std::size_t d) const noexcept {
327
- std::int64_t ab = 0;
328
- #if defined(__GNUC__)
329
- #pragma GCC ivdep
330
- #elif defined(__clang__)
331
- #pragma clang loop vectorize(enable)
332
- #elif defined(_OPENMP)
333
- #pragma omp simd reduction(+ : ab)
334
- #endif
335
- for (std::size_t i = 0; i < d; ++i)
336
- ab += std::int16_t(a[i]) * std::int16_t(b[i]);
337
- return 1.f - ab / 10000.f;
338
- }
339
- };
340
-
341
362
  /**
342
363
  * @brief Oversimplified type-punned index for equidimensional floating-point
343
364
  * vectors with automatic down-casting and hardware acceleration.
@@ -368,10 +389,10 @@ class auto_index_gt {
368
389
  accuracy_t accuracy_ = accuracy_t::f32_k;
369
390
  isa_t acceleration_ = isa_t::auto_k;
370
391
 
371
- std::unique_ptr<index_t> index_;
372
- mutable std::vector<byte_t> cast_buffer_;
392
+ index_t* index_{};
393
+ mutable std::vector<byte_t> cast_buffer_{};
373
394
  struct casts_t {
374
- cast_t from_i8q100{};
395
+ cast_t from_f8{};
375
396
  cast_t from_f16{};
376
397
  cast_t from_f32{};
377
398
  cast_t from_f64{};
@@ -380,23 +401,43 @@ class auto_index_gt {
380
401
  punned_stateful_metric_t root_metric_;
381
402
  config_t root_config_;
382
403
 
404
+ mutable std::vector<std::size_t> available_threads_;
405
+ mutable std::mutex available_threads_mutex_;
406
+
383
407
  public:
384
408
  auto_index_gt() = default;
385
- auto_index_gt(auto_index_gt&& other)
386
- : dimensions_(other.dimensions_), casted_vector_bytes_(other.casted_vector_bytes_), accuracy_(other.accuracy_),
387
- acceleration_(other.acceleration_), index_(std::move(other.index_)),
388
- cast_buffer_(std::move(other.cast_buffer_)), casts_(std::move(other.casts_)),
389
- root_metric_(std::move(other.root_metric_)), root_config_(std::move(other.root_config_)) {}
409
+ auto_index_gt(auto_index_gt&& other) { swap(other); }
410
+ auto_index_gt& operator=(auto_index_gt&& other) noexcept {
411
+ swap(other);
412
+ return *this;
413
+ }
414
+
415
+ ~auto_index_gt() { aligned_index_free(index_); }
416
+
417
+ void swap(auto_index_gt& other) noexcept {
418
+ std::swap(dimensions_, other.dimensions_);
419
+ std::swap(casted_vector_bytes_, other.casted_vector_bytes_);
420
+ std::swap(accuracy_, other.accuracy_);
421
+ std::swap(acceleration_, other.acceleration_);
422
+ std::swap(index_, other.index_);
423
+ std::swap(cast_buffer_, other.cast_buffer_);
424
+ std::swap(casts_, other.casts_);
425
+ std::swap(root_metric_, other.root_metric_);
426
+ std::swap(root_config_, other.root_config_);
427
+ std::swap(available_threads_, other.available_threads_);
428
+ }
390
429
 
391
430
  std::size_t dimensions() const noexcept { return dimensions_; }
392
431
  std::size_t connectivity() const noexcept { return index_->connectivity(); }
393
432
  std::size_t size() const noexcept { return index_->size(); }
394
433
  std::size_t capacity() const noexcept { return index_->capacity(); }
434
+ config_t const& config() const noexcept { return root_config_; }
395
435
  void clear() noexcept { return index_->clear(); }
396
436
 
437
+ accuracy_t accuracy() const noexcept { return accuracy_; }
397
438
  isa_t acceleration() const noexcept { return acceleration_; }
398
439
  std::size_t concurrency() const noexcept {
399
- return std::min(root_config_.max_threads_add, root_config_.max_threads_search);
440
+ return (std::min)(root_config_.max_threads_add, root_config_.max_threads_search);
400
441
  }
401
442
 
402
443
  void save(char const* path) const { index_->save(path); }
@@ -405,13 +446,21 @@ class auto_index_gt {
405
446
  void reserve(std::size_t capacity) { index_->reserve(capacity); }
406
447
 
407
448
  // clang-format off
408
- void add(label_t label, f16_converted_t const* vector, std::size_t thread = 0, bool copy = true) { return add(label, vector, thread, copy, casts_.from_f16); }
409
- void add(label_t label, f32_t const* vector, std::size_t thread = 0, bool copy = true) { return add(label, vector, thread, copy, casts_.from_f32); }
410
- void add(label_t label, f64_t const* vector, std::size_t thread = 0, bool copy = true) { return add(label, vector, thread, copy, casts_.from_f64); }
411
-
412
- std::size_t search(f16_converted_t const* vector, std::size_t wanted, label_t* matches, distance_t* distances, std::size_t thread = 0) const { return search(vector, wanted, matches, distances, thread, casts_.from_f16); }
413
- std::size_t search(f32_t const* vector, std::size_t wanted, label_t* matches, distance_t* distances, std::size_t thread = 0) const { return search(vector, wanted, matches, distances, thread, casts_.from_f32); }
414
- std::size_t search(f64_t const* vector, std::size_t wanted, label_t* matches, distance_t* distances, std::size_t thread = 0) const { return search(vector, wanted, matches, distances, thread, casts_.from_f64); }
449
+ void add(label_t label, f16_bits_t const* vector) { return add(label, vector, true, casts_.from_f16); }
450
+ void add(label_t label, f32_t const* vector) { return add(label, vector, true, casts_.from_f32); }
451
+ void add(label_t label, f64_t const* vector) { return add(label, vector, true, casts_.from_f64); }
452
+
453
+ void add(label_t label, f16_bits_t const* vector, std::size_t thread, bool copy) { return add(label, vector, thread, copy, casts_.from_f16); }
454
+ void add(label_t label, f32_t const* vector, std::size_t thread, bool copy) { return add(label, vector, thread, copy, casts_.from_f32); }
455
+ void add(label_t label, f64_t const* vector, std::size_t thread, bool copy) { return add(label, vector, thread, copy, casts_.from_f64); }
456
+
457
+ std::size_t search(f16_bits_t const* vector, std::size_t wanted, label_t* matches, distance_t* distances) const { return search(vector, wanted, matches, distances, casts_.from_f16); }
458
+ std::size_t search(f32_t const* vector, std::size_t wanted, label_t* matches, distance_t* distances) const { return search(vector, wanted, matches, distances, casts_.from_f32); }
459
+ std::size_t search(f64_t const* vector, std::size_t wanted, label_t* matches, distance_t* distances) const { return search(vector, wanted, matches, distances, casts_.from_f64); }
460
+
461
+ std::size_t search(f16_bits_t const* vector, std::size_t wanted, label_t* matches, distance_t* distances, std::size_t thread) const { return search(vector, wanted, matches, distances, thread, casts_.from_f16); }
462
+ std::size_t search(f32_t const* vector, std::size_t wanted, label_t* matches, distance_t* distances, std::size_t thread) const { return search(vector, wanted, matches, distances, thread, casts_.from_f32); }
463
+ std::size_t search(f64_t const* vector, std::size_t wanted, label_t* matches, distance_t* distances, std::size_t thread) const { return search(vector, wanted, matches, distances, thread, casts_.from_f64); }
415
464
 
416
465
  static auto_index_gt ip(std::size_t dimensions, accuracy_t accuracy = accuracy_t::f16_k, config_t config = {}) { return make(dimensions, accuracy, ip_metric(dimensions, accuracy), make_casts(accuracy), config); }
417
466
  static auto_index_gt l2(std::size_t dimensions, accuracy_t accuracy = accuracy_t::f16_k, config_t config = {}) { return make(dimensions, accuracy, l2_metric(dimensions, accuracy), make_casts(accuracy), config); }
@@ -437,12 +486,41 @@ class auto_index_gt {
437
486
 
438
487
  result.root_metric_ = root_metric_;
439
488
  result.root_config_ = root_config_;
440
- result.index_.reset(new index_t(result.root_config_, result.root_metric_));
489
+ index_t* raw = aligned_index_alloc();
490
+ new (raw) index_t(root_config_, root_metric_);
491
+ result.index_ = raw;
441
492
 
442
493
  return result;
443
494
  }
444
495
 
445
496
  private:
497
+ static index_t* aligned_index_alloc() noexcept {
498
+ return (index_t*)aligned_alloc(64, 64 * divide_round_up<64>(sizeof(index_t)));
499
+ }
500
+
501
+ static void aligned_index_free(index_t* raw) noexcept { free(raw); }
502
+
503
+ struct thread_lock_t {
504
+ auto_index_gt const& parent;
505
+ std::size_t thread_id;
506
+
507
+ ~thread_lock_t() { parent.thread_unlock(thread_id); }
508
+ };
509
+
510
+ thread_lock_t thread_lock() const {
511
+ available_threads_mutex_.lock();
512
+ std::size_t thread_id = available_threads_.back();
513
+ available_threads_.pop_back();
514
+ available_threads_mutex_.unlock();
515
+ return {*this, thread_id};
516
+ }
517
+
518
+ void thread_unlock(std::size_t thread_id) const {
519
+ available_threads_mutex_.lock();
520
+ available_threads_.push_back(thread_id);
521
+ available_threads_mutex_.unlock();
522
+ }
523
+
446
524
  template <typename scalar_at>
447
525
  void add(label_t label, scalar_at const* vector, std::size_t thread, bool copy, cast_t const& cast) {
448
526
  byte_t const* vector_data = reinterpret_cast<byte_t const*>(vector);
@@ -473,29 +551,54 @@ class auto_index_gt {
473
551
  return index_->search({vector_data, vector_bytes}, wanted, matches, distances, thread);
474
552
  }
475
553
 
554
+ template <typename scalar_at> void add(label_t label, scalar_at const* vector, bool copy, cast_t const& cast) {
555
+ thread_lock_t lock = thread_lock();
556
+ return add(label, vector, lock.thread_id, copy, cast);
557
+ }
558
+
559
+ template <typename scalar_at>
560
+ std::size_t search( //
561
+ scalar_at const* vector, std::size_t wanted, //
562
+ label_t* matches, distance_t* distances, //
563
+ cast_t const& cast) const {
564
+ thread_lock_t lock = thread_lock();
565
+ return search(vector, wanted, matches, distances, lock.thread_id, cast);
566
+ }
567
+
476
568
  static auto_index_gt make( //
477
569
  std::size_t dimensions, accuracy_t accuracy, //
478
570
  metric_and_meta_t metric_and_meta, casts_t casts, //
479
571
  config_t config) {
480
572
 
481
- std::size_t max_threads = std::max(config.max_threads_add, config.max_threads_search);
573
+ std::size_t hardware_threads = std::thread::hardware_concurrency();
574
+ config.max_threads_add = config.max_threads_add ? config.max_threads_add : hardware_threads;
575
+ config.max_threads_search = config.max_threads_search ? config.max_threads_search : hardware_threads;
576
+ std::size_t max_threads = (std::max)(config.max_threads_add, config.max_threads_search);
482
577
  auto_index_gt result;
483
578
  result.dimensions_ = dimensions;
484
579
  result.accuracy_ = accuracy;
485
580
  result.casted_vector_bytes_ = bytes_per_scalar(accuracy) * dimensions;
486
581
  result.cast_buffer_.resize(max_threads * result.casted_vector_bytes_);
487
582
  result.casts_ = casts;
488
- result.index_.reset(new index_t(config, metric_and_meta.metric));
489
583
  result.acceleration_ = metric_and_meta.acceleration;
490
584
  result.root_metric_ = metric_and_meta.metric;
491
585
  result.root_config_ = config;
586
+
587
+ // Fill the thread IDs.
588
+ result.available_threads_.resize(max_threads);
589
+ std::iota(result.available_threads_.begin(), result.available_threads_.end(), 0ul);
590
+
591
+ // Availible since C11, but only C++17, so we use the C version.
592
+ index_t* raw = aligned_index_alloc();
593
+ new (raw) index_t(config, metric_and_meta.metric);
594
+ result.index_ = raw;
492
595
  return result;
493
596
  }
494
597
 
495
598
  template <typename to_scalar_at> static casts_t make_casts() {
496
599
  casts_t result;
497
- result.from_i8q100 = cast_gt<i8q100_converted_t, to_scalar_at>{};
498
- result.from_f16 = cast_gt<f16_converted_t, to_scalar_at>{};
600
+ result.from_f8 = cast_gt<f8_bits_t, to_scalar_at>{};
601
+ result.from_f16 = cast_gt<f16_bits_t, to_scalar_at>{};
499
602
  result.from_f32 = cast_gt<f32_t, to_scalar_at>{};
500
603
  result.from_f64 = cast_gt<f64_t, to_scalar_at>{};
501
604
  return result;
@@ -505,8 +608,8 @@ class auto_index_gt {
505
608
  switch (accuracy) {
506
609
  case accuracy_t::f64_k: return make_casts<f64_t>();
507
610
  case accuracy_t::f32_k: return make_casts<f32_t>();
508
- case accuracy_t::f16_k: return make_casts<f16_converted_t>();
509
- case accuracy_t::i8q100_k: return make_casts<i8q100_converted_t>();
611
+ case accuracy_t::f16_k: return make_casts<f16_bits_t>();
612
+ case accuracy_t::f8_k: return make_casts<f8_bits_t>();
510
613
  default: return {};
511
614
  }
512
615
  }
@@ -525,7 +628,7 @@ class auto_index_gt {
525
628
  if (dimensions % 4 == 0)
526
629
  return {
527
630
  pun_metric<simsimd_f32_t>([](simsimd_f32_t const* a, simsimd_f32_t const* b, size_t d) noexcept {
528
- return 1 - simsimd_dot_f32x4avx2(a, b, d);
631
+ return 1.f - simsimd_dot_f32x4avx2(a, b, d);
529
632
  }),
530
633
  isa_t::avx2_k,
531
634
  };
@@ -533,14 +636,14 @@ class auto_index_gt {
533
636
  if (supports_arm_sve())
534
637
  return {
535
638
  pun_metric<simsimd_f32_t>([](simsimd_f32_t const* a, simsimd_f32_t const* b, size_t d) noexcept {
536
- return 1 - simsimd_dot_f32sve(a, b, d);
639
+ return 1.f - simsimd_dot_f32sve(a, b, d);
537
640
  }),
538
641
  isa_t::sve_k,
539
642
  };
540
643
  if (dimensions % 4 == 0)
541
644
  return {
542
645
  pun_metric<simsimd_f32_t>([](simsimd_f32_t const* a, simsimd_f32_t const* b, size_t d) noexcept {
543
- return 1 - simsimd_dot_f32x4neon(a, b, d);
646
+ return 1.f - simsimd_dot_f32x4neon(a, b, d);
544
647
  }),
545
648
  isa_t::neon_k,
546
649
  };
@@ -553,31 +656,39 @@ class auto_index_gt {
553
656
  (void)dimensions;
554
657
  #if defined(USEARCH_USE_SIMD)
555
658
  #if defined(__x86_64__)
659
+ if (dimensions % 32 == 0)
660
+ return {
661
+ pun_metric<simsimd_f16_t>([](simsimd_f16_t const* a, simsimd_f16_t const* b, size_t d) noexcept {
662
+ return 1.f - simsimd_dot_f16x16avx512(a, b, d);
663
+ }),
664
+ isa_t::avx512_k,
665
+ };
556
666
  #elif defined(__aarch64__)
557
667
  if (supports_arm_sve())
558
668
  return {
559
669
  pun_metric<simsimd_f16_t>([](simsimd_f16_t const* a, simsimd_f16_t const* b, size_t d) noexcept {
560
- return 1 - simsimd_dot_f16sve(a, b, d);
670
+ return 1.f - simsimd_dot_f16sve(a, b, d);
561
671
  }),
562
672
  isa_t::sve_k,
563
673
  };
564
674
  if (dimensions % 8 == 0)
565
675
  return {
566
676
  pun_metric<simsimd_f16_t>([](simsimd_f16_t const* a, simsimd_f16_t const* b, size_t d) noexcept {
567
- return 1 - simsimd_dot_f16x8neon(a, b, d);
677
+ return 1.f - simsimd_dot_f16x8neon(a, b, d);
568
678
  }),
569
679
  isa_t::neon_k,
570
680
  };
571
681
  #endif
572
682
  #endif
573
- return {pun_metric<f16_converted_t>(ip_gt<f16_converted_t>{}), isa_t::auto_k};
683
+ return {pun_metric<f16_bits_t>(ip_gt<f16_bits_t, f32_t>{}), isa_t::auto_k};
574
684
  }
575
685
 
576
686
  static metric_and_meta_t ip_metric(std::size_t dimensions, accuracy_t accuracy) {
577
687
  switch (accuracy) {
578
- case accuracy_t::i8q100_k: return {pun_metric<i8q100_converted_t>(ip_i8q100_converted_t{}), isa_t::auto_k};
688
+ // The tow most common numeric types for the most common metric have afew optimized versions:
579
689
  case accuracy_t::f16_k: return ip_metric_f16(dimensions);
580
690
  case accuracy_t::f32_k: return ip_metric_f32(dimensions);
691
+ case accuracy_t::f8_k: return {pun_metric<f8_bits_t>(ip_gt<f8_bits_t, f32_t>{}), isa_t::auto_k};
581
692
  case accuracy_t::f64_k: return {pun_metric<f64_t>(ip_gt<f64_t>{}), isa_t::auto_k};
582
693
  default: return {};
583
694
  }
@@ -585,18 +696,18 @@ class auto_index_gt {
585
696
 
586
697
  static metric_and_meta_t l2_metric(std::size_t, accuracy_t accuracy) {
587
698
  switch (accuracy) {
588
- case accuracy_t::i8q100_k: return {};
589
- case accuracy_t::f16_k: return {pun_metric<f16_converted_t>(l2_squared_gt<f16_converted_t>{}), isa_t::auto_k};
590
- case accuracy_t::f32_k: return {pun_metric<f32_t>(l2_squared_gt<f32_t>{}), isa_t::auto_k};
591
- case accuracy_t::f64_k: return {pun_metric<f64_t>(l2_squared_gt<f64_t>{}), isa_t::auto_k};
699
+ case accuracy_t::f8_k: return {pun_metric<f8_bits_t>(l2sq_gt<f8_bits_t, f32_t>{}), isa_t::auto_k};
700
+ case accuracy_t::f16_k: return {pun_metric<f16_bits_t>(l2sq_gt<f16_bits_t, f32_t>{}), isa_t::auto_k};
701
+ case accuracy_t::f32_k: return {pun_metric<f32_t>(l2sq_gt<f32_t>{}), isa_t::auto_k};
702
+ case accuracy_t::f64_k: return {pun_metric<f64_t>(l2sq_gt<f64_t>{}), isa_t::auto_k};
592
703
  default: return {};
593
704
  }
594
705
  }
595
706
 
596
707
  static metric_and_meta_t cos_metric(std::size_t, accuracy_t accuracy) {
597
708
  switch (accuracy) {
598
- case accuracy_t::i8q100_k: return {};
599
- case accuracy_t::f16_k: return {pun_metric<f16_converted_t>(cos_gt<f16_converted_t>{}), isa_t::auto_k};
709
+ case accuracy_t::f8_k: return {pun_metric<f8_bits_t>(cos_gt<f8_bits_t, f32_t>{}), isa_t::auto_k};
710
+ case accuracy_t::f16_k: return {pun_metric<f16_bits_t>(cos_gt<f16_bits_t, f32_t>{}), isa_t::auto_k};
600
711
  case accuracy_t::f32_k: return {pun_metric<f32_t>(cos_gt<f32_t>{}), isa_t::auto_k};
601
712
  case accuracy_t::f64_k: return {pun_metric<f64_t>(cos_gt<f64_t>{}), isa_t::auto_k};
602
713
  default: return {};
@@ -605,8 +716,8 @@ class auto_index_gt {
605
716
 
606
717
  static metric_and_meta_t haversine_metric(accuracy_t accuracy) {
607
718
  switch (accuracy) {
608
- case accuracy_t::i8q100_k: return {};
609
- case accuracy_t::f16_k: return {pun_metric<f16_converted_t>(haversine_gt<f16_converted_t>{}), isa_t::auto_k};
719
+ case accuracy_t::f8_k: return {pun_metric<f8_bits_t>(haversine_gt<f8_bits_t, f32_t>{}), isa_t::auto_k};
720
+ case accuracy_t::f16_k: return {pun_metric<f16_bits_t>(haversine_gt<f16_bits_t, f32_t>{}), isa_t::auto_k};
610
721
  case accuracy_t::f32_k: return {pun_metric<f32_t>(haversine_gt<f32_t>{}), isa_t::auto_k};
611
722
  case accuracy_t::f64_k: return {pun_metric<f64_t>(haversine_gt<f64_t>{}), isa_t::auto_k};
612
723
  default: return {};