Eigen  5.0.1
 
Loading...
Searching...
No Matches
ThreadedSparseProduct.h
1// This file is part of Eigen, a lightweight C++ template library
2// for linear algebra.
3//
4// Copyright (C) 2026 Rasmus Munk Larsen <rmlarsen@gmail.com>
5//
6// This Source Code Form is subject to the terms of the Mozilla
7// Public License v. 2.0. If a copy of the MPL was not distributed
8// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
9// SPDX-License-Identifier: MPL-2.0
10
11#ifndef EIGEN_THREADED_SPARSE_PRODUCT_H
12#define EIGEN_THREADED_SPARSE_PRODUCT_H
13
14// IWYU pragma: private
15#include "./InternalHeaderCheck.h"
16
17namespace Eigen {
18
19namespace internal {
20
21inline ThreadPool& default_threaded_sparse_pool() {
22 static ThreadPool pool(numext::maxi<unsigned>(1u, std::thread::hardware_concurrency()));
23 return pool;
24}
25
26// nnz-balanced partition of an outer range [0, outerSize) into numChunks
27// contiguous chunks. boundaries[t] = first outer index owned by partition t;
28// boundaries[numChunks] = outerSize.
29//
30// The split uses std::lower_bound on the outer-index array. Targets are
31// monotonically increasing in t, so each search starts from the previous
32// boundary; total work is bounded by O(numChunks + log outerSize) rather than
33// numChunks * log(outerSize). Each chunk's nnz count differs from the ideal by
34// at most max_nnz_per_outer.
35template <typename StorageIndex>
36inline void compute_nnz_balanced_partition(const StorageIndex* outer, Index outerSize, Index totalNnz, int numChunks,
37 std::vector<Index>& boundaries) {
38 boundaries.assign(numChunks + 1, 0);
39 boundaries[numChunks] = outerSize;
40 if (numChunks <= 1 || outerSize == 0 || totalNnz == 0) return;
41 const StorageIndex* const last = outer + outerSize + 1;
42 const StorageIndex* lo = outer;
43 for (int t = 1; t < numChunks; ++t) {
44 Index target = (static_cast<Index>(t) * totalNnz) / numChunks;
45 lo = std::lower_bound(lo, last, static_cast<StorageIndex>(target));
46 boundaries[t] = lo - outer;
47 }
48}
49
50// Single-row dot-product kernel. Used as the body of a per-row OpenMP
51// `parallel for` (which can do its own dynamic scheduling) and from within
52// run_dot_chunk for the ThreadPool dispatch path. Marked ALWAYS_INLINE so
53// the loop body is visible to the OMP iteration scheduler for vectorization.
54template <bool Conjugate, bool Overwrite, typename Scalar, typename StorageIndex, typename XScalar, typename YScalar,
55 typename AlphaT>
56EIGEN_ALWAYS_INLINE void run_dot_row(const Scalar* EIGEN_RESTRICT vals, const StorageIndex* EIGEN_RESTRICT inner,
57 const StorageIndex* EIGEN_RESTRICT outer,
58 const StorageIndex* EIGEN_RESTRICT innerNnz, const XScalar* EIGEN_RESTRICT x,
59 YScalar* EIGEN_RESTRICT y, Index i, const AlphaT& alpha) {
60 const Index k0 = outer[i];
61 const Index end = innerNnz ? (k0 + innerNnz[i]) : outer[i + 1];
62 Scalar s0(0), s1(0);
63 Index k = k0;
64 const conj_if<Conjugate> cj{};
65 // Loop structure mirrors the existing kernel in SparseDenseProduct.h so
66 // the compiler vectorizes identically.
67 for (; k < end; ++k) {
68 s0 += cj(vals[k]) * x[inner[k]];
69 ++k;
70 if (k < end) s1 += cj(vals[k]) * x[inner[k]];
71 }
72 const Scalar s = s0 + s1;
73 EIGEN_IF_CONSTEXPR (Overwrite) {
74 y[i] = alpha * s;
75 } else {
76 y[i] += alpha * s;
77 }
78}
79
80// Dot-product-per-outer kernel. Used by both forward and adjoint paths; the
81// adjoint path sets Conjugate=true (no-op for real Scalar).
82//
83// Processes outer indices [lo, hi). For each outer i:
84// if Overwrite: y[i] = alpha * sum_k op(val[k]) * x[inner[k]]
85// else: y[i] += alpha * sum_k op(val[k]) * x[inner[k]]
86// where op(.) is conj(.) iff Conjugate.
87//
88// Writes are independent across threads (each thread owns a contiguous output
89// range), so no synchronization is required.
90template <bool Conjugate, bool Overwrite, typename Scalar, typename StorageIndex, typename XScalar, typename YScalar,
91 typename AlphaT>
92EIGEN_STRONG_INLINE void run_dot_chunk(const Scalar* EIGEN_RESTRICT vals, const StorageIndex* EIGEN_RESTRICT inner,
93 const StorageIndex* EIGEN_RESTRICT outer,
94 const StorageIndex* EIGEN_RESTRICT innerNnz, const XScalar* EIGEN_RESTRICT x,
95 YScalar* EIGEN_RESTRICT y, Index lo, Index hi, const AlphaT& alpha) {
96 for (Index i = lo; i < hi; ++i) {
97 run_dot_row<Conjugate, Overwrite, Scalar, StorageIndex>(vals, inner, outer, innerNnz, x, y, i, alpha);
98 }
99}
100
101} // namespace internal
102
150template <typename SparseMatrixType_>
151class ThreadedSparseProduct {
152 public:
153 typedef SparseMatrixType_ SparseMatrixType;
154 typedef typename SparseMatrixType::Scalar Scalar;
155 typedef typename SparseMatrixType::RealScalar RealScalar;
156 typedef typename SparseMatrixType::StorageIndex StorageIndex;
157
158 enum { IsRowMajor = static_cast<int>(SparseMatrixType::IsRowMajor) };
159
160 // Opposite-storage-order mirror used by the conflict-free path for whichever
161 // direction doesn't match A's native order.
163
164 // The dot-product kernel walks x/y via raw `Scalar*` with unit inner
165 // stride; constraining the public API to `Ref` forces compatible layout
166 // (binding for plain matrices, vectors, and unit-stride Maps/Blocks;
167 // copy-evaluation for non-conforming const inputs; runtime error for
168 // non-conforming mutable outputs).
169 typedef Matrix<Scalar, Dynamic, 1> DenseVector;
170 typedef Ref<const DenseVector> ConstVectorRef;
171 typedef Ref<DenseVector> MutableVectorRef;
172
173 ThreadedSparseProduct() = default;
174
175 explicit ThreadedSparseProduct(const SparseMatrixType& mat, ThreadPool* pool = nullptr) : m_pool(pool) {
176 analyzePattern(mat);
177 }
178
179 ~ThreadedSparseProduct() { delete m_mirror.load(std::memory_order_acquire); }
180
181 ThreadedSparseProduct(const ThreadedSparseProduct&) = delete;
182 ThreadedSparseProduct& operator=(const ThreadedSparseProduct&) = delete;
183
186 ThreadedSparseProduct& analyzePattern(const SparseMatrixType& mat) {
187 eigen_assert(mat.isCompressed() && "ThreadedSparseProduct requires a compressed SparseMatrix");
188 m_mat = &mat;
189 delete m_mirror.exchange(nullptr, std::memory_order_acq_rel);
190 m_adj_partition.clear();
191
192 build_partition(mat.outerIndexPtr(), mat.outerSize(), mat.nonZeros(), m_native_partition);
193 return *this;
194 }
195
196 ThreadedSparseProduct& compute(const SparseMatrixType& mat) { return analyzePattern(mat); }
197
202 ThreadedSparseProduct& refreshValues() {
203 delete m_mirror.exchange(nullptr, std::memory_order_acq_rel);
204 return *this;
205 }
206
207 Index rows() const { return m_mat ? m_mat->rows() : Index(0); }
208 Index cols() const { return m_mat ? m_mat->cols() : Index(0); }
209
211 void apply(const ConstVectorRef& x, MutableVectorRef y) const {
212 apply_impl<false, /*Overwrite=*/true>(x, y, Scalar(1));
213 }
214
215 void applyAdjoint(const ConstVectorRef& x, MutableVectorRef y) const {
216 apply_impl<true, /*Overwrite=*/true>(x, y, Scalar(1));
217 }
218
219 void applyAddTo(const ConstVectorRef& x, MutableVectorRef y, const Scalar& alpha) const {
220 apply_impl<false, /*Overwrite=*/false>(x, y, alpha);
221 }
222
223 void applyAdjointAddTo(const ConstVectorRef& x, MutableVectorRef y, const Scalar& alpha) const {
224 apply_impl<true, /*Overwrite=*/false>(x, y, alpha);
225 }
226
228 ThreadPool* pool() const { return m_pool ? m_pool : &internal::default_threaded_sparse_pool(); }
229
231 bool hasMirror() const { return m_mirror.load(std::memory_order_acquire) != nullptr; }
232
233 private:
234 template <bool Adjoint, bool Overwrite>
235 void apply_impl(const ConstVectorRef& x, MutableVectorRef& y, const Scalar& alpha) const {
236 eigen_assert(m_mat && "ThreadedSparseProduct: matrix not set; call analyzePattern() first");
237 if (Adjoint) {
238 eigen_assert(x.size() == m_mat->rows());
239 eigen_assert(y.size() == m_mat->cols());
240 } else {
241 eigen_assert(x.size() == m_mat->cols());
242 eigen_assert(y.size() == m_mat->rows());
243 }
244 // The kernel reads x and writes y concurrently across threads; aliasing
245 // (x and y overlap) would mis-compute because some x[k] reads see y
246 // writes from the same SpMV call. Cheap address-range check; cast to
247 // uintptr_t because comparing pointers from unrelated allocations is
248 // technically UB in C++.
249 eigen_assert((x.size() == 0 || y.size() == 0 || std::uintptr_t(x.data() + x.size()) <= std::uintptr_t(y.data()) ||
250 std::uintptr_t(y.data() + y.size()) <= std::uintptr_t(x.data())) &&
251 "ThreadedSparseProduct: x and y must not overlap");
252 // Decide which storage to read from.
253 // Forward kernel iterates a RowMajor view of A; adjoint kernel iterates
254 // a ColMajor view of A. If A already has the required order, read it
255 // directly; otherwise build/use the mirror.
256 constexpr bool NeedRowMajorView = !Adjoint;
257 constexpr bool NativeIsRowMajor = IsRowMajor;
258 constexpr bool UseMirror = NeedRowMajorView != NativeIsRowMajor;
259
260 EIGEN_IF_CONSTEXPR (UseMirror) {
261 const MirrorType& m = ensure_mirror();
262 run<Adjoint, Overwrite>(m.valuePtr(), m.innerIndexPtr(), m.outerIndexPtr(), m.innerNonZeroPtr(),
263 /*outerSize=*/m.outerSize(), m_adj_partition, x, y, alpha);
264 } else {
265 run<Adjoint, Overwrite>(m_mat->valuePtr(), m_mat->innerIndexPtr(), m_mat->outerIndexPtr(),
266 m_mat->innerNonZeroPtr(),
267 /*outerSize=*/m_mat->outerSize(), m_native_partition, x, y, alpha);
268 }
269 }
270
271 // Serial-fallback threshold; matches the same constant in SparseDenseProduct.h.
272 static constexpr Index kThreadingThreshold = 20000;
273
274 template <bool Conjugate, bool Overwrite>
275 void run(const Scalar* vals, const StorageIndex* inner, const StorageIndex* outer, const StorageIndex* innerNnz,
276 Index outerSize, const std::vector<Index>& part, const ConstVectorRef& x, MutableVectorRef& y,
277 const Scalar& alpha) const {
278 // OpenMP path doesn't use the cached partition (dynamic scheduling balances
279 // itself), so derive T fresh from the current Eigen::nbThreads() /
280 // OMP_NUM_THREADS at apply time -- otherwise `setNbThreads()` after
281 // analyzePattern() would be silently ignored. The ThreadPool path is
282 // bound to the partition built for a specific T at analyzePattern() time.
283#ifdef EIGEN_HAS_OPENMP
284 const int T = target_thread_count();
285#else
286 const int T = static_cast<int>(part.size()) - 1;
287#endif
288 const Index total_nnz = m_mat->nonZeros();
289 // Ref construction already enforced unit inner stride for x/y.
290 const Scalar* xp = x.data();
291 Scalar* yp = y.data();
292 if (T <= 1 || total_nnz < kThreadingThreshold) {
293 internal::run_dot_chunk<Conjugate, Overwrite, Scalar, StorageIndex>(vals, inner, outer, innerNnz, xp, yp, 0,
294 outerSize, alpha);
295 return;
296 }
297
298#ifdef EIGEN_HAS_OPENMP
299 // Prefer OpenMP for dispatch when available. Use dynamic scheduling
300 // over rows with chunks sized so the OMP runtime gets ~4*T chunks to
301 // distribute -- enough granularity to absorb residual nnz imbalance
302 // without blowing dispatch overhead.
303 const Index chunk = numext::maxi<Index>(Index(1), (outerSize + Index(T) * 4 - 1) / (Index(T) * 4));
304#pragma omp parallel for schedule(dynamic, chunk) num_threads(T)
305 for (Index i = 0; i < outerSize; ++i) {
306 internal::run_dot_row<Conjugate, Overwrite, Scalar, StorageIndex>(vals, inner, outer, innerNnz, xp, yp, i, alpha);
307 }
308#else
309 // ThreadPool path: enqueue T-1 worker tasks, run partition 0 on this
310 // thread, then wait on the barrier. Avoids ForkJoin's log(T)-hop critical
311 // path before the last leaf starts.
312 Barrier barrier(static_cast<unsigned>(T));
313 ThreadPool* p = pool();
314 for (int t = 1; t < T; ++t) {
315 const Index lo = part[t], hi = part[t + 1];
316 if (lo == hi) {
317 barrier.Notify();
318 continue;
319 }
320 p->Schedule([=, &barrier]() {
321 internal::run_dot_chunk<Conjugate, Overwrite, Scalar, StorageIndex>(vals, inner, outer, innerNnz, xp, yp, lo,
322 hi, alpha);
323 barrier.Notify();
324 });
325 }
326 internal::run_dot_chunk<Conjugate, Overwrite, Scalar, StorageIndex>(vals, inner, outer, innerNnz, xp, yp, part[0],
327 part[1], alpha);
328 barrier.Notify();
329 barrier.Wait();
330#endif
331 }
332
333 // Lazy mirror construction. Double-checked atomic init avoids per-call
334 // std::call_once cost (which historically varies across libstdc++ versions).
335 const MirrorType& ensure_mirror() const {
336 MirrorType* m = m_mirror.load(std::memory_order_acquire);
337 if (m) return *m;
338 std::lock_guard<std::mutex> lock(m_mirror_init_mu);
339 m = m_mirror.load(std::memory_order_relaxed);
340 if (!m) {
341 // Same logical matrix, opposite storage order. Eigen detects the
342 // storage-order mismatch in assignment and reorganizes the data
343 // (a "structural transpose"); the logical matrix is preserved, no
344 // conjugation. The kernel applies conj at use when needed.
345 // Hold the mirror in a unique_ptr while building: the assignment,
346 // makeCompressed(), and build_partition() can all throw (bad_alloc), and a
347 // raw owning pointer would leak. Hand ownership to the atomic only once the
348 // mirror is fully built; on throw the buffer is freed and m_mirror stays
349 // null so a later call rebuilds cleanly.
350 std::unique_ptr<MirrorType> built(new MirrorType(m_mat->rows(), m_mat->cols()));
351 *built = *m_mat;
352 built->makeCompressed();
353 build_partition(built->outerIndexPtr(), built->outerSize(), built->nonZeros(), m_adj_partition);
354 m = built.get();
355 m_mirror.store(built.release(), std::memory_order_release);
356 }
357 return *m;
358 }
359
360 // Thread count target. Under OpenMP, respect Eigen::setNbThreads() /
361 // OMP_NUM_THREADS instead of the lazy default ThreadPool's size (which
362 // also avoids constructing that pool when OMP does the dispatch).
363 int target_thread_count() const {
364#ifdef EIGEN_HAS_OPENMP
365 return numext::maxi(1, Eigen::nbThreads());
366#else
367 return pool()->NumThreads();
368#endif
369 }
370
371 // nnz-balanced partition of A's outer dim into T contiguous chunks, with
372 // a guard against pathological skew: if more than half the partitions end
373 // up empty (one hub outer holding most of the nnz), fall back to a single
374 // serial chunk so the inner kernel doesn't pay parallel dispatch for no
375 // parallel work.
376 void build_partition(const StorageIndex* outer, Index outerSize, Index nnz, std::vector<Index>& part) const {
377 const int T = target_thread_count();
378 internal::compute_nnz_balanced_partition(outer, outerSize, nnz, T, part);
379 int non_empty = 0;
380 for (std::size_t t = 0; t + 1 < part.size(); ++t)
381 if (part[t + 1] > part[t]) ++non_empty;
382 if (non_empty * 2 < T) {
383 part.assign(2, 0);
384 part[1] = outerSize;
385 }
386 }
387
388 private:
389 const SparseMatrixType* m_mat = nullptr;
390 ThreadPool* m_pool = nullptr;
391
392 // Partition of A's native outer dim by nnz balance. Used by the direction
393 // whose kernel matches A's storage order.
394 std::vector<Index> m_native_partition;
395
396 // Lazy adjoint-direction mirror in the opposite storage order, plus its
397 // own nnz-balanced partition. Constructed on first use.
398 mutable std::atomic<MirrorType*> m_mirror{nullptr};
399 mutable std::mutex m_mirror_init_mu;
400 mutable std::vector<Index> m_adj_partition;
401};
402
403} // namespace Eigen
404
405#endif // EIGEN_THREADED_SPARSE_PRODUCT_H
The matrix class, also used for vectors and row-vectors.
Definition Matrix.h:188
A matrix or vector expression mapping an existing expression.
Definition Ref.h:262
A versatile sparse matrix representation.
Definition SparseMatrix.h:122
Cached, thread-parallel sparse matrix * dense vector product.
Definition ThreadedSparseProduct.h:151
ThreadPool * pool() const
Returns the thread pool used by this operator.
Definition ThreadedSparseProduct.h:228
ThreadedSparseProduct & analyzePattern(const SparseMatrixType &mat)
Definition ThreadedSparseProduct.h:186
ThreadedSparseProduct & refreshValues()
Definition ThreadedSparseProduct.h:202
void applyAdjointAddTo(const ConstVectorRef &x, MutableVectorRef y, const Scalar &alpha) const
Accumulating adjoint apply: y += alpha * A^H * x.
Definition ThreadedSparseProduct.h:223
bool hasMirror() const
True iff the lazy adjoint mirror has been materialized.
Definition ThreadedSparseProduct.h:231
void apply(const ConstVectorRef &x, MutableVectorRef y) const
Overwriting forward apply: y = A * x.
Definition ThreadedSparseProduct.h:211
void applyAdjoint(const ConstVectorRef &x, MutableVectorRef y) const
Overwriting adjoint apply: y = A^H * x.
Definition ThreadedSparseProduct.h:215
void applyAddTo(const ConstVectorRef &x, MutableVectorRef y, const Scalar &alpha) const
Accumulating forward apply: y += alpha * A * x.
Definition ThreadedSparseProduct.h:219