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.
- package/README.md +2 -2
- package/cpp/test.cpp +66 -3
- package/include/usearch/index.hpp +9 -1
- package/include/usearch/index_dense.hpp +16 -5
- package/numkong/CITATION.cff +13 -9
- package/numkong/CONTRIBUTING.md +29 -24
- package/numkong/Cargo.toml +1 -1
- package/numkong/README.md +21 -7
- package/numkong/binding.gyp +47 -31
- package/numkong/build.rs +15 -5
- package/numkong/c/dispatch.h +0 -1
- package/numkong/c/parallel.c +139 -0
- package/numkong/c/parallel.h +47 -0
- package/numkong/cmake/toolchain-wasi.cmake +39 -5
- package/numkong/include/README.md +24 -17
- package/numkong/include/numkong/attention/sapphireamx.h +6 -6
- package/numkong/include/numkong/attention/sme.h +2 -2
- package/numkong/include/numkong/capabilities.h +2 -2
- package/numkong/include/numkong/cast/README.md +9 -7
- package/numkong/include/numkong/cast/loongsonasx.h +48 -40
- package/numkong/include/numkong/cast/serial.h +89 -11
- package/numkong/include/numkong/cast.h +3 -3
- package/numkong/include/numkong/curved/README.md +10 -10
- package/numkong/include/numkong/curved/neon.h +1 -0
- package/numkong/include/numkong/curved/neonbfdot.h +1 -0
- package/numkong/include/numkong/curved/smef64.h +1 -0
- package/numkong/include/numkong/dot/README.md +18 -18
- package/numkong/include/numkong/dot/alder.h +8 -8
- package/numkong/include/numkong/dot/haswell.h +8 -8
- package/numkong/include/numkong/dot/icelake.h +12 -12
- package/numkong/include/numkong/dot/loongsonasx.h +6 -6
- package/numkong/include/numkong/dot/neon.h +2 -1
- package/numkong/include/numkong/dot/neonbfdot.h +1 -0
- package/numkong/include/numkong/dot/neonfhm.h +1 -0
- package/numkong/include/numkong/dot/neonsdot.h +1 -0
- package/numkong/include/numkong/dot/powervsx.h +3 -3
- package/numkong/include/numkong/dot/serial.h +19 -6
- package/numkong/include/numkong/dot/skylake.h +4 -4
- package/numkong/include/numkong/dot/svehalf.h +1 -1
- package/numkong/include/numkong/dot/v128relaxed.h +24 -23
- package/numkong/include/numkong/dots/README.md +26 -26
- package/numkong/include/numkong/dots/genoa.h +3 -1
- package/numkong/include/numkong/dots/icelake.h +1 -0
- package/numkong/include/numkong/dots/powervsx.h +1 -1
- package/numkong/include/numkong/dots/serial.h +32 -33
- package/numkong/include/numkong/dots/sierra.h +4 -3
- package/numkong/include/numkong/each/README.md +5 -3
- package/numkong/include/numkong/each/sapphire.h +2 -0
- package/numkong/include/numkong/each/skylake.h +1 -0
- package/numkong/include/numkong/geospatial/README.md +6 -6
- package/numkong/include/numkong/geospatial/v128relaxed.h +1 -0
- package/numkong/include/numkong/maxsim/README.md +5 -5
- package/numkong/include/numkong/maxsim/genoa.h +4 -0
- package/numkong/include/numkong/maxsim/icelake.h +1 -0
- package/numkong/include/numkong/maxsim/serial.h +22 -9
- package/numkong/include/numkong/maxsim/sme.h +3 -2
- package/numkong/include/numkong/mesh/README.md +7 -4
- package/numkong/include/numkong/mesh/genoa.h +2 -2
- package/numkong/include/numkong/mesh/haswell.h +8 -8
- package/numkong/include/numkong/mesh/neon.h +6 -6
- package/numkong/include/numkong/mesh/neonbfdot.h +3 -2
- package/numkong/include/numkong/mesh/neonfhm.h +3 -2
- package/numkong/include/numkong/mesh/rvv.h +4 -4
- package/numkong/include/numkong/mesh/serial.h +17 -17
- package/numkong/include/numkong/mesh/skylake.h +4 -4
- package/numkong/include/numkong/mesh/v128relaxed.h +4 -4
- package/numkong/include/numkong/mesh.hpp +2 -2
- package/numkong/include/numkong/numkong.h +1 -1
- package/numkong/include/numkong/numkong.hpp +1 -1
- package/numkong/include/numkong/probability/README.md +17 -15
- package/numkong/include/numkong/probability/skylake.h +1 -1
- package/numkong/include/numkong/reduce/README.md +27 -21
- package/numkong/include/numkong/reduce/genoa.h +2 -1
- package/numkong/include/numkong/reduce/icelake.h +1 -0
- package/numkong/include/numkong/reduce/neon.h +1 -1
- package/numkong/include/numkong/reduce/serial.h +9 -9
- package/numkong/include/numkong/reduce/skylake.h +4 -4
- package/numkong/include/numkong/reduce.hpp +38 -0
- package/numkong/include/numkong/scalar/README.md +29 -28
- package/numkong/include/numkong/set/README.md +17 -14
- package/numkong/include/numkong/set/haswell.h +15 -16
- package/numkong/include/numkong/set/icelake.h +15 -16
- package/numkong/include/numkong/set/loongsonasx.h +12 -10
- package/numkong/include/numkong/set/neon.h +15 -19
- package/numkong/include/numkong/set/powervsx.h +15 -21
- package/numkong/include/numkong/set/serial.h +19 -20
- package/numkong/include/numkong/set/v128relaxed.h +10 -8
- package/numkong/include/numkong/sets/icelake.h +1 -0
- package/numkong/include/numkong/sparse/README.md +4 -4
- package/numkong/include/numkong/sparse/icelake.h +1 -0
- package/numkong/include/numkong/sparse/neon.h +1 -0
- package/numkong/include/numkong/sparse/serial.h +10 -3
- package/numkong/include/numkong/sparse/turin.h +1 -0
- package/numkong/include/numkong/spatial/README.md +25 -24
- package/numkong/include/numkong/spatial/alder.h +2 -1
- package/numkong/include/numkong/spatial/haswell.h +48 -40
- package/numkong/include/numkong/spatial/icelake.h +2 -1
- package/numkong/include/numkong/spatial/neon.h +54 -46
- package/numkong/include/numkong/spatial/neonbfdot.h +1 -1
- package/numkong/include/numkong/spatial/powervsx.h +54 -46
- package/numkong/include/numkong/spatial/serial.h +56 -46
- package/numkong/include/numkong/spatial/sierra.h +4 -3
- package/numkong/include/numkong/spatial/skylake.h +27 -22
- package/numkong/include/numkong/spatial/sve.h +6 -6
- package/numkong/include/numkong/spatial/svebfdot.h +1 -1
- package/numkong/include/numkong/spatial/svehalf.h +1 -1
- package/numkong/include/numkong/spatial/v128relaxed.h +36 -30
- package/numkong/include/numkong/spatials/README.md +15 -15
- package/numkong/include/numkong/spatials/genoa.h +3 -0
- package/numkong/include/numkong/spatials/graniteamx.h +2 -0
- package/numkong/include/numkong/spatials/icelake.h +1 -0
- package/numkong/include/numkong/spatials/sierra.h +1 -0
- package/numkong/include/numkong/tensor.hpp +236 -123
- package/numkong/include/numkong/trigonometry/README.md +4 -4
- package/numkong/include/numkong/trigonometry/serial.h +6 -6
- package/numkong/include/numkong/trigonometry/v128relaxed.h +1 -0
- package/numkong/include/numkong/types.hpp +46 -16
- package/numkong/include/numkong/vector.hpp +116 -23
- package/numkong/javascript/@numkong-darwin-arm64/package.json +1 -1
- package/numkong/javascript/@numkong-darwin-x64/package.json +1 -1
- package/numkong/javascript/@numkong-linux-arm64/package.json +1 -1
- package/numkong/javascript/@numkong-linux-x64/package.json +1 -1
- package/numkong/javascript/@numkong-win32-arm64/package.json +1 -1
- package/numkong/javascript/@numkong-win32-x64/package.json +1 -1
- package/numkong/javascript/README.md +18 -16
- package/numkong/javascript/numkong.c +70 -40
- package/numkong/package.json +8 -8
- package/numkong/rust/README.md +19 -9
- package/numkong/rust/cast.rs +52 -1
- package/numkong/rust/each.rs +480 -7
- package/numkong/rust/matrix.rs +877 -216
- package/numkong/rust/numkong.rs +12 -8
- package/numkong/rust/reduce.rs +433 -5
- package/numkong/rust/tensor.rs +450 -842
- package/numkong/rust/vector.rs +520 -101
- package/numkong/swift/README.md +5 -3
- package/numkong/test/README.md +4 -4
- package/numkong/test/test-wasi.mjs +17 -0
- package/numkong/test/test_each.cpp +30 -1
- package/numkong/test/test_each.py +74 -1
- package/numkong/test/test_reduce.cpp +51 -0
- package/numkong/test/test_tensor.cpp +2 -63
- package/numkong/test/test_tensor.py +93 -0
- package/package.json +1 -1
- package/prebuilds/darwin-arm64+x64/usearch.node +0 -0
- package/prebuilds/linux-arm64/usearch.node +0 -0
- package/prebuilds/linux-x64/usearch.node +0 -0
- 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` | `
|
|
40
|
-
| `
|
|
41
|
-
| `
|
|
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
|
-
| `
|
|
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`, `
|
|
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 (`
|
|
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
|
|
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
|
|
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
|
|
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: $
|
|
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
|
|
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"
|
|
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" {
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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`)
|
|
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` | `
|
|
59
|
-
| `bf16` | `
|
|
60
|
-
| `e4m3` | `
|
|
61
|
-
| `e5m2` | `
|
|
62
|
-
| `e2m3` | `
|
|
63
|
-
| `e3m2` | `
|
|
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 `
|
|
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 `
|
|
77
|
-
The magic constant $
|
|
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`
|
|
83
|
-
Positive floats are already ordered by their bit patterns; negative floats need bit inversion
|
|
84
|
-
This
|
|
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
|
|
56
|
+
### Popcount Without Harley-Seal Carry-Save Adders
|
|
57
57
|
|
|
58
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
The
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
`
|
|
79
|
-
|
|
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
|
|
85
|
+
### Byte Hamming via Compare Masks
|
|
82
86
|
|
|
83
|
-
`nk_hamming_u8_haswell
|
|
84
|
-
|
|
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,
|
|
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,
|
|
244
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
291
|
-
nk_b128_vec_t *
|
|
292
|
-
__m128i dots_i32x4 =
|
|
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 =
|
|
295
|
-
|
|
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
|
|
300
|
-
nk_b128_vec_t *
|
|
301
|
-
__m128 dot_f32x4 = _mm_cvtepi32_ps(
|
|
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(
|
|
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
|
-
|
|
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,
|
|
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,
|
|
374
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
443
|
-
nk_b128_vec_t *
|
|
444
|
-
__m128i dots_i32x4 =
|
|
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 =
|
|
447
|
-
|
|
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
|
|
452
|
-
nk_b128_vec_t *
|
|
453
|
-
__m128 dot_f32x4 = _mm_cvtepi32_ps(
|
|
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(
|
|
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
|
-
|
|
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
|
|
148
|
-
nk_b128_vec_t
|
|
149
|
-
|
|
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 =
|
|
152
|
-
|
|
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
|
|
157
|
-
nk_b128_vec_t
|
|
158
|
-
|
|
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(
|
|
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
|
-
|
|
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
|