Clustering
C++20 header-only: DBSCAN, HDBSCAN, k-means.
Loading...
Searching...
No Matches
pairwise_argmin.h
Go to the documentation of this file.
1#pragma once
2
3#include <algorithm>
4#include <array>
5#include <cstddef>
6#include <cstdint>
7#include <type_traits>
8
11#include "clustering/math/detail/pairwise_argmin_outer.h"
14#include "clustering/ndarray.h"
15
16namespace clustering::math {
17
24inline constexpr std::size_t pairwiseArgminChunkRows = 256;
25
36[[nodiscard]] inline std::array<std::size_t, 2>
37chunkedMaterializedScratchShape(std::size_t n, std::size_t k) noexcept {
38 const std::size_t rows = (n < pairwiseArgminChunkRows) ? n : pairwiseArgminChunkRows;
39 return {rows == 0 ? std::size_t{1} : rows, k == 0 ? std::size_t{1} : k};
40}
41
42namespace detail {
43
51enum class ArgminPath : std::uint8_t { Fused, Materialized };
52
78template <class T, Layout LX, Layout LC>
81 NDArray<T, 1> &outMinSq, NDArray<T, 2> &distsScratch,
82 Pool pool) {
83 const std::size_t n = X.dim(0);
84 const std::size_t k = C.dim(0);
85 const std::size_t d = X.dim(1);
86 if (n == 0 || k == 0) {
87 return;
88 }
89 const std::size_t chunkCap = pairwiseArgminChunkRows;
90 CLUSTERING_ALWAYS_ASSERT(distsScratch.dim(1) >= k);
91 CLUSTERING_ALWAYS_ASSERT(distsScratch.dim(0) >= (n < chunkCap ? n : chunkCap));
92
93 auto runChunk = [&](std::size_t iBase, std::size_t chunkRows, const auto &xChunk) noexcept {
94 // The scratch tile may be wider than k when the caller pre-sized with k padded up; the
95 // dispatch view narrows it back to (chunkRows, k) so the callee sees exactly one chunk.
96 NDArray<T, 2> distsView = NDArray<T, 2>::borrow(distsScratch.data(), {chunkRows, k});
97 pairwiseSqEuclidean(xChunk, C, distsView, pool);
98
99 auto scanRange = [&](std::size_t lo, std::size_t hi) noexcept {
100 for (std::size_t i = lo; i < hi; ++i) {
101 const T *row = distsView.data() + (i * k);
102 T bestVal = row[0];
103 std::int32_t bestIdx = 0;
104 for (std::size_t j = 1; j < k; ++j) {
105 const T v = row[j];
106 if (v < bestVal) {
107 bestVal = v;
108 bestIdx = static_cast<std::int32_t>(j);
109 }
110 }
111 outMinSq(iBase + i) = bestVal;
112 labels(iBase + i) = bestIdx;
113 }
114 };
115
116 if (pool.shouldParallelize(chunkRows, 4, 2)) {
117 pool.parallelForBlocks(std::size_t{0}, chunkRows, std::size_t{0},
118 [&](std::size_t lo, std::size_t hi) { scanRange(lo, hi); });
119 } else {
120 scanRange(0, chunkRows);
121 }
122 };
123
124 for (std::size_t iBase = 0; iBase < n; iBase += chunkCap) {
125 const std::size_t chunkRows = (iBase + chunkCap <= n) ? chunkCap : (n - iBase);
126 if constexpr (LX == Layout::Contig) {
127 auto xChunk = NDArray<T, 2, Layout::Contig>::borrow(X.data() + (iBase * d), {chunkRows, d});
128 runChunk(iBase, chunkRows, xChunk);
129 } else {
130 auto xChunk = X.slice(0, iBase, iBase + chunkRows);
131 runChunk(iBase, chunkRows, xChunk);
132 }
133 }
134}
135
143template <class T, Layout LX, Layout LC>
145 NDArray<std::int32_t, 1> &labels, NDArray<T, 1> &outMinSq,
146 Pool pool) {
147 const std::size_t n = X.dim(0);
148 const std::size_t k = C.dim(0);
149 if (n == 0 || k == 0) {
150 return;
151 }
152
153 const auto shape = chunkedMaterializedScratchShape(n, k);
154 NDArray<T, 2> distsScratch({shape[0], shape[1]});
155 pairwiseArgminMaterializedWithScratch(X, C, labels, outMinSq, distsScratch, pool);
156}
157
170template <class T, Layout LX, Layout LC>
172 const NDArray<T, 1> &cSqNorms) noexcept {
173#ifdef CLUSTERING_USE_AVX2
174 if constexpr (std::is_same_v<T, float> && LX == Layout::Contig && LC == Layout::Contig) {
175 const std::size_t n = X.dim(0);
176 const std::size_t k = C.dim(0);
177 const std::size_t d = X.dim(1);
178 if (n == 0 || k == 0 || d == 0) {
179 return false;
180 }
182 return false;
183 }
184 if (!X.template isAligned<32>() || !C.template isAligned<32>()) {
185 return false;
186 }
187 if (!cSqNorms.isContiguous()) {
188 return false;
189 }
190 return true;
191 } else {
192 (void)X;
193 (void)C;
194 (void)cSqNorms;
195 return false;
196 }
197#else
198 (void)X;
199 (void)C;
200 (void)cSqNorms;
201 return false;
202#endif
203}
204
205} // namespace detail
206
229template <class T, Layout LX = Layout::Contig, Layout LC = Layout::Contig>
231 const NDArray<T, 1> &cSqNorms, NDArray<std::int32_t, 1> &labels,
232 NDArray<T, 1> &outMinDistSq, Pool pool) {
233 static_assert(std::is_same_v<T, float> || std::is_same_v<T, double>,
234 "pairwiseArgminSqEuclidean<T> requires T to be float or double");
235
237 CLUSTERING_ALWAYS_ASSERT(outMinDistSq.isMutable());
238 CLUSTERING_ALWAYS_ASSERT(X.dim(1) == C.dim(1));
239 CLUSTERING_ALWAYS_ASSERT(labels.dim(0) == X.dim(0));
240 CLUSTERING_ALWAYS_ASSERT(outMinDistSq.dim(0) == X.dim(0));
241 CLUSTERING_ALWAYS_ASSERT(cSqNorms.dim(0) == C.dim(0));
242
243 const std::size_t n = X.dim(0);
244 const std::size_t k = C.dim(0);
245 if (n == 0 || k == 0) {
246 return;
247 }
248
249#ifdef CLUSTERING_USE_AVX2
250 if constexpr (std::is_same_v<T, float> && LX == Layout::Contig && LC == Layout::Contig) {
251 if (detail::canUseFusedArgmin(X, C, cSqNorms)) {
252 detail::pairwiseArgminOuterAvx2F32(X, C, cSqNorms, labels, outMinDistSq, pool);
253 return;
254 }
255 }
256#endif
257
258 detail::pairwiseArgminMaterialized(X, C, labels, outMinDistSq, pool);
259}
260
261namespace detail {
262
271template <class T, Layout LX = Layout::Contig, Layout LC = Layout::Contig>
273 const NDArray<T, 2, LC> &C,
274 const NDArray<T, 1> &cSqNorms,
276 NDArray<T, 1> &outMinDistSq, Pool pool) {
277 static_assert(std::is_same_v<T, float> || std::is_same_v<T, double>,
278 "pairwiseArgminSqEuclideanWithDispatchInfo<T> requires T to be float or double");
279
281 CLUSTERING_ALWAYS_ASSERT(outMinDistSq.isMutable());
282 CLUSTERING_ALWAYS_ASSERT(X.dim(1) == C.dim(1));
283 CLUSTERING_ALWAYS_ASSERT(labels.dim(0) == X.dim(0));
284 CLUSTERING_ALWAYS_ASSERT(outMinDistSq.dim(0) == X.dim(0));
285 CLUSTERING_ALWAYS_ASSERT(cSqNorms.dim(0) == C.dim(0));
286
287 const std::size_t n = X.dim(0);
288 const std::size_t k = C.dim(0);
289 if (n == 0 || k == 0) {
291 }
292
293#ifdef CLUSTERING_USE_AVX2
294 if constexpr (std::is_same_v<T, float> && LX == Layout::Contig && LC == Layout::Contig) {
295 if (canUseFusedArgmin(X, C, cSqNorms)) {
296 pairwiseArgminOuterAvx2F32(X, C, cSqNorms, labels, outMinDistSq, pool);
297 return ArgminPath::Fused;
298 }
299 }
300#endif
301
302 pairwiseArgminMaterialized(X, C, labels, outMinDistSq, pool);
304}
305
306} // namespace detail
307
308} // 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, N, Layout::MaybeStrided > slice(std::size_t axis, std::size_t begin, std::size_t end) noexcept
Borrowed half-open slice along a single axis.
Definition ndarray.h:774
static NDArray borrow(T *ptr, std::array< std::size_t, N > shape) noexcept
Borrows a contiguous buffer as an NDArray without taking ownership.
Definition ndarray.h:571
const T * data() const noexcept
Provides read-only access to the internal data array.
Definition ndarray.h:504
bool isMutable() const noexcept
Reports whether writes through operator(), Accessor, or flatIndex are allowed.
Definition ndarray.h:489
constexpr std::size_t pairwiseArgminMaxD
Maximum feature dimension for which the fused pairwiseArgminSqEuclidean driver is used.
Definition defaults.h:76
ArgminPath
Tag identifying which outer driver executed for a pairwiseArgminSqEuclidean request.
void pairwiseArgminMaterializedWithScratch(const NDArray< T, 2, LX > &X, const NDArray< T, 2, LC > &C, NDArray< std::int32_t, 1 > &labels, NDArray< T, 1 > &outMinSq, NDArray< T, 2 > &distsScratch, Pool pool)
Compute per-row argmin + minimum squared distance over n in 256-row strips using a caller-owned dista...
void pairwiseArgminMaterialized(const NDArray< T, 2, LX > &X, const NDArray< T, 2, LC > &C, NDArray< std::int32_t, 1 > &labels, NDArray< T, 1 > &outMinSq, Pool pool)
Compute per-row argmin and minimum squared distance via the materialized two-step.
bool canUseFusedArgmin(const NDArray< T, 2, LX > &X, const NDArray< T, 2, LC > &C, const NDArray< T, 1 > &cSqNorms) noexcept
Runtime predicate: true when the fused AVX2 path is eligible for this call.
ArgminPath pairwiseArgminSqEuclideanWithDispatchInfo(const NDArray< T, 2, LX > &X, const NDArray< T, 2, LC > &C, const NDArray< T, 1 > &cSqNorms, NDArray< std::int32_t, 1 > &labels, NDArray< T, 1 > &outMinDistSq, Pool pool)
Test-only: runs the same dispatch as pairwiseArgminSqEuclidean and reports which outer driver fired.
std::array< std::size_t, 2 > chunkedMaterializedScratchShape(std::size_t n, std::size_t k) noexcept
Required shape for the chunked materialized argmin scratch buffer.
void pairwiseArgminSqEuclidean(const NDArray< T, 2, LX > &X, const NDArray< T, 2, LC > &C, const NDArray< T, 1 > &cSqNorms, NDArray< std::int32_t, 1 > &labels, NDArray< T, 1 > &outMinDistSq, Pool pool)
Per-row argmin and minimum squared distance of rows of X against rows of C.
constexpr std::size_t pairwiseArgminChunkRows
Chunk height used by the materialized argmin path when striping over n.
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
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