Using GMRES and KrylovPrecond classes

This tutorial demonstrates how to use the GMRES solver with a Krylov preconditioner to solve linear systems efficiently. We’ll walk through constructing a linear operator, creating instances of GMRES and KrylovPrecond, and solving a linear system. For more advanced usage and additional features, please refer to the API in lin-solve.hpp.

Step 1: Constructing a Linear Operator

First, we need to define our linear operator. This operator represents the matrix-vector multiplication in the linear system Ax = b. We’ll use a lambda function to define this operator.

auto LinOp = [&A](Vector<double>* Ax, const Vector<double>& x) {
    // Define the action of A on x and store the result in Ax
};

Note

GMRES passes an Ax that is already sized to x.Dim() and is backed by fixed-size scratch storage. Do not call Ax->ReInit(...) when the size already matches — it asserts. The safe idiom is:

if (Ax->Dim() != x.Dim()) Ax->ReInit(x.Dim());

Step 2: Creating Instances of GMRES and KrylovPrecond

Next, we’ll create instances of the GMRES solver and the Krylov preconditioner.

GMRES<double> solver;
KrylovPrecond<double> krylov_precond;

The GMRES constructor accepts two optional knobs that control the Arnoldi orthogonalization step:

// Classical Gram-Schmidt with one reorthogonalization pass — recommended
// for distributed (MPI) runs: one batched Allreduce per iteration, with
// MGS-equivalent stability.
GMRES<double> solver(comm, /*verbose=*/true,
                     GMRES<double>::GramSchmidt::CGS, /*num_reorth=*/1);

The default (MGS, 0) matches the legacy behavior and is a good choice for serial or shared-memory runs. See lin-solve.hpp for the full latency/stability trade-off.

Step 3: Solving the Linear System

With our linear operator and solver instances ready, we can now solve the linear system Ax = b. We’ll call the GMRES solver with the necessary arguments.

Vector<double> x, b;
Real tol = 1e-10;
solver(&x, LinOp, b, tol, -1, false, nullptr, &krylov_precond);

The above call to the GMRES solver will initialize the Krylov preconditioner and solve the linear system. Subsequent solves with the same linear operator will benefit from the preconditioner and may require fewer iterations.

Note

Ensure that your linear operator and right-hand side vector b are properly defined before calling the solver.

Example Code

The complete example code is shown below:

#include "sctl.hpp"
using namespace sctl;

Matrix<double> LowRankMatrix(const Long N, const Long rank) {
  Matrix<double> M(N, N);
  M.SetZero();

  for (Long r = 0; r < rank; r++) {
    Matrix<double> U(N,1), Vt(1, N);
    for (auto& a : U ) a = drand48();
    for (auto& a : Vt) a = drand48();
    M += U * Vt * exp(log(machine_eps<double>())*r/rank);
  }
  return M;
}

double max_norm(const Vector<double>& v) {
  double max_val = 0;
  for (const auto a : v) max_val = std::max<double>(max_val, fabs(a));
  return max_val;
}

int main(int argc, char** argv) {
  Comm::MPI_Init(&argc, &argv);

  { // Example usage for GMRES and KrylovPrecond
    Long N = 200, rank = 200;

    // Build A = I + <low-rank>
    Matrix<double> A = LowRankMatrix(N, rank);
    for (Long i = 0; i < N; i++) A[i][i] += 1;

    // Build linear operator
    auto LinOp = [&A](Vector<double>* Ax, const Vector<double>& x) {
      const Long N = x.Dim();
      if (Ax->Dim() != N) Ax->ReInit(N);
      Matrix<double> Ax_(N, 1, Ax->begin(), false);
      Ax_ = A * Matrix<double>(N, 1, (Iterator<double>)x.begin(), false);
    };

    // Set exact solution x0 and the RHS b := A * x
    Vector<double> x0(N), b(N), x;
    for (auto& a : x0) a = drand48();
    LinOp(&b, x0);

    // Solve using GMRES
    GMRES<double> solver;
    KrylovPrecond<double> krylov_precond;
    solver(&x, LinOp, b, 1e-10, -1, false, nullptr, &krylov_precond);
    std::cout<<"Solution error = "<<max_norm(x-x0)<<"\n\n";

    // Solve a new problem, reusing the Krylov preconditioner
    for (auto& a : x0) a = drand48();
    LinOp(&b, x0);
    solver(&x, LinOp, b, 1e-10, -1, false, nullptr, &krylov_precond);
    std::cout<<"Solution error = "<<max_norm(x-x0)<<"\n\n";
  }

  Comm::MPI_Finalize();
  return 0;
}

This generates the following output, where the first solve requires 23 GMRES iterations, while the second solve converges just 8 iterations due to preconditioning.

  0 KSP Residual norm 2.099176367385e+03
  1 KSP Residual norm 9.003181335456e+00
  2 KSP Residual norm 4.229781314809e+00
  3 KSP Residual norm 3.014948102699e+00
  4 KSP Residual norm 2.848098976816e+00
  5 KSP Residual norm 2.353313340801e+00
  6 KSP Residual norm 1.317737385363e+00
  7 KSP Residual norm 1.299136900499e+00
  8 KSP Residual norm 5.830786318937e-01
  9 KSP Residual norm 4.104966218497e-01
 10 KSP Residual norm 2.296154200197e-01
 11 KSP Residual norm 1.132160334702e-01
 12 KSP Residual norm 7.357733656165e-02
 13 KSP Residual norm 6.190744358418e-02
 14 KSP Residual norm 5.755616763787e-02
 15 KSP Residual norm 5.741807896259e-02
 16 KSP Residual norm 1.680949998420e-02
 17 KSP Residual norm 1.816481776180e-03
 18 KSP Residual norm 6.359376010342e-04
 19 KSP Residual norm 1.981085110869e-04
 20 KSP Residual norm 5.229997256823e-05
 21 KSP Residual norm 1.046989208963e-05
 22 KSP Residual norm 1.443750846938e-06
 23 KSP Residual norm 1.212803427478e-07
Solution error = 1.67549e-06

  0 KSP Residual norm 1.834616605684e+01
  1 KSP Residual norm 7.976544634403e+00
  2 KSP Residual norm 1.159404821738e+00
  3 KSP Residual norm 2.388777265664e-02
  4 KSP Residual norm 4.215249431341e-03
  5 KSP Residual norm 1.516499740199e-04
  6 KSP Residual norm 1.152629619068e-05
  7 KSP Residual norm 1.254922886814e-06
  8 KSP Residual norm 1.542127320799e-08
Solution error = 5.41102e-08