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
@@ -31,36 +31,39 @@ def minmax(a: np.ndarray) -> tuple[float, int, float, int]:
31
31
 
32
32
  ## Input & Output Types
33
33
 
34
+ The Output Type column lists the accumulator type of `nk_reduce_moments_*`.
35
+ `nk_reduce_minmax_*` instead reports extrema in the input type — `i4`, `u4`, and `u1` unpack to `i8`, `u8`, and `u8` — alongside two `nk_size_t` indices.
36
+
34
37
  Float reductions:
35
38
 
36
- | Input Type | Output Type | Description |
37
- | ---------- | ----------- | ------------------------------------- |
38
- | `f64` | `f64` | 64-bit double precision |
39
- | `f32` | `f32` | 32-bit single precision |
40
- | `f16` | `f32` | 16-bit half precision, widened output |
41
- | `bf16` | `f32` | 16-bit brain float, widened output |
39
+ | Input Type | Output Type | Description |
40
+ | ---------- | ----------- | --------------------------------------- |
41
+ | `f64` | `f64` | 64-bit double precision |
42
+ | `f32` | `f64` | 32-bit single precision, widened output |
43
+ | `bf16` | `f32` | 16-bit brain float, widened output |
44
+ | `f16` | `f32` | 16-bit half precision, widened output |
42
45
 
43
46
  Mini-float reductions:
44
47
 
45
48
  | Input Type | Output Type | Description |
46
49
  | ---------- | ----------- | -------------------------------------------- |
47
- | `e4m3` | `f32` | 8-bit Float8: 4 exponent, 3 mantissa bits |
48
50
  | `e5m2` | `f32` | 8-bit Float8: 5 exponent, 2 mantissa bits |
49
- | `e2m3` | `f32` | 8-bit MX format: 2 exponent, 3 mantissa bits |
51
+ | `e4m3` | `f32` | 8-bit Float8: 4 exponent, 3 mantissa bits |
50
52
  | `e3m2` | `f32` | 8-bit MX format: 3 exponent, 2 mantissa bits |
53
+ | `e2m3` | `f32` | 8-bit MX format: 2 exponent, 3 mantissa bits |
51
54
 
52
55
  Integer reductions:
53
56
 
54
57
  | Input Type | Output Type | Description |
55
58
  | ---------- | ----------- | ---------------------------------- |
56
- | `i8` | `i64` | 8-bit signed, widened to 64-bit |
57
- | `u8` | `u64` | 8-bit unsigned, widened to 64-bit |
58
- | `i16` | `i64` | 16-bit signed, widened to 64-bit |
59
- | `u16` | `u64` | 16-bit unsigned, widened to 64-bit |
60
- | `i32` | `i64` | 32-bit signed, widened to 64-bit |
61
- | `u32` | `u64` | 32-bit unsigned, widened to 64-bit |
62
59
  | `i64` | `i64` | 64-bit signed |
60
+ | `i32` | `i64` | 32-bit signed, widened to 64-bit |
61
+ | `i16` | `i64` | 16-bit signed, widened to 64-bit |
62
+ | `i8` | `i64` | 8-bit signed, widened to 64-bit |
63
63
  | `u64` | `u64` | 64-bit unsigned |
64
+ | `u32` | `u64` | 32-bit unsigned, widened to 64-bit |
65
+ | `u16` | `u64` | 16-bit unsigned, widened to 64-bit |
66
+ | `u8` | `u64` | 8-bit unsigned, widened to 64-bit |
64
67
 
65
68
  Sub-byte reductions:
66
69
 
@@ -82,29 +85,32 @@ RVV uses native strided loads (`__riscv_vlse32_v_f32m1`) that accept arbitrary b
82
85
 
83
86
  ### Kahan-Neumaier Compensated Summation
84
87
 
85
- `nk_reduce_moments_f32_serial`, `nk_reduce_moments_f32_haswell` use Neumaier's variant of Kahan summation — maintaining a running compensation term that captures rounding errors.
88
+ `nk_reduce_moments_f32_serial`, `nk_reduce_moments_f64_serial` use Neumaier's variant of Kahan summation — maintaining a running compensation term that captures rounding errors.
86
89
  Standard pairwise summation accumulates $O(\sqrt{n})$ ULP error for n elements; Neumaier compensation bounds error to $O(1)$ ULP regardless of vector length.
87
90
  The serial path uses Neumaier's adaptive branch: `if (abs(sum) >= abs(val))` selects the larger summand first, minimizing relative error in the compensation term.
88
- SIMD backends (`nk_reduce_moments_f32_haswell`) carry 8 independent compensation lanes in a YMM register — computing `round_error = tentative - sum; correction = (sum - (tentative - round_error)) + (val - round_error)` without branches, folding all lanes into a single scalar correction at the end.
91
+ SIMD backends (`nk_reduce_moments_f64_haswell`, `nk_reduce_moments_f64_skylake`) carry 4 or 8 independent compensation lanes in a YMM or ZMM register — computing `round_error = tentative - sum; correction = (sum - (tentative - round_error)) + (val - round_error)` without branches, folding all lanes into a single scalar correction at the end.
92
+ The Float32 SIMD kernels skip compensation: they widen every input to Float64 first, and the wider accumulator already absorbs the rounding error.
89
93
 
90
94
  ### Fused Moments in a Single Pass
91
95
 
92
- `nk_reduce_moments_f32_haswell`, `nk_reduce_moments_f64_skylake` compute sum and sum-of-squares simultaneously — one load feeds both a `VADDPS` (sum accumulator) and a `VFMADD231PS` (square accumulator).
96
+ `nk_reduce_moments_f32_haswell`, `nk_reduce_moments_f64_skylake` compute sum and sum-of-squares simultaneously — one load feeds both a sum accumulator and a square accumulator.
97
+ On Haswell the Float32 inputs are widened with `VCVTPS2PD`, then folded in with `VADDPD` and `VFMADD231PD`; on Skylake the Float64 path squares with `VMULPD` so the product can go through the compensated add.
93
98
  Two accumulators share the same loaded data, halving memory bandwidth compared to separate sum + norm passes.
94
99
  The squared-norm $\|a\|^2 = \sum a_i^2$ is a self-dot-product, reused by packing routines (`nk_dots_pack_f32_haswell`) to precompute per-vector norms during layout transformation.
