usearch 2.26.0 → 2.26.2

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.
Files changed (148) hide show
  1. package/README.md +2 -2
  2. package/cpp/test.cpp +66 -3
  3. package/include/usearch/index.hpp +9 -1
  4. package/include/usearch/index_dense.hpp +16 -5
  5. package/numkong/CITATION.cff +13 -9
  6. package/numkong/CONTRIBUTING.md +29 -24
  7. package/numkong/Cargo.toml +1 -1
  8. package/numkong/README.md +21 -7
  9. package/numkong/binding.gyp +47 -31
  10. package/numkong/build.rs +15 -5
  11. package/numkong/c/dispatch.h +0 -1
  12. package/numkong/c/parallel.c +139 -0
  13. package/numkong/c/parallel.h +47 -0
  14. package/numkong/cmake/toolchain-wasi.cmake +39 -5
  15. package/numkong/include/README.md +24 -17
  16. package/numkong/include/numkong/attention/sapphireamx.h +6 -6
  17. package/numkong/include/numkong/attention/sme.h +2 -2
  18. package/numkong/include/numkong/capabilities.h +2 -2
  19. package/numkong/include/numkong/cast/README.md +9 -7
  20. package/numkong/include/numkong/cast/loongsonasx.h +48 -40
  21. package/numkong/include/numkong/cast/serial.h +89 -11
  22. package/numkong/include/numkong/cast.h +3 -3
  23. package/numkong/include/numkong/curved/README.md +10 -10
  24. package/numkong/include/numkong/curved/neon.h +1 -0
  25. package/numkong/include/numkong/curved/neonbfdot.h +1 -0
  26. package/numkong/include/numkong/curved/smef64.h +1 -0
  27. package/numkong/include/numkong/dot/README.md +18 -18
  28. package/numkong/include/numkong/dot/alder.h +8 -8
  29. package/numkong/include/numkong/dot/haswell.h +8 -8
  30. package/numkong/include/numkong/dot/icelake.h +12 -12
  31. package/numkong/include/numkong/dot/loongsonasx.h +6 -6
  32. package/numkong/include/numkong/dot/neon.h +2 -1
  33. package/numkong/include/numkong/dot/neonbfdot.h +1 -0
  34. package/numkong/include/numkong/dot/neonfhm.h +1 -0
  35. package/numkong/include/numkong/dot/neonsdot.h +1 -0
  36. package/numkong/include/numkong/dot/powervsx.h +3 -3
  37. package/numkong/include/numkong/dot/serial.h +19 -6
  38. package/numkong/include/numkong/dot/skylake.h +4 -4
  39. package/numkong/include/numkong/dot/svehalf.h +1 -1
  40. package/numkong/include/numkong/dot/v128relaxed.h +24 -23
  41. package/numkong/include/numkong/dots/README.md +26 -26
  42. package/numkong/include/numkong/dots/genoa.h +3 -1
  43. package/numkong/include/numkong/dots/icelake.h +1 -0
  44. package/numkong/include/numkong/dots/powervsx.h +1 -1
  45. package/numkong/include/numkong/dots/serial.h +32 -33
  46. package/numkong/include/numkong/dots/sierra.h +4 -3
  47. package/numkong/include/numkong/each/README.md +5 -3
  48. package/numkong/include/numkong/each/sapphire.h +2 -0
  49. package/numkong/include/numkong/each/skylake.h +1 -0
  50. package/numkong/include/numkong/geospatial/README.md +6 -6
  51. package/numkong/include/numkong/geospatial/v128relaxed.h +1 -0
  52. package/numkong/include/numkong/maxsim/README.md +5 -5
  53. package/numkong/include/numkong/maxsim/genoa.h +4 -0
  54. package/numkong/include/numkong/maxsim/icelake.h +1 -0
  55. package/numkong/include/numkong/maxsim/serial.h +22 -9
  56. package/numkong/include/numkong/maxsim/sme.h +3 -2
  57. package/numkong/include/numkong/mesh/README.md +7 -4
  58. package/numkong/include/numkong/mesh/genoa.h +2 -2
  59. package/numkong/include/numkong/mesh/haswell.h +8 -8
  60. package/numkong/include/numkong/mesh/neon.h +6 -6
  61. package/numkong/include/numkong/mesh/neonbfdot.h +3 -2
  62. package/numkong/include/numkong/mesh/neonfhm.h +3 -2
  63. package/numkong/include/numkong/mesh/rvv.h +4 -4
  64. package/numkong/include/numkong/mesh/serial.h +17 -17
  65. package/numkong/include/numkong/mesh/skylake.h +4 -4
  66. package/numkong/include/numkong/mesh/v128relaxed.h +4 -4
  67. package/numkong/include/numkong/mesh.hpp +2 -2
  68. package/numkong/include/numkong/numkong.h +1 -1
  69. package/numkong/include/numkong/numkong.hpp +1 -1
  70. package/numkong/include/numkong/probability/README.md +17 -15
  71. package/numkong/include/numkong/probability/skylake.h +1 -1
  72. package/numkong/include/numkong/reduce/README.md +27 -21
  73. package/numkong/include/numkong/reduce/genoa.h +2 -1
  74. package/numkong/include/numkong/reduce/icelake.h +1 -0
  75. package/numkong/include/numkong/reduce/neon.h +1 -1
  76. package/numkong/include/numkong/reduce/serial.h +9 -9
  77. package/numkong/include/numkong/reduce/skylake.h +4 -4
  78. package/numkong/include/numkong/reduce.hpp +38 -0
  79. package/numkong/include/numkong/scalar/README.md +29 -28
  80. package/numkong/include/numkong/set/README.md +17 -14
  81. package/numkong/include/numkong/set/haswell.h +15 -16
  82. package/numkong/include/numkong/set/icelake.h +15 -16
  83. package/numkong/include/numkong/set/loongsonasx.h +12 -10
  84. package/numkong/include/numkong/set/neon.h +15 -19
  85. package/numkong/include/numkong/set/powervsx.h +15 -21
  86. package/numkong/include/numkong/set/serial.h +19 -20
  87. package/numkong/include/numkong/set/v128relaxed.h +10 -8
  88. package/numkong/include/numkong/sets/icelake.h +1 -0
  89. package/numkong/include/numkong/sparse/README.md +4 -4
  90. package/numkong/include/numkong/sparse/icelake.h +1 -0
  91. package/numkong/include/numkong/sparse/neon.h +1 -0
  92. package/numkong/include/numkong/sparse/serial.h +10 -3
  93. package/numkong/include/numkong/sparse/turin.h +1 -0
  94. package/numkong/include/numkong/spatial/README.md +25 -24
  95. package/numkong/include/numkong/spatial/alder.h +2 -1
  96. package/numkong/include/numkong/spatial/haswell.h +48 -40
  97. package/numkong/include/numkong/spatial/icelake.h +2 -1
  98. package/numkong/include/numkong/spatial/neon.h +54 -46
  99. package/numkong/include/numkong/spatial/neonbfdot.h +1 -1
  100. package/numkong/include/numkong/spatial/powervsx.h +54 -46
  101. package/numkong/include/numkong/spatial/serial.h +56 -46
  102. package/numkong/include/numkong/spatial/sierra.h +4 -3
  103. package/numkong/include/numkong/spatial/skylake.h +27 -22
  104. package/numkong/include/numkong/spatial/sve.h +6 -6
  105. package/numkong/include/numkong/spatial/svebfdot.h +1 -1
  106. package/numkong/include/numkong/spatial/svehalf.h +1 -1
  107. package/numkong/include/numkong/spatial/v128relaxed.h +36 -30
  108. package/numkong/include/numkong/spatials/README.md +15 -15
  109. package/numkong/include/numkong/spatials/genoa.h +3 -0
  110. package/numkong/include/numkong/spatials/graniteamx.h +2 -0
  111. package/numkong/include/numkong/spatials/icelake.h +1 -0
  112. package/numkong/include/numkong/spatials/sierra.h +1 -0
  113. package/numkong/include/numkong/tensor.hpp +236 -123
  114. package/numkong/include/numkong/trigonometry/README.md +4 -4
  115. package/numkong/include/numkong/trigonometry/serial.h +6 -6
  116. package/numkong/include/numkong/trigonometry/v128relaxed.h +1 -0
  117. package/numkong/include/numkong/types.hpp +46 -16
  118. package/numkong/include/numkong/vector.hpp +116 -23
  119. package/numkong/javascript/@numkong-darwin-arm64/package.json +1 -1
  120. package/numkong/javascript/@numkong-darwin-x64/package.json +1 -1
  121. package/numkong/javascript/@numkong-linux-arm64/package.json +1 -1
  122. package/numkong/javascript/@numkong-linux-x64/package.json +1 -1
  123. package/numkong/javascript/@numkong-win32-arm64/package.json +1 -1
  124. package/numkong/javascript/@numkong-win32-x64/package.json +1 -1
  125. package/numkong/javascript/README.md +18 -16
  126. package/numkong/javascript/numkong.c +70 -40
  127. package/numkong/package.json +8 -8
  128. package/numkong/rust/README.md +19 -9
  129. package/numkong/rust/cast.rs +52 -1
  130. package/numkong/rust/each.rs +480 -7
  131. package/numkong/rust/matrix.rs +877 -216
  132. package/numkong/rust/numkong.rs +12 -8
  133. package/numkong/rust/reduce.rs +433 -5
  134. package/numkong/rust/tensor.rs +450 -842
  135. package/numkong/rust/vector.rs +520 -101
  136. package/numkong/swift/README.md +5 -3
  137. package/numkong/test/README.md +4 -4
  138. package/numkong/test/test-wasi.mjs +17 -0
  139. package/numkong/test/test_each.cpp +30 -1
  140. package/numkong/test/test_each.py +74 -1
  141. package/numkong/test/test_reduce.cpp +51 -0
  142. package/numkong/test/test_tensor.cpp +2 -63
  143. package/numkong/test/test_tensor.py +93 -0
  144. package/package.json +1 -1
  145. package/prebuilds/darwin-arm64+x64/usearch.node +0 -0
  146. package/prebuilds/linux-arm64/usearch.node +0 -0
  147. package/prebuilds/linux-x64/usearch.node +0 -0
  148. package/prebuilds/win32-x64/usearch.node +0 -0
package/README.md CHANGED
@@ -545,11 +545,11 @@ index = Index(ndim=ndim, metric=CompiledMetric(
545
545
 
546
546
  ```bibtex
547
547
  @software{Vardanian_USearch,
548
- doi = {10.5281/zenodo.7949416},
548
+ doi = {10.5281/zenodo.7949415},
549
549
  author = {Vardanian, Ash},
550
550
  title = {{USearch by Unum Cloud}},
551
551
  url = {https://github.com/unum-cloud/USearch},
552
- version = {2.26.0},
552
+ version = {2.26.2},
553
553
  year = {2026},
554
554
  }
555
555
  ```
package/cpp/test.cpp CHANGED
@@ -1112,9 +1112,6 @@ template <typename key_at, typename slot_at> void test_strings() {
1112
1112
  }
1113
1113
  }
1114
1114
 
1115
- /**
1116
- * @brief Tests replacing and updating entries in index_dense_gt to ensure consistency after modifications.
1117
- */
1118
1115
  /**
1119
1116
  * @brief Churns a tightly-reserved index, so `slot_lookup_` accumulates tombstones.
1120
1117
  *
@@ -1181,6 +1178,10 @@ template <typename key_at, typename slot_at> void test_slot_lookup_churn() {
1181
1178
  expect(copy.contains(key));
1182
1179
  }
1183
1180
 
1181
+ /**
1182
+ * @brief Tests replacing and updating entries in `index_dense_gt`, to ensure consistency
1183
+ * after modifications.
1184
+ */
1184
1185
  template <typename key_at, typename slot_at> void test_replacing_update() {
1185
1186
 
1186
1187
  using vector_key_t = key_at;
@@ -1427,6 +1428,67 @@ void test_load_after_metric_make() {
1427
1428
  std::remove(path);
1428
1429
  }
1429
1430
 
1431
+ /**
1432
+ * @brief Feeds `view` a truncated index file.
1433
+ *
1434
+ * The matrix dimensions and the node count come from the file itself, so a short file
1435
+ * describes a matrix and a level array that reach past the mapping. Both must be rejected
1436
+ * rather than walked, and a caller-supplied offset past the end must fail the same way.
1437
+ */
1438
+ void test_view_of_truncated_file() {
1439
+ std::printf("Testing view of a truncated index file\n");
1440
+
1441
+ using index_t = index_dense_gt<std::int64_t, std::uint32_t>;
1442
+ std::size_t const dimensions = 64;
1443
+ std::size_t const collection = 2048;
1444
+ char const* path = "tmp_truncated.usearch";
1445
+
1446
+ metric_punned_t metric(dimensions, metric_kind_t::l2sq_k, scalar_kind<f32_t>());
1447
+ index_t::state_result_t built = index_t::make(metric);
1448
+ expect(built);
1449
+ expect(built.index.try_reserve(collection));
1450
+ std::vector<float> vector(dimensions);
1451
+ for (std::size_t i = 0; i != collection; ++i) {
1452
+ for (std::size_t d = 0; d != dimensions; ++d)
1453
+ vector[d] = static_cast<float>((i * 31 + d) % 997) / 997.f;
1454
+ expect(built.index.add(static_cast<std::int64_t>(i), vector.data()));
1455
+ }
1456
+ expect(built.index.save(path));
1457
+
1458
+ std::vector<char> prefix;
1459
+ {
1460
+ std::FILE* file = std::fopen(path, "rb");
1461
+ expect(file != nullptr);
1462
+ std::fseek(file, 0, SEEK_END);
1463
+ long const bytes = std::ftell(file);
1464
+ std::fseek(file, 0, SEEK_SET);
1465
+ prefix.resize(static_cast<std::size_t>(bytes) / 8);
1466
+ expect(std::fread(prefix.data(), 1, prefix.size(), file) == prefix.size());
1467
+ std::fclose(file);
1468
+ }
1469
+ {
1470
+ std::FILE* file = std::fopen(path, "wb");
1471
+ expect(file != nullptr);
1472
+ std::fwrite(prefix.data(), 1, prefix.size(), file);
1473
+ std::fclose(file);
1474
+ }
1475
+
1476
+ index_t::state_result_t viewed = index_t::make(metric);
1477
+ expect(viewed);
1478
+ serialization_result_t truncated = viewed.index.view(path);
1479
+ expect(!truncated);
1480
+ truncated.error.release();
1481
+
1482
+ // A caller-supplied offset past the end must fail the same way.
1483
+ index_t::state_result_t offset_viewed = index_t::make(metric);
1484
+ expect(offset_viewed);
1485
+ serialization_result_t beyond = offset_viewed.index.view(memory_mapped_file_t(path), prefix.size() * 4);
1486
+ expect(!beyond);
1487
+ beyond.error.release();
1488
+
1489
+ std::remove(path);
1490
+ }
1491
+
1430
1492
  int main(int, char**) {
1431
1493
  install_crash_handlers();
1432
1494
 
@@ -1523,5 +1585,6 @@ int main(int, char**) {
1523
1585
  test_filtered_search();
1524
1586
  test_isolate();
1525
1587
  test_load_after_metric_make();
1588
+ test_view_of_truncated_file();
1526
1589
  return 0;
1527
1590
  }
@@ -9,7 +9,7 @@
9
9
 
10
10
  #define USEARCH_VERSION_MAJOR 2
11
11
  #define USEARCH_VERSION_MINOR 26
12
- #define USEARCH_VERSION_PATCH 0
12
+ #define USEARCH_VERSION_PATCH 2
13
13
 
14
14
  // Inferring C++ version
15
15
  // https://stackoverflow.com/a/61552074
@@ -2084,6 +2084,10 @@ class memory_mapped_file_t {
2084
2084
 
2085
2085
  #if defined(USEARCH_DEFINED_LINUX)
2086
2086
  int descriptor = open(path_, O_RDONLY | O_NOATIME);
2087
+ if (descriptor < 0 && errno == EPERM)
2088
+ // `O_NOATIME` requires owning the file or holding `CAP_FOWNER` (see `open(2)`).
2089
+ // Retry without it instead of failing outright.
2090
+ descriptor = open(path_, O_RDONLY);
2087
2091
  #else
2088
2092
  int descriptor = open(path_, O_RDONLY);
2089
2093
  #endif
@@ -3970,6 +3974,10 @@ class index_gt {
3970
3974
  : checked_size_overflow();
3971
3975
  if (!first_offset)
3972
3976
  return result.failed("Index is too large");
3977
+ if (file.size() < first_offset.value) {
3978
+ reset();
3979
+ return result.failed("File is corrupted and can't fit the node levels");
3980
+ }
3973
3981
  offsets[0u] = first_offset.value;
3974
3982
  for (std::size_t i = 1; i < header_size.value; ++i) {
3975
3983
  checked_size_result_t next_offset = checked_add(offsets[i - 1], node_bytes_(levels[i - 1]));
@@ -1050,17 +1050,18 @@ class index_dense_gt {
1050
1050
  */
1051
1051
  bool try_reserve(index_limits_t limits) {
1052
1052
 
1053
+ // Reserving is monotonic: reducing thread limits must not shrink member-addressed storage.
1054
+ limits.members = (std::max)(limits.members, typed_->capacity());
1055
+ limits.members = (std::max)(limits.members, vectors_lookup_.size());
1056
+
1053
1057
  // The slot lookup system will generally prefer power-of-two sizes.
1054
1058
  if (config_.enable_key_lookups) {
1055
1059
  unique_lock_t lock(slot_lookup_mutex_);
1056
1060
  if (!slot_lookup_.try_reserve(limits.members))
1057
1061
  return false;
1058
- limits.members = slot_lookup_.capacity();
1059
1062
  }
1060
1063
 
1061
- // Once the `slot_lookup_` grows, let's use its capacity as the new
1062
- // target for the `vectors_lookup_` to synchronize allocations and
1063
- // expensive index re-organizations.
1064
+ // Hash-table load-factor slack does not need corresponding vector or graph slots.
1064
1065
  if (limits.members != vectors_lookup_.size()) {
1065
1066
  vectors_lookup_t new_vectors_lookup(limits.members);
1066
1067
  if (!new_vectors_lookup)
@@ -1358,6 +1359,11 @@ class index_dense_gt {
1358
1359
  if (!result)
1359
1360
  return result;
1360
1361
 
1362
+ // `offset` is caller-supplied, so every `file.size() - offset` below would
1363
+ // otherwise underflow into a huge span instead of failing.
1364
+ if (offset > file.size())
1365
+ return result.failed("File is corrupted and lacks matrix dimensions");
1366
+
1361
1367
  // Infer the new index size
1362
1368
  std::uint64_t matrix_rows = 0;
1363
1369
  std::uint64_t matrix_cols = 0;
@@ -1383,7 +1389,12 @@ class index_dense_gt {
1383
1389
  matrix_cols = dimensions[1];
1384
1390
  offset += sizeof(dimensions);
1385
1391
  }
1386
- vectors_buffer = {file.data() + offset, static_cast<std::size_t>(matrix_rows * matrix_cols)};
1392
+ // bound the vectors matrix span against the file (overflow-safe; offset <= file.size() holds here)
1393
+ checked_size_result_t vectors_bytes =
1394
+ checked_mul(static_cast<std::size_t>(matrix_rows), static_cast<std::size_t>(matrix_cols));
1395
+ if (!vectors_bytes || file.size() - offset < vectors_bytes.value)
1396
+ return result.failed("File is corrupted: vectors matrix exceeds file size");
1397
+ vectors_buffer = {file.data() + offset, vectors_bytes.value};
1387
1398
  offset += vectors_buffer.size();
1388
1399
  }
1389
1400
 
@@ -3,18 +3,22 @@ title: "NumKong: 2000 Mixed Precision Kernels For All"
3
3
  message: "If you use this software, please cite it as below."
4
4
  type: software
5
5
  license: Apache-2.0
6
- version: 7.6.0
6
+ version: 7.8.1
7
7
  repository-code: https://github.com/ashvardanian/NumKong
8
8
  url: https://ashvardanian.com/posts/numkong/
9
+ doi: 10.5281/zenodo.21480519
10
+ identifiers:
11
+ - type: doi
12
+ value: 10.5281/zenodo.21839607
13
+ description: "v7.8.0 — version DOI"
9
14
  abstract: >-
10
- Portable mixed-precision math, linear-algebra, & retrieval library
11
- with 2'000+ SIMD kernels for x86, Arm, RISC-V, LoongArch, Power, & WebAssembly,
12
- leveraging rare algebraic transforms with both 1D & 2D registers like AMX & SME,
13
- covering 15+ numeric types from 4-bit integers & 6-bit floats to 128-bit complex numbers,
14
- validated against 118-bit extended-precision baselines
15
- with saturation, casting, & rounding edge-case coverage,
16
- in a 5-100x smaller binary than other BLAS-like alternatives,
17
- co-designed with Tensor abstractions in C++, Python, Rust, JavaScript, GoLang, & Swift.
15
+ Portable mixed-precision math, linear-algebra, & retrieval library with 2'000+ SIMD kernels for
16
+ x86, Arm, RISC-V, LoongArch, Power, & WebAssembly, leveraging rare algebraic transforms with
17
+ both 1D & 2D registers like AMX & SME, covering 15+ numeric types from 4-bit integers & 6-bit
18
+ floats to 128-bit complex numbers, validated against 118-bit extended-precision baselines with
19
+ saturation, casting, & rounding edge-case coverage, in a 5-100x smaller binary than other
20
+ BLAS-like alternatives, co-designed with Tensor abstractions in C++, Python, Rust, JavaScript,
21
+ GoLang, & Swift.
18
22
 
19
23
  authors:
20
24
  - family-names: Vardanian
@@ -32,21 +32,20 @@ build_release/nk_bench
32
32
  build_release/nk_test
33
33
  ```
34
34
 
35
- | CMake Flag | Default | Description |
36
- | -------------------------- | ------------------ | ------------------------------------------------------ |
37
- | `NK_BUILD_TEST` | `OFF` | Compile precision tests with ULP error analysis |
38
- | `NK_BUILD_BENCH` | `OFF` | Compile micro-benchmarks |
39
- | `NK_BUILD_SHARED` | `ON`, if top-level | Compile dynamic library |
40
- | `NK_BUILD_SHARED_TEST` | `OFF` | Compile tests against the shared library |
41
- | `NK_COMPARE_TO_BLAS` | `AUTO` | Include OpenBLAS into test/bench comparisons |
42
- | `NK_COMPARE_TO_ACCELERATE` | `AUTO` | Include Apple's Accelerate into test/bench comparisons |
43
- | `NK_COMPARE_TO_MKL` | `AUTO` | Include Intel' MKL into test/bench comparisons |
44
- | `NK_MARCH_NATIVE` | `OFF` | Tune for host CPU with `-march=native` |
35
+ | CMake Flag | Default | Description |
36
+ | ---------------------- | ------------------ | ----------------------------------------------------------------------------- |
37
+ | `NK_BUILD_TEST` | `OFF` | Compile precision tests with ULP error analysis |
38
+ | `NK_BUILD_BENCH` | `OFF` | Compile micro-benchmarks |
39
+ | `NK_BUILD_SHARED` | `ON`, if top-level | Compile dynamic library |
40
+ | `NK_BUILD_SHARED_TEST` | `OFF` | Compile tests against the shared library |
41
+ | `NK_COMPARE_TO_BLAS` | `AUTO` | Include OpenBLAS, or Apple's Accelerate on macOS, into test/bench comparisons |
42
+ | `NK_COMPARE_TO_MKL` | `AUTO` | Include Intel' MKL into test/bench comparisons |
43
+ | `NK_MARCH_NATIVE` | `OFF` | Tune for host CPU with `-march=native` |
45
44
 
46
45
  ### Target Baseline Policy
47
46
 
48
47
  `CMakeLists.txt`, `build.rs`, `setup.py`, and `binding.gyp` pin the TU-level baseline to each architecture's ABI floor so distributable artifacts run on any CPU matching the ABI, not just the build host.
49
- SIMD kernels live inside `#pragma GCC target(...)` regions and are only called after runtime probing — see the README's [Compile-Time and Run-Time Dispatch](../README.md#compile-time-and-run-time-dispatch) section.
48
+ SIMD kernels live inside `#pragma GCC target(...)` regions and are only called after runtime probing — see the README's [Compile-Time and Run-Time Dispatch](README.md#compile-time-and-run-time-dispatch) section.
50
49
 
51
50
  | Target arch | GCC/Clang baseline | MSVC baseline | Notes |
52
51
  | ------------- | --------------------------- | --------------- | ----------------------------------------------------------- |
@@ -89,19 +88,25 @@ sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 100
89
88
 
90
89
  ### Cross-Compilation
91
90
 
92
- NumKong ships 8 toolchain files in `cmake/` for cross-compiling to non-native targets.
91
+ NumKong ships 11 toolchain files in `cmake/`, each named `toolchain-<name>.cmake`.
93
92
  Tests and benchmarks run transparently under QEMU via `CMAKE_CROSSCOMPILING_EMULATOR`.
94
-
95
- | Target | Toolchain File | Emulator | Prerequisites |
96
- | ------------------------- | ------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------ |
97
- | ARM64 Linux | `toolchain-aarch64-gnu.cmake` | `qemu-aarch64 -cpu max` | `gcc-aarch64-linux-gnu`, `qemu-user` |
98
- | RISC-V 64 GCC | `toolchain-riscv64-gnu.cmake` | `qemu-riscv64 -cpu max` | [riscv-gnu-toolchain](https://github.com/riscv-collab/riscv-gnu-toolchain/releases), `qemu-user` |
99
- | RISC-V 64 LLVM | `toolchain-riscv64-llvm.cmake` | `qemu-riscv64 -cpu max` | LLVM 17+, `RISCV_SYSROOT` |
100
- | Android ARM64 | `toolchain-android-arm64.cmake` | — | `ANDROID_NDK_ROOT` |
101
- | x86_64 from Apple Silicon | `toolchain-x86_64-llvm.cmake` | `arch -x86_64` | Homebrew LLVM |
102
- | WASM Emscripten | `toolchain-wasm.cmake` | Node.js | Emscripten 3.1.27+ |
103
- | WASM64 Memory64 | `toolchain-wasm64.cmake` | Node.js | Emscripten 3.1.35+ |
104
- | WASI | `toolchain-wasi.cmake` | Wasmtime / Wasmer | WASI SDK 24+ |
93
+ Targets with a `qemu-*` emulator additionally require `qemu-user`.
94
+
95
+ | Target | Toolchain | Emulator | Prerequisites |
96
+ | ----------------------- | ----------------- | --------------------------- | ------------------------------ |
97
+ | ARM64 Linux | `aarch64-gnu` | `qemu-aarch64 -cpu max` | `gcc-aarch64-linux-gnu` |
98
+ | RISC-V 64 GCC | `riscv64-gnu` | `qemu-riscv64 -cpu max` | [`riscv-gnu-toolchain`][riscv] |
99
+ | RISC-V 64 LLVM | `riscv64-llvm` | `qemu-riscv64 -cpu max` | LLVM 17+, `RISCV_SYSROOT` |
100
+ | ppc64le Linux | `ppc64le-gnu` | `qemu-ppc64le -cpu power9` | `gcc-powerpc64le-linux-gnu` |
101
+ | LoongArch 64 | `loongarch64-gnu` | `qemu-loongarch64 -cpu max` | `LOONGARCH_TOOLCHAIN_PATH` |
102
+ | Android ARM64 | `android-arm64` | — | `ANDROID_NDK_ROOT` |
103
+ | Android ARMv7 | `android-armv7` | — | `ANDROID_NDK_ROOT` |
104
+ | x86_64 on Apple Silicon | `x86_64-llvm` | `arch -x86_64` | Homebrew LLVM |
105
+ | WASM Emscripten | `wasm` | Node.js | Emscripten 3.1.27+ |
106
+ | WASM64 Memory64 | `wasm64` | Node.js | Emscripten 3.1.35+ |
107
+ | WASI | `wasi` | Wasmtime / Wasmer | WASI SDK 24+ |
108
+
109
+ [riscv]: https://github.com/riscv-collab/riscv-gnu-toolchain/releases
105
110
 
106
111
  Set `NK_IN_QEMU=1` to relax half-precision accuracy thresholds under emulation.
107
112
 
@@ -236,7 +241,7 @@ See [test/README.md](test/README.md) for test framework details and [bench/READM
236
241
  Once done editing the code, please run analyzers and formatters:
237
242
 
238
243
  ```bash
239
- git ls-files '*.h' '*.c' '*.hpp' '*.cpp' | xargs clang-format -i # Use Clang Format 21 or newer
244
+ git ls-files '*.h' '*.c' '*.hpp' '*.cpp' | xargs clang-format -i # Use Clang Format 22 or newer
240
245
  ```
241
246
 
242
247
  ## Python
@@ -19,7 +19,7 @@ name = "numkong"
19
19
  readme = "rust/README.md"
20
20
  repository = "https://github.com/ashvardanian/NumKong"
21
21
  rust-version = "1.64" # Introduced Core C FFI in stable Rust
22
- version = "7.6.0"
22
+ version = "7.8.1"
23
23
 
24
24
  [lib]
25
25
  name = "numkong"
package/numkong/README.md CHANGED
@@ -68,12 +68,12 @@ A broader throughput comparison is maintained in [NumWars](https://github.com/as
68
68
  | Python | `pip install` | Linux, macOS, Windows | [python/README.md](python/README.md) |
69
69
  | Rust | `cargo add` | Linux, macOS, Windows | [rust/README.md](rust/README.md) |
70
70
  | JS | `npm install` & `import` | Node.js, Bun, Deno & browsers | [javascript/README.md](javascript/README.md) |
71
- | Swift | Swift Package Manager | macOS, iOS, tvOS, watchOS | [swift/README.md](swift/README.md) |
71
+ | Swift | Swift Package Manager | Apple platforms & Linux | [swift/README.md](swift/README.md) |
72
72
  | Go | `go get` | Linux, macOS, Windows via cGo | [golang/README.md](golang/README.md) |
73
73
 
74
74
  ## What's Inside
75
75
 
76
- NumKong covers 17 numeric types — from 6-bit floats to 64-bit complex numbers — across dozens of operations and 30+ SIMD backends, with hardware-aware defaults: Arm prioritizes `f16`, x86 prioritizes `bf16`.
76
+ NumKong covers 17 numeric types — from 6-bit floats to 128-bit complex numbers — across dozens of operations and 30+ SIMD backends, with hardware-aware defaults: Arm prioritizes `f16`, x86 prioritizes `bf16`.
77
77
 
78
78
  ### Language Bindings
79
79
 
@@ -215,7 +215,7 @@ The [GIL](https://docs.python.org/3/glossary.html#term-global-interpreter-lock)
215
215
  import concurrent.futures, numkong as nk, numpy as np
216
216
 
217
217
  vectors, num_threads = np.random.randn(1000, 768).astype(np.float32), 4
218
- output = nk.zeros((1000, 1000), dtype="float32")
218
+ output = nk.zeros((1000, 1000), dtype="float64")
219
219
 
220
220
  def compute_slice(t):
221
221
  start = t * (len(vectors) // num_threads)
@@ -320,7 +320,7 @@ Every such decision — saturation thresholds, Newton-Raphson iteration counts,
320
320
 
321
321
  ### Calling Convention & Error Handling
322
322
 
323
- NumKong never throws exceptions, never sets `errno`, and never calls `setjmp`/`longjmp` — [exceptions bloat call sites with unwind tables](https://monkeywritescode.blogspot.com/p/c-exceptions-under-hood.html) and are invisible to C, Python, Rust, Swift, Go, and JavaScript FFI; `errno` is thread-local state whose [storage model varies across C runtimes](https://en.cppreference.com/w/c/error/errno).
323
+ NumKong never throws exceptions, never sets `errno`, and never calls `setjmp`/`longjmp` on any kernel path — [exceptions bloat call sites with unwind tables](https://monkeywritescode.blogspot.com/p/c-exceptions-under-hood.html) and are invisible to C, Python, Rust, Swift, Go, and JavaScript FFI; `errno` is thread-local state whose [storage model varies across C runtimes](https://en.cppreference.com/w/c/error/errno).
324
324
  Instead, every function takes inputs as `const` pointers, writes outputs through caller-provided pointers, and returns `void`:
325
325
 
326
326
  ```c
@@ -352,7 +352,7 @@ nk_capability_t used = nk_cap_serial_k;
352
352
  nk_find_kernel_punned(
353
353
  nk_kernel_angular_k, nk_f32_k, // what functionality? for which input type?
354
354
  nk_capabilities(), // which capabilities are viable?
355
- (nk_kernel_punned_t *)&angular, &used); // the kernel found and capabilties used!
355
+ (nk_kernel_punned_t *)&angular, &used); // the kernel found and capabilities used!
356
356
  ```
357
357
 
358
358
  The first call to `nk_capabilities()` initializes the dispatch table; all subsequent calls are lock-free.
@@ -489,8 +489,8 @@ On Arm, NEON FHM extensions bring widening `FMLAL` dot-products for Float16 —
489
489
  > On NEON+FP8DOT, E3M2 is first promoted to E5M2 and E2M3 to E4M3 before the hardware `FDOT` instruction.
490
490
  > Sierra Forest and Alder Lake use native `VPDPBSSD` (signed×signed) and `VPDPBUSD` (unsigned×signed) respectively for E2M3.
491
491
 
492
- E4M3 and E5M2 cannot use the integer path.
493
- E4M3 scaled by 16 reaches 7,680 — too large for Int8, barely fitting Int16 with a 128-entry table.
492
+ E4M3 and E5M2 cannot use the same 32-entry LUT integer path.
493
+ E4M3 scaled by 16 reaches 7,168 — too large for Int8, barely fitting Int16 with a 128-entry table.
494
494
  E5M2's range (±57,344) makes the scaled product exceed Int32 entirely.
495
495
  Without the integer path, E5M2 falls back to Float32 accumulation — where its [2-bit mantissa (only 4 values per binade)](https://developer.nvidia.com/blog/floating-point-8-an-introduction-to-efficient-lower-precision-ai-training/) creates a [catastrophic cancellation risk](https://www.ac.uma.es/arith2024/papers/Fused%20FP8%204-Way%20Dot%20Product%20with%20Scaling%20and%20FP32%20Accumulation.pdf) that E2M3's integer path avoids completely:
496
496
 
@@ -564,6 +564,20 @@ Beyond the READMEs in this repository, there are several standalone articles cov
564
564
  - [Accelerating JavaScript arrays by 10x for Vector Search](https://ashvardanian.com/posts/javascript-ai-vector-search/)
565
565
  - [SciPy distances... up to 200x faster with AVX-512 & SVE](https://ashvardanian.com/posts/simsimd-faster-scipy/)
566
566
 
567
+ ## Citation
568
+
569
+ If NumKong helps your research or product, please cite it:
570
+
571
+ ```bibtex
572
+ @software{Vardanian_NumKong,
573
+ author = {Vardanian, Ash},
574
+ title = {{NumKong: 2000 Mixed Precision Kernels For All}},
575
+ doi = {10.5281/zenodo.21480519},
576
+ url = {https://github.com/ashvardanian/NumKong},
577
+ license = {Apache-2.0}
578
+ }
579
+ ```
580
+
567
581
  ## License
568
582
 
569
583
  Feel free to use the project under Apache 2.0 or the Three-clause BSD license at your preference.
@@ -1,6 +1,8 @@
1
1
  {
2
2
  "variables": {
3
- "openssl_fips": ""
3
+ "openssl_fips": "",
4
+ # `NK_MARCH_NATIVE=1` opts into a host-tuned, non-portable build, as in CMakeLists.txt.
5
+ "nk_march_native%": "<!(node -p \"['1','true','TRUE'].includes(process.env.NK_MARCH_NATIVE||'')?1:0\")"
4
6
  },
5
7
  "targets": [
6
8
  {
@@ -8,6 +10,7 @@
8
10
  "sources": [
9
11
  "javascript/numkong.c",
10
12
  "c/numkong.c",
13
+ "c/parallel.c",
11
14
  "c/dispatch_f64.c",
12
15
  "c/dispatch_f32.c",
13
16
  "c/dispatch_f16.c",
@@ -34,18 +37,17 @@
34
37
  "c/dispatch_e3m2.c",
35
38
  ],
36
39
  "include_dirs": [
37
- "include"
40
+ "include",
41
+ "c"
38
42
  ],
39
43
  "defines": [
40
44
  "NK_NATIVE_F16=0",
41
45
  "NK_NATIVE_BF16=0",
42
- "NK_DYNAMIC_DISPATCH=1",
43
- "NK_USE_OPENMP=1"
46
+ "NK_DYNAMIC_DISPATCH=1"
44
47
  ],
45
48
  "cflags": [
46
49
  "-std=c11",
47
50
  "-O3",
48
- "-fopenmp",
49
51
  "-Wno-unknown-pragmas",
50
52
  "-Wno-maybe-uninitialized",
51
53
  "-Wno-cast-function-type",
@@ -54,25 +56,35 @@
54
56
  "-include",
55
57
  "<(module_root_dir)/nk_probes.h",
56
58
  ],
57
- "ldflags": [
58
- "-fopenmp"
59
- ],
60
59
  "msvs_settings": {
61
60
  "VCCLCompilerTool": {
62
61
  "ForcedIncludeFiles": [
63
62
  "<(module_root_dir)/nk_probes.h"
64
63
  ],
65
64
  "AdditionalOptions": [
66
- "/Zc:preprocessor",
67
- "/openmp:llvm"
65
+ "/Zc:preprocessor"
68
66
  ],
69
67
  },
70
68
  },
71
69
  "conditions": [
70
+ # Only this branch gets OpenMP; macOS and Windows use their own pools.
71
+ [
72
+ "OS!='mac' and OS!='win'",
73
+ {
74
+ "cflags": [
75
+ "-fopenmp"
76
+ ],
77
+ "ldflags": [
78
+ "-fopenmp"
79
+ ]
80
+ }
81
+ ],
72
82
  # Pin TU baseline to each arch's ABI floor; SIMD kernels use per-function pragmas.
73
- # Keep per-arch table in sync with cmake/nk_compiler_flags.cmake, build.rs, setup.py.
83
+ # Keep per-arch table in sync with CMakeLists.txt, build.rs, setup.py.
84
+ # macOS is excluded: `-arch` already pins the slice, and a per-arch `-march=`
85
+ # conflicts with the other slice of a universal build.
74
86
  [
75
- "OS!='win' and target_arch=='arm64'",
87
+ "nk_march_native==0 and OS!='win' and OS!='mac' and target_arch=='arm64'",
76
88
  {
77
89
  "cflags": [
78
90
  "-march=armv8-a"
@@ -80,7 +92,7 @@
80
92
  }
81
93
  ],
82
94
  [
83
- "OS!='win' and target_arch=='x64'",
95
+ "nk_march_native==0 and OS!='win' and OS!='mac' and target_arch=='x64'",
84
96
  {
85
97
  "cflags": [
86
98
  "-march=x86-64"
@@ -88,7 +100,7 @@
88
100
  }
89
101
  ],
90
102
  [
91
- "OS!='win' and target_arch=='riscv64'",
103
+ "nk_march_native==0 and OS!='win' and OS!='mac' and target_arch=='riscv64'",
92
104
  {
93
105
  "cflags": [
94
106
  "-march=rv64gc"
@@ -96,7 +108,7 @@
96
108
  }
97
109
  ],
98
110
  [
99
- "OS!='win' and target_arch=='ppc64'",
111
+ "nk_march_native==0 and OS!='win' and OS!='mac' and target_arch=='ppc64'",
100
112
  {
101
113
  "cflags": [
102
114
  "-mcpu=power8"
@@ -104,7 +116,7 @@
104
116
  }
105
117
  ],
106
118
  [
107
- "OS!='win' and target_arch=='loong64'",
119
+ "nk_march_native==0 and OS!='win' and OS!='mac' and target_arch=='loong64'",
108
120
  {
109
121
  "cflags": [
110
122
  "-march=loongarch64",
@@ -112,6 +124,14 @@
112
124
  ]
113
125
  }
114
126
  ],
127
+ [
128
+ "nk_march_native==1 and OS!='win' and OS!='mac'",
129
+ {
130
+ "cflags": [
131
+ "-march=native"
132
+ ]
133
+ }
134
+ ],
115
135
  # Forbid auto-vectorization so serial fallbacks don't get silently
116
136
  # promoted to NEON/SSE2/VSX. SIMD kernels use explicit intrinsics
117
137
  # and per-function `target` pragmas; unaffected. MSVC has no
@@ -125,27 +145,23 @@
125
145
  ]
126
146
  }
127
147
  ],
148
+ # gyp ignores `cflags` on the mac flavor, so every flag above must be
149
+ # repeated here or the addon builds unoptimized and without the probes.
128
150
  [
129
151
  "OS=='mac'",
130
152
  {
131
153
  "xcode_settings": {
132
154
  "MACOSX_DEPLOYMENT_TARGET": "11.0",
133
- # Apple Clang ships no `omp.h`; the CI step
134
- # `brew install libomp` makes it keg-only under
135
- # `/opt/homebrew/opt/libomp` (arm64) or
136
- # `/usr/local/opt/libomp` (x86_64). Clang silently
137
- # ignores `-I` / `-L` dirs that don't exist, so
138
- # listing both keeps the file arch-agnostic.
139
155
  "OTHER_CFLAGS": [
140
- "-Xpreprocessor",
141
- "-fopenmp",
142
- "-I/opt/homebrew/opt/libomp/include",
143
- "-I/usr/local/opt/libomp/include"
144
- ],
145
- "OTHER_LDFLAGS": [
146
- "-lomp",
147
- "-L/opt/homebrew/opt/libomp/lib",
148
- "-L/usr/local/opt/libomp/lib"
156
+ "-std=c11",
157
+ "-O3",
158
+ "-fno-tree-vectorize",
159
+ "-fno-tree-slp-vectorize",
160
+ "-Wno-unknown-pragmas",
161
+ "-Wno-cast-function-type",
162
+ "-Wno-switch",
163
+ "-include",
164
+ "<(module_root_dir)/nk_probes.h"
149
165
  ]
150
166
  }
151
167
  }
package/numkong/build.rs CHANGED
@@ -376,21 +376,31 @@ fn build_numkong() -> Result<HashMap<String, bool>, String> {
376
376
 
377
377
  // Pin TU baseline to each arch's ABI floor; SIMD kernels carry per-function pragmas.
378
378
  // `NK_MARCH_NATIVE=1` opts into a host-tuned, non-portable build (ignored on MSVC).
379
- // Keep per-arch table in sync with cmake/nk_compiler_flags.cmake, setup.py, binding.gyp.
380
- let march_native = env::var("NK_MARCH_NATIVE").is_ok_and(|v| v == "1" || v == "true");
379
+ // Keep per-arch table in sync with CMakeLists.txt, setup.py, binding.gyp.
380
+ let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
381
+ let is_apple = target_os == "macos" || target_os == "ios";
382
+ let is_cross = env::var("HOST").unwrap_or_default() != env::var("TARGET").unwrap_or_default();
383
+ let march_native = env::var("NK_MARCH_NATIVE").is_ok_and(|v| v == "1" || v == "true" || v == "TRUE");
381
384
  // Portable baseline: pin TU ISA floor + forbid auto-vectorization so serial
382
385
  // fallbacks don't get silently promoted to NEON/SSE2/VSX. SIMD kernels use
383
386
  // explicit intrinsics; unaffected. MSVC has no command-line vectorizer
384
387
  // toggle; `NK_MARCH_NATIVE=1` opts out for host-tuned builds.
385
- if march_native && !is_msvc {
386
- println!("cargo:warning=NK_MARCH_NATIVE=1: building -march=native, result will not run on older CPUs");
387
- build.flag_if_supported("-march=native");
388
+ if march_native && !is_msvc && !is_cross {
389
+ println!("cargo:warning=NK_MARCH_NATIVE=1: building host-tuned, result will not run on older CPUs");
390
+ // Apple Clang's `-march=native` advertises only a subset of host features
391
+ // (no SME/SME2/FP16_FML); `-mcpu=native` is the complete knob on macOS.
392
+ build.flag_if_supported(if is_apple { "-mcpu=native" } else { "-march=native" });
388
393
  } else if is_msvc {
389
394
  match target_arch.as_str() {
390
395
  "x86_64" => { build.flag_if_supported("/arch:SSE2"); }
391
396
  "aarch64" => { build.flag_if_supported("/arch:armv8.0"); }
392
397
  _ => {}
393
398
  }
399
+ } else if is_apple {
400
+ // `-arch` already pins the ABI floor per slice, and a universal build passes both
401
+ // to one clang invocation, where a per-arch `-march=` conflicts with the other slice.
402
+ build.flag_if_supported("-fno-tree-vectorize");
403
+ build.flag_if_supported("-fno-tree-slp-vectorize");
394
404
  } else {
395
405
  build.flag_if_supported("-fno-tree-vectorize");
396
406
  build.flag_if_supported("-fno-tree-slp-vectorize");
@@ -33,7 +33,6 @@
33
33
  extern "C" {
34
34
  #endif
35
35
 
36
- // Forward declaration of dispatch table type (same structure as in numkong.c)
37
36
  typedef struct {
38
37
  // Dot products
39
38
  nk_metric_dense_punned_t dot_f64c;