blob: 24949cdef754899703b7eead3f2216c05e0a291e [file] [edit]
// This file is part of Eigen, a lightweight C++ template library
// for linear algebra.
//
// Copyright (C) 2026 Rasmus Munk Larsen <rmlarsen@gmail.com>
//
// This Source Code Form is subject to the terms of the Mozilla
// Public License v. 2.0. If a copy of the MPL was not distributed
// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// SPDX-License-Identifier: MPL-2.0
// End-to-end tests of conjugate gradient on the GPU types: Eigen's ConjugateGradient
// class on DeviceSparseView and DeviceMatrix through solveWithGuessInPlace(), and the
// module's hand-written loop with DeviceScalar reductions. Both are checked against
// the CPU ConjugateGradient.
#define EIGEN_USE_GPU
#include "main.h"
#include <Eigen/Sparse>
#include <Eigen/IterativeLinearSolvers>
#include <contrib/Eigen/GPU>
#include "gpu_test_helpers.h"
using namespace Eigen;
// ---- Helpers ----------------------------------------------------------------
// A = R^T R + n I: lambda_min >= n and lambda_max <= ||A||_F, so kappa(A) <= ||A||_F / n.
template <typename Scalar>
SparseMatrix<Scalar, ColMajor, int> make_spd(Index n, double density = 0.1) {
using SpMat = SparseMatrix<Scalar, ColMajor, int>;
using RealScalar = typename NumTraits<Scalar>::Real;
SpMat R(n, n);
R.reserve(VectorXi::Constant(n, static_cast<int>(n * density) + 1));
for (Index j = 0; j < n; ++j) {
for (Index i = 0; i < n; ++i) {
if (i == j || (std::rand() / double(RAND_MAX)) < density) {
R.insert(i, j) = Scalar(std::rand() / double(RAND_MAX) - 0.5);
}
}
}
R.makeCompressed();
SpMat A = R.adjoint() * R;
for (Index i = 0; i < n; ++i) A.coeffRef(i, i) += Scalar(RealScalar(n));
A.makeCompressed();
return A;
}
template <typename Scalar>
double condition_bound(const SparseMatrix<Scalar, ColMajor, int>& A) {
return double(A.norm()) / double(A.rows());
}
// The recursively updated residual keeps shrinking below eps, so a tolerance under eps
// still reports convergence while certifying nothing about x.
template <typename Scalar>
typename NumTraits<Scalar>::Real cg_tolerance() {
return typename NumTraits<Scalar>::Real(100) * NumTraits<Scalar>::epsilon();
}
template <typename Scalar>
Matrix<Scalar, Dynamic, 1> cpu_reference(const SparseMatrix<Scalar, ColMajor, int>& A,
const Matrix<Scalar, Dynamic, 1>& b, typename NumTraits<Scalar>::Real tol) {
ConjugateGradient<SparseMatrix<Scalar, ColMajor, int>, Lower | Upper, IdentityPreconditioner> cpu_cg;
cpu_cg.setMaxIterations(1000);
cpu_cg.setTolerance(tol);
cpu_cg.compute(A);
Matrix<Scalar, Dynamic, 1> x = cpu_cg.solve(b);
VERIFY_IS_EQUAL(cpu_cg.info(), Success);
return x;
}
// At exit the recursive residual is below tol ||b||; the true residual differs by the
// rounding of k <~ 10 iterations of SpMV and vector updates, ~ k sqrt(n) eps ||b|| for random
// signs. Both solutions satisfy that bound, so
// ||x_gpu - x_cpu|| <= ||A^-1|| (||r_gpu|| + ||r_cpu||) <= 2 kappa relres_bound ||x_cpu||.
// The residuals are evaluated in double so that a float check carries no rounding of its own.
template <typename Scalar>
void check_cg_solution(const SparseMatrix<Scalar, ColMajor, int>& A, const Matrix<Scalar, Dynamic, 1>& b,
const Matrix<Scalar, Dynamic, 1>& x_gpu, const Matrix<Scalar, Dynamic, 1>& x_cpu,
typename NumTraits<Scalar>::Real tol) {
const double eps = double(NumTraits<Scalar>::epsilon());
const double relres_bound = double(tol) + 10 * std::sqrt(double(A.rows())) * eps;
const VectorXd r = A.template cast<double>() * x_gpu.template cast<double>() - b.template cast<double>();
VERIFY(r.norm() <= relres_bound * b.template cast<double>().norm());
const VectorXd dx = (x_gpu - x_cpu).template cast<double>();
VERIFY(dx.norm() <= 2 * condition_bound(A) * relres_bound * x_cpu.template cast<double>().norm());
}
// ---- Eigen's ConjugateGradient class on device types ------------------------
// DeviceSparseView is a matrix-free matrix type, so the public solver class
// instantiates with it; solveWithGuessInPlace() is the entry point that takes
// non-Eigen vectors.
template <typename Scalar>
void test_gpu_cg_class(Index n) {
using SpMat = SparseMatrix<Scalar, ColMajor, int>;
using Vec = Matrix<Scalar, Dynamic, 1>;
using RealScalar = typename NumTraits<Scalar>::Real;
SpMat A = make_spd<Scalar>(n);
Vec b = Vec::Random(n);
const RealScalar tol = cg_tolerance<Scalar>();
Vec x_cpu = cpu_reference(A, b, tol);
gpu::Context ctx;
gpu::Context::setThreadLocal(&ctx);
gpu::SparseContext<Scalar> spmv_ctx(ctx);
auto mat = spmv_ctx.deviceView(A);
auto d_b = gpu::DeviceMatrix<Scalar>::fromHost(b, ctx.stream());
gpu::DeviceMatrix<Scalar> d_x(n, 1);
d_x.setZero(ctx);
ConjugateGradient<gpu::DeviceSparseView<Scalar>, Lower | Upper, IdentityPreconditioner> cg;
cg.setMaxIterations(1000);
cg.setTolerance(tol);
cg.compute(mat);
cg.solveWithGuessInPlace(d_b, d_x);
VERIFY_IS_EQUAL(cg.info(), Success);
VERIFY(cg.iterations() > 0 && cg.iterations() < 1000);
VERIFY(cg.error() <= tol);
// The deep copy and the scalar division the algorithm relies on (`p = precond.solve(r)`,
// `residual /= residualScale`). 3 is not a power of two, so the quotient rounds; each element
// must be within 2 eps of the host division.
gpu::DeviceMatrix<Scalar> d_c = d_b;
d_c /= Scalar(3);
gpu::Context::setThreadLocal(nullptr);
const Vec c = d_c.toHost(ctx.stream());
const Vec c_ref = b / Scalar(3);
VERIFY(((c - c_ref).cwiseAbs().array() <= RealScalar(2) * NumTraits<Scalar>::epsilon() * c_ref.cwiseAbs().array())
.all());
Vec x_gpu = d_x.toHost(ctx.stream());
check_cg_solution(A, b, x_gpu, x_cpu, tol);
}
// ---- Extreme right-hand sides -----------------------------------------------
// GPU analog of test_conjugate_gradient_extreme_rhs, at scales where ||b||^2 underflows to 0 or
// overflows: a nrm2 without scaling then gives rhsNorm = 0 and x = 0 reported as converged, or
// rhsNorm = inf and NaN in x. At max/4 the residual norm is within a factor 3 of max, so
// `residual /= residualScale` must divide and `x += (residualScale * alpha) * p` stay finite.
template <typename Scalar>
void test_gpu_cg_extreme_rhs() {
using SpMat = SparseMatrix<Scalar, ColMajor, int>;
using Vec = Matrix<Scalar, Dynamic, 1>;
using RealScalar = typename NumTraits<Scalar>::Real;
using Limits = std::numeric_limits<RealScalar>;
SpMat A(2, 2);
A.insert(0, 0) = Scalar(1);
A.insert(1, 1) = Scalar(1);
A.makeCompressed();
Vec direction(2);
direction << Scalar(1), Scalar(-1);
gpu::Context ctx;
gpu::Context::setThreadLocal(&ctx);
gpu::SparseContext<Scalar> spmv_ctx(ctx);
auto mat = spmv_ctx.deviceView(A);
ConjugateGradient<gpu::DeviceSparseView<Scalar>, Lower | Upper, IdentityPreconditioner> cg;
cg.setTolerance(cg_tolerance<Scalar>());
cg.compute(mat);
const RealScalar scales[] = {numext::sqrt(Limits::denorm_min()) * RealScalar(1e-10),
numext::sqrt((Limits::max)()) * RealScalar(1e10), (Limits::max)() / RealScalar(4)};
for (RealScalar scale : scales) {
const Vec rhs = scale * direction;
auto d_b = gpu::DeviceMatrix<Scalar>::fromHost(rhs, ctx.stream());
for (RealScalar guess : {RealScalar(0), RealScalar(0.5)}) {
const Vec x0 = guess * rhs;
auto d_x = gpu::DeviceMatrix<Scalar>::fromHost(x0, ctx.stream());
cg.solveWithGuessInPlace(d_b, d_x);
VERIFY_IS_EQUAL(cg.info(), Success);
VERIFY(cg.iterations() <= 1);
const Vec x = d_x.toHost(ctx.stream());
VERIFY(x.allFinite());
VERIFY_IS_APPROX(x / scale, direction);
}
}
gpu::Context::setThreadLocal(nullptr);
}
// ---- The module's hand-written CG loop --------------------------------------
// The README's loop: every scalar intermediate stays on device as a DeviceScalar and the
// convergence check is the one host sync per iteration. `jacobi` selects the diagonal
// preconditioner, applied as a cwiseProduct with 1 / diag(A).
template <typename Scalar>
void test_gpu_cg_loop(Index n, bool jacobi) {
using SpMat = SparseMatrix<Scalar, ColMajor, int>;
using Vec = Matrix<Scalar, Dynamic, 1>;
using RealScalar = typename NumTraits<Scalar>::Real;
SpMat A = make_spd<Scalar>(n);
Vec b = Vec::Random(n);
const RealScalar tol = cg_tolerance<Scalar>();
Vec x_cpu = cpu_reference(A, b, tol);
Vec invdiag(n);
for (Index j = 0; j < n; ++j) invdiag(j) = Scalar(1) / A.coeff(j, j);
gpu::Context ctx;
gpu::Context::setThreadLocal(&ctx);
gpu::SparseContext<Scalar> spmv_ctx(ctx);
auto mat = spmv_ctx.deviceView(A);
auto d_invdiag = gpu::DeviceMatrix<Scalar>::fromHost(invdiag, ctx.stream());
auto d_b = gpu::DeviceMatrix<Scalar>::fromHost(b, ctx.stream());
gpu::DeviceMatrix<Scalar> d_x(n, 1);
d_x.setZero(ctx);
// r = b (since x = 0)
gpu::DeviceMatrix<Scalar> residual(n, 1);
residual.copyFrom(ctx, d_b);
RealScalar rhsNorm2 = d_b.squaredNorm(ctx);
RealScalar threshold = tol * tol * rhsNorm2;
RealScalar residualNorm2 = residual.squaredNorm(ctx);
// p = precond.solve(r)
gpu::DeviceMatrix<Scalar> p(n, 1);
if (jacobi) {
p.cwiseProduct(ctx, d_invdiag, residual);
} else {
p.copyFrom(ctx, residual);
}
gpu::DeviceMatrix<Scalar> z(n, 1), tmp(n, 1);
auto absNew = residual.dot(ctx, p);
Index maxIters = 1000;
Index i = 0;
while (i < maxIters) {
tmp.noalias() = mat * p;
auto alpha = absNew / p.dot(ctx, tmp);
d_x += alpha * p;
residual -= alpha * tmp;
residualNorm2 = residual.squaredNorm(ctx);
if (residualNorm2 < threshold) break;
// z = precond.solve(r)
if (jacobi) {
z.cwiseProduct(ctx, d_invdiag, residual);
} else {
z.copyFrom(ctx, residual);
}
auto absOld = std::move(absNew);
absNew = residual.dot(ctx, z);
auto beta = absNew / absOld;
p *= beta;
p += z;
i++;
}
gpu::Context::setThreadLocal(nullptr);
Vec x_gpu = d_x.toHost(ctx.stream());
check_cg_solution(A, b, x_gpu, x_cpu, tol);
}
EIGEN_DECLARE_TEST(gpu_cg) {
gpu_test::require_cusparse_context();
// Split by scalar so each part compiles in parallel.
CALL_SUBTEST_1(test_gpu_cg_class<double>(64));
CALL_SUBTEST_1(test_gpu_cg_class<double>(256));
CALL_SUBTEST_1(test_gpu_cg_extreme_rhs<double>());
CALL_SUBTEST_1(test_gpu_cg_loop<double>(64, false));
CALL_SUBTEST_1(test_gpu_cg_loop<double>(256, false));
CALL_SUBTEST_1(test_gpu_cg_loop<double>(256, true));
CALL_SUBTEST_2(test_gpu_cg_class<float>(64));
CALL_SUBTEST_2(test_gpu_cg_class<float>(256));
CALL_SUBTEST_2(test_gpu_cg_extreme_rhs<float>());
CALL_SUBTEST_2(test_gpu_cg_loop<float>(64, false));
CALL_SUBTEST_2(test_gpu_cg_loop<float>(256, false));
CALL_SUBTEST_2(test_gpu_cg_loop<float>(256, true));
}