11#include "clustering/math/detail/pairwise_threshold_outer.h"
12#include "clustering/math/detail/pairwise_threshold_outer_i16.h"
17#ifdef CLUSTERING_USE_AVX2
25static_assert(
sizeof(std::size_t) >= 8,
"pairwise dispatch assumes a 64-bit std::size_t");
40#ifdef CLUSTERING_USE_AVX2
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);
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));
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;
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);
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);
91 const float diff = xRow[k] - yRow[k];
97inline double sqEuclideanRowAvx2(
const double *xRow,
const double *yRow, std::size_t d)
noexcept {
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;
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);
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);
123 const double diff = xRow[k] - yRow[k];
131template <
class T>
constexpr std::size_t
kAvx2Lanes = std::is_same_v<T, float> ? 8 : 4;
133template <
class T, Layout LX, Layout LY>
135 std::size_t j)
noexcept {
136 const std::size_t d = X.dim(1);
137#ifdef CLUSTERING_USE_AVX2
140 const T *xRow = X.data() + (i * d);
141 const T *yRow = Y.data() + (j * d);
147 for (std::size_t k = 0; k < d; ++k) {
148 const T diff = X(i, k) - Y(j, k);
154#ifdef CLUSTERING_USE_AVX2
157 __m256 ae = _mm256_setzero_ps();
158 __m256 ao = _mm256_setzero_ps();
159 const bool aligned = (
reinterpret_cast<std::uintptr_t
>(xRow) % 32) == 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);
168 const __m256 v = aligned ? _mm256_load_ps(xRow + k) : _mm256_loadu_ps(xRow + k);
169 ae = _mm256_fmadd_ps(v, v, ae);
174 tail += xRow[k] * xRow[k];
180 __m256d ae = _mm256_setzero_pd();
181 __m256d ao = _mm256_setzero_pd();
182 const bool aligned = (
reinterpret_cast<std::uintptr_t
>(xRow) % 32) == 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);
191 const __m256d v = aligned ? _mm256_load_pd(xRow + k) : _mm256_loadu_pd(xRow + k);
192 ae = _mm256_fmadd_pd(v, v, ae);
197 tail += xRow[k] * xRow[k];
204template <
class T, Layout LX>
206 const std::size_t d = X.dim(1);
207#ifdef CLUSTERING_USE_AVX2
210 const T *xRow = X.data() + (i * d);
216 for (std::size_t k = 0; k < d; ++k) {
237template <
class T, Layout LX>
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");
245 const std::size_t n = X.
dim(0);
250 auto runRowRange = [&](std::size_t lo, std::size_t hi)
noexcept {
251 for (std::size_t i = lo; i < hi; ++i) {
258 [&](std::size_t lo, std::size_t hi) { runRowRange(lo, hi); });
281template <
class T, Layout LX, Layout LY>
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");
292 const std::size_t n = X.
dim(0);
293 const std::size_t m = Y.
dim(0);
294 if (n == 0 || m == 0) {
303 gemm(X, Y.
t(), out, pool, T{-2}, T{0});
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) {
311 const T v = (out(i, j) + xi) + yNorms(j);
312 out(i, j) = std::max(v, T{0});
317 const std::size_t totalCells = n * m;
320 [&](std::size_t lo, std::size_t hi) { runBroadcastRange(lo, hi); });
322 runBroadcastRange(0, n);
342template <
class T, Layout LX, Layout LY>
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");
353 const std::size_t n = X.
dim(0);
354 const std::size_t m = Y.
dim(0);
355 if (n == 0 || m == 0) {
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) {
369 [&](std::size_t lo, std::size_t hi) { runRowRange(lo, hi); });
394template <
class T, Layout LX = Layout::Contig, Layout LY = Layout::Contig>
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");
405 const std::size_t n = X.
dim(0);
406 const std::size_t m = Y.
dim(0);
407 if (n == 0 || m == 0) {
411 const std::size_t work = n * m * X.
dim(1);
438template <
class T, Layout LX = Layout::Contig, Layout LY = Layout::Contig>
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");
450 const std::size_t n = X.
dim(0);
451 const std::size_t m = Y.
dim(0);
452 if (n == 0 || m == 0) {
456 const std::size_t work = n * m * X.
dim(1);
476template <
class T, Layout LX, Layout LY>
478#ifdef CLUSTERING_USE_AVX2
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) {
486 if (d < 8 || d > kThresholdMaxD) {
489 if (!X.template isAligned<32>() || !Y.template isAligned<32>()) {
515template <
class T, Layout LX, Layout LY,
class Emit>
516 requires std::invocable<Emit &, std::size_t, std::size_t>
520 const std::size_t n = X.
dim(0);
521 const std::size_t m = Y.
dim(0);
522 if (n == 0 || m == 0) {
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) {
530 if (distSq <= radiusSq) {
542 [&](std::size_t lo, std::size_t hi) { runRowRange(lo, hi); });
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");
580 const std::size_t n = X.
dim(0);
581 const std::size_t m = Y.
dim(0);
582 if (n == 0 || m == 0) {
586#ifdef CLUSTERING_USE_AVX2
593 detail::pairwiseThresholdOuterAvx2F32(X, Y, xNorms, yNorms, radiusSq, pool, emit);
624template <
class T, Layout LX = Layout::Contig,
class Emit>
625 requires std::invocable<Emit &, std::size_t, std::size_t>
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");
631 const std::size_t n = X.
dim(0);
636#ifdef CLUSTERING_USE_AVX2
643 if (detail::pairwiseThresholdOuterAvx2I16FilteredSymmetric(X, xNorms, radiusSq, pool, emit)) {
646 detail::pairwiseThresholdOuterAvx2F32Symmetric(X, xNorms, radiusSq, pool, emit);
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) {
659 if (distSq <= radiusSq) {
668 [&](std::size_t lo, std::size_t hi) { runRowRange(lo, hi); });
#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.
size_t dim(std::size_t index) const noexcept
Returns the size of a specific dimension of the NDArray.
NDArray< T, 2, Layout::MaybeStrided > t() noexcept
Transposes a rank-2 NDArray into a borrowed view with swapped axes.
bool isMutable() const noexcept
Reports whether writes through operator(), Accessor, or flatIndex are allowed.
constexpr std::size_t pairwiseGemmThreshold
Workload threshold at which pairwiseSqEuclidean switches from the per-pair SIMD kernel to the GEMM-id...
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.
float horizontalSumAvx2(__m256 v) noexcept
float sqNormRowAvx2(const float *xRow, std::size_t d) noexcept
PairwisePath
Tag identifying which inner kernel executed for a pairwise distance request.
float sqEuclideanRowAvx2(const float *xRow, const float *yRow, std::size_t d) noexcept
T sqEuclideanRow(const NDArray< T, 2, LX > &X, std::size_t i, const NDArray< T, 2, LY > &Y, std::size_t j) noexcept
constexpr std::size_t kAvx2Lanes
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.
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...
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.
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.
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.
T sqNormRow(const NDArray< T, 2, LX > &X, std::size_t i) noexcept
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.
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.
void pairwiseSqEuclideanThresholdedSymmetric(const NDArray< T, 2, LX > &X, T radiusSq, Pool pool, Emit &&emit)
Symmetric variant of pairwiseSqEuclideanThresholded for the X == Y case.
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.
T sum(const NDArray< T, 1, L > &x) noexcept
Naive single-pass sum of a rank-1 array.
Thin compile-time-templated wrapper around the underlying OwnedPool.
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.
bool shouldParallelize(std::size_t totalWork, std::size_t minChunk, std::size_t minTasksPerWorker=2) const noexcept
Decide whether totalWork warrants parallel dispatch.