usearch 2.2.0 → 2.3.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.
@@ -148,7 +148,10 @@ jobs:
148
148
  - uses: actions/setup-node@v3
149
149
  with:
150
150
  node-version: 18
151
- - run: npm install
151
+ - run: |
152
+ export CC=/usr/bin/gcc-12
153
+ export CXX=/usr/bin/g++-12
154
+ npm install
152
155
  - run: npm ci
153
156
  - run: npm test
154
157
 
@@ -176,7 +179,10 @@ jobs:
176
179
  - name: Setup Gradle
177
180
  uses: gradle/gradle-build-action@v2.4.2
178
181
  - name: Execute Gradle build
179
- run: gradle clean build
182
+ run:
183
+ export CC=/usr/bin/gcc-12
184
+ export CXX=/usr/bin/g++-12
185
+ gradle clean build
180
186
 
181
187
  test_swift:
182
188
  name: Test ObjC & Swift
@@ -251,7 +257,10 @@ jobs:
251
257
 
252
258
  - name: Build library
253
259
  run: |
254
- cd golang && make -C ../c libusearch_c.so
260
+ cd golang/
261
+ export CC=/usr/bin/gcc-12
262
+ export CXX=/usr/bin/g++-12
263
+ make -C ../c libusearch_c.so
255
264
  sudo mv ../c/libusearch_c.so /usr/local/lib/libusearch.a
256
265
  sudo mv ../c/usearch.h /usr/local/include/usearch.h
257
266
 
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "usearch"
3
- version = "2.2.0"
3
+ version = "2.3.0"
4
4
  authors = ["Ash Vardanian <1983160+ashvardanian@users.noreply.github.com>"]
5
5
  description = "Smaller & Faster Single-File Vector Search Engine from Unum"
6
6
  edition = "2021"
package/conanfile.py CHANGED
@@ -4,7 +4,7 @@ from conans import ConanFile
4
4
  class USearchConan(ConanFile):
5
5
 
6
6
  name = 'USearch'
7
- version = '2.2.0'
7
+ version = '2.3.0'
8
8
  license = 'Apache 2.0'
9
9
  url = 'https://github.com/unum-cloud/usearch'
10
10
  description = 'Smaller & Faster Single-File Vector Search Engine from Unum'
package/cpp/bench.cpp CHANGED
@@ -372,6 +372,7 @@ static void single_shot(dataset_at& dataset, index_at& index, bool construct = t
372
372
  executor, [&](std::size_t progress, std::size_t total) {
373
373
  if (progress % 1000 == 0)
374
374
  printer.print(progress, total);
375
+ return true;
375
376
  });
376
377
  join_attempts = result.visited_members;
377
378
  }
@@ -1212,9 +1212,10 @@ struct dummy_callback_t {
1212
1212
  *
1213
1213
  * This is particularly helpful when handling long-running tasks, like serialization,
1214
1214
  * saving, and loading from disk, or index-level joins.
1215
+ * The reporter checks return value to continue or stop the process, `false` means need to stop.
1215
1216
  */
1216
1217
  struct dummy_progress_t {
1217
- inline void operator()(std::size_t, std::size_t) const noexcept {}
1218
+ inline bool operator()(std::size_t /*processed*/, std::size_t /*total*/) const noexcept { return true; }
1218
1219
  };
1219
1220
 
1220
1221
  /**
@@ -2690,6 +2691,10 @@ class index_gt {
2690
2691
  if (!output(&header, sizeof(header)))
2691
2692
  return result.failed("Failed to serialize the header into stream");
2692
2693
 
2694
+ // Progress status
2695
+ std::size_t processed = 0;
2696
+ std::size_t const total = 2 * header.size;
2697
+
2693
2698
  // Export the number of levels per node
2694
2699
  // That is both enough to estimate the overall memory consumption,
2695
2700
  // and to be able to estimate the offsets of every entry in the file.
@@ -2697,15 +2702,18 @@ class index_gt {
2697
2702
  node_t node = node_at_(i);
2698
2703
  level_t level = node.level();
2699
2704
  if (!output(&level, sizeof(level)))
2700
- return result.failed("Failed to serialize nodes levels into stream");
2705
+ return result.failed("Failed to serialize into stream");
2706
+ if (!progress(++processed, total))
2707
+ return result.failed("Terminated by user");
2701
2708
  }
2702
2709
 
2703
2710
  // After that dump the nodes themselves
2704
2711
  for (std::size_t i = 0; i != header.size; ++i) {
2705
2712
  span_bytes_t node_bytes = node_bytes_(node_at_(i));
2706
2713
  if (!output(node_bytes.data(), node_bytes.size()))
2707
- return result.failed("Failed to serialize nodes into stream");
2708
- progress(i, header.size);
2714
+ return result.failed("Failed to serialize into stream");
2715
+ if (!progress(++processed, total))
2716
+ return result.failed("Terminated by user");
2709
2717
  }
2710
2718
 
2711
2719
  return {};
@@ -2763,7 +2771,8 @@ class index_gt {
2763
2771
  return result.failed("Failed to pull nodes from the stream");
2764
2772
  }
2765
2773
  nodes_[i] = node_t{node_bytes.data()};
2766
- progress(i, header.size);
2774
+ if (!progress(i + 1, header.size))
2775
+ return result.failed("Terminated by user");
2767
2776
  }
2768
2777
  return {};
2769
2778
  }
@@ -2936,7 +2945,8 @@ class index_gt {
2936
2945
  // Rapidly address all the nodes
2937
2946
  for (std::size_t i = 0; i != header.size; ++i) {
2938
2947
  nodes_[i] = node_t{(byte_t*)file.data() + offsets[i]};
2939
- progress(i, header.size);
2948
+ if (!progress(i + 1, header.size))
2949
+ return result.failed("Terminated by user");
2940
2950
  }
2941
2951
  viewed_file_ = std::move(file);
2942
2952
  return {};
@@ -2986,8 +2996,13 @@ class index_gt {
2986
2996
  using slot_level_allocator_t = typename dynamic_allocator_traits_t::template rebind_alloc<slot_level_t>;
2987
2997
  buffer_gt<slot_level_t, slot_level_allocator_t> slots_and_levels(size());
2988
2998
 
2999
+ // Progress status
3000
+ std::atomic<bool> do_tasks{true};
3001
+ std::atomic<std::size_t> processed{0};
3002
+ std::size_t const total = 3 * slots_and_levels.size();
3003
+
2989
3004
  // For every bottom level node, determine its parent cluster
2990
- executor.fixed(slots_and_levels.size(), [&](std::size_t thread_idx, std::size_t old_slot) {
3005
+ executor.dynamic(slots_and_levels.size(), [&](std::size_t thread_idx, std::size_t old_slot) {
2991
3006
  context_t& context = contexts_[thread_idx];
2992
3007
  std::size_t cluster = search_for_one_( //
2993
3008
  values[citerator_at(old_slot)], //
@@ -2997,7 +3012,13 @@ class index_gt {
2997
3012
  static_cast<compressed_slot_t>(old_slot), //
2998
3013
  static_cast<compressed_slot_t>(cluster), //
2999
3014
  node_at_(old_slot).level()};
3015
+ ++processed;
3016
+ if (thread_idx == 0)
3017
+ do_tasks = progress(processed.load(), total);
3018
+ return do_tasks.load();
3000
3019
  });
3020
+ if (!do_tasks.load())
3021
+ return;
3001
3022
 
3002
3023
  // Where the actual permutation happens:
3003
3024
  std::sort(slots_and_levels.begin(), slots_and_levels.end(), [](slot_level_t const& a, slot_level_t const& b) {
@@ -3027,8 +3048,8 @@ class index_gt {
3027
3048
  neighbor = static_cast<compressed_slot_t>(old_slot_to_new[compressed_slot_t(neighbor)]);
3028
3049
 
3029
3050
  reordered_nodes[new_slot] = new_node;
3030
-
3031
- progress(new_slot, slots_and_levels.size());
3051
+ if (!progress(++processed, total))
3052
+ return;
3032
3053
  }
3033
3054
 
3034
3055
  for (std::size_t new_slot = 0; new_slot != slots_and_levels.size(); ++new_slot) {
@@ -3036,6 +3057,8 @@ class index_gt {
3036
3057
  slot_transition(node_at_(old_slot).ckey(), //
3037
3058
  static_cast<compressed_slot_t>(old_slot), //
3038
3059
  static_cast<compressed_slot_t>(new_slot));
3060
+ if (!progress(++processed, total))
3061
+ return;
3039
3062
  }
3040
3063
 
3041
3064
  nodes_ = std::move(reordered_nodes);
@@ -3064,9 +3087,13 @@ class index_gt {
3064
3087
  executor_at&& executor = executor_at{}, //
3065
3088
  progress_at&& progress = progress_at{}) noexcept {
3066
3089
 
3090
+ // Progress status
3091
+ std::atomic<bool> do_tasks{true};
3092
+ std::atomic<std::size_t> processed{0};
3093
+
3067
3094
  // Erase all the incoming links
3068
3095
  std::size_t nodes_count = size();
3069
- executor.fixed(nodes_count, [&](std::size_t, std::size_t node_idx) {
3096
+ executor.dynamic(nodes_count, [&](std::size_t thread_idx, std::size_t node_idx) {
3070
3097
  node_t node = node_at_(node_idx);
3071
3098
  for (level_t level = 0; level <= node.level(); ++level) {
3072
3099
  neighbors_ref_t neighbors = neighbors_(node, level);
@@ -3079,8 +3106,14 @@ class index_gt {
3079
3106
  neighbors.push_back(neighbor_slot);
3080
3107
  }
3081
3108
  }
3082
- progress(node_idx, nodes_count);
3109
+ ++processed;
3110
+ if (thread_idx == 0)
3111
+ do_tasks = progress(processed.load(), nodes_count);
3112
+ return do_tasks.load();
3083
3113
  });
3114
+
3115
+ // At the end report the latest numbers, because the reporter thread may be finished earlier
3116
+ progress(processed.load(), nodes_count);
3084
3117
  }
3085
3118
 
3086
3119
  private:
@@ -3710,7 +3743,10 @@ static join_result_t join( //
3710
3743
  passed_rounds = ++rounds;
3711
3744
  total_rounds = passed_rounds + free_men.size();
3712
3745
  }
3713
- progress(passed_rounds, total_rounds);
3746
+ if (thread_idx == 0 && !progress(passed_rounds, total_rounds)) {
3747
+ atomic_error.store("Terminated by user");
3748
+ break;
3749
+ }
3714
3750
  while (men_locks.atomic_set(free_man_slot))
3715
3751
  ;
3716
3752
 
@@ -767,7 +767,9 @@ class index_dense_gt {
767
767
  * @brief Saves serialized binary index representation to a stream.
768
768
  */
769
769
  template <typename output_callback_at, typename progress_at = dummy_progress_t>
770
- serialization_result_t save_to_stream(output_callback_at&& output, serialization_config_t config = {}) const {
770
+ serialization_result_t save_to_stream(output_callback_at&& output, //
771
+ serialization_config_t config = {}, //
772
+ progress_at&& progress = {}) const {
771
773
 
772
774
  serialization_result_t result;
773
775
  std::uint64_t matrix_rows = 0;
@@ -831,7 +833,7 @@ class index_dense_gt {
831
833
  }
832
834
 
833
835
  // Save the actual proximity graph
834
- return typed_->save_to_stream(std::forward<output_callback_at>(output));
836
+ return typed_->save_to_stream(std::forward<output_callback_at>(output), std::forward<progress_at>(progress));
835
837
  }
836
838
 
837
839
  /**
@@ -853,8 +855,10 @@ class index_dense_gt {
853
855
  * @param[in] config Configuration parameters for imports.
854
856
  * @return Outcome descriptor explicitly convertible to boolean.
855
857
  */
856
- template <typename input_callback_at>
857
- serialization_result_t load_from_stream(input_callback_at&& input, serialization_config_t config = {}) {
858
+ template <typename input_callback_at, typename progress_at = dummy_progress_t>
859
+ serialization_result_t load_from_stream(input_callback_at&& input, //
860
+ serialization_config_t config = {}, //
861
+ progress_at&& progress = {}) {
858
862
 
859
863
  // Discard all previous memory allocations of `vectors_tape_allocator_`
860
864
  reset();
@@ -915,7 +919,7 @@ class index_dense_gt {
915
919
  }
916
920
 
917
921
  // Pull the actual proximity graph
918
- result = typed_->load_from_stream(std::forward<input_callback_at>(input));
922
+ result = typed_->load_from_stream(std::forward<input_callback_at>(input), std::forward<progress_at>(progress));
919
923
  if (!result)
920
924
  return result;
921
925
  if (typed_->size() != static_cast<std::size_t>(matrix_rows))
@@ -931,7 +935,10 @@ class index_dense_gt {
931
935
  * @param[in] config Configuration parameters for imports.
932
936
  * @return Outcome descriptor explicitly convertible to boolean.
933
937
  */
934
- serialization_result_t view(memory_mapped_file_t file, std::size_t offset = 0, serialization_config_t config = {}) {
938
+ template <typename progress_at = dummy_progress_t>
939
+ serialization_result_t view(memory_mapped_file_t file, //
940
+ std::size_t offset = 0, serialization_config_t config = {}, //
941
+ progress_at&& progress = {}) {
935
942
 
936
943
  // Discard all previous memory allocations of `vectors_tape_allocator_`
937
944
  reset();
@@ -997,7 +1004,7 @@ class index_dense_gt {
997
1004
  }
998
1005
 
999
1006
  // Pull the actual proximity graph
1000
- result = typed_->view(std::move(file), offset);
1007
+ result = typed_->view(std::move(file), offset, std::forward<progress_at>(progress));
1001
1008
  if (!result)
1002
1009
  return result;
1003
1010
  if (typed_->size() != static_cast<std::size_t>(matrix_rows))
@@ -1019,7 +1026,9 @@ class index_dense_gt {
1019
1026
  * @param[in] config Configuration parameters for exports.
1020
1027
  * @return Outcome descriptor explicitly convertible to boolean.
1021
1028
  */
1022
- serialization_result_t save(output_file_t file, serialization_config_t config = {}) const {
1029
+ template <typename progress_at = dummy_progress_t>
1030
+ serialization_result_t save(output_file_t file, serialization_config_t config = {},
1031
+ progress_at&& progress = {}) const {
1023
1032
 
1024
1033
  serialization_result_t io_result = file.open_if_not();
1025
1034
  if (!io_result)
@@ -1030,7 +1039,7 @@ class index_dense_gt {
1030
1039
  io_result = file.write(buffer, length);
1031
1040
  return !!io_result;
1032
1041
  },
1033
- config);
1042
+ config, std::forward<progress_at>(progress));
1034
1043
 
1035
1044
  if (!stream_result)
1036
1045
  return stream_result;
@@ -1041,8 +1050,11 @@ class index_dense_gt {
1041
1050
  * @brief Memory-maps the serialized binary index representation from disk,
1042
1051
  * @b without copying data into RAM, and fetching it on-demand.
1043
1052
  */
1044
- serialization_result_t save(memory_mapped_file_t file, std::size_t offset = 0,
1045
- serialization_config_t config = {}) const {
1053
+ template <typename progress_at = dummy_progress_t>
1054
+ serialization_result_t save(memory_mapped_file_t file, //
1055
+ std::size_t offset = 0, //
1056
+ serialization_config_t config = {}, //
1057
+ progress_at&& progress = {}) const {
1046
1058
 
1047
1059
  serialization_result_t io_result = file.open_if_not();
1048
1060
  if (!io_result)
@@ -1056,7 +1068,7 @@ class index_dense_gt {
1056
1068
  offset += length;
1057
1069
  return true;
1058
1070
  },
1059
- config);
1071
+ config, std::forward<progress_at>(progress));
1060
1072
 
1061
1073
  return stream_result;
1062
1074
  }
@@ -1067,7 +1079,8 @@ class index_dense_gt {
1067
1079
  * @param[in] config Configuration parameters for imports.
1068
1080
  * @return Outcome descriptor explicitly convertible to boolean.
1069
1081
  */
1070
- serialization_result_t load(input_file_t file, serialization_config_t config = {}) {
1082
+ template <typename progress_at = dummy_progress_t>
1083
+ serialization_result_t load(input_file_t file, serialization_config_t config = {}, progress_at&& progress = {}) {
1071
1084
 
1072
1085
  serialization_result_t io_result = file.open_if_not();
1073
1086
  if (!io_result)
@@ -1078,7 +1091,7 @@ class index_dense_gt {
1078
1091
  io_result = file.read(buffer, length);
1079
1092
  return !!io_result;
1080
1093
  },
1081
- config);
1094
+ config, std::forward<progress_at>(progress));
1082
1095
 
1083
1096
  if (!stream_result)
1084
1097
  return stream_result;
@@ -1089,7 +1102,11 @@ class index_dense_gt {
1089
1102
  * @brief Memory-maps the serialized binary index representation from disk,
1090
1103
  * @b without copying data into RAM, and fetching it on-demand.
1091
1104
  */
1092
- serialization_result_t load(memory_mapped_file_t file, std::size_t offset = 0, serialization_config_t config = {}) {
1105
+ template <typename progress_at = dummy_progress_t>
1106
+ serialization_result_t load(memory_mapped_file_t file, //
1107
+ std::size_t offset = 0, //
1108
+ serialization_config_t config = {}, //
1109
+ progress_at&& progress = {}) {
1093
1110
 
1094
1111
  serialization_result_t io_result = file.open_if_not();
1095
1112
  if (!io_result)
@@ -1103,17 +1120,23 @@ class index_dense_gt {
1103
1120
  offset += length;
1104
1121
  return true;
1105
1122
  },
1106
- config);
1123
+ config, std::forward<progress_at>(progress));
1107
1124
 
1108
1125
  return stream_result;
1109
1126
  }
1110
1127
 
1111
- serialization_result_t save(char const* file_path, serialization_config_t config = {}) const {
1112
- return save(output_file_t(file_path), config);
1128
+ template <typename progress_at = dummy_progress_t>
1129
+ serialization_result_t save(char const* file_path, //
1130
+ serialization_config_t config = {}, //
1131
+ progress_at&& progress = {}) const {
1132
+ return save(output_file_t(file_path), config, std::forward<progress_at>(progress));
1113
1133
  }
1114
1134
 
1115
- serialization_result_t load(char const* file_path, serialization_config_t config = {}) {
1116
- return load(input_file_t(file_path), config);
1135
+ template <typename progress_at = dummy_progress_t>
1136
+ serialization_result_t load(char const* file_path, //
1137
+ serialization_config_t config = {}, //
1138
+ progress_at&& progress = {}) {
1139
+ return load(input_file_t(file_path), config, std::forward<progress_at>(progress));
1117
1140
  }
1118
1141
 
1119
1142
  /**
@@ -217,19 +217,47 @@ inline bool hardware_supports(isa_kind_t isa_kind) noexcept {
217
217
  #endif
218
218
 
219
219
  // When compiling with GCC, one may use the "built-ins", including ones
220
- // designed for CPU capability detection.
221
- #if defined(USEARCH_DEFINED_X86) && defined(USEARCH_DEFINED_GCC)
222
- __builtin_cpu_init();
220
+ // designed for CPU capability detection, but they are not fully compatible with Clang.
221
+ // So instead I've decided to add a pinch of inline assembly.
222
+ #if defined(USEARCH_DEFINED_X86)
223
+ auto check_cpu_feature = [](std::uint64_t feature_mask, int function_id, int register_id) -> bool {
224
+ std::uint32_t eax, ebx, ecx, edx;
225
+ // Execute CPUID instruction and store results in eax, ebx, ecx, edx
226
+ // Setting %ecx to 0 as well
227
+ __asm__ volatile("cpuid" : "=a"(eax), "=b"(ebx), "=c"(ecx), "=d"(edx) : "a"(function_id), "c"(0));
228
+ std::uint32_t register_value;
229
+ switch (register_id) {
230
+ case 0: register_value = eax; break;
231
+ case 1: register_value = ebx; break;
232
+ case 2: register_value = ecx; break;
233
+ case 3: register_value = edx; break;
234
+ default: return false;
235
+ }
236
+
237
+ return (register_value & feature_mask) != 0;
238
+ };
239
+
240
+ // Check for AVX2 (Function ID 7, EBX register)
241
+ // https://github.com/llvm/llvm-project/blob/50598f0ff44f3a4e75706f8c53f3380fe7faa896/clang/lib/Headers/cpuid.h#L148
242
+ bool supports_avx2 = check_cpu_feature(1 << 5, 7, 1);
243
+
244
+ // Check for F16C (Function ID 1, ECX register)
245
+ // https://github.com/llvm/llvm-project/blob/50598f0ff44f3a4e75706f8c53f3380fe7faa896/clang/lib/Headers/cpuid.h#L107
246
+ bool supports_f16c = check_cpu_feature(1 << 29, 1, 2);
247
+
248
+ // Check for AVX512F (Function ID 7, EBX register)
249
+ // https://github.com/llvm/llvm-project/blob/50598f0ff44f3a4e75706f8c53f3380fe7faa896/clang/lib/Headers/cpuid.h#L155
250
+ bool supports_avx512f = check_cpu_feature(1 << 16, 7, 1);
251
+
252
+ // Check for AVX512FP16 (Function ID 7, EDX register)
253
+ // https://github.com/llvm/llvm-project/blob/50598f0ff44f3a4e75706f8c53f3380fe7faa896/clang/lib/Headers/cpuid.h#L198C9-L198C23
254
+ bool supports_avx512fp16 = check_cpu_feature(1 << 23, 7, 3);
255
+
223
256
  switch (isa_kind) {
224
- // Half-precision is only supported in newer versions of GCC
225
- #if __GNUC__ > 11 || (__GNUC__ == 11 && __GNUC_MINOR__ >= 0)
226
- case isa_kind_t::avx2f16_k: return __builtin_cpu_supports("avx2") && __builtin_cpu_supports("f16c");
227
- #endif
228
- #if __GNUC__ > 12 || (__GNUC__ == 12 && __GNUC_MINOR__ >= 0)
229
- case isa_kind_t::avx512f16_k: return __builtin_cpu_supports("avx512f") && __builtin_cpu_supports("avx512fp16");
230
- #endif
231
- case isa_kind_t::avx2_k: return __builtin_cpu_supports("avx2");
232
- case isa_kind_t::avx512_k: return __builtin_cpu_supports("avx512f");
257
+ case isa_kind_t::avx2f16_k: return supports_avx2 && supports_f16c;
258
+ case isa_kind_t::avx512f16_k: return supports_avx512fp16 && supports_avx512f;
259
+ case isa_kind_t::avx2_k: return supports_avx2;
260
+ case isa_kind_t::avx512_k: return supports_avx512f;
233
261
  default: return false;
234
262
  }
235
263
  #endif
@@ -1426,6 +1454,21 @@ class metric_punned_t {
1426
1454
  return {to_stl_<metric_cos_gt<f16_t, f32_t>>(bytes_per_vector), bytes_per_vector, metric_kind_t::cos_k, scalar_kind_t::f16_k, isa_kind_t::auto_k};
1427
1455
  }
1428
1456
 
1457
+ static metric_punned_t ip_metric_f16_(std::size_t bytes_per_vector) {
1458
+ #if USEARCH_USE_SIMSIMD
1459
+ #if SIMSIMD_TARGET_X86_AVX512
1460
+ if (hardware_supports(isa_kind_t::avx512f16_k) && bytes_per_vector % 32 == 0) return {pun_stl_<simsimd_f16_t>([=](simsimd_f16_t const* a, simsimd_f16_t const* b) { return simsimd_dot_f16x16_avx512(a, b, bytes_per_vector / 2); }), bytes_per_vector, metric_kind_t::ip_k, scalar_kind_t::f16_k, isa_kind_t::avx512f16_k};
1461
+ #endif
1462
+ #if SIMSIMD_TARGET_X86_AVX2
1463
+ if (hardware_supports(isa_kind_t::avx2f16_k) && bytes_per_vector % 16 == 0) return {pun_stl_<simsimd_f16_t>([=](simsimd_f16_t const* a, simsimd_f16_t const* b) { return simsimd_dot_f16x8_avx2(a, b, bytes_per_vector / 2); }), bytes_per_vector, metric_kind_t::ip_k, scalar_kind_t::f16_k, isa_kind_t::avx2f16_k};
1464
+ #endif
1465
+ #if SIMSIMD_TARGET_ARM_NEON
1466
+ if (hardware_supports(isa_kind_t::neon_k) && bytes_per_vector % 8 == 0) return {pun_stl_<simsimd_f16_t>([=](simsimd_f16_t const* a, simsimd_f16_t const* b) { return simsimd_dot_f16x4_neon(a, b, bytes_per_vector / 2); }), bytes_per_vector, metric_kind_t::ip_k, scalar_kind_t::f16_k, isa_kind_t::neon_k};
1467
+ #endif
1468
+ #endif
1469
+ return {to_stl_<metric_cos_gt<f16_t, f32_t>>(bytes_per_vector), bytes_per_vector, metric_kind_t::ip_k, scalar_kind_t::f16_k, isa_kind_t::auto_k};
1470
+ }
1471
+
1429
1472
  static metric_punned_t l2sq_metric_f16_(std::size_t bytes_per_vector) {
1430
1473
  #if USEARCH_USE_SIMSIMD
1431
1474
  #if SIMSIMD_TARGET_X86_AVX512
@@ -1446,6 +1489,9 @@ class metric_punned_t {
1446
1489
  #if SIMSIMD_TARGET_ARM_NEON
1447
1490
  if (hardware_supports(isa_kind_t::neon_k) && bytes_per_vector % 16 == 0) return {pun_stl_<int8_t>([=](int8_t const* a, int8_t const* b) { return simsimd_cos_i8x16_neon(a, b, bytes_per_vector); }), bytes_per_vector, metric_kind_t::cos_k, scalar_kind_t::i8_k, isa_kind_t::neon_k};
1448
1491
  #endif
1492
+ #if SIMSIMD_TARGET_X86_AVX2
1493
+ if (hardware_supports(isa_kind_t::avx2_k) && bytes_per_vector % 32 == 0) return {pun_stl_<int8_t>([=](int8_t const* a, int8_t const* b) { return simsimd_cos_i8x32_avx2(a, b, bytes_per_vector); }), bytes_per_vector, metric_kind_t::cos_k, scalar_kind_t::i8_k, isa_kind_t::avx2_k};
1494
+ #endif
1449
1495
  #endif
1450
1496
  return {to_stl_<cos_i8_t>(bytes_per_vector), bytes_per_vector, metric_kind_t::cos_k, scalar_kind_t::i8_k, isa_kind_t::auto_k};
1451
1497
  }
@@ -1453,7 +1499,7 @@ class metric_punned_t {
1453
1499
  static metric_punned_t ip_metric_(std::size_t bytes_per_vector, scalar_kind_t scalar_kind) {
1454
1500
  switch (scalar_kind) { // The two most common numeric types for the most common metric have optimized versions
1455
1501
  case scalar_kind_t::f32_k: return ip_metric_f32_(bytes_per_vector);
1456
- case scalar_kind_t::f16_k: return cos_metric_f16_(bytes_per_vector); // Dot-product accumulates error, Cosine-distance normalizes it
1502
+ case scalar_kind_t::f16_k: return ip_metric_f16_(bytes_per_vector);
1457
1503
  case scalar_kind_t::i8_k: return cos_metric_i8_(bytes_per_vector);
1458
1504
  case scalar_kind_t::f64_k: return {to_stl_<metric_ip_gt<f64_t>>(bytes_per_vector), bytes_per_vector, metric_kind_t::ip_k, scalar_kind_t::f64_k, isa_kind_t::auto_k};
1459
1505
  default: return {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usearch",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Smaller & Faster Single-File Vector Search Engine from Unum",
5
5
  "author": "Ash Vardanian",
6
6
  "license": "Apache 2.0",