Clustering
C++20 header-only: DBSCAN, HDBSCAN, k-means.
Loading...
Searching...
No Matches
pairwise.h
Go to the documentation of this file.
1#pragma once
2
3#include <algorithm>
4#include <concepts>
5#include <cstddef>
6#include <cstdint>
7#include <type_traits>
8
11#include "clustering/math/detail/pairwise_threshold_outer.h"
12#include "clustering/math/detail/pairwise_threshold_outer_i16.h"
15#include "clustering/ndarray.h"
16
17#ifdef CLUSTERING_USE_AVX2
18#include <immintrin.h>
19#endif
20
21// The dispatch metric n*m*d must not wrap. Realistic clustering sizes stay well inside 2^63 on
22// any LP64 / LLP64 platform we target; a 32-bit size_t would overflow the metric long before it
23// overflows an allocation. Pin the platform expectation so a stray cross-compile flags instead of
24// silently under-counting.
25static_assert(sizeof(std::size_t) >= 8, "pairwise dispatch assumes a 64-bit std::size_t");
26
27namespace clustering::math {
28
29namespace detail {
30
38enum class PairwisePath : std::uint8_t { Simd, Gemm };
39
40#ifdef CLUSTERING_USE_AVX2
41
42inline float horizontalSumAvx2(__m256 v) noexcept {
43 // Fold the 256-bit accumulator to a scalar with shuffles plus adds rather than `vhaddps`, which
44 // is two micro-ops on Zen and saturates the shuffle port. The lane reduction is the same eight
45 // sums in a different association; the final bit can differ but the clustering is invariant to
46 // that last-bit jitter.
47 const __m128 lo = _mm256_castps256_ps128(v);
48 const __m128 hi = _mm256_extractf128_ps(v, 1);
49 __m128 s = _mm_add_ps(lo, hi);
50 s = _mm_add_ps(s, _mm_movehl_ps(s, s));
51 s = _mm_add_ss(s, _mm_movehdup_ps(s));
52 return _mm_cvtss_f32(s);
53}
54
55inline double horizontalSumAvx2(__m256d v) noexcept {
56 const __m256d permute = _mm256_permute2f128_pd(v, v, 1);
57 const __m256d s1 = _mm256_add_pd(v, permute);
58 const __m256d s2 = _mm256_hadd_pd(s1, s1);
59 return _mm_cvtsd_f64(_mm256_castpd256_pd128(s2));
60}
61
62inline float sqEuclideanRowAvx2(const float *xRow, const float *yRow, std::size_t d) noexcept {
63 // Two independent FMA chains halve the dependency height: a single-accumulator chain stalls
64 // on the FMA latency at every iteration, while two interleaved chains let the second FMA
65 // issue while the first is in flight. Splitting even/odd 8-lane chunks into two
66 // accumulators bounds the per-chain depth at d/16.
67 __m256 ae = _mm256_setzero_ps();
68 __m256 ao = _mm256_setzero_ps();
69 const bool xAligned = (reinterpret_cast<std::uintptr_t>(xRow) % 32) == 0;
70 const bool yAligned = (reinterpret_cast<std::uintptr_t>(yRow) % 32) == 0;
71 std::size_t k = 0;
72 for (; k + 16 <= d; k += 16) {
73 const __m256 vx0 = xAligned ? _mm256_load_ps(xRow + k) : _mm256_loadu_ps(xRow + k);
74 const __m256 vy0 = yAligned ? _mm256_load_ps(yRow + k) : _mm256_loadu_ps(yRow + k);
75 const __m256 d0 = _mm256_sub_ps(vx0, vy0);
76 ae = _mm256_fmadd_ps(d0, d0, ae);
77 const __m256 vx1 = xAligned ? _mm256_load_ps(xRow + k + 8) : _mm256_loadu_ps(xRow + k + 8);
78 const __m256 vy1 = yAligned ? _mm256_load_ps(yRow + k + 8) : _mm256_loadu_ps(yRow + k + 8);
79 const __m256 d1 = _mm256_sub_ps(vx1, vy1);
80 ao = _mm256_fmadd_ps(d1, d1, ao);
81 }
82 if (k + 8 <= d) {
83 const __m256 vx = xAligned ? _mm256_load_ps(xRow + k) : _mm256_loadu_ps(xRow + k);
84 const __m256 vy = yAligned ? _mm256_load_ps(yRow + k) : _mm256_loadu_ps(yRow + k);
85 const __m256 diff = _mm256_sub_ps(vx, vy);
86 ae = _mm256_fmadd_ps(diff, diff, ae);
87 k += 8;
88 }
89 float tail = 0.0F;
90 for (; k < d; ++k) {
91 const float diff = xRow[k] - yRow[k];
92 tail += diff * diff;
93 }
94 return horizontalSumAvx2(_mm256_add_ps(ae, ao)) + tail;
95}
96
97inline double sqEuclideanRowAvx2(const double *xRow, const double *yRow, std::size_t d) noexcept {
98 // See float overload: two FMA chains break the latency-bound add chain at high d.
99 __m256d ae = _mm256_setzero_pd();
100 __m256d ao = _mm256_setzero_pd();
101 const bool xAligned = (reinterpret_cast<std::uintptr_t>(xRow) % 32) == 0;
102 const bool yAligned = (reinterpret_cast<std::uintptr_t>(yRow) % 32) == 0;
103 std::size_t k = 0;
104 for (; k + 8 <= d; k += 8) {
105 const __m256d vx0 = xAligned ? _mm256_load_pd(xRow + k) : _mm256_loadu_pd(xRow + k);
106 const __m256d vy0 = yAligned ? _mm256_load_pd(yRow + k) : _mm256_loadu_pd(yRow + k);
107 const __m256d d0 = _mm256_sub_pd(vx0, vy0);
108 ae = _mm256_fmadd_pd(d0, d0, ae);
109 const __m256d vx1 = xAligned ? _mm256_load_pd(xRow + k + 4) : _mm256_loadu_pd(xRow + k + 4);
110 const __m256d vy1 = yAligned ? _mm256_load_pd(yRow + k + 4) : _mm256_loadu_pd(yRow + k + 4);
111 const __m256d d1 = _mm256_sub_pd(vx1, vy1);
112 ao = _mm256_fmadd_pd(d1, d1, ao);
113 }
114 if (k + 4 <= d) {
115 const __m256d vx = xAligned ? _mm256_load_pd(xRow + k) : _mm256_loadu_pd(xRow + k);
116 const __m256d vy = yAligned ? _mm256_load_pd(yRow + k) : _mm256_loadu_pd(yRow + k);
117 const __m256d diff = _mm256_sub_pd(vx, vy);
118 ae = _mm256_fmadd_pd(diff, diff, ae);
119 k += 4;
120 }
121 double tail = 0.0;
122 for (; k < d; ++k) {
123 const double diff = xRow[k] - yRow[k];
124 tail += diff * diff;
125 }
126 return horizontalSumAvx2(_mm256_add_pd(ae, ao)) + tail;
127}
128
129#endif // CLUSTERING_USE_AVX2
130
131template <class T> constexpr std::size_t kAvx2Lanes = std::is_same_v<T, float> ? 8 : 4;
132
133template <class T, Layout LX, Layout LY>
134inline T sqEuclideanRow(const NDArray<T, 2, LX> &X, std::size_t i, const NDArray<T, 2, LY> &Y,
135 std::size_t j) noexcept {
136 const std::size_t d = X.dim(1);
137#ifdef CLUSTERING_USE_AVX2
138 if constexpr (LX == Layout::Contig && LY == Layout::Contig) {
139 if (d >= kAvx2Lanes<T>) {
140 const T *xRow = X.data() + (i * d);
141 const T *yRow = Y.data() + (j * d);
142 return sqEuclideanRowAvx2(xRow, yRow, d);
143 }
144 }
145#endif
146 T sum = T{0};
147 for (std::size_t k = 0; k < d; ++k) {
148 const T diff = X(i, k) - Y(j, k);
149 sum += diff * diff;
150 }
151 return sum;
152}
153
154#ifdef CLUSTERING_USE_AVX2
155
156inline float sqNormRowAvx2(const float *xRow, std::size_t d) noexcept {
157 __m256 ae = _mm256_setzero_ps();
158 __m256 ao = _mm256_setzero_ps();
159 const bool aligned = (reinterpret_cast<std::uintptr_t>(xRow) % 32) == 0;
160 std::size_t k = 0;
161 for (; k + 16 <= d; k += 16) {
162 const __m256 v0 = aligned ? _mm256_load_ps(xRow + k) : _mm256_loadu_ps(xRow + k);
163 ae = _mm256_fmadd_ps(v0, v0, ae);
164 const __m256 v1 = aligned ? _mm256_load_ps(xRow + k + 8) : _mm256_loadu_ps(xRow + k + 8);
165 ao = _mm256_fmadd_ps(v1, v1, ao);
166 }
167 if (k + 8 <= d) {
168 const __m256 v = aligned ? _mm256_load_ps(xRow + k) : _mm256_loadu_ps(xRow + k);
169 ae = _mm256_fmadd_ps(v, v, ae);
170 k += 8;
171 }
172 float tail = 0.0F;
173 for (; k < d; ++k) {
174 tail += xRow[k] * xRow[k];
175 }
176 return horizontalSumAvx2(_mm256_add_ps(ae, ao)) + tail;
177}
178
179inline double sqNormRowAvx2(const double *xRow, std::size_t d) noexcept {
180 __m256d ae = _mm256_setzero_pd();
181 __m256d ao = _mm256_setzero_pd();
182 const bool aligned = (reinterpret_cast<std::uintptr_t>(xRow) % 32) == 0;
183 std::size_t k = 0;
184 for (; k + 8 <= d; k += 8) {
185 const __m256d v0 = aligned ? _mm256_load_pd(xRow + k) : _mm256_loadu_pd(xRow + k);
186 ae = _mm256_fmadd_pd(v0, v0, ae);
187 const __m256d v1 = aligned ? _mm256_load_pd(xRow + k + 4) : _mm256_loadu_pd(xRow + k + 4);
188 ao = _mm256_fmadd_pd(v1, v1, ao);
189 }
190 if (k + 4 <= d) {
191 const __m256d v = aligned ? _mm256_load_pd(xRow + k) : _mm256_loadu_pd(xRow + k);
192 ae = _mm256_fmadd_pd(v, v, ae);
193 k += 4;
194 }
195 double tail = 0.0;
196 for (; k < d; ++k) {
197 tail += xRow[k] * xRow[k];
198 }
199 return horizontalSumAvx2(_mm256_add_pd(ae, ao)) + tail;
200}
201
202#endif // CLUSTERING_USE_AVX2
203
204template <class T, Layout LX>
205inline T sqNormRow(const NDArray<T, 2, LX> &X, std::size_t i) noexcept {
206 const std::size_t d = X.dim(1);
207#ifdef CLUSTERING_USE_AVX2
208 if constexpr (LX == Layout::Contig) {
209 if (d >= kAvx2Lanes<T>) {
210 const T *xRow = X.data() + (i * d);
211 return sqNormRowAvx2(xRow, d);
212 }
213 }
214#endif
215 T sum = T{0};
216 for (std::size_t k = 0; k < d; ++k) {
217 const T v = X(i, k);
218 sum += v * v;
219 }
220 return sum;
221}
222
237template <class T, Layout LX>
238void rowNormsSq(const NDArray<T, 2, LX> &X, NDArray<T, 1> &norms, Pool pool) {
239 static_assert(std::is_same_v<T, float> || std::is_same_v<T, double>,
240 "rowNormsSq<T> requires T to be float or double");
241
243 CLUSTERING_ALWAYS_ASSERT(norms.dim(0) == X.dim(0));
244
245 const std::size_t n = X.dim(0);
246 if (n == 0) {
247 return;
248 }
249
250 auto runRowRange = [&](std::size_t lo, std::size_t hi) noexcept {
251 for (std::size_t i = lo; i < hi; ++i) {
252 norms(i) = sqNormRow<T, LX>(X, i);
253 }
254 };
255
256 if (pool.shouldParallelize(n, 4, 2)) {
257 pool.parallelForBlocks(std::size_t{0}, n, std::size_t{0},
258 [&](std::size_t lo, std::size_t hi) { runRowRange(lo, hi); });
259 } else {
260 runRowRange(0, n);
261 }
262}
263
281template <class T, Layout LX, Layout LY>
283 NDArray<T, 2> &out, Pool pool) {
284 static_assert(std::is_same_v<T, float> || std::is_same_v<T, double>,
285 "pairwiseSqEuclideanGemm<T> requires T to be float or double");
286
288 CLUSTERING_ALWAYS_ASSERT(X.dim(1) == Y.dim(1));
289 CLUSTERING_ALWAYS_ASSERT(out.dim(0) == X.dim(0));
290 CLUSTERING_ALWAYS_ASSERT(out.dim(1) == Y.dim(0));
291
292 const std::size_t n = X.dim(0);
293 const std::size_t m = Y.dim(0);
294 if (n == 0 || m == 0) {
295 return;
296 }
297
298 NDArray<T, 1> xNorms({n});
299 NDArray<T, 1> yNorms({m});
300 rowNormsSq(X, xNorms, pool);
301 rowNormsSq(Y, yNorms, pool);
302
303 gemm(X, Y.t(), out, pool, T{-2}, T{0});
304
305 auto runBroadcastRange = [&](std::size_t lo, std::size_t hi) noexcept {
306 for (std::size_t i = lo; i < hi; ++i) {
307 const T xi = xNorms(i);
308 for (std::size_t j = 0; j < m; ++j) {
309 // Cancellation in ||x||^2 + ||y||^2 - 2 x . y can produce tiny negatives when x ~= y;
310 // squared distance is non-negative by definition, so clamp.
311 const T v = (out(i, j) + xi) + yNorms(j);
312 out(i, j) = std::max(v, T{0});
313 }
314 }
315 };
316
317 const std::size_t totalCells = n * m;
318 if (pool.shouldParallelize(totalCells, 64, 2)) {
319 pool.parallelForBlocks(std::size_t{0}, n, std::size_t{0},
320 [&](std::size_t lo, std::size_t hi) { runBroadcastRange(lo, hi); });
321 } else {
322 runBroadcastRange(0, n);
323 }
324}
325
342template <class T, Layout LX, Layout LY>
344 NDArray<T, 2> &out, Pool pool) {
345 static_assert(std::is_same_v<T, float> || std::is_same_v<T, double>,
346 "pairwiseSqEuclideanSimd<T> requires T to be float or double");
347
349 CLUSTERING_ALWAYS_ASSERT(X.dim(1) == Y.dim(1));
350 CLUSTERING_ALWAYS_ASSERT(out.dim(0) == X.dim(0));
351 CLUSTERING_ALWAYS_ASSERT(out.dim(1) == Y.dim(0));
352
353 const std::size_t n = X.dim(0);
354 const std::size_t m = Y.dim(0);
355 if (n == 0 || m == 0) {
356 return;
357 }
358
359 auto runRowRange = [&](std::size_t lo, std::size_t hi) noexcept {
360 for (std::size_t i = lo; i < hi; ++i) {
361 for (std::size_t j = 0; j < m; ++j) {
362 out(i, j) = sqEuclideanRow<T, LX, LY>(X, i, Y, j);
363 }
364 }
365 };
366
367 if (pool.shouldParallelize(n, 4, 2)) {
368 pool.parallelForBlocks(std::size_t{0}, n, std::size_t{0},
369 [&](std::size_t lo, std::size_t hi) { runRowRange(lo, hi); });
370 } else {
371 runRowRange(0, n);
372 }
373}
374
375} // namespace detail
376
394template <class T, Layout LX = Layout::Contig, Layout LY = Layout::Contig>
396 Pool pool) {
397 static_assert(std::is_same_v<T, float> || std::is_same_v<T, double>,
398 "pairwiseSqEuclidean<T> requires T to be float or double");
399
401 CLUSTERING_ALWAYS_ASSERT(X.dim(1) == Y.dim(1));
402 CLUSTERING_ALWAYS_ASSERT(out.dim(0) == X.dim(0));
403 CLUSTERING_ALWAYS_ASSERT(out.dim(1) == Y.dim(0));
404
405 const std::size_t n = X.dim(0);
406 const std::size_t m = Y.dim(0);
407 if (n == 0 || m == 0) {
408 return;
409 }
410
411 const std::size_t work = n * m * X.dim(1);
413 detail::pairwiseSqEuclideanGemm(X, Y, out, pool);
414 } else {
415 detail::pairwiseSqEuclideanSimd(X, Y, out, pool);
416 }
417}
418
419namespace detail {
420
438template <class T, Layout LX = Layout::Contig, Layout LY = Layout::Contig>
440 const NDArray<T, 2, LY> &Y, NDArray<T, 2> &out,
441 Pool pool) {
442 static_assert(std::is_same_v<T, float> || std::is_same_v<T, double>,
443 "pairwiseSqEuclideanWithDispatchInfo<T> requires T to be float or double");
444
446 CLUSTERING_ALWAYS_ASSERT(X.dim(1) == Y.dim(1));
447 CLUSTERING_ALWAYS_ASSERT(out.dim(0) == X.dim(0));
448 CLUSTERING_ALWAYS_ASSERT(out.dim(1) == Y.dim(0));
449
450 const std::size_t n = X.dim(0);
451 const std::size_t m = Y.dim(0);
452 if (n == 0 || m == 0) {
453 return PairwisePath::Simd;
454 }
455
456 const std::size_t work = n * m * X.dim(1);
458 pairwiseSqEuclideanGemm(X, Y, out, pool);
459 return PairwisePath::Gemm;
460 }
461 pairwiseSqEuclideanSimd(X, Y, out, pool);
462 return PairwisePath::Simd;
463}
464
476template <class T, Layout LX, Layout LY>
478#ifdef CLUSTERING_USE_AVX2
479 if constexpr (std::is_same_v<T, float> && LX == Layout::Contig && LY == Layout::Contig) {
480 const std::size_t n = X.dim(0);
481 const std::size_t m = Y.dim(0);
482 const std::size_t d = X.dim(1);
483 if (n == 0 || m == 0 || d == 0) {
484 return false;
485 }
486 if (d < 8 || d > kThresholdMaxD) {
487 return false;
488 }
489 if (!X.template isAligned<32>() || !Y.template isAligned<32>()) {
490 return false;
491 }
492 return true;
493 } else {
494 (void)X;
495 (void)Y;
496 return false;
497 }
498#else
499 (void)X;
500 (void)Y;
501 return false;
502#endif
503}
504
515template <class T, Layout LX, Layout LY, class Emit>
516 requires std::invocable<Emit &, std::size_t, std::size_t>
518 const NDArray<T, 2, LY> &Y, T radiusSq, Pool pool,
519 Emit &&emit) {
520 const std::size_t n = X.dim(0);
521 const std::size_t m = Y.dim(0);
522 if (n == 0 || m == 0) {
523 return;
524 }
525
526 auto runRowRange = [&](std::size_t lo, std::size_t hi) {
527 for (std::size_t i = lo; i < hi; ++i) {
528 for (std::size_t j = 0; j < m; ++j) {
529 const T distSq = sqEuclideanRow<T, LX, LY>(X, i, Y, j);
530 if (distSq <= radiusSq) {
531 emit(i, j);
532 }
533 }
534 }
535 };
536
537 // Only fan out across rows: column emit within a row is order-sensitive and consumers rely
538 // on the per-row contract. Parallelism at the seed (row) level is safe because each row's
539 // emits land in a distinct key space, but the caller owns thread-safety of @p emit.
540 if (pool.shouldParallelize(n * m, 64, 2)) {
541 pool.parallelForBlocks(std::size_t{0}, n, std::size_t{0},
542 [&](std::size_t lo, std::size_t hi) { runRowRange(lo, hi); });
543 } else {
544 runRowRange(0, n);
545 }
546}
547
548} // namespace detail
549
572template <class T, Layout LX = Layout::Contig, Layout LY = Layout::Contig, class Emit>
573 requires std::invocable<Emit &, std::size_t, std::size_t>
575 T radiusSq, Pool pool, Emit &&emit) {
576 static_assert(std::is_same_v<T, float> || std::is_same_v<T, double>,
577 "pairwiseSqEuclideanThresholded<T> requires T to be float or double");
578 CLUSTERING_ALWAYS_ASSERT(X.dim(1) == Y.dim(1));
579
580 const std::size_t n = X.dim(0);
581 const std::size_t m = Y.dim(0);
582 if (n == 0 || m == 0) {
583 return;
584 }
585
586#ifdef CLUSTERING_USE_AVX2
587 if constexpr (std::is_same_v<T, float> && LX == Layout::Contig && LY == Layout::Contig) {
589 NDArray<T, 1> xNorms({n});
590 NDArray<T, 1> yNorms({m});
591 detail::rowNormsSq(X, xNorms, pool);
592 detail::rowNormsSq(Y, yNorms, pool);
593 detail::pairwiseThresholdOuterAvx2F32(X, Y, xNorms, yNorms, radiusSq, pool, emit);
594 return;
595 }
596 }
597#endif
598
599 detail::pairwiseSqEuclideanThresholdedMaterialized(X, Y, radiusSq, pool, emit);
600}
601
624template <class T, Layout LX = Layout::Contig, class Emit>
625 requires std::invocable<Emit &, std::size_t, std::size_t>
627 Emit &&emit) {
628 static_assert(std::is_same_v<T, float> || std::is_same_v<T, double>,
629 "pairwiseSqEuclideanThresholdedSymmetric<T> requires T to be float or double");
630
631 const std::size_t n = X.dim(0);
632 if (n == 0) {
633 return;
634 }
635
636#ifdef CLUSTERING_USE_AVX2
637 if constexpr (std::is_same_v<T, float> && LX == Layout::Contig) {
639 NDArray<T, 1> xNorms({n});
640 detail::rowNormsSq(X, xNorms, pool);
641 // The quantized filter declines degenerate scales (wide-range data whose quantization
642 // slack would swallow the pruning); the f32 sweep then covers the call.
643 if (detail::pairwiseThresholdOuterAvx2I16FilteredSymmetric(X, xNorms, radiusSq, pool, emit)) {
644 return;
645 }
646 detail::pairwiseThresholdOuterAvx2F32Symmetric(X, xNorms, radiusSq, pool, emit);
647 return;
648 }
649 }
650#endif
651
652 // Scalar fallback: walk only j >= i and forward each surviving upper-triangular cell to the
653 // caller. Mirrors the @c pairwiseSqEuclideanThresholdedMaterialized contract for the
654 // non-symmetric case; the caller's emit is responsible for any adj-side mirror push.
655 auto runRowRange = [&](std::size_t lo, std::size_t hi) {
656 for (std::size_t i = lo; i < hi; ++i) {
657 for (std::size_t j = i; j < n; ++j) {
658 const T distSq = detail::sqEuclideanRow<T, LX, LX>(X, i, X, j);
659 if (distSq <= radiusSq) {
660 emit(i, j);
661 }
662 }
663 }
664 };
665
666 if (pool.shouldParallelize(n * n / 2, 64, 2)) {
667 pool.parallelForBlocks(std::size_t{0}, n, std::size_t{0},
668 [&](std::size_t lo, std::size_t hi) { runRowRange(lo, hi); });
669 } else {
670 runRowRange(0, n);
671 }
672}
673
674} // namespace clustering::math
#define CLUSTERING_ALWAYS_ASSERT(cond)
Release-active assertion: evaluates cond in every build configuration.
Represents a multidimensional array (NDArray) of a fixed number of dimensions N and element type T.
Definition ndarray.h:136
size_t dim(std::size_t index) const noexcept
Returns the size of a specific dimension of the NDArray.
Definition ndarray.h:462
NDArray< T, 2, Layout::MaybeStrided > t() noexcept
Transposes a rank-2 NDArray into a borrowed view with swapped axes.
Definition ndarray.h:684
bool isMutable() const noexcept
Reports whether writes through operator(), Accessor, or flatIndex are allowed.
Definition ndarray.h:489
constexpr std::size_t pairwiseGemmThreshold
Workload threshold at which pairwiseSqEuclidean switches from the per-pair SIMD kernel to the GEMM-id...
Definition defaults.h:52
PairwisePath pairwiseSqEuclideanWithDispatchInfo(const NDArray< T, 2, LX > &X, const NDArray< T, 2, LY > &Y, NDArray< T, 2 > &out, Pool pool)
Test-only: runs the same dispatch as pairwiseSqEuclidean and reports which kernel fired.
Definition pairwise.h:439
float horizontalSumAvx2(__m256 v) noexcept
Definition pairwise.h:42
float sqNormRowAvx2(const float *xRow, std::size_t d) noexcept
Definition pairwise.h:156
PairwisePath
Tag identifying which inner kernel executed for a pairwise distance request.
Definition pairwise.h:38
float sqEuclideanRowAvx2(const float *xRow, const float *yRow, std::size_t d) noexcept
Definition pairwise.h:62
T sqEuclideanRow(const NDArray< T, 2, LX > &X, std::size_t i, const NDArray< T, 2, LY > &Y, std::size_t j) noexcept
Definition pairwise.h:134
constexpr std::size_t kAvx2Lanes
Definition pairwise.h:131
void rowNormsSq(const NDArray< T, 2, LX > &X, NDArray< T, 1 > &norms, Pool pool)
Row-wise sum of squares: norms(i) = sum_k X(i, k)^2.
Definition pairwise.h:238
void pairwiseSqEuclideanThresholdedMaterialized(const NDArray< T, 2, LX > &X, const NDArray< T, 2, LY > &Y, T radiusSq, Pool pool, Emit &&emit)
Materialized fallback for the thresholded-emit API: compute each pair's squared distance via sqEuclid...
Definition pairwise.h:517
bool canUseFusedThreshold(const NDArray< T, 2, LX > &X, const NDArray< T, 2, LY > &Y) noexcept
Runtime predicate: true when the fused AVX2 threshold path is eligible.
Definition pairwise.h:477
void pairwiseSqEuclideanSimd(const NDArray< T, 2, LX > &X, const NDArray< T, 2, LY > &Y, NDArray< T, 2 > &out, Pool pool)
Small-path pairwise squared Euclidean via SIMD accumulation per (i, j) pair.
Definition pairwise.h:343
void pairwiseSqEuclideanGemm(const NDArray< T, 2, LX > &X, const NDArray< T, 2, LY > &Y, NDArray< T, 2 > &out, Pool pool)
Large-path pairwise squared Euclidean via the GEMM identity.
Definition pairwise.h:282
T sqNormRow(const NDArray< T, 2, LX > &X, std::size_t i) noexcept
Definition pairwise.h:205
void gemm(const NDArray< T, 2, LA > &A, const NDArray< T, 2, LB > &B, NDArray< T, 2 > &C, Pool pool, T alpha=T{1}, T beta=T{0})
One-shot dense matrix-matrix multiply: C := alpha * A * B + beta * C.
Definition gemm.h:31
void pairwiseSqEuclideanThresholded(const NDArray< T, 2, LX > &X, const NDArray< T, 2, LY > &Y, T radiusSq, Pool pool, Emit &&emit)
Emit every row pair (i, j) whose squared Euclidean distance is at most radiusSq.
Definition pairwise.h:574
void pairwiseSqEuclideanThresholdedSymmetric(const NDArray< T, 2, LX > &X, T radiusSq, Pool pool, Emit &&emit)
Symmetric variant of pairwiseSqEuclideanThresholded for the X == Y case.
Definition pairwise.h:626
void pairwiseSqEuclidean(const NDArray< T, 2, LX > &X, const NDArray< T, 2, LY > &Y, NDArray< T, 2 > &out, Pool pool)
Pairwise squared Euclidean distances between rows of two matrices.
Definition pairwise.h:395
T sum(const NDArray< T, 1, L > &x) noexcept
Naive single-pass sum of a rank-1 array.
Definition reduce.h:25
Thin compile-time-templated wrapper around the underlying OwnedPool.
Definition thread.h:109
void parallelForBlocks(std::size_t first, std::size_t last, std::size_t numBlocks, Body body)
Run body in parallel over [first, last) partitioned into numBlocks blocks.
Definition thread.h:239
bool shouldParallelize(std::size_t totalWork, std::size_t minChunk, std::size_t minTasksPerWorker=2) const noexcept
Decide whether totalWork warrants parallel dispatch.
Definition thread.h:147