95
- For Float16/BFloat16/Float8 inputs, all backends widen to Float32 before accumulation — NEON FHM (`nk_reduce_moments_e4m3_neonfhm`) converts e4m3->f16 via lookup, then uses `vfmlalq_low_f16` to fuse the Float16 → Float32 widening with the FMA into the Float32 accumulator.
100
+ For Float16/BFloat16/Float8 inputs, all backends widen to Float32 before accumulation — NEON FHM (`nk_reduce_moments_e4m3_neonfhm`) converts e4m3->f16 by bit manipulation, then uses `vfmlalq_low_f16` and `vfmlalq_high_f16` to fuse the Float16 → Float32 widening with the FMA into the Float32 accumulator.
96
101
 
97
102
  ### Integer Saturation in Sum-of-Squares
98
103
 
99
104
  Integer moments accumulate sums in the widest available type: Int8/UInt8/Int16 inputs produce Int64/UInt64 outputs.
100
105
  Sums use widening addition chains — NEON uses pairwise widening (`vpaddlq_s16` -> UInt32 -> UInt64 stages); Haswell biases Int8 inputs with 0x80 and uses unsigned SAD (`_mm256_sad_epu8`) for the sum, correcting by subtracting $128 \times \text{count}$ at the end.
101
- Sum-of-squares can overflow UInt64 when squaring large Int32 values — backends use explicit saturating multiply: checks if `abs(val) < 2^32` (square fits in UInt64), otherwise saturates to I64_MAX.
106
+ Sum-of-squares can overflow UInt64 when squaring Int64/UInt64 inputs — only `nk_reduce_moments_i64_*` and `nk_reduce_moments_u64_*` need an explicit saturating multiply: it checks whether `abs(val) < 2^32` (so the square fits in UInt64) and otherwise saturates to I64_MAX for signed inputs, U64_MAX for unsigned ones.
107
+ Narrower inputs cannot overflow the product itself, so they only need the saturating accumulate.
102
108
  Haswell emulates UInt64 saturating add via XOR-based unsigned comparison: flip sign bits to convert unsigned overflow detection into a signed comparison, then OR with the overflow mask to produce all-ones on saturation.
103
109
 
104
110
  ### Recursive Blocking for Counter Overflow
105
111
 
106
112
  All SIMD backends use loop iteration counters narrower than `nk_size_t` to save register pressure — UInt8 for Int8 minmax lanes, UInt16 for Float32 moments lanes.
107
- When `count` exceeds the counter's range x lane count (e.g., Haswell Float32: $256 \times 8 = 2048$ elements for UInt8 counters), the reduction splits recursively: process the left half, process the right half, combine results with saturating arithmetic.
113
+ When `count` exceeds the counter's range x lane count (e.g., Haswell Float32 moments: $65536 \times 8 = 524288$ elements for UInt16 counters), the reduction splits recursively: process the left half, process the right half, combine results with saturating arithmetic.
108
114
  Block caps vary by backend and element width: Haswell Int8 minmax uses UInt8 loop counters (cap = $256 \times 32 = 8192$); Skylake Float32 moments uses UInt16 counters (cap = $65536 \times 16 = 1048576$).
109
115
  The recursive split is invisible to the caller — the public API accepts arbitrary `count` values; internal dispatch chooses between single-pass and recursive based on the cap.
110
116
 
@@ -118,7 +124,7 @@ NEON uses same-width counters (u8x16 for i8x16 minmax), limiting block size to $
118
124
  ### NaN-Aware Extrema Tracking
119
125
 
120
126
  `nk_reduce_minmax_f32_haswell`, `nk_reduce_minmax_f64_skylake` use IEEE ordered-quiet comparisons (`_CMP_LT_OQ`, `_CMP_GT_OQ`) — returning false when either operand is NaN, so NaN inputs never replace the running extremum.
121
- Tail elements beyond the vector-aligned portion are masked by loading into a NaN-filled register via `_mm256_mask_loadu_ps(nan_vec, mask, ptr)` — NaN tails cannot win any comparison, eliminating out-of-bounds artifacts.
127
+ Tail elements beyond the vector-aligned portion are blended into a quiet-NaN register — `nk_reduce_minmax_f64_haswell` partial-loads the tail and `_mm256_blendv_pd`-s it over a NaN vector, so the padding lanes cannot win any comparison; Skylake instead predicates the tail comparison with the same `__mmask` used for the load, and `nk_reduce_minmax_f32_haswell` finishes its tail in scalar code.
122
128
  If all inputs are NaN, the sentinels remain (min = F32_MAX, max = F32_MIN) and indices are set to NK_SIZE_MAX, signaling no valid extremum.
123
129
  The final horizontal reduction across lanes uses pairwise `VSHUFPS` + `VMINPS` chains — 3 shuffles for a 256-bit register, $O(\log_2 w)$ for width $w$.
124
130
 
@@ -22,7 +22,8 @@
22
22
  #if NK_TARGET_GENOA
23
23
 
24
24
  #include "numkong/reduce/serial.h"
25
- #include "numkong/cast/icelake.h" // `nk_e4m3x32_to_bf16x32_icelake_` etc.
25
+ #include "numkong/cast/icelake.h" // `nk_e4m3x32_to_bf16x32_icelake_` etc.
26
+ #include "numkong/reduce/skylake.h" // `nk_reduce_add_f32x16_skylake_`
26
27
 
27
28
  #if defined(__cplusplus)
28
29
  extern "C" {
@@ -19,6 +19,7 @@
19
19
  #if NK_TARGET_ICELAKE
20
20
 
21
21
  #include "numkong/reduce/serial.h"
22
+ #include "numkong/reduce/skylake.h" // `nk_reduce_add_i64x8_skylake_`, `nk_reduce_add_u64x8_skylake_`, `nk_stride_mask_b16x32_`
22
23
 
23
24
  #if defined(__cplusplus)
24
25
  extern "C" {
@@ -2218,7 +2218,7 @@ NK_INTERNAL void nk_reduce_moments_i64_neon_contiguous_( //
2218
2218
  nk_i64_t *sum_ptr, nk_u64_t *sumsq_ptr) {
2219
2219
  uint64x2_t sum_low_u64x2 = vdupq_n_u64(0);
2220
2220
  int64x2_t sum_high_i64x2 = vdupq_n_s64(0);
2221
- // NEON can still load/extract i64 vectors for sumsq via scalar nk_i64_smul_
2221
+ // NEON can still load/extract i64 vectors for sumsq via nk_i64_smul_sq_i64x2_neon_
2222
2222
  uint64x2_t sumsq_u64x2 = vdupq_n_u64(0);
2223
2223
  int sumsq_overflow = 0;
2224
2224
  int64x2_t sign_bit_i64x2 = vdupq_n_s64((nk_i64_t)0x8000000000000000ULL);
@@ -21,15 +21,6 @@
21
21
  extern "C" {
22
22
  #endif
23
23
 
24
- /* Keep the serial instantiations below actually scalar, regardless of build type.
25
- * See dots/serial.h for rationale. */
26
- #if defined(__clang__)
27
- #pragma clang attribute push(__attribute__((noinline)), apply_to = function)
28
- #elif defined(__GNUC__)
29
- #pragma GCC push_options
30
- #pragma GCC optimize("no-tree-vectorize", "no-tree-slp-vectorize", "no-ipa-cp-clone", "no-inline")
31
- #endif
32
-
33
24
  NK_INTERNAL nk_f64_t nk_reduce_sum_f64_serial_(nk_f64_t const *values, nk_f64_t const *compensations, int count) {
34
25
  nk_f64_t running_sum = 0, accumulated_error = 0;
35
26
  for (int i = 0; i < count; i++) {
@@ -47,6 +38,15 @@ NK_INTERNAL nk_f64_t nk_reduce_sum_f64_serial_(nk_f64_t const *values, nk_f64_t
47
38
  return running_sum + accumulated_error;
48
39
  }
49
40
 
41
+ /* Keep the serial instantiations below actually scalar, regardless of build type.
42
+ * See dots/serial.h for rationale. */
43
+ #if defined(__clang__)
44
+ #pragma clang attribute push(__attribute__((noinline)), apply_to = function)
45
+ #elif defined(__GNUC__)
46
+ #pragma GCC push_options
47
+ #pragma GCC optimize("no-tree-vectorize", "no-tree-slp-vectorize", "no-ipa-cp-clone", "no-inline")
48
+ #endif
49
+
50
50
  NK_PUBLIC void nk_reduce_moments_f32_serial( //
51
51
  nk_f32_t const *data, nk_size_t count, nk_size_t stride_bytes, //
52
52
  nk_f64_t *sum_ptr, nk_f64_t *sumsq_ptr) {
@@ -953,7 +953,7 @@ NK_PUBLIC void nk_reduce_moments_f64_skylake( //
953
953
  NK_INTERNAL void nk_reduce_moments_i8_skylake_contiguous_( //
954
954
  nk_i8_t const *data_ptr, nk_size_t count, //
955
955
  nk_i64_t *sum_ptr, nk_u64_t *sumsq_ptr) {
956
- // Sum: VPSADBW with XOR bias (same as nk_reduce_add_i8_skylake_contiguous_).
956
+ // Sum: VPSADBW with XOR bias.
957
957
  // Sumsq: widen i8→i16, VPMADDWD(x,x) → i32 (pairs of squares), accumulate i32.
958
958
  // i32 overflow safe: max per lane = (128² + 128²) * 65536 iters ≈ 2.1B = safe limit.
959
959
  // The dispatch recurses at (NK_U16_MAX+1)*64 elements → at most 65536 iterations here.
@@ -1147,7 +1147,7 @@ NK_PUBLIC void nk_reduce_minmax_i8_skylake( //
1147
1147
  NK_INTERNAL void nk_reduce_moments_u8_skylake_contiguous_( //
1148
1148
  nk_u8_t const *data_ptr, nk_size_t count, //
1149
1149
  nk_u64_t *sum_ptr, nk_u64_t *sumsq_ptr) {
1150
- // Sum: VPSADBW directly (same as nk_reduce_add_u8_skylake_contiguous_).
1150
+ // Sum: VPSADBW directly.
1151
1151
  // Sumsq: widen u8→i16, VPMADDWD(x,x) → i32 (pairs of squares), accumulate i32.
1152
1152
  // i32 overflow safe: max per lane = (255² + 255²) * 1024 iters ≈ 133M < 2.1B.
1153
1153
  __m512i zero_u8x64 = _mm512_setzero_si512();
@@ -1697,7 +1697,7 @@ NK_PUBLIC void nk_reduce_minmax_u16_skylake( //
1697
1697
  max_index_ptr);
1698
1698
  }
1699
1699
 
1700
- /** @brief Unsigned saturating add of two i64x8 vectors (3 uops). */
1700
+ /** @brief Unsigned saturating add of two u64x8 vectors (3 uops). */
1701
1701
  NK_INTERNAL __m512i nk_u64_sadd_epi64_skylake_(__m512i a, __m512i b) {
1702
1702
  __m512i result_u64x8 = _mm512_add_epi64(a, b);
1703
1703
  __mmask8 ovf = _mm512_cmp_epu64_mask(result_u64x8, a, _MM_CMPINT_LT);
@@ -3371,7 +3371,7 @@ NK_PUBLIC void nk_reduce_moments_u4_skylake( //
3371
3371
  NK_INTERNAL void nk_reduce_moments_u1_skylake_contiguous_( //
3372
3372
  nk_u1x8_t const *data_ptr, nk_size_t count, //
3373
3373
  nk_u64_t *sum_ptr, nk_u64_t *sumsq_ptr) {
3374
- // Sum = popcount via 4-bit LUT (same as nk_reduce_add_u1_skylake). Sumsq = sum for bits.
3374
+ // Sum = popcount via 4-bit LUT. Sumsq = sum for bits.
3375
3375
  __m512i lut_i8x64 = _mm512_set_epi8( //
3376
3376
  4, 3, 3, 2, 3, 2, 2, 1, 3, 2, 2, 1, 2, 1, 1, 0, //
3377
3377
  4, 3, 3, 2, 3, 2, 2, 1, 3, 2, 2, 1, 2, 1, 1, 0, //
@@ -629,6 +629,44 @@ std::size_t argmax(vector_view<value_type_> input) noexcept {
629
629
  return minmax(input).max_index;
630
630
  }
631
631
 
632
+ /** @brief Population count of a packed bit tensor — number of set bits.
633
+ * Wraps `nk_reduce_moments_u1` via `sum`; returns 0 for empty inputs. */
634
+ template <std::size_t max_rank_ = 8>
635
+ u64_t popcount(tensor_view<u1x8_t, max_rank_> input) noexcept {
636
+ return sum<u1x8_t, max_rank_>(input);
637
+ }
638
+
639
+ /** @brief True if any bit of the packed bit tensor is set. Suffixed `_set` to
640
+ * avoid colliding with the `all_t`/`all` slice marker in `vector.hpp`. */
641
+ template <std::size_t max_rank_ = 8>
642
+ bool any_set(tensor_view<u1x8_t, max_rank_> input) noexcept {
643
+ return popcount<max_rank_>(input).raw_ != 0;
644
+ }
645
+
646
+ /** @brief True if no bit of the packed bit tensor is set. */
647
+ template <std::size_t max_rank_ = 8>
648
+ bool none_set(tensor_view<u1x8_t, max_rank_> input) noexcept {
649
+ return !any_set<max_rank_>(input);
650
+ }
651
+
652
+ /** @brief True if every bit of the packed bit tensor is set. */
653
+ template <std::size_t max_rank_ = 8>
654
+ bool all_set(tensor_view<u1x8_t, max_rank_> input) noexcept {
655
+ return popcount<max_rank_>(input).raw_ == input.numel();
656
+ }
657
+
658
+ /** @brief Population count over a 1D bit-vector view. */
659
+ inline u64_t popcount(vector_view<u1x8_t> input) noexcept { return sum<u1x8_t>(input); }
660
+
661
+ /** @brief True if any bit of the 1D bit-vector view is set. */
662
+ inline bool any_set(vector_view<u1x8_t> input) noexcept { return popcount(input).raw_ != 0; }
663
+
664
+ /** @brief True if no bit of the 1D bit-vector view is set. */
665
+ inline bool none_set(vector_view<u1x8_t> input) noexcept { return !any_set(input); }
666
+
667
+ /** @brief True if every bit of the 1D bit-vector view is set. */
668
+ inline bool all_set(vector_view<u1x8_t> input) noexcept { return popcount(input).raw_ == input.size(); }
669
+
632
670
  #pragma endregion Scalar Reductions
633
671
 
634
672
  #pragma region Axis Reductions
@@ -2,7 +2,7 @@
2
2
 
3
3
  NumKong provides single-element math operations — square root, reciprocal square root, fused multiply-add, and saturating integer arithmetic — with per-ISA implementations.
4
4
  These primitives serve as building blocks for vectorized kernels: distance finalizers call `nk_f32_rsqrt` for angular normalization, packing routines call `nk_f32_sqrt` for norm computation.
5
- Ordering functions (`nk_f16_order`, `nk_bf16_order`, `nk_e4m3_order`) convert floating-point bit patterns to integers that sort in the same order as the original floats.
5
+ Ordering functions (`nk_f16_order`, `nk_bf16_order`, `nk_e4m3_order`) compare two values through their bit patterns, returning a negative, zero, or positive `int` like `strcmp`.
6
6
 
7
7
  Reciprocal square root:
8
8
 
@@ -40,27 +40,27 @@ def saturating_add(a: int, b: int, bits: int, signed: bool) -> int:
40
40
 
41
41
  ## Input & Output Types
42
42
 
43
- | Input Type | Output Type | Description |
44
- | ---------- | ----------- | ----------------------------------------------- |
45
- | `f64` | `f64` | sqrt, rsqrt, fma for 64-bit doubles |
46
- | `f32` | `f32` | sqrt, rsqrt, fma for 32-bit floats |
47
- | `f16` | `f16` | sqrt, rsqrt, fma for 16-bit halfs |
48
- | `i8` | `i8` | Saturating add and multiply |
49
- | `u8` | `u8` | Saturating add and multiply |
50
- | `i16` | `i16` | Saturating add and multiply |
51
- | `u16` | `u16` | Saturating add and multiply |
52
- | `i32` | `i32` | Saturating add and multiply |
53
- | `u32` | `u32` | Saturating add and multiply |
54
- | `i64` | `i64` | Saturating add and multiply |
55
- | `u64` | `u64` | Saturating add and multiply |
56
- | `i4x2` | `i4x2` | Saturating add for packed signed nibble pairs |
57
- | `u4x2` | `u4x2` | Saturating add for packed unsigned nibble pairs |
58
- | `f16` | `u16` | Ordering: bit pattern to sortable integer |
59
- | `bf16` | `u16` | Ordering: bit pattern to sortable integer |
60
- | `e4m3` | `u8` | Ordering: bit pattern to sortable integer |
61
- | `e5m2` | `u8` | Ordering: bit pattern to sortable integer |
62
- | `e2m3` | `u8` | Ordering: bit pattern to sortable integer |
63
- | `e3m2` | `u8` | Ordering: bit pattern to sortable integer |
43
+ | Input Type | Output Type | Description |
44
+ | ---------- | ----------- | ------------------------------------------------------------ |
45
+ | `f64` | `f64` | sqrt, rsqrt, fma for 64-bit doubles |
46
+ | `f32` | `f32` | sqrt, rsqrt, fma for 32-bit floats |
47
+ | `f16` | `f16` | sqrt, rsqrt, fma for 16-bit halfs |
48
+ | `i8` | `i8` | Saturating add and multiply |
49
+ | `u8` | `u8` | Saturating add and multiply |
50
+ | `i16` | `i16` | Saturating add and multiply |
51
+ | `u16` | `u16` | Saturating add and multiply |
52
+ | `i32` | `i32` | Saturating add and multiply |
53
+ | `u32` | `u32` | Saturating add and multiply |
54
+ | `i64` | `i64` | Saturating add and multiply |
55
+ | `u64` | `u64` | Saturating add and multiply |
56
+ | `i4x2` | `i4x2` | Saturating add and multiply for packed signed nibble pairs |
57
+ | `u4x2` | `u4x2` | Saturating add and multiply for packed unsigned nibble pairs |
58
+ | `f16` | `int` | Ordering: three-way comparison of two values |
59
+ | `bf16` | `int` | Ordering: three-way comparison of two values |
60
+ | `e4m3` | `int` | Ordering: three-way comparison of two values |
61
+ | `e5m2` | `int` | Ordering: three-way comparison of two values |
62
+ | `e2m3` | `int` | Ordering: three-way comparison of two values |
63
+ | `e3m2` | `int` | Ordering: three-way comparison of two values |
64
64
 
65
65
  ## Optimizations
66
66
 
@@ -68,20 +68,21 @@ def saturating_add(a: int, b: int, bits: int, signed: bool) -> int:
68
68
 
69
69
  `nk_f32_rsqrt_serial` uses the classic bit-manipulation trick: reinterpret Float32 bits as Int32, compute `0x5F375A86 - (bits >> 1)`, reinterpret back to Float32, then refine with 3 Newton-Raphson iterations reaching ~34.9 correct bits.
70
70
  Each Newton-Raphson iteration: `y = y * (1.5f - 0.5f * x * y * y)` — 2 multiplies and 1 subtract, ~4cy per iteration.
71
- `nk_f32_rsqrt_haswell` replaces this with hardware `VRSQRT14PS` ($2^{-14}$ relative error, ~4cy latency) plus one Newton-Raphson refinement (~22-24 correct bits).
71
+ `nk_f32_rsqrt_haswell` replaces this with hardware `RSQRTSS` ($2^{-14}$ relative error, ~4cy latency) plus one Newton-Raphson refinement (~22-24 correct bits).
72
72
  `nk_f64_rsqrt_serial` uses the Float64 magic constant `0x5FE6EB50C7B537A9` with 4 iterations for 52-bit mantissa coverage.
73
73
 
74
74
  ### Dekker Error-Free Multiplication for FMA
75
75
 
76
- `nk_f32_fma_serial` emulates fused multiply-add on platforms without hardware FMA using Dekker's algorithm: splits each operand into high and low halves via `a_hi = (a * 134217729.0f) - ((a * 134217729.0f) - a)`, then computes the exact product error term.
77
- The magic constant $134217729 = 2^{27} + 1$ splits a 24-bit mantissa into two 12-bit halves that multiply without rounding.
76
+ `nk_f32_fma_serial` emulates fused multiply-add on platforms without hardware FMA using Dekker's algorithm: splits each operand into high and low halves via `a_high = (a * 4097.0f) - ((a * 4097.0f) - a)`, then computes the exact product error term.
77
+ The magic constant $4097 = 2^{12} + 1$ splits a 24-bit mantissa into two 12-bit halves that multiply without rounding; `nk_f64_fma_serial` uses $134217729 = 2^{27} + 1$ for the 53-bit mantissa.
78
78
  `nk_f32_fma_haswell` uses hardware `VFMADD231SS` — single instruction, single cycle, exact to the last bit.
79
79
 
80
80
  ### Float-to-Integer Ordering
81
81
 
82
- `nk_f16_order_serial`, `nk_bf16_order_serial`, `nk_e4m3_order_serial` convert floating-point bit patterns to unsigned integers that preserve the total order.
83
- Positive floats are already ordered by their bit patterns; negative floats need bit inversion: `if (bits & sign_bit) bits = ~bits; else bits ^= sign_bit`.
84
- This enables integer comparison instructions (`VPCMPUD`) for floating-point sorting without branching — used by `nk_reduce_minmax_*` for Float8 and sub-32-bit types that lack native SIMD comparison.
82
+ `nk_f16_order_serial`, `nk_bf16_order_serial`, `nk_e4m3_order_serial` map floating-point bit patterns onto signed integers that preserve the total order, then return the difference of the two keys.
83
+ Positive floats are already ordered by their bit patterns; negative floats need bit inversion, applied branchlessly as `bits ^ -sign`.
84
+ This keeps floating-point ordering on the integer pipeline — used by `nk_reduce_minmax_*` for Float8 and sub-32-bit types that lack native SIMD comparison.
85
+ `nk_f16_order_sapphire` instead uses the native `VCOMISH` half-precision comparisons.
85
86
 
86
87
  ## Performance
87
88
 
@@ -53,36 +53,39 @@ def jaccard_words(a: np.ndarray, b: np.ndarray) -> float:
53
53
 
54
54
  ## Optimizations
55
55
 
56
- ### Harley-Seal Carry-Save Adders for U1
56
+ ### Popcount Without Harley-Seal Carry-Save Adders
57
57
 
58
- `nk_hamming_u1_haswell`, `nk_jaccard_u1_haswell` amortize the cost of popcount by using Harley-Seal carry-save adder trees.
59
- Instead of computing popcount on every XOR/AND/OR result independently, three intermediate values are combined through a full-adder circuit:
58
+ The textbook way to amortize population counts is a Harley-Seal carry-save adder tree, which folds three inputs through a full-adder circuit before any popcount is issued:
60
59
 
61
60
  ```
62
61
  ones = a ^ b ^ c
63
62
  twos = (a & b) | (c & (a ^ b))
64
63
  ```
65
64
 
66
- This circuit takes three popcount inputs and produces a ones and twos accumulator, where `twos` has double the weight of `ones`.
67
- By chaining two levels, a fours accumulator is also produced, so the actual `VPSHUFB`-based popcount is called only on the final accumulated ones, twos, and fours values.
68
- The total number of popcount operations is reduced by roughly a factor of three compared to computing popcount on every vector independently.
65
+ Chaining levels yields `fours` and `eights` accumulators, each weighted by a power of two, so popcount runs on the accumulators instead of on every input vector — roughly a third of the calls.
66
+ NumKong does not do this.
67
+ The cycles-per-byte measurements in `set.h` put the crossover above 1 KB per vector: below it, loop and reduction overhead outweighs the saved popcounts, and the largest binary input benchmarked here is 4096 bits, or 512 bytes.
68
+ Ice Lake and later expose `VPOPCNTQ` outright, and on Genoa it dual-issues on ports 0-1, so the popcount pressure that CSA trees relieve is largely absent on the hardware that could run them.
69
+
70
+ AVX2 has no vector population count at all, so `nk_hamming_u1_haswell` and `nk_jaccard_u1_haswell` walk the packed octets 8 bytes at a time and call the scalar `POPCNT` through `_mm_popcnt_u64`.
71
+ `nk_hamming_u1_neon` and `nk_jaccard_u1_neon` use the NEON `CNT` instruction instead, accumulating per-byte counts in a `uint8x16_t` for at most 31 iterations before widening, since 31 x 8 still fits in a byte.
69
72
 
70
73
  ### Native VPOPCNTQ on Ice Lake
71
74
 
72
75
  `nk_hamming_u1_icelake`, `nk_jaccard_u1_icelake` use `VPOPCNTQ` on 512-bit vectors, which directly produces per-quadword population counts for 8 quadwords at once.
73
- This single instruction replaces the entire nibble-LUT + Harley-Seal pipeline used on Haswell.
74
- The kernels batch 16 vectors before horizontal reduction to minimize `VPSADBW` overhead, accumulating the per-quadword counts into a running total via `VPADDQ`.
76
+ Inputs of up to 256 bytes are fully unrolled into one, two, three, or four masked register pairs with no loop at all, since binary metrics are hard to speed up at tiny lengths.
77
+ Longer inputs enter a loop that accumulates the per-quadword counts via `VPADDQ` and reduces once at the end.
75
78
 
76
79
  ### Jaccard via Precomputed Norms
77
80
 
78
- `nk_jaccard_u1_haswell`, `nk_jaccard_u1_icelake` exploit the identity $|A \cup B| = |A| + |B| - |A \cap B|$ to avoid computing both AND-popcount and OR-popcount in the inner loop.
79
- When vector norms (popcount of each vector) are precomputed and passed via the streaming API, only the intersection popcount is needed per pair, halving the work in the critical path.
81
+ The streaming helpers `nk_jaccard_u1x64_finalize_haswell` and `nk_jaccard_u1x512_finalize_icelake` exploit the identity $|A \cup B| = |A| + |B| - |A \cap B|$ to avoid computing both AND-popcount and OR-popcount in the inner loop.
82
+ The matching `update` step accumulates only the intersection popcount; `finalize` takes the query popcount and a vector of four target popcounts and recovers the union from them, halving the work in the critical path.
83
+ Both finalizers replace the division by a reciprocal estimate — `RCPPS` on Haswell, `VRCP14PS` on Ice Lake — refined with one Newton-Raphson step.
80
84
 
81
- ### Byte Hamming via VPSADBW
85
+ ### Byte Hamming via Compare Masks
82
86
 
83
- `nk_hamming_u8_haswell`, `nk_hamming_u8_icelake` compute byte-level Hamming distance using XOR to produce per-byte difference indicators, then `VPSADBW` against zero to horizontally sum the nonzero bytes.
84
- XOR produces 0 for equal bytes and nonzero for different ones, and `VPSADBW` sums the absolute values of byte differences within each 64-bit lane.
85
- Since XOR results are either 0 or nonzero (not necessarily 1), the kernel masks XOR output through `VPMIN` with a vector of ones to clamp each byte to 0 or 1 before feeding `VPSADBW`.
87
+ `nk_hamming_u8_haswell` compares 32 bytes at a time with `VPCMPEQB`, extracts the per-byte sign bits of the two 128-bit halves with `VPMOVMSKB`, inverts the masks, and counts the set bits with scalar `POPCNT`.
88
+ `nk_hamming_u8_icelake` does the same over 64 bytes with `VPCMPNEQB`, which writes the mismatch mask straight into a `__mmask64`, so no extract step is needed.
86
89
 
87
90
  ## Performance
88
91
 
@@ -31,8 +31,7 @@
31
31
  * nk_jaccard_u1x64_init_haswell(&state_first);
32
32
  * // ... stream through packed binary vectors ...
33
33
  * nk_jaccard_u1x64_finalize_haswell(&state_first, &state_second, &state_third, &state_fourth,
34
- * query_popcount, target_popcount_a, target_popcount_b, target_popcount_c, target_popcount_d,
35
- * total_dimensions, &results);
34
+ * query_popcount, &target_popcounts_vec, total_dimensions, &result_vec);
36
35
  * @endcode
37
36
  */
38
37
  #ifndef NK_SET_HASWELL_H
@@ -240,8 +239,8 @@ NK_INTERNAL void nk_jaccard_u1x64_update_haswell(nk_jaccard_u1x64_state_haswell_
240
239
  NK_INTERNAL void nk_jaccard_u1x64_finalize_haswell( //
241
240
  nk_jaccard_u1x64_state_haswell_t const *state_a, nk_jaccard_u1x64_state_haswell_t const *state_b,
242
241
  nk_jaccard_u1x64_state_haswell_t const *state_c, nk_jaccard_u1x64_state_haswell_t const *state_d,
243
- nk_f32_t query_popcount, nk_f32_t target_popcount_a, nk_f32_t target_popcount_b, nk_f32_t target_popcount_c,
244
- nk_f32_t target_popcount_d, nk_size_t total_dimensions, nk_b128_vec_t *result) {
242
+ nk_f32_t query_popcount, nk_b128_vec_t const *target_popcounts_vec, nk_size_t total_dimensions,
243
+ nk_b128_vec_t *result_vec) {
245
244
  nk_unused_(total_dimensions);
246
245
 
247
246
  // 4-way SIMD Jaccard computation with fast reciprocal.
@@ -263,7 +262,7 @@ NK_INTERNAL void nk_jaccard_u1x64_finalize_haswell( //
263
262
  __m128 intersection_f32x4 = _mm_setr_ps(intersection_a_f32, intersection_b_f32, intersection_c_f32,
264
263
  intersection_d_f32);
265
264
  __m128 query_f32x4 = _mm_set1_ps(query_popcount);
266
- __m128 targets_f32x4 = _mm_setr_ps(target_popcount_a, target_popcount_b, target_popcount_c, target_popcount_d);
265
+ __m128 targets_f32x4 = target_popcounts_vec->xmm_ps;
267
266
  __m128 union_f32x4 = _mm_sub_ps(_mm_add_ps(query_f32x4, targets_f32x4), intersection_f32x4);
268
267
 
269
268
  // Handle zero-union edge case
@@ -283,24 +282,24 @@ NK_INTERNAL void nk_jaccard_u1x64_finalize_haswell( //
283
282
 
284
283
  __m128 ratio_f32x4 = _mm_mul_ps(intersection_f32x4, union_reciprocal_f32x4);
285
284
  __m128 jaccard_f32x4 = _mm_sub_ps(one_f32x4, ratio_f32x4);
286
- result->xmm_ps = _mm_blendv_ps(jaccard_f32x4, _mm_setzero_ps(), zero_union_mask);
285
+ result_vec->xmm_ps = _mm_blendv_ps(jaccard_f32x4, _mm_setzero_ps(), zero_union_mask);
287
286
  }
288
287
 
289
288
  /** @brief Hamming from_dot: computes pop_a + pop_b - 2*dot for 4 pairs (Haswell). */
290
- NK_INTERNAL void nk_hamming_u32x4_from_dot_haswell_(nk_b128_vec_t dots, nk_u32_t query_pop, nk_b128_vec_t target_pops,
291
- nk_b128_vec_t *results) {
292
- __m128i dots_i32x4 = dots.xmm;
289
+ NK_INTERNAL void nk_hamming_u32x4_from_dot_haswell_(nk_b128_vec_t const *dots_vec, nk_u32_t query_pop,
290
+ nk_b128_vec_t const *target_pops_vec, nk_b128_vec_t *result_vec) {
291
+ __m128i dots_i32x4 = dots_vec->xmm;
293
292
  __m128i query_i32x4 = _mm_set1_epi32((int)query_pop);
294
- __m128i target_i32x4 = target_pops.xmm;
295
- results->xmm = _mm_sub_epi32(_mm_add_epi32(query_i32x4, target_i32x4), _mm_slli_epi32(dots_i32x4, 1));
293
+ __m128i target_i32x4 = target_pops_vec->xmm;
294
+ result_vec->xmm = _mm_sub_epi32(_mm_add_epi32(query_i32x4, target_i32x4), _mm_slli_epi32(dots_i32x4, 1));
296
295
  }
297
296
 
298
297
  /** @brief Jaccard from_dot: computes 1 - dot / (pop_a + pop_b - dot) for 4 pairs (Haswell). */
299
- NK_INTERNAL void nk_jaccard_f32x4_from_dot_haswell_(nk_b128_vec_t dots, nk_u32_t query_pop, nk_b128_vec_t target_pops,
300
- nk_b128_vec_t *results) {
301
- __m128 dot_f32x4 = _mm_cvtepi32_ps(dots.xmm);
298
+ NK_INTERNAL void nk_jaccard_f32x4_from_dot_haswell_(nk_b128_vec_t const *dots_vec, nk_u32_t query_pop,
299
+ nk_b128_vec_t const *target_pops_vec, nk_b128_vec_t *result_vec) {
300
+ __m128 dot_f32x4 = _mm_cvtepi32_ps(dots_vec->xmm);
302
301
  __m128 query_f32x4 = _mm_set1_ps((nk_f32_t)query_pop);
303
- __m128 target_f32x4 = _mm_cvtepi32_ps(target_pops.xmm);
302
+ __m128 target_f32x4 = _mm_cvtepi32_ps(target_pops_vec->xmm);
304
303
  __m128 union_f32x4 = _mm_sub_ps(_mm_add_ps(query_f32x4, target_f32x4), dot_f32x4);
305
304
 
306
305
  __m128 zero_union_mask = _mm_cmpeq_ps(union_f32x4, _mm_setzero_ps());
@@ -314,7 +313,7 @@ NK_INTERNAL void nk_jaccard_f32x4_from_dot_haswell_(nk_b128_vec_t dots, nk_u32_t
314
313
 
315
314
  __m128 ratio_f32x4 = _mm_mul_ps(dot_f32x4, union_reciprocal_f32x4);
316
315
  __m128 jaccard_f32x4 = _mm_sub_ps(one_f32x4, ratio_f32x4);
317
- results->xmm_ps = _mm_blendv_ps(jaccard_f32x4, _mm_setzero_ps(), zero_union_mask);
316
+ result_vec->xmm_ps = _mm_blendv_ps(jaccard_f32x4, _mm_setzero_ps(), zero_union_mask);
318
317
  }
319
318
 
320
319
  #pragma endregion Stateful Streaming
@@ -30,8 +30,7 @@
30
30
  * nk_jaccard_u1x512_init_icelake(&state_first);
31
31
  * // ... stream through packed binary vectors ...
32
32
  * nk_jaccard_u1x512_finalize_icelake(&state_first, &state_second, &state_third, &state_fourth,
33
- * query_popcount, target_popcount_a, target_popcount_b, target_popcount_c, target_popcount_d,
34
- * total_dimensions, &results);
33
+ * query_popcount, &target_popcounts_vec, total_dimensions, &result_vec);
35
34
  * @endcode
36
35
  */
37
36
  #ifndef NK_SET_ICELAKE_H
@@ -370,8 +369,8 @@ NK_INTERNAL void nk_jaccard_u1x512_update_icelake(nk_jaccard_u1x512_state_icelak
370
369
  NK_INTERNAL void nk_jaccard_u1x512_finalize_icelake( //
371
370
  nk_jaccard_u1x512_state_icelake_t const *state_a, nk_jaccard_u1x512_state_icelake_t const *state_b,
372
371
  nk_jaccard_u1x512_state_icelake_t const *state_c, nk_jaccard_u1x512_state_icelake_t const *state_d,
373
- nk_f32_t query_popcount, nk_f32_t target_popcount_a, nk_f32_t target_popcount_b, nk_f32_t target_popcount_c,
374
- nk_f32_t target_popcount_d, nk_size_t total_dimensions, nk_b128_vec_t *result) {
372
+ nk_f32_t query_popcount, nk_b128_vec_t const *target_popcounts_vec, nk_size_t total_dimensions,
373
+ nk_b128_vec_t *result_vec) {
375
374
  nk_unused_(total_dimensions);
376
375
 
377
376
  // Port-optimized 4-way horizontal reduction using early i64 → i32 truncation.
@@ -415,7 +414,7 @@ NK_INTERNAL void nk_jaccard_u1x512_finalize_icelake( //
415
414
  // Compute Jaccard distance: 1 - intersection ÷ union
416
415
  // where union = query_popcount + target_popcount - intersection
417
416
  __m128 query_f32x4 = _mm_set1_ps(query_popcount);
418
- __m128 targets_f32x4 = _mm_setr_ps(target_popcount_a, target_popcount_b, target_popcount_c, target_popcount_d);
417
+ __m128 targets_f32x4 = target_popcounts_vec->xmm_ps;
419
418
  __m128 union_f32x4 = _mm_sub_ps(_mm_add_ps(query_f32x4, targets_f32x4), intersection_f32x4);
420
419
 
421
420
  // Handle zero-union edge case: if union == 0, result = 0.0
@@ -435,24 +434,24 @@ NK_INTERNAL void nk_jaccard_u1x512_finalize_icelake( //
435
434
 
436
435
  __m128 ratio_f32x4 = _mm_mul_ps(intersection_f32x4, union_reciprocal_f32x4);
437
436
  __m128 jaccard_f32x4 = _mm_sub_ps(one_f32x4, ratio_f32x4);
438
- result->xmm_ps = _mm_blendv_ps(jaccard_f32x4, _mm_setzero_ps(), zero_union_mask);
437
+ result_vec->xmm_ps = _mm_blendv_ps(jaccard_f32x4, _mm_setzero_ps(), zero_union_mask);
439
438
  }
440
439
 
441
440
  /** @brief Hamming from_dot: computes pop_a + pop_b - 2*dot for 4 pairs (Icelake). */
442
- NK_INTERNAL void nk_hamming_u32x4_from_dot_icelake_(nk_b128_vec_t dots, nk_u32_t query_pop, nk_b128_vec_t target_pops,
443
- nk_b128_vec_t *results) {
444
- __m128i dots_i32x4 = dots.xmm;
441
+ NK_INTERNAL void nk_hamming_u32x4_from_dot_icelake_(nk_b128_vec_t const *dots_vec, nk_u32_t query_pop,
442
+ nk_b128_vec_t const *target_pops_vec, nk_b128_vec_t *result_vec) {
443
+ __m128i dots_i32x4 = dots_vec->xmm;
445
444
  __m128i query_i32x4 = _mm_set1_epi32((int)query_pop);
446
- __m128i target_i32x4 = target_pops.xmm;
447
- results->xmm = _mm_sub_epi32(_mm_add_epi32(query_i32x4, target_i32x4), _mm_slli_epi32(dots_i32x4, 1));
445
+ __m128i target_i32x4 = target_pops_vec->xmm;
446
+ result_vec->xmm = _mm_sub_epi32(_mm_add_epi32(query_i32x4, target_i32x4), _mm_slli_epi32(dots_i32x4, 1));
448
447
  }
449
448
 
450
449
  /** @brief Jaccard from_dot: computes 1 - dot / (pop_a + pop_b - dot) for 4 pairs (Icelake). */
451
- NK_INTERNAL void nk_jaccard_f32x4_from_dot_icelake_(nk_b128_vec_t dots, nk_u32_t query_pop, nk_b128_vec_t target_pops,
452
- nk_b128_vec_t *results) {
453
- __m128 dot_f32x4 = _mm_cvtepi32_ps(dots.xmm);
450
+ NK_INTERNAL void nk_jaccard_f32x4_from_dot_icelake_(nk_b128_vec_t const *dots_vec, nk_u32_t query_pop,
451
+ nk_b128_vec_t const *target_pops_vec, nk_b128_vec_t *result_vec) {
452
+ __m128 dot_f32x4 = _mm_cvtepi32_ps(dots_vec->xmm);
454
453
  __m128 query_f32x4 = _mm_set1_ps((nk_f32_t)query_pop);
455
- __m128 target_f32x4 = _mm_cvtepi32_ps(target_pops.xmm);
454
+ __m128 target_f32x4 = _mm_cvtepi32_ps(target_pops_vec->xmm);
456
455
  __m128 union_f32x4 = _mm_sub_ps(_mm_add_ps(query_f32x4, target_f32x4), dot_f32x4);
457
456
 
458
457
  __m128 zero_union_mask = _mm_cmpeq_ps(union_f32x4, _mm_setzero_ps());
@@ -465,7 +464,7 @@ NK_INTERNAL void nk_jaccard_f32x4_from_dot_icelake_(nk_b128_vec_t dots, nk_u32_t
465
464
 
466
465
  __m128 ratio_f32x4 = _mm_mul_ps(dot_f32x4, union_reciprocal_f32x4);
467
466
  __m128 jaccard_f32x4 = _mm_sub_ps(one_f32x4, ratio_f32x4);
468
- results->xmm_ps = _mm_blendv_ps(jaccard_f32x4, _mm_setzero_ps(), zero_union_mask);
467
+ result_vec->xmm_ps = _mm_blendv_ps(jaccard_f32x4, _mm_setzero_ps(), zero_union_mask);
469
468
  }
470
469
 
471
470
  #pragma endregion Stateful Streaming
@@ -144,20 +144,22 @@ NK_PUBLIC void nk_hamming_u8_loongsonasx(nk_u8_t const *a, nk_u8_t const *b, nk_
144
144
  #pragma region Batched Finalizers
145
145
 
146
146
  /** @brief Hamming from_dot: computes pop_a + pop_b − 2 × dot for 4 pairs (LSX). */
147
- NK_INTERNAL void nk_hamming_u32x4_from_dot_loongsonasx_(nk_b128_vec_t dots, nk_u32_t query_pop,
148
- nk_b128_vec_t target_pops, nk_b128_vec_t *results) {
149
- __m128i dots_u32x4 = dots.xmm;
147
+ NK_INTERNAL void nk_hamming_u32x4_from_dot_loongsonasx_(nk_b128_vec_t const *dots_vec, nk_u32_t query_pop,
148
+ nk_b128_vec_t const *target_pops_vec,
149
+ nk_b128_vec_t *result_vec) {
150
+ __m128i dots_u32x4 = dots_vec->xmm;
150
151
  __m128i query_u32x4 = __lsx_vreplgr2vr_w((int)query_pop);
151
- __m128i target_u32x4 = target_pops.xmm;
152
- results->xmm = __lsx_vsub_w(__lsx_vadd_w(query_u32x4, target_u32x4), __lsx_vslli_w(dots_u32x4, 1));
152
+ __m128i target_u32x4 = target_pops_vec->xmm;
153
+ result_vec->xmm = __lsx_vsub_w(__lsx_vadd_w(query_u32x4, target_u32x4), __lsx_vslli_w(dots_u32x4, 1));
153
154
  }
154
155
 
155
156
  /** @brief Jaccard from_dot: computes 1 − dot / (pop_a + pop_b − dot) for 4 pairs (LSX). */
156
- NK_INTERNAL void nk_jaccard_f32x4_from_dot_loongsonasx_(nk_b128_vec_t dots, nk_u32_t query_pop,
157
- nk_b128_vec_t target_pops, nk_b128_vec_t *results) {
158
- __m128 dot_f32x4 = __lsx_vffint_s_wu(dots.xmm);
157
+ NK_INTERNAL void nk_jaccard_f32x4_from_dot_loongsonasx_(nk_b128_vec_t const *dots_vec, nk_u32_t query_pop,
158
+ nk_b128_vec_t const *target_pops_vec,
159
+ nk_b128_vec_t *result_vec) {
160
+ __m128 dot_f32x4 = __lsx_vffint_s_wu(dots_vec->xmm);
159
161
  __m128 query_f32x4 = nk_xvreplgr2vr_s_128_((nk_f32_t)query_pop);
160
- __m128 target_f32x4 = __lsx_vffint_s_wu(target_pops.xmm);
162
+ __m128 target_f32x4 = __lsx_vffint_s_wu(target_pops_vec->xmm);
161
163
  __m128 union_f32x4 = __lsx_vfsub_s(__lsx_vfadd_s(query_f32x4, target_f32x4), dot_f32x4);
162
164
 
163
165
  __m128 zero_f32x4 = (__m128)__lsx_vreplgr2vr_w(0);
@@ -167,7 +169,7 @@ NK_INTERNAL void nk_jaccard_f32x4_from_dot_loongsonasx_(nk_b128_vec_t dots, nk_u
167
169
 
168
170
  __m128 ratio_f32x4 = __lsx_vfdiv_s(dot_f32x4, safe_union_f32x4);
169
171
  __m128 jaccard_f32x4 = __lsx_vfsub_s(one_f32x4, ratio_f32x4);
170
- results->xmm_ps = (__m128)__lsx_vbitsel_v((__m128i)jaccard_f32x4, (__m128i)zero_f32x4, zero_union_mask_u32x4);
172
+ result_vec->xmm_ps = (__m128)__lsx_vbitsel_v((__m128i)jaccard_f32x4, (__m128i)zero_f32x4, zero_union_mask_u32x4);
171
173
  }
172
174
 
173
175
  #pragma endregion Batched Finalizers