blob: 07e2fdf3d41924a0b4a891409b12965fb52d6f9a [file] [view] [edit]
# Eigen GPU Module (`contrib/Eigen/GPU`)
GPU-accelerated linear algebra for Eigen users, dispatching to NVIDIA CUDA
Math Libraries (cuBLAS, cuSOLVER, cuFFT, cuSPARSE, cuDSS). Requires CUDA 11.4+;
cuDSS features require CUDA 12.0+ and a separate cuDSS install. Header-only.
This module dispatches rather than reimplements, so numerical behavior,
supported shapes and scalar types, and performance characteristics are the
vendor libraries'. Their documentation is the reference for anything this file
does not state:
| Library | Used for | Documentation |
|---------|----------|---------------|
| CUDA Toolkit | streams, memory, error codes | <https://docs.nvidia.com/cuda/> |
| cuBLAS (incl. cuBLASLt) | `DeviceMatrix` products, BLAS-1 | <https://docs.nvidia.com/cuda/cublas/> |
| cuSOLVER | dense LLT / LU / QR / SVD / EVD | <https://docs.nvidia.com/cuda/cusolver/> |
| cuSPARSE | SpMV / SpMM (CSC and BSR) | <https://docs.nvidia.com/cuda/cusparse/> |
| cuFFT | `gpu::FFT` | <https://docs.nvidia.com/cuda/cufft/> |
| NPP | device-side scalar and coefficient-wise arithmetic | <https://docs.nvidia.com/cuda/npp/> |
| cuDSS | sparse direct solvers (separate install) | <https://docs.nvidia.com/cuda/cudss/> |
## Why this module
Eigen is the linear algebra foundation for a large ecosystem of C++ projects
in robotics (ROS, Drake, MoveIt, Pinocchio), computer vision (OpenCV, COLMAP,
Open3D), scientific computing (Ceres, Stan), and beyond. Many of these
projects run on GPU-equipped hardware but cannot use GPUs for Eigen operations
without dropping down to raw CUDA library APIs.
GPU sparse solvers are a particularly acute gap. Sparse factorization is the
bottleneck in SLAM, bundle adjustment, FEM, and nonlinear optimization --
exactly the workloads where GPU acceleration matters most. Downstream projects
like [Ceres](https://github.com/ceres-solver/ceres-solver/issues/1151) and
[COLMAP](https://github.com/colmap/colmap/issues/4018) have open requests for
GPU-accelerated sparse solvers, and third-party projects like
[cholespy](https://github.com/rgl-epfl/cholespy) exist specifically because
Eigen lacks them. The `contrib/Eigen/GPU` module provides GPU sparse Cholesky, LDL^T,
and LU factorization via cuDSS, alongside dense solvers (cuSOLVER), matrix
products (cuBLAS), FFT (cuFFT), and sparse matrix-vector products (cuSPARSE).
Existing Eigen users should be able to move performance-critical dense or
sparse linear algebra to the GPU with minimal code changes and without
learning CUDA library APIs directly.
## Design philosophy
**CPU and GPU coexist.** There is no global compile-time switch that replaces
CPU implementations (unlike `EIGEN_USE_LAPACKE`). Users choose GPU solvers
explicitly -- `gpu::LLT<double>` vs `Eigen::LLT<MatrixXd>`,
`gpu::SparseLLT<double>` vs `SimplicialLLT<SparseMatrix<double>>` -- and both
coexist in the same binary.
This also lets users keep the factored matrix on device across multiple solves,
something impossible with compile-time replacement.
**Familiar syntax.** GPU operations use the same expression patterns as CPU
Eigen. Here is a side-by-side comparison:
```cpp
// ---- CPU (Eigen) ---- // ---- GPU (contrib/Eigen/GPU) ----
#include <Eigen/Dense> #define EIGEN_USE_GPU
#include <contrib/Eigen/GPU>
// Dense
MatrixXd A = ...; auto d_A = gpu::DeviceMatrix<double>::fromHost(A);
MatrixXd B = ...; auto d_B = gpu::DeviceMatrix<double>::fromHost(B);
MatrixXd C = A * B; gpu::DeviceMatrix<double> d_C = d_A * d_B;
MatrixXd X = A.llt().solve(B); gpu::DeviceMatrix<double> d_X = d_A.llt().solve(d_B);
MatrixXd X = d_X.toHost();
// Sparse (using SpMat = SparseMatrix<double>)
SimplicialLLT<SpMat> llt(A); gpu::SparseLLT<double> llt(A);
VectorXd x = llt.solve(b); VectorXd x = llt.solve(b);
```
The GPU version reads like CPU Eigen with explicit upload/download for dense
operations, and an almost identical API for sparse solvers. Expressions can
copy-initialize a `DeviceMatrix` directly (as above), scalar factors accept
plain literals (`2 * d_A`, `d_A / 2`, `-d_A`), and unsupported expressions are
compile errors.
**Standalone module.** `contrib/Eigen/GPU` does not modify or depend on Eigen's Core
expression template system (`MatrixBase`, `CwiseBinaryOp`, etc.).
`DeviceMatrix` is not an Eigen expression type and does not inherit from
`MatrixBase`. The expression layer is a thin compile-time dispatch where every
supported expression maps to a single NVIDIA library call. There is no
coefficient-level evaluation, lazy fusion, or packet operations.
**Interoperability where useful.** `DeviceMatrix` provides the same operator
signatures as `Matrix` for common vector operations: `+=`, `-=`, `*=`, `/=`,
`dot()`, `squaredNorm()`, `norm()`, `stableNorm()`, `setZero()`, `noalias()`,
and copy construction and assignment (a device-to-device copy). This makes
`DeviceMatrix` usable as a drop-in `VectorType` in Eigen algorithm templates
that rely on these operations, and `DeviceSparseView` is a matrix-free matrix
type for Eigen's iterative solvers: `ConjugateGradient<gpu::DeviceSparseView<double>,
Lower | Upper>` runs Eigen's own algorithm, unmodified, on device vectors (see
[Eigen algorithm interop](#eigen_gpu_interop)).
Conjugate gradient is just the motivating example; we are open to expanding
operator coverage as needed to support other high-level Eigen algorithms on the
GPU.
**Explicit over implicit.** Host-device transfers, stream management, and
library handle lifetimes are visible in the API. There are no hidden
allocations or synchronizations except where documented (e.g., `toHost()` must
synchronize to deliver data to the host).
## Key concepts
### `gpu::DeviceMatrix<Scalar>`
A typed RAII wrapper for a dense column-major matrix in GPU device memory.
This is the GPU counterpart of Eigen's `MatrixX<Scalar>`. A vector is simply
a `DeviceMatrix` with one column. All public GPU classes live in `namespace
Eigen::gpu`.
```cpp
// Upload from host
auto d_A = gpu::DeviceMatrix<double>::fromHost(A);
// Allocate uninitialized
gpu::DeviceMatrix<double> d_C(m, n);
// Download to host
MatrixXd C = d_C.toHost();
// Async download (returns a future)
auto transfer = d_C.toHostAsync();
// ... do other work ...
MatrixXd C = transfer.get();
```
`DeviceMatrix` supports expression methods that mirror Eigen's API:
`adjoint()`, `transpose()`, `triangularView<UpLo>()`,
`selfadjointView<UpLo>()`, `llt()`, `lu()`. These return lightweight
expression objects that are evaluated when assigned.
For BLAS Level-1 operations, `DeviceMatrix` also provides `dot()`, `norm()`,
`squaredNorm()`, `setZero()`, `noalias()`, and arithmetic operators
(`+=`, `-=`, `*=`) that dispatch to cuBLAS `axpy`, `nrm2`, `dot`, `scal`,
and `geam`. These are the operations needed by iterative solvers.
### `gpu::DeviceScalar<Scalar>`
A device-resident scalar value. Reductions like `dot()`, `norm()`, and
`squaredNorm()` return `DeviceScalar` instead of a host scalar, deferring
the host synchronization until the value is actually needed:
```cpp
auto dot_val = d_x.dot(d_y); // DeviceScalar -- no sync
auto norm_sq = d_r.squaredNorm(); // DeviceScalar -- no sync
Scalar alpha = dot_val / norm_sq; // sync here (implicit conversion)
d_x += alpha * d_p; // host scalar * DeviceMatrix (axpy)
```
Every reduction also has an overload that writes into an existing
`DeviceScalar` on a given Context, `d_x.dot(ctx, d_y, s)`, where `s` lives on
`ctx.stream()`: it reuses the scalar's storage, so a loop, or a captured CUDA
graph, repeats the reduction without allocating.
Division between `DeviceScalar` values (real types only) is performed on
device via NPP, avoiding extra synchronizations. Small device allocations
(including `DeviceScalar`) go through the stream-ordered allocator like every
other block when the device has memory pools; on the `cudaMalloc` fallback
path they are recycled through a thread-local `DeviceBufferPool` instead, to
avoid `cudaMalloc`/`cudaFree` overhead in tight loops.
Pool contract: a released block is recycled only after the device has retired
every operation enqueued before the release on any blocking stream, so pooled
buffers may move between the streams of one thread. The release is tracked by
an event on the legacy default stream, the same ordering the stream-ordered
allocator relies on. The pool is thread-local, so sharing a pooled buffer
across threads needs external synchronization, and `cudaStreamNonBlocking`
streams are outside the guarantee.
### `gpu::Context`
Every GPU operation needs a CUDA stream and library handles (cuBLAS eagerly,
cuSOLVER / cuBLASLt / cuSPARSE lazily on first use). `gpu::Context` bundles
these together. A single `Context` is not thread-safe -- use one per thread
(or external synchronization), since the underlying NVIDIA library handles
are not thread-safe per handle.
For simple usage, you don't need to create one -- a per-thread default context
is created lazily on first use:
```cpp
// These use the thread-local default context automatically
d_C = d_A * d_B;
d_X = d_A.llt().solve(d_B);
```
For concurrent multi-stream execution, create explicit contexts:
```cpp
gpu::Context ctx1, ctx2;
d_C1.device(ctx1) = d_A1 * d_B1; // runs on stream 1
d_C2.device(ctx2) = d_A2 * d_B2; // runs on stream 2 (concurrently)
```
To integrate with existing CUDA code, borrow an existing stream:
```cpp
gpu::Context ctx(my_existing_stream); // wraps stream, does not take ownership
```
To override the thread-local default (e.g., in CG where all ops share one
context):
```cpp
gpu::Context ctx;
gpu::Context::setThreadLocal(&ctx); // all threadLocal() calls return ctx
// ... GPU operations ...
gpu::Context::setThreadLocal(nullptr); // restore lazy-created default
```
### Linking {#eigen_gpu_linking}
The module is header-only, but each feature pulls in the corresponding NVIDIA
library at link time. cuSOLVER, cuBLASLt, and cuSPARSE are created lazily on
first use, so a translation unit that only uses cuBLAS or cuFFT does not need
to link the others:
| Feature | Link flags |
|-----------------------------------------|---------------------------|
| `DeviceMatrix`, GEMM, TRSM, SYMM, SYRK | `-lcublas -lcublasLt` |
| Dense solvers (LLT, LU, QR, SVD, EVD) | `-lcusolver -lcublas` |
| FFT (`gpu::FFT`) | `-lcufft -lcublas` |
| SpMV / SpMM (`gpu::SparseContext`) | `-lcusparse -lcublas` |
| `norm()`, `DeviceScalar` arithmetic, `/=`, `cwiseProduct` | `-lnpps -lnppc` |
| Sparse direct solvers (cuDSS) | `-lcudss -lcublas` |
cuBLAS is required by `DeviceMatrix` itself (every `Context` creates a cuBLAS
handle eagerly) and is also a runtime dependency of cuDSS, so it is the one
constant. cuDSS additionally requires `EIGEN_CUDSS` to be defined before
including `contrib/Eigen/GPU`.
## Usage
### Matrix operations (cuBLAS)
Products dispatch to [cuBLAS](https://docs.nvidia.com/cuda/cublas/), GEMM
through its cuBLASLt API; see [Precision control](#eigen_gpu_precision) for which
compute type that selects.
```cpp
auto d_A = gpu::DeviceMatrix<double>::fromHost(A);
auto d_B = gpu::DeviceMatrix<double>::fromHost(B);
// GEMM: C = A * B, C = A^H * B, C = A * B^T, ...
gpu::DeviceMatrix<double> d_C = d_A * d_B;
d_C = d_A.adjoint() * d_B;
d_C = d_A * d_B.transpose();
// Scaled and accumulated
d_C += 2.0 * d_A * d_B; // alpha=2, beta=1
d_C -= d_A * d_B; // alpha=-1, beta=1
d_C.device(ctx) -= d_A * d_B; // same, on an explicit stream
// Triangular solve (TRSM)
d_X = d_A.triangularView<Lower>().solve(d_B);
// Symmetric/Hermitian multiply (SYMM/HEMM)
d_C = d_A.selfadjointView<Lower>() * d_B;
// Rank-k update (SYRK/HERK)
d_C.selfadjointView<Lower>().rankUpdate(d_A); // C += A * A^H
```
### BLAS Level-1 operations
Dot products, norms and vector arithmetic map to the corresponding
[cuBLAS](https://docs.nvidia.com/cuda/cublas/) Level-1 routines, except for
device-side scalar arithmetic, which uses the signal-processing functions of
[NPP](https://docs.nvidia.com/cuda/npp/).
```cpp
// Dot product and norms (return DeviceScalar -- no sync until read)
auto dot_val = d_x.dot(d_y); // cublasDdot / cublasCdotc
auto norm_val = d_r.norm(); // sqrt(dot): cublasDdot, then NPP sqrt on device
double n = norm_val; // implicit conversion triggers sync
// Vector arithmetic (cuBLAS axpy / geam)
d_x += alpha * d_p; // axpy: x = x + alpha * p
d_x -= alpha * d_p; // axpy: x = x - alpha * p
d_x *= alpha; // scal: x = alpha * x
d_x /= alpha; // NPP divide-by-constant: x = x / alpha (true division for real Scalar)
d_r.setZero(); // cudaMemsetAsync
auto s = d_r.stableNorm(); // cublasDnrm2: scaled, overflow-safe
// Copies are device-to-device (cuBLAS copy) on the thread-local Context; no host round trip.
// They exist so that existing Eigen algorithm code runs on the GPU unchanged; code written for
// the GPU should avoid them (move, or reuse a buffer with copyFrom), since each is a transfer.
gpu::DeviceMatrix<double> d_q = d_p; // copy construction
d_q = d_r; // copy assignment, resizes if needed
// DeviceScalar arithmetic (stays on device, real types only)
auto alpha = absNew / dot_val; // device-side division via NPP
d_x += alpha * d_p; // DeviceScalar * DeviceMatrix (axpy with device pointer)
// Matrix add/subtract/scale (cuBLAS geam)
gpu::DeviceMatrix<double> d_C = d_A + d_B; // C = A + B
d_C = d_A + 2.0 * d_B; // C = A + 2*B
d_C = d_A - d_B; // C = A - B
d_C = 2 * d_A - 3 * d_B; // scaled both sides, int literals fine
d_C = -d_A; // unary minus
d_C = d_A / 2.0; // divide by scalar
d_C = 0.5 * d_C; // in-place rescale (aliasing-safe)
```
### Dense solvers (cuSOLVER)
Backed by the dense part of [cuSOLVER](https://docs.nvidia.com/cuda/cusolver/)
(cuSolverDN), whose documentation defines what each factorization returns and
when it reports a numerical failure.
**One-shot expression syntax** -- "one-shot" means factorization and solve
run as a single fused call with no persistent factorization object; each
evaluation re-factorizes:
```cpp
// Cholesky solve (potrf + potrs)
gpu::DeviceMatrix<double> d_X = d_A.llt().solve(d_B);
// LU solve (getrf + getrs)
d_Y = d_A.lu().solve(d_B);
```
Scratch for the one-shot form (factor copy, cuSOLVER workspace, info words)
lives in the `gpu::Context` and grows monotonically — repeated one-shot solves
perform no per-call allocations. In debug builds each call verifies the
factorization status (one stream synchronization); release builds
(`EIGEN_NO_DEBUG`/`NDEBUG`) skip the check *and* the sync, making the
expression fully asynchronous — use the cached `gpu::LLT` / `gpu::LU` classes
and `info()` when numerical failure must be detected.
cuSOLVER 11.4.1 and 11.4.2 (CUDA 11.8 and 12.0) have a defect here:
`cusolverDnXpotrf` reports success on a matrix that is not positive definite
and returns a factor full of NaN, so `gpu::LLT::info()` cannot detect that
failure on those versions. cuSOLVER 11.4.4 (CUDA 12.1) and later report it.
**Cached factorization** -- Factor once, solve many times:
```cpp
gpu::LLT<double> llt;
llt.compute(d_A); // factorize (async)
if (llt.info() != Success) { ... } // lazy sync on first info() call
auto d_X1 = llt.solve(d_B1); // reuses factor (async)
auto d_X2 = llt.solve(d_B2); // reuses factor (async)
auto d_X3 = llt.solve(std::move(d_B3)); // in-place: consumes RHS, no copy/alloc
MatrixXd X2 = d_X2.toHost();
// Bind a solver to an existing Context: work runs on ctx's stream with its
// handles, so it chains with GEMM/SpMV on the same Context without
// cross-stream event waits. All five dense solvers (LLT, LU, QR, SVD,
// SelfAdjointEigenSolver) support this.
gpu::Context ctx;
gpu::LLT<double> llt_ctx(ctx, d_A);
// LU with transpose solve
gpu::LU<double> lu;
lu.compute(d_A);
auto d_Y = lu.solve(d_B, gpu::GpuOp::Trans); // A^T Y = B
// QR solve (overdetermined least squares)
gpu::QR<double> qr;
qr.compute(d_A); // factorize on device (async)
auto d_X = qr.solve(d_B); // Q^H * B via ormqr, then trsm on R
MatrixXd X = d_X.toHost();
// SVD (results downloaded on access)
gpu::SVD<double> svd;
svd.compute(d_A, ComputeThinU | ComputeThinV);
VectorXd S = svd.singularValues(); // downloads to host
MatrixXd U = svd.matrixU(); // downloads to host
MatrixXd V = svd.matrixV(); // V (matches JacobiSVD)
MatrixXd VT = svd.matrixVT(); // V^T (matches cuSOLVER)
// SVD: device-side views (no D2H transfer; svd must outlive the views)
auto d_S = svd.d_singularValues(); // DeviceMatrix view of singular values
auto d_U = svd.d_matrixU(); // DeviceMatrix view of U
auto d_VT = svd.d_matrixVT(); // DeviceMatrix view of V^T
// Self-adjoint eigenvalue decomposition
gpu::SelfAdjointEigenSolver<double> es;
es.compute(d_A);
VectorXd eigenvals = es.eigenvalues(); // downloads to host
MatrixXd eigenvecs = es.eigenvectors(); // downloads to host
auto d_W = es.d_eigenvalues(); // DeviceMatrix view of eigenvalues
auto d_V = es.d_eigenvectors(); // DeviceMatrix view of eigenvectors
```
The cached API keeps the factored matrix on device, avoiding redundant
host-device transfers and re-factorizations. All five solvers accept
`compute(DeviceMatrix&&)` to adopt the input and factor it in place with no
copy (for QR/SVD with m < n the internal transpose still copies, and a view is
always copied because its storage belongs to another object), and all five
can bind to a `gpu::Context` to share its stream and handles. All solvers also
accept host dense expressions directly as a convenience (e.g.,
`gpu::LLT<double> llt(A)` or `qr.solve(B)`), which handles upload/download
internally. Host `compute()` finishes its upload before returning, while
factorization remains asynchronous. The `d_*` accessors
on `gpu::SVD` and `gpu::SelfAdjointEigenSolver` return non-owning
`DeviceMatrix` views so downstream cuBLAS/cuSOLVER work can chain without
round-tripping through host memory.
### Sparse direct solvers (cuDSS)
Requires [cuDSS](https://docs.nvidia.com/cuda/cudss/) (separate install, CUDA
12.0+), which is distributed outside the CUDA Toolkit and versioned separately
from it. Define `EIGEN_CUDSS` before including `contrib/Eigen/GPU`; see
[Linking](#eigen_gpu_linking) for link flags.
```cpp
SparseMatrix<double> A = ...; // symmetric positive definite
VectorXd b = ...;
// Sparse Cholesky -- one-liner
gpu::SparseLLT<double> llt(A);
VectorXd x = llt.solve(b);
// Three-phase workflow for repeated solves with the same sparsity pattern
gpu::SparseLLT<double> llt;
llt.analyzePattern(A); // symbolic analysis (once)
llt.factorize(A); // numeric factorization
VectorXd x = llt.solve(b);
llt.factorize(A_new_values); // refactorize (reuses symbolic analysis)
VectorXd x2 = llt.solve(b);
// Sparse LDL^T (symmetric indefinite)
gpu::SparseLDLT<double> ldlt(A);
VectorXd x = ldlt.solve(b);
// Sparse LU (general non-symmetric)
gpu::SparseLU<double> lu(A);
VectorXd x = lu.solve(b);
// Bind to an existing Context (same stream as SpMV / cuBLAS work) and solve
// with a device-resident RHS — result stays on device, no host sync:
gpu::Context ctx;
gpu::SparseLLT<double> llt_ctx(ctx, A);
auto d_b = gpu::DeviceMatrix<double>::fromHost(b, ctx.stream());
gpu::DeviceMatrix<double> d_x = llt_ctx.solve(d_b);
```
#### Solver configuration (cuDSS >= 0.8)
`gpu::SparseSolverConfig` passes cuDSS tuning knobs through to the solver:
fill-reducing reordering, matching, pivoting strategy / threshold / epsilon,
iterative refinement, and the hybrid host/device memory and execute modes.
Fields left at their defaults keep the cuDSS defaults, which favor speed over
maximum robustness — for badly scaled or nearly singular systems, consider
enabling matching and iterative refinement:
```cpp
gpu::SparseSolverConfig cfg;
cfg.reordering = gpu::SparseReordering::Amd;
cfg.matching = gpu::SparseMatching::Auto; // off by cuDSS default
cfg.refinementSteps = 2; // iterative refinement in solve()
gpu::SparseLU<double> lu;
lu.setConfig(cfg); // before compute(): reordering and
lu.compute(A); // matching apply at analysis time
VectorXd x = lu.solve(b);
```
Each knob is consumed by the phase it affects (reordering/matching by
`analyzePattern()`, pivoting by `factorize()`, refinement by `solve()`), so
`setConfig()` must run before the first phase whose behavior it changes.
Every field is a pass-through, so which values are admissible for a given matrix
type — and what each one does — is cuDSS's contract, not ours:
[cuDSS Data Types](https://docs.nvidia.com/cuda/cudss/types.html) documents
`cudssConfigParam_t` and the `cudssReorderingAlg_t` / `cudssMatchingAlg_t` /
`cudssPivotType_t` values these enums mirror, and
[cuDSS Advanced Features](https://docs.nvidia.com/cuda/cudss/advanced_features.html)
describes the hybrid host/device memory and execute modes.
cuDSS < 0.8 names none of these algorithms. There the `SparseReordering`,
`SparseMatching` and `SparsePivoting` enumerators other than `Default` are not
declared, so selecting one is a compile error rather than a request the linked
cuDSS cannot honor. The remaining fields — thresholds, refinement, the hybrid
modes — still exist, and `setConfig()` refuses any non-default value of them: it
asserts, and `info()` reports `InvalidInput` until the config is reset to
default, so the request cannot be silently downgraded to the cuDSS defaults.
`EIGEN_HAS_CUDSS_SOLVER_CONFIG` is 1 or 0 accordingly, for callers that need to
branch at compile time.
### FFT (cuFFT)
Plans and data layouts are [cuFFT](https://docs.nvidia.com/cuda/cufft/)'s. The
scaling convention is not: cuFFT leaves its transforms unnormalized, and
`gpu::FFT` applies the 1/n on the inverse so that `inv(fwd(x)) == x`, matching
`contrib/Eigen/FFT`.
```cpp
gpu::FFT<float> fft; // shares stream + cuBLAS with the
// thread-local default Context
gpu::Context ctx;
gpu::FFT<float> fft_on_ctx(ctx); // share stream + cuBLAS with an
// explicit Context (e.g. for
// multi-stream pipelines)
// 1D complex-to-complex
VectorXcf X = fft.fwd(x); // forward
VectorXcf y = fft.inv(X); // inverse (scaled by 1/n)
// 1D real-to-complex / complex-to-real
VectorXcf R = fft.fwd(r); // returns n/2+1 complex (half-spectrum)
VectorXf s = fft.invReal(R, n); // C2R inverse, caller specifies n
// 2D complex-to-complex
MatrixXcf B = fft.fwd2(A); // 2D forward
MatrixXcf C = fft.inv2(B); // 2D inverse (scaled by 1/(rows*cols))
// Plans are cached and reused across calls with the same size/type.
// Device-resident transforms: DeviceMatrix in/out, no host transfer, no sync.
gpu::DeviceMatrix<std::complex<float>> d_X, d_y;
fft.fwd(d_x, d_X); // 1D C2C forward (d_x: complex column vector)
fft.inv(d_X, d_y); // 1D C2C inverse (scaled by 1/n)
fft.fwd(d_r, d_R); // 1D R2C (d_r: real column vector)
fft.invReal(d_R, d_s, n); // 1D C2R (input preserved)
fft.fwd2(d_A, d_B); // 2D C2C forward
fft.inv2(d_B, d_C); // 2D C2C inverse
```
### Sparse matrix-vector multiply (cuSPARSE)
Uses the [cuSPARSE](https://docs.nvidia.com/cuda/cusparse/) generic API
(`cusparseSpMV` / `cusparseSpMM`), which fixes the supported index and value
type combinations.
```cpp
SparseMatrix<double> A = ...;
VectorXd x = ...;
// Host vectors (upload/download handled internally)
gpu::SparseContext<double> spmv;
VectorXd y = spmv.multiply(A, x); // y = A * x
VectorXd z = spmv.multiplyT(A, x); // z = A^T * x
spmv.multiply(A, x, y, 2.0, 1.0); // y = 2*A*x + y
spmv.multiply(A, x, y, 1.0, 0.0, // y = A^H * x (Hermitian SpMV)
gpu::GpuOp::ConjTrans);
// Multiple RHS (SpMM)
MatrixXd Y = spmv.multiplyMat(A, X); // Y = A * X
MatrixXd Z = spmv.multiplyMat(A, X, gpu::GpuOp::Trans); // Z = A^T * X
// Device-resident SpMV / SpMM (sparse matrix cached on device)
gpu::Context ctx;
gpu::SparseContext<double> spmv_dev(ctx); // share gpu::Context for same-stream
auto d_A = spmv_dev.deviceView(A); // upload sparse matrix once
d_y = d_A * d_x; // SpMV, stays on device
d_Y = d_A * d_X; // SpMM when the RHS has > 1 column
d_r = d_b - d_A * d_x; // residual: copy of d_b, then one SpMV with beta = 1
d_r = d_r - d_A * d_x; // in place when the addend is the destination: no copy
```
Host-input calls re-upload the sparse values *and* index arrays on every call
(host pointer identity cannot detect a pattern rewritten in place or assigned
into the same allocations, so the structure is never assumed unchanged). The
cuSPARSE descriptors and workspace-size queries are cached across calls with
matching shapes; `deviceView()` is the upload-once path. A `DeviceSparseView`
carries a generation counter — using a view after any later upload through its
context asserts instead of silently multiplying by the wrong matrix.
#### Block sparse matrices (BSR) {#eigen_gpu_bsr}
Every `SparseContext` entry point above also accepts a `BlockSparseMatrix`.
Square blocks of size at least 2 upload in cuSPARSE's BSR (block sparse row)
format on cuSPARSE 12.6.3 (CUDA 13.0 Update 1) or newer, where the generic
SpMV and SpMM run on BSR descriptors; `EIGEN_HAS_CUSPARSE_BSR` is 1 there and
0 on older toolkits. Any other block shape, and every `BlockSparseMatrix` on an
older toolkit, takes the CSC path instead: the matrix is expanded with
`toSparse()` on the host, once per host-input call or once per `deviceView()`,
so source compatibility does not depend on the toolkit.
```cpp
BlockSparseMatrix<double, RowMajor, 3, 3> A = ...; // 3x3 blocks, block-row storage
VectorXd y = spmv.multiply(A, x); // y = A * x, no host copy
VectorXd z = spmv.multiplyT(A, x); // z = A^T * x, transposed on the host
MatrixXd Y = spmv.multiplyMat(A, X); // SpMM
auto d_A = spmv_dev.deviceView(A); // upload once as BSR
d_y = d_A * d_x; // SpMV / SpMM, stays on device
```
cuSPARSE multiplies a BSR descriptor only as `op == NoTrans` with row-major
blocks (`CUSPARSE_ORDER_ROW`), so the op is applied on the host rather than
passed to the library. A `RowMajor` matrix is BSR of itself and uploads
without a host copy for `NoTrans`; a `ColMajor` one — column-major blocks in
block-column order — is BSR of its transpose and uploads without a copy for
`Trans` (and `ConjTrans` on real scalars). Every other op / storage-order
combination transposes (and conjugates) the matrix on the host first, once per
host-input call or once per `deviceView()`. `spmv_device_exec()` and
`spmm_device_exec()` consequently accept only `NoTrans` against a BSR upload
(debug builds assert); a `BlockSparseMatrix` on the CSC path passes the op to
cuSPARSE like a `SparseMatrix`.
The CSC fallback exists because cuSPARSE rejects rectangular blocks at
descriptor creation and runs no BSR SpMV on 1 x 1 blocks;
`internal::use_cusparse_bsr<BlockRows, BlockCols>` is the exact selector. The
`int` index type and the `int` limits on dimensions and nonzeros are as for
`SparseMatrix`.
### Eigen algorithm interop (example: Conjugate gradient) {#eigen_gpu_interop}
Eigen's `ConjugateGradient` runs on the GPU types. `DeviceSparseView` is a
matrix-free matrix type (it inherits `EigenBase` and carries `SparseMatrix`
traits, so `IterativeSolverBase` holds it by pointer), the vectors are
`DeviceMatrix`, and the algorithm's `VectorType` (`Dest::PlainObject`) is
`DeviceMatrix` itself. `solve()` and `solveWithGuess()` return Eigen
expressions and need Eigen operands; `solveWithGuessInPlace(b, x)` is the entry
point for device vectors, with `x` holding the initial guess. The identity
preconditioner works as is; `DiagonalPreconditioner` and the other Eigen
preconditioners evaluate host expressions and do not. `compute()` stores a
pointer to the view, so the view and the `SparseContext` behind it must outlive
the solver. The class form is real-`Scalar` only: `DeviceScalar` arithmetic
covers real types, and `numext::real()` of a complex `DeviceScalar` has no host
conversion to `RealScalar`.
```cpp
gpu::Context ctx;
gpu::Context::setThreadLocal(&ctx);
gpu::SparseContext<double> spmv(ctx);
auto mat = spmv.deviceView(A);
auto d_b = gpu::DeviceMatrix<double>::fromHost(b, ctx.stream());
gpu::DeviceMatrix<double> d_x(n, 1);
d_x.setZero(ctx);
ConjugateGradient<gpu::DeviceSparseView<double>, Lower | Upper, IdentityPreconditioner> cg;
cg.setTolerance(1e-10);
cg.compute(mat); // matrix-free: stores a pointer to mat
cg.solveWithGuessInPlace(d_b, d_x); // Eigen's algorithm, cuSPARSE and cuBLAS underneath
// cg.info(), cg.iterations(), cg.error() as usual
gpu::Context::setThreadLocal(nullptr);
```
The algorithm reads three values on the host per iteration -- `alpha`, the
`stableNorm()` convergence check and `absNew` -- so this form synchronizes three
times per iteration. The division `absNew / p.dot(tmp)` resolves to the
device-side `operator/(Scalar, DeviceScalar)`: `absNew` is uploaded into a
fresh `DeviceScalar`, divided through NPP and read back, two small allocations
and a kernel launch per iteration on top of the sync. The hand-written loop
below is the same algorithm with one host sync per iteration (the convergence
check); all scalar intermediates (`alpha`, `beta`, `absNew`) stay on device as
`DeviceScalar` values. Its convergence test squares the residual norm
(`squaredNorm()`, a cuBLAS dot), which overflows for `||r|| > sqrt(max)` and
underflows to 0 for `||r|| < sqrt(min)`, so it lacks the extreme-scale
robustness the template gets from `stableNorm()` and its residual scaling:
```cpp
gpu::Context ctx;
gpu::Context::setThreadLocal(&ctx);
gpu::SparseContext<double> spmv(ctx);
auto mat = spmv.deviceView(A); // upload sparse matrix once
auto rhs = gpu::DeviceMatrix<double>::fromHost(b, ctx.stream());
gpu::DeviceMatrix<double> x(n, 1);
x.setZero();
gpu::DeviceMatrix<double> residual(n, 1);
residual.copyFrom(ctx, rhs); // r = b (x=0)
gpu::DeviceMatrix<double> p(n, 1);
p.copyFrom(ctx, residual); // p = r
gpu::DeviceMatrix<double> z(n, 1), tmp(n, 1);
auto absNew = residual.dot(p); // DeviceScalar -- no sync
while (i < maxIters) {
tmp.noalias() = mat * p; // SpMV, device-resident
auto alpha = absNew / p.dot(tmp); // DeviceScalar / DeviceScalar -- no sync
x += alpha * p; // DeviceScalar * DeviceMatrix axpy -- no sync
residual -= alpha * tmp; // DeviceScalar * DeviceMatrix axpy -- no sync
residualNorm2 = residual.squaredNorm(); // THE one sync per iteration
if (residualNorm2 < threshold) break;
z.copyFrom(ctx, residual); // no preconditioner: z = r
auto absOld = std::move(absNew); // no sync, no alloc
absNew = residual.dot(z); // DeviceScalar -- no sync
auto beta = absNew / absOld; // DeviceScalar / DeviceScalar -- no sync
p *= beta; // DeviceScalar scal -- no sync
p += z; // axpy -- no sync
}
MatrixXd result = x.toHost();
```
### Precision control {#eigen_gpu_precision}
GEMM dispatch routes through `cublasLtMatmul`. The compute type is selected
per scalar via the `cuda_compute_type` trait in `CuBlasSupport.h`, gated by
two compile-time macros:
| Macro | Effect |
|---|---|
| (default) | `CUBLAS_COMPUTE_32F` / `CUBLAS_COMPUTE_64F`. cublasLt heuristics may pick tensor-core algorithms; on `sm_80+` doubles can land on Ozaki-emulated tensor cores. |
| `EIGEN_CUDA_TF32` | `CUBLAS_COMPUTE_32F_FAST_TF32` for `float` and `complex<float>` (~2x faster, 10-bit mantissa). No effect on `double` / `complex<double>`. |
| `EIGEN_NO_CUDA_TENSOR_OPS` | Pedantic compute types (`CUBLAS_COMPUTE_*_PEDANTIC`) for every scalar — disables tensor-core algorithms. Use for bit-exact reproducibility. Takes precedence over `EIGEN_CUDA_TF32`. |
These are independent of cuBLAS's runtime `cublasSetMathMode()` /
`CUBLAS_TF32_OVERRIDE` controls; the cublasLt path keys off the compile-time
compute type instead. The `cublasGemmEx` fallback (used when cublasLt's
heuristic returns no candidate) honors `EIGEN_NO_CUDA_TENSOR_OPS` via its
algorithm hint (`CUBLAS_GEMM_DEFAULT` vs `CUBLAS_GEMM_DEFAULT_TENSOR_OP`).
### Stream control and async execution
Operations are asynchronous by default. The compute-solve chain runs without
host synchronization until you need a result on the host:
```text
fromHost(A) --sync--> compute() --async--> solve() --async--> toHost()
H2D potrf potrs D2H
sync
```
Mandatory sync points:
- `fromHost()` -- Synchronizes to complete the upload before returning
- `toHost()` / `HostTransfer::get()` -- Must deliver data to host
- `info()` -- Must read the factorization status
- `DeviceScalar` implicit conversion -- Downloads scalar from device
Debug-only sync points (compiled out under `EIGEN_NO_DEBUG`/`NDEBUG`): every
solver `solve()` and accessor verifies `info() == Success` via `eigen_assert`,
which forces one stream synchronization the first time after each
`compute()`/`factorize()`. Release builds perform no such check — call
`info()` explicitly where failure detection matters. `gpu::SVD`'s device
solve additionally downloads the singular values once per (truncation,
lambda) setting to build its cached inverse diagonal.
**Cross-stream safety** is automatic. `DeviceMatrix` tracks write completion
via CUDA events. When a matrix written on stream A is read on stream B, the
module automatically inserts `cudaStreamWaitEvent`. Same-stream operations
skip the wait (CUDA guarantees in-order execution within a stream).
**Device memory allocation is stream-ordered.** All module allocations go
through `cudaMallocAsync` / `cudaFreeAsync` on devices that support memory
pools (detected at runtime; `cudaMalloc`/`cudaFree` fallback otherwise, or
force the fallback with `EIGEN_GPU_NO_STREAM_ORDERED_ALLOC` — required when
borrowing `cudaStreamNonBlocking` streams, which do not synchronize with the
legacy stream the allocator uses for ordering). Consequences:
- Allocating/destroying `DeviceMatrix` temporaries no longer performs a
device-wide synchronization; freed blocks recycle through the driver pool
(the pool's release threshold is raised so steady-state loops reallocate at
user-space speed).
- Destroying a solver (or `DeviceMatrix`) with work still in flight is safe
*and* async: the stream-ordered free waits for previously enqueued work
without stalling the host.
- `DeviceMatrix::resize()` is capacity-aware: shrinking or same-size reshapes
reuse the existing allocation (contents are still discarded).
### Error handling {#eigen_gpu_errors}
Every CUDA runtime and library call the module makes is checked, in release
builds as in debug builds. A failed call prints `file:line: call: error` to
`stderr` and stops the program:
- `std::abort()` when `EIGEN_NO_DEBUG` (or `NDEBUG`) is defined;
- a failed `eigen_assert` otherwise.
`error` is the status name where the library provides one
(`cudaErrorInvalidValue`, `CUBLAS_STATUS_INVALID_VALUE`, ...) and
`<library> status <code>` for cuFFT, cuDSS, NPP and cuBLAS before 11.6.1.
There is no mode that ignores a failure: the failed call has not done its work,
and a sticky error (say, an illegal address in a kernel) makes every later call
on the device fail as well.
To handle failures yourself, define
`EIGEN_GPU_CHECK_FAILED(error, expression, file, line)` before including the
module. `error`, `expression` and `file` are C strings; `line` is an `int`. For
example, to turn failures into exceptions:
```cpp
#define EIGEN_GPU_CHECK_FAILED(error, expression, file, line) \
throw std::runtime_error(std::string(file) + ": " + (expression) + ": " + (error))
#include <contrib/Eigen/GPU>
```
Destructors release their resources without the checks, so a throwing handler
never runs inside one. A throw also restores the library-handle state an
operation changes temporarily (the cuBLAS pointer mode), so the context stays
usable; only the interrupted operation's output is unspecified. A handler that
returns lets execution continue past the failed call, which is only useful in
tests.
Numerical failures (a matrix that is not positive definite, a singular
factorization) are not call failures: they are reported by `info()` as
described above.
## Reference
### Supported scalar types
`float`, `double`, `std::complex<float>`, `std::complex<double>` (unless
noted otherwise).
### Expression -> library call mapping
| DeviceMatrix expression | Library call | Parameters |
|---|---|---|
| `C = A * B` | `cublasLtMatmul` (with `cublasGemmEx` fallback) | transA=N, transB=N, alpha=1, beta=0 |
| `C = A.adjoint() * B` | `cublasLtMatmul` | transA=C, transB=N |
| `C = A.transpose() * B` | `cublasLtMatmul` | transA=T, transB=N |
| `C = A * B.adjoint()` | `cublasLtMatmul` | transA=N, transB=C |
| `C = A * B.transpose()` | `cublasLtMatmul` | transA=N, transB=T |
| `C = alpha * A * B` | `cublasLtMatmul` | alpha from LHS |
| `C = A * (alpha * B)` | `cublasLtMatmul` | alpha from RHS |
| `C += A * B` | `cublasLtMatmul` | alpha=1, beta=1 |
| `C -= A * B` | `cublasLtMatmul` | alpha=-1, beta=1 |
| `X = A.llt().solve(B)` | `cusolverDnXpotrf` + `Xpotrs` | uplo, n, nrhs |
| `X = A.llt<Upper>().solve(B)` | same | uplo=Upper |
| `X = A.lu().solve(B)` | `cusolverDnXgetrf` + `Xgetrs` | n, nrhs |
| `X = A.triangularView<L>().solve(B)` | `cublasXtrsm` | side=L, uplo, diag=NonUnit |
| `C = A.selfadjointView<L>() * B` | `cublasXsymm` / `cublasXhemm` | side=L, uplo |
| `C.selfadjointView<L>().rankUpdate(A)` | `cublasXsyrk` / `cublasXherk` | uplo, trans=N |
| `C = A + B` | `cublasXgeam` | alpha=1, beta=1 |
| `C = A + alpha * B` | `cublasXgeam` | alpha=1, beta from scaled |
| `C = A - B` | `cublasXgeam` | alpha=1, beta=-1 |
| `C = A - alpha * B` | `cublasXgeam` | alpha=1, beta=-scaled |
| `C = alpha * A + beta * B` | `cublasXgeam` | both sides scaled |
| `C = alpha * A`, `C = -A`, `C = A / alpha` | `cublasXgeam` | beta=0, aliasing-safe |
| `x += alpha * y` | `cublasXaxpy` | alpha (host scalar) |
| `x += dAlpha * y` | `cublasXaxpy` | alpha (DeviceScalar, device pointer mode) |
| `x -= alpha * y` | `cublasXaxpy` | alpha negated |
| `x *= alpha` | `cublasXscal` | alpha (host or DeviceScalar) |
| `x.dot(y)` | `cublasXdot` / `cublasXdotc` | returns `DeviceScalar` |
| `x.norm()` | `cublasXdot(x, x)`, then `nppsSqrt` | as `squaredNorm()`, then its square root on device |
| `x.stableNorm()` | `cublasXnrm2` | returns `DeviceScalar<RealScalar>` |
| `x.squaredNorm()` | `cublasXdot(x, x)` | real dot over the `2n` real and imaginary parts for complex `x`; returns `DeviceScalar<RealScalar>` |
| `d_y = view * d_x` | `cusparseSpMV` | device-resident SpMV |
| `d_Y = view * d_X` | `cusparseSpMM` | device-resident SpMM (RHS with >1 column) |
| same, `view` of a `BlockSparseMatrix` | `cusparseSpMV` / `cusparseSpMM` on a BSR descriptor | opA=N, row-major blocks; op(A) formed on the host |
### `DeviceMatrix<Scalar>`
Typed RAII wrapper for a dense column-major matrix in GPU device memory.
Always dense (leading dimension = rows). A vector is a `DeviceMatrix` with
one column.
```cpp
// Construction
DeviceMatrix<Scalar>() // Empty (0x0)
DeviceMatrix<Scalar>(Index n) // Allocate column vector (n x 1)
DeviceMatrix<Scalar>(rows, cols) // Allocate uninitialized
DeviceMatrix<Scalar>(expr) // Copy-init from any supported expression
// (GEMM, geam/scaled, LLT/LU solve, TRSM,
// SYMM, SpMV/SpMM)
// Upload / download / pointer adoption
static DeviceMatrix fromHost(matrix, stream=nullptr) // -> DeviceMatrix (syncs)
static DeviceMatrix fromHostAsync(ptr, rows, cols, stream) // -> DeviceMatrix (no sync, caller manages ptr lifetime)
static DeviceMatrix adopt(Scalar* device_ptr, rows, cols) // Owning wrapper over a raw device pointer
static DeviceMatrix view(Scalar* device_ptr, rows, cols) // Non-owning view (does not free on destruction)
PlainMatrix toHost(stream=nullptr) // -> host Matrix (syncs)
HostTransfer toHostAsync(stream=nullptr) // -> HostTransfer future (no sync)
DeviceMatrix clone(stream=nullptr) // -> DeviceMatrix (D2D copy, async)
// Dimensions and access
Index rows()
Index cols()
size_t sizeInBytes()
bool empty()
bool isView() // Holds storage borrowed through view()
Scalar* data() // Raw device pointer
void resize(Index rows, Index cols) // Discard contents; keeps the allocation
// when it is already large enough
// Expression builders (return lightweight views, evaluated on assignment)
AdjointView adjoint() // GEMM with ConjTrans
TransposeView transpose() // GEMM with Trans
LltExpr llt() / llt<UpLo>() // -> .solve(d_B) -> DeviceMatrix
LuExpr lu() // -> .solve(d_B) -> DeviceMatrix
TriangularView triangularView<UpLo>() // -> .solve(d_B) -> DeviceMatrix (TRSM)
SelfAdjointView selfadjointView<UpLo>() // -> * d_B (SYMM), .rankUpdate(d_A) (SYRK)
Assignment device(gpu::Context& ctx) // Bind assignment to explicit stream
DeviceMatrix& noalias() // No-op (all ops are implicitly noalias)
// BLAS Level-1 (all have overloads with explicit gpu::Context& parameter)
DeviceScalar<Scalar> dot(const DeviceMatrix& other) // cuBLAS dot/dotc -> DeviceScalar
DeviceScalar<RealScalar> norm() // sqrt(squaredNorm()) -> DeviceScalar, unscaled like MatrixBase::norm()
DeviceScalar<RealScalar> stableNorm() // cuBLAS nrm2 (scaled, overflow-safe) -> DeviceScalar
DeviceScalar<RealScalar> squaredNorm() // dot(self, self) -> DeviceScalar (no sync)
void dot(ctx, other, DeviceScalar<Scalar>& result) // Each reduction into an existing DeviceScalar
void squaredNorm / norm / stableNorm(ctx, result) // on ctx's stream, reusing its storage: no allocation
void setZero() // cudaMemsetAsync
void addScaled(gpu::Context&, Scalar alpha, const DeviceMatrix& x) // this += alpha * x (axpy)
void scale(gpu::Context&, Scalar alpha) // this *= alpha (scal)
void copyFrom(gpu::Context&, const DeviceMatrix& other) // this = other (D2D copy)
DeviceMatrix& operator+=(const Scaled<DeviceMatrix>&) // axpy; spelled `mat += alpha * other`
DeviceMatrix& operator-=(const Scaled<DeviceMatrix>&) // axpy negated; spelled `mat -= alpha * other`
DeviceMatrix& operator+=(const DeviceMatrix&) // cuBLAS axpy (alpha=1)
DeviceMatrix& operator-=(const DeviceMatrix&) // cuBLAS axpy (alpha=-1)
DeviceMatrix& operator+=(const DeviceScaledDevice<Scalar>&) // axpy with device scalar; spelled `mat += d_alpha * other`
DeviceMatrix& operator-=(const DeviceScaledDevice<Scalar>&) // negated; spelled `mat -= d_alpha * other`
DeviceMatrix& operator*=(Scalar) // cuBLAS scal (host pointer mode)
DeviceMatrix& operator*=(const DeviceScalar<Scalar>&) // cuBLAS scal (device pointer mode, no host sync)
DeviceMatrix cwiseProduct(gpu::Context&, const DeviceMatrix&) // NPP nppsMul (float/double only)
void cwiseProduct(gpu::Context&, const DeviceMatrix&, const DeviceMatrix&) // in-place: this = a .* b
// geam expressions (evaluated on assignment)
DeviceMatrix& operator=(const DeviceAddExpr&) // C = A + B, C = A + alpha*B, C = A - B, etc.
```
### `DeviceScalar<Scalar>`
Device-resident scalar. Returned by `dot()`, `norm()`, and `squaredNorm()`.
Implicit conversion to `Scalar` triggers `cudaStreamSynchronize` + download.
```cpp
DeviceScalar(cudaStream_t stream = nullptr) // Allocate uninitialized
DeviceScalar(Scalar host_val, cudaStream_t stream) // Upload host value
Scalar get() // Download (syncs stream)
operator Scalar() // Implicit conversion (syncs)
Scalar* devicePtr() // Raw device pointer
cudaStream_t stream()
// Device-side arithmetic (no host sync, real types only)
DeviceScalar operator/(DeviceScalar, DeviceScalar) // NPP nppsDiv
DeviceScalar operator/(Scalar, DeviceScalar) // upload + div
DeviceScalar operator/(DeviceScalar, Scalar) // upload + div
DeviceScalar operator-() // NPP nppsMulC(-1)
```
### `gpu::Context`
Unified GPU execution context owning a CUDA stream and library handles. Not
thread-safe -- use one `Context` per thread, or external synchronization
across threads.
```cpp
gpu::Context() // Creates dedicated stream + cuBLAS handle
// (cuSOLVER / cuBLASLt / cuSPARSE handles
// are created lazily on first use)
gpu::Context(cudaStream_t stream) // Borrow existing stream (not owned)
static gpu::Context& threadLocal() // Per-thread default (lazy-created)
static void setThreadLocal(gpu::Context* ctx) // Override thread-local default (nullptr restores)
cudaStream_t stream()
cublasHandle_t cublasHandle()
cusolverDnHandle_t cusolverHandle() // Lazy: creates the handle on first call
cublasLtHandle_t cublasLtHandle() // Lazy-initialized
cusparseHandle_t cusparseHandle() // Lazy-initialized
const NppStreamContext& nppStreamContext() // NPP context for stream(), filled in at construction
internal::DeviceBuffer& gemmWorkspace() // cublasLtMatmul scratch (lazy-grown per context)
internal::CublasLtPlanCache& gemmPlanCache() // shape-keyed plan cache (per context, ~8-entry LRU)
internal::OneShotSolverScratch& oneshotSolverScratch() // LLT/LU expression scratch (lazy-grown per context)
```
Non-copyable, non-movable (owns library handles). Translation units that
never call `cusolverHandle()` do not pull cuSOLVER symbols at link time --
see [Linking](#eigen_gpu_linking).
### `gpu::LLT<Scalar, UpLo>` -- Dense Cholesky (cuSOLVER)
Caches the Cholesky factor on device for repeated solves.
```cpp
gpu::LLT() // Default construct, then call compute()
gpu::LLT(Context& ctx) // Bind to ctx's stream + handles
gpu::LLT(const DenseBase<D>& A) // Convenience: upload + factorize
gpu::LLT(const DeviceMatrix& d_A) // Convenience: D2D copy + factorize
gpu::LLT(DeviceMatrix&& d_A) // Convenience: adopt + factorize
gpu::LLT(Context& ctx, ...) // Bind + factorize in one step
gpu::LLT& compute(const DenseBase<D>& A) // Upload + factorize
gpu::LLT& compute(const DeviceMatrix& d_A) // D2D copy + factorize
gpu::LLT& compute(DeviceMatrix&& d_A) // Adopt + factorize (a view is copied)
PlainMatrix solve(const MatrixBase<D>& B) // -> host Matrix (syncs)
DeviceMatrix solve(const DeviceMatrix& d_B) // -> DeviceMatrix (async, stays on device)
DeviceMatrix solve(DeviceMatrix&& d_B) // In-place: consumes RHS, no copy/alloc (a view is copied)
ComputationInfo info() // Lazy sync on first call: Success or NumericalIssue
Index rows() / cols()
cudaStream_t stream()
```
### `gpu::LU<Scalar>` -- Dense LU (cuSOLVER)
Same pattern as `gpu::LLT`. Adds a `gpu::GpuOp` parameter on `solve()`.
```cpp
PlainMatrix solve(const MatrixBase<D>& B, GpuOp op = GpuOp::NoTrans) // -> host Matrix
DeviceMatrix solve(const DeviceMatrix& d_B, GpuOp op = GpuOp::NoTrans) // -> DeviceMatrix
```
`gpu::GpuOp`: `NoTrans`, `Trans`, `ConjTrans`.
### `gpu::QR<Scalar>` -- Dense QR (cuSOLVER)
QR factorization via `cusolverDnXgeqrf`. Solve uses ORMQR (apply Q^H) + TRSM
(back-substitute on R) -- Q is never formed explicitly.
```cpp
gpu::QR() // Default construct
gpu::QR(Context& ctx) // Bind to ctx's stream + handles
gpu::QR(const DenseBase<D>& A) // Convenience: upload + factorize
gpu::QR(const DeviceMatrix& d_A) // Convenience: D2D copy + factorize
gpu::QR(DeviceMatrix&& d_A) // Convenience: adopt (m >= n) + factorize
gpu::QR(Context& ctx, ...) // Bind + factorize in one step
gpu::QR& compute(const DenseBase<D>& A) // Upload + factorize
gpu::QR& compute(const DeviceMatrix& d_A) // D2D copy + factorize
gpu::QR& compute(DeviceMatrix&& d_A) // Adopt + factorize (copies when m < n or for a view)
PlainMatrix solve(const MatrixBase<D>& B) // -> host Matrix (syncs)
DeviceMatrix solve(const DeviceMatrix& d_B) // -> DeviceMatrix (async)
PlainMatrix matrixR() // -> host Matrix (m >= n only)
ComputationInfo info() // Lazy sync
Index rows() / cols()
cudaStream_t stream()
```
### `gpu::SVD<Scalar>` -- Dense SVD (cuSOLVER)
SVD via `cusolverDnXgesvd`. Supports `ComputeThinU | ComputeThinV`,
`ComputeFullU | ComputeFullV`, or `0` (values only). Wide matrices (m < n)
handled by internal transpose.
```cpp
gpu::SVD() // Default construct, then call compute()
gpu::SVD(Context& ctx) // Bind to ctx's stream + handles
gpu::SVD(const DenseBase<D>& A, unsigned options = ComputeThinU | ComputeThinV) // Convenience
gpu::SVD(const DeviceMatrix& d_A, unsigned options = ComputeThinU | ComputeThinV) // D2D copy
gpu::SVD(DeviceMatrix&& d_A, unsigned options = ComputeThinU | ComputeThinV) // Adopt (m >= n)
gpu::SVD(Context& ctx, ...) // Bind + decompose in one step
gpu::SVD& compute(const DenseBase<D>& A, unsigned options = ComputeThinU | ComputeThinV)
gpu::SVD& compute(const DeviceMatrix& d_A, unsigned options = ComputeThinU | ComputeThinV)
gpu::SVD& compute(DeviceMatrix&& d_A, unsigned options = ComputeThinU | ComputeThinV)
RealVector singularValues() // -> host vector (syncs, downloads)
PlainMatrix matrixU() // -> host Matrix (syncs, downloads)
PlainMatrix matrixV() // -> host Matrix (V = VT^H, matches JacobiSVD)
PlainMatrix matrixVT() // -> host Matrix (syncs, downloads V^T)
DeviceMatrix d_singularValues() // -> DeviceMatrix view (zero-copy)
DeviceMatrix d_matrixU() // -> DeviceMatrix view (zero-copy when m >= n)
DeviceMatrix d_matrixVT() // -> DeviceMatrix view (zero-copy when m >= n)
PlainMatrix solve(const MatrixBase<D>& B) // -> host Matrix (pseudoinverse)
PlainMatrix solve(const MatrixBase<D>& B, Index k) // Truncated (top k triplets)
PlainMatrix solve(const MatrixBase<D>& B, RealScalar l) // Tikhonov regularized
DeviceMatrix solve(const DeviceMatrix& d_B) // Device-resident pseudoinverse solve
DeviceMatrix solve(const DeviceMatrix& d_B, Index k) // Truncated, device-resident
DeviceMatrix solve(const DeviceMatrix& d_B, RealScalar l) // Tikhonov, device-resident
Index rank(RealScalar threshold = -1)
ComputationInfo info() // Lazy sync
Index rows() / cols()
cudaStream_t stream()
```
**Note:** `singularValues()`, `matrixU()`, `matrixV()`, and `matrixVT()`
download to host on each call. The `d_*` accessors return non-owning
`DeviceMatrix` views into the solver's internal buffers; the `gpu::SVD` object
must outlive any view derived from it. For wide matrices (m < n) the U/V^T
views are owning (one `cublasXgeam` adjoint pass).
### `gpu::SelfAdjointEigenSolver<Scalar>` -- Eigendecomposition (cuSOLVER)
Symmetric/Hermitian eigenvalue decomposition via `cusolverDnXsyevd`.
`options`: `ComputeEigenvectors` (the default) or `EigenvaluesOnly`.
```cpp
gpu::SelfAdjointEigenSolver() // Default construct, then call compute()
gpu::SelfAdjointEigenSolver(Context& ctx) // Bind to ctx's stream + handles
gpu::SelfAdjointEigenSolver(const DenseBase<D>& A, int options = ComputeEigenvectors) // Convenience
gpu::SelfAdjointEigenSolver(const DeviceMatrix& d_A, int options = ComputeEigenvectors) // D2D copy
gpu::SelfAdjointEigenSolver(DeviceMatrix&& d_A, int options = ComputeEigenvectors) // Adopt (a view is copied)
gpu::SelfAdjointEigenSolver(Context& ctx, ...) // Bind + decompose in one step
gpu::SelfAdjointEigenSolver& compute(const DenseBase<D>& A, int options = ComputeEigenvectors)
gpu::SelfAdjointEigenSolver& compute(const DeviceMatrix& d_A, int options = ComputeEigenvectors)
gpu::SelfAdjointEigenSolver& compute(DeviceMatrix&& d_A, int options = ComputeEigenvectors)
RealVector eigenvalues() // -> host vector (syncs, downloads, ascending order)
PlainMatrix eigenvectors() // -> host Matrix (syncs, downloads, columns)
DeviceMatrix d_eigenvalues() // -> DeviceMatrix view (zero-copy)
DeviceMatrix d_eigenvectors() // -> DeviceMatrix view (zero-copy, requires ComputeEigenvectors)
ComputationInfo info() // Lazy sync
Index rows() / cols()
cudaStream_t stream()
```
**Note:** `eigenvalues()` and `eigenvectors()` download to host on each call.
The `d_*` accessors return non-owning `DeviceMatrix` views into the solver's
internal buffers; the `gpu::SelfAdjointEigenSolver` object must outlive any
view derived from it.
### `HostTransfer<Scalar>`
Future for async device-to-host transfer. Returned by
`DeviceMatrix::toHostAsync()`.
```cpp
PlainMatrix& get() // Block until complete, return host Matrix ref. Idempotent.
bool ready() // Non-blocking poll
```
### `gpu::SparseLLT<Scalar, UpLo>` -- Sparse Cholesky (cuDSS)
Requires cuDSS (CUDA 12.0+, `#define EIGEN_CUDSS`). Three-phase workflow
with symbolic reuse. Accepts `SparseMatrix<Scalar, ColMajor, int>` (CSC).
Matrix dimensions and nonzero count must fit in `int` (cuDSS limitation;
debug builds assert).
```cpp
gpu::SparseLLT() // Default construct
gpu::SparseLLT(const SparseMatrixBase<D>& A) // Analyze + factorize
gpu::SparseLLT& analyzePattern(const SparseMatrixBase<D>& A) // Symbolic analysis (reusable)
gpu::SparseLLT& factorize(const SparseMatrixBase<D>& A) // Numeric factorization
gpu::SparseLLT& compute(const SparseMatrixBase<D>& A) // analyzePattern + factorize
DenseMatrix solve(const MatrixBase<D>& B) // -> host Matrix (syncs)
DeviceMatrix solve(const DeviceMatrix& d_B) // -> DeviceMatrix (async, stays on device)
gpu::SparseLLT& setConfig(const SparseSolverConfig&) // cuDSS knobs (>= 0.8); call before the affected phase
const SparseSolverConfig& config() // Last configuration set
ComputationInfo info() // Lazy sync
Index rows() / cols()
cudaStream_t stream()
```
All three cuDSS solvers also accept a `gpu::Context&` as first constructor
argument to borrow its stream (`gpu::SparseLLT<double> llt(ctx)` or
`llt(ctx, A)`).
### `gpu::SparseLDLT<Scalar, UpLo>` -- Sparse LDL^T (cuDSS)
Symmetric indefinite. Same API as `gpu::SparseLLT`.
### `gpu::SparseLU<Scalar>` -- Sparse LU (cuDSS)
General non-symmetric. Same API as `gpu::SparseLLT` (without `UpLo`).
### `gpu::FFT<Scalar>` -- FFT (cuFFT)
Plans cached by (size, type) in a bounded LRU and reused; the
least-recently-used plan is destroyed via `cufftDestroy` on overflow. Cache
capacity is set at construction (default
`kDefaultCufftPlanCacheCapacity = 16`). Inverse transforms scaled so
`inv(fwd(x)) == x`. Supported scalars: `float`, `double`. Stream and cuBLAS
handle borrowed from a `gpu::Context` (default: `Context::threadLocal()`),
so by default the FFT shares a stream with other GPU operations on the same
thread.
```cpp
gpu::FFT(std::size_t plan_cache_capacity = kDefaultCufftPlanCacheCapacity)
// bind to Context::threadLocal()
gpu::FFT(gpu::Context& ctx,
std::size_t plan_cache_capacity = kDefaultCufftPlanCacheCapacity)
// bind to an explicit Context
// 1D transforms (host vectors in and out)
ComplexVector fwd(const MatrixBase<D>& x) // C2C forward (complex input)
ComplexVector fwd(const MatrixBase<D>& x) // R2C forward (real input, returns n/2+1)
ComplexVector inv(const MatrixBase<D>& X) // C2C inverse, scaled by 1/n
RealVector invReal(const MatrixBase<D>& X, Index n) // C2R inverse, scaled by 1/n
// 2D transforms (host matrices in and out)
ComplexMatrix fwd2(const MatrixBase<D>& A) // 2D C2C forward
ComplexMatrix inv2(const MatrixBase<D>& A) // 2D C2C inverse, scaled by 1/(rows*cols)
// Device-resident transforms (DeviceMatrix in/out, async, no host transfer)
void fwd(const DeviceMatrix<Complex>&, DeviceMatrix<Complex>&) // 1D C2C
void inv(const DeviceMatrix<Complex>&, DeviceMatrix<Complex>&) // 1D C2C inverse
void fwd(const DeviceMatrix<Scalar>&, DeviceMatrix<Complex>&) // 1D R2C
void invReal(const DeviceMatrix<Complex>&, DeviceMatrix<Scalar>&, Index nfft) // 1D C2R
void fwd2 / inv2(const DeviceMatrix<Complex>&, DeviceMatrix<Complex>&) // 2D C2C
cudaStream_t stream() // borrowed from the bound Context
gpu::Context& context() // the bound Context
std::size_t plan_cache_capacity() // configured capacity
std::size_t plan_cache_size() // currently cached plan count
```
All FFT methods accept host data and return host data. Upload/download is
handled internally. The C2C and R2C overloads of `fwd()` are distinguished by
the input scalar type (complex vs real).
### `gpu::SparseContext<Scalar>` -- SpMV/SpMM (cuSPARSE)
Accepts `SparseMatrix<Scalar, ColMajor>` and `BlockSparseMatrix<Scalar,
Options, BlockRows, BlockCols, int>` (the `BlockSpMat<Options, BlockRows,
BlockCols>` alias; see [Block sparse matrices](#eigen_gpu_bsr) for
which block shapes upload as BSR and which op / storage-order combinations do
so without a host copy). Host-input
methods accept host data and return host data; device-input methods
(`deviceView()`, `multiply(A, d_x, d_y)`) operate on `DeviceMatrix`. Matrix
dimensions and nonzero count must fit in `int` (cuSPARSE limitation; debug
builds assert).
```cpp
gpu::SparseContext() // Creates own stream + cuSPARSE handle
gpu::SparseContext(gpu::Context& ctx) // Borrow gpu::Context for same-stream execution
// Host data in/out
DenseVector multiply(A, x) // y = A * x
void multiply(A, x, y, alpha=1, beta=0, // y = alpha*op(A)*x + beta*y
op=GpuOp::NoTrans)
DenseVector multiplyT(A, x) // y = A^T * x
DenseVector multiplyAdjoint(A, x) // y = A^H * x
DenseMatrix multiplyMat(A, X, op=GpuOp::NoTrans) // Y = op(A) * X (SpMM)
// DeviceMatrix in/out (sparse matrix re-uploaded per call)
void multiply(A, d_x, d_y) // SpMV with device vectors
void multiply(A, d_x, d_y, alpha, beta, op=GpuOp::NoTrans)
// Device-resident sparse matrix (upload once, reuse)
DeviceSparseView deviceView(A) // Upload sparse matrix, return view
uint64_t uploadGeneration() // Generation of the cached upload
// Advanced: run SpMV/SpMM against the already-uploaded matrix (op must be
// NoTrans after a BSR upload)
void spmv_device_exec(d_x, d_y, alpha=1, beta=0, op=GpuOp::NoTrans)
void spmm_device_exec(d_X, d_Y, alpha=1, beta=0, op=GpuOp::NoTrans)
cudaStream_t stream()
```
### `DeviceSparseView<Scalar>` -- Device-resident sparse matrix
Returned by `gpu::SparseContext::deviceView()`. Holds a sparse matrix on device
for repeated SpMV without re-uploading.
```cpp
SpMVExpr operator*(const DeviceMatrix& d_x) // d_y = view * d_x (evaluated on assignment;
// dispatches to SpMM when d_x has > 1 column)
uint64_t generation() // Upload generation (stale views assert)
```
### Aliasing
Unlike Eigen's `Matrix`, where omitting `.noalias()` triggers a copy to a
temporary, DeviceMatrix dispatches directly to NVIDIA library calls which have
no built-in aliasing protection. All operations are implicitly noalias.
The caller must ensure operands don't alias the destination for GEMM, TRSM,
SYMM/HEMM, and SYRK/HERK. Debug builds assert on these violations before
dispatching to cuBLAS. `geam` expressions (`d_C = d_A + alpha * d_B`) are
safe with aliasing. The `.noalias()` method exists as a no-op for Eigen
template compatibility.
## Future work
- **Reassess host-input vs. device-input API surface.** Each solver currently
exposes both host-input (`compute(MatrixXd)`, `solve(MatrixXd)`) and
device-input (`compute(DeviceMatrix)`, `solve(DeviceMatrix)`) overloads, plus
host- and device-side accessors (`matrixU()` vs `d_matrixU()`). This eases
migration from CPU Eigen but may invite accidental host ↔ device round-trips
when users mix the two without realising the cost. Revisit once the module
is in users' hands; if the convenience overloads cause more confusion than
they save, narrow toward a single explicit `fromHost` / `toHost` boundary.
- **Robustness-oriented cuDSS defaults.** `gpu::SparseSolverConfig` exposes
matching, pivoting, refinement, and the hybrid modes, but a
default-constructed solver still runs with cuDSS's performance-tuned
defaults (matching off). Consider flipping the shipped defaults toward
robustness now that users can override them.
- **cuDSS threading layer for host-side reordering.** As of cuDSS 0.7.1
fill-reducing reordering runs on the CPU. cuDSS supports a "threading
layer" plugin that parallelises this stage; for reordering-dominated
problems it can materially close the gap with multithreaded CPU sparse
direct solvers. We don't currently configure a threading layer.
- **Complex symmetric (non-Hermitian) sparse LDL^T.** `gpu::SparseLDLT`
treats complex inputs as Hermitian (matching `Eigen::SimplicialLDLT`).
cuDSS also supports `CUDSS_MTYPE_SYMMETRIC` for complex matrices
(A = A^T, no conjugation); exposing this would need a separate solver
mode.
## File layout
| File | Depends on | Contents |
|------|-----------|----------|
| `GpuSupport.h` | `<cuda_runtime.h>` | `EIGEN_GPU_CHECK_FAILED`, runtime error macro, `DeviceBuffer`, `DeviceBufferPool`, `cuda_data_type<>` |
| `DeviceMatrix.h` | `GpuSupport.h` | `gpu::DeviceMatrix<>`, `gpu::HostTransfer<>` |
| `DeviceExpr.h` | `DeviceMatrix.h` | GEMM, geam, and device-scalar expression wrappers |
| `DeviceBlasExpr.h` | `DeviceMatrix.h` | TRSM, SYMM, SYRK expression wrappers |
| `DeviceSolverExpr.h` | `DeviceMatrix.h` | Solver expression wrappers (LLT, LU) |
| `DeviceScalar.h` | `GpuSupport.h`, `DeviceScalarOps.h` | `gpu::DeviceScalar<>` (device-resident scalar) |
| `DeviceScalarOps.h` | `<npps_*.h>` | Scalar div/neg/sqrt/cwiseProduct via NPP, NPP error macro |
| `DeviceDispatch.h` | all above | All dispatch functions, BLAS-1 out-of-line defs, `gpu::Assignment` |
| `GpuContext.h` | `CuBlasSupport.h`, `CuSolverSupport.h`, `CuSparseSupport.h` | `gpu::Context` |
| `CuBlasSupport.h` | `GpuSupport.h`, `<cublas_v2.h>`, `<cublasLt.h>` | cuBLAS error macro, type-specific wrappers |
| `CuSolverSupport.h` | `GpuSupport.h`, `<cusolverDn.h>` | cuSOLVER params, fill-mode mapping |
| `GpuSolverContext.h` | `CuSolverSupport.h`, `CuBlasSupport.h` | Shared solver context (stream, handles, scratch) |
| `GpuLLT.h` | `GpuSolverContext.h` | `gpu::LLT<>` -- Cached dense Cholesky factorization |
| `GpuLU.h` | `GpuSolverContext.h` | `gpu::LU<>` -- Cached dense LU factorization |
| `GpuQR.h` | `GpuSolverContext.h` | `gpu::QR<>` -- Dense QR decomposition |
| `GpuSVD.h` | `GpuSolverContext.h` | `gpu::SVD<>` -- Dense SVD decomposition |
| `GpuEigenSolver.h` | `GpuSolverContext.h` | `gpu::SelfAdjointEigenSolver<>` |
| `CuFftSupport.h` | `GpuSupport.h`, `<cufft.h>` | cuFFT error macro, type-dispatch wrappers |
| `GpuFFT.h` | `CuFftSupport.h`, `CuBlasSupport.h`, `GpuContext.h` | `gpu::FFT<>` -- 1D/2D FFT with plan caching |
| `CuSparseSupport.h` | `GpuSupport.h`, `<cusparse.h>` | cuSPARSE error macro, `EIGEN_HAS_CUSPARSE_BSR` |
| `GpuSparseContext.h` | `CuSparseSupport.h` | `gpu::SparseContext<>`, `gpu::DeviceSparseView<>`, BSR binding of `BlockSparseMatrix` |
| `CuDssSupport.h` | `GpuSupport.h`, `<cudss.h>` | cuDSS error macro, type traits (optional) |
| `GpuSparseSolverBase.h` | `CuDssSupport.h` | CRTP base for sparse solvers (optional) |
| `GpuSparseLLT.h` | `GpuSparseSolverBase.h` | `gpu::SparseLLT<>` -- Sparse Cholesky via cuDSS (optional) |
| `GpuSparseLDLT.h` | `GpuSparseSolverBase.h` | `gpu::SparseLDLT<>` -- Sparse LDL^T via cuDSS (optional) |
| `GpuSparseLU.h` | `GpuSparseSolverBase.h` | `gpu::SparseLU<>` -- Sparse LU via cuDSS (optional) |
## Building and testing
```bash
cmake -G Ninja -B build -S . \
-DEIGEN_TEST_CUDA=ON \
-DEIGEN_CUDA_COMPUTE_ARCH="70"
cmake --build build --target cublas cusolver_llt cusolver_lu \
cusolver_qr cusolver_svd cusolver_eigen \
device_matrix cufft cusparse_spmv cusparse_bsr cg
ctest --test-dir build -L gpu --output-on-failure
# Sparse solvers (cuDSS -- separate install required)
cmake -G Ninja -B build -S . \
-DEIGEN_TEST_CUDA=ON \
-DEIGEN_CUDA_COMPUTE_ARCH="70" \
-DEIGEN_TEST_CUDSS=ON
cmake --build build --target cudss_llt cudss_ldlt cudss_lu
ctest --test-dir build -R '^cudss_' --output-on-failure
```
## Future enhancements
- **Batched API (`DeviceBatchMatrix`).** A strided batch of N identical-size
matrices dispatching to cuBLAS/cuSOLVER batched APIs (`cublasDgemmBatched`,
`cusolverDnXpotrfBatched`, etc.). This enables robotics and model-predictive
control workloads where many small independent systems are solved in
parallel.
- **cuTENSOR for Tensor module.** Replace the hand-written GPU tensor
contraction and reduction kernels (~2300 lines in
`TensorContractionGpu.h` / `TensorReductionGpu.h`) with cuTENSOR dispatch,
following the same library-dispatch pattern used by `contrib/Eigen/GPU`.
- **Unified/zero-copy memory for Jetson.** Use `cudaMallocManaged` or
`cudaHostAllocMapped` to eliminate `fromHost()` / `toHost()` copies on
integrated GPUs (Jetson) where CPU and GPU share DRAM.
- **Device-side Eigen interop.** Bridge between host-side `DeviceMatrix`
dispatch and device-side Eigen expression templates (Core + Tensor) running
inside CUDA kernels. Raw-pointer + `Map` / `TensorMap` as the zero-copy
interop surface.
- **Per-stream CUDA memory pools.** Allocation is now stream-ordered through
the device's default memory pool. Attaching a dedicated `cudaMemPool_t` per
stream (`cudaDeviceSetMempool` / `cudaMallocFromPoolAsync`) could further
reduce cross-stream allocator contention for workloads that fan out many
concurrent solves.