Matrix-free operators

In addition to supporting computation with the workhorse of sparse linear algebra, an assembled sparse matrix, Firedrake also supports computing “matrix-free”. In this case, the matrix returned from assemble() implements matrix-vector multiplication by the assembly of a 1-form subject to boundary conditions rather than direct construction of a sparse matrix (“aij” format) followed by traditional CSR algorithms. This functionality is documented in more detail in [KM18].

There are two ways of accessing this functionality. One can either request a matrix-free operator by passing mat_type="matfree" to assemble(). In this case, the returned object is an ImplicitMatrix. This object can be used in the normal way with a LinearSolver. Alternately, when solving a variational problem, an ImplicitMatrix is requested through the solver_parameters dict, by setting the option mat_type to matfree. The type of the preconditioning matrix can be controlled separately by setting pmat_type.

Generically, one can expect such a matrix to be cheaper to “assemble” and to use less memory, especially for high-order discretizations or complicated systems. The downside is that traditional algebraic preconditioners will not work with such unassembled matrices. To take advantage of these features, we need to configure our solvers correctly. To expedite this, the matrix-free operator, implemented as a PETSc shell matrix, contains an application context of type ImplicitMatrixContext. This context provides some important features to enabled advanced solver configuration.

Splitting unassembled matrices

For the purposes of fieldsplit preconditioners, the PETSc matrix object must be able to extract submatrices. For unassembled matrices, this is achieved through a specialized ImplicitMatrixContext.createSubMatrix() method that partitions the UFL form defining the operator into pieces corresponding to the integer labels of the unknown fields. This is in contrast to the normal splitting of assembled matrices which operates at a purely algebraic level. With unassembled operators, the PDE description is available in the matrix, and is therefore propagated down to the split operators.

Preconditioning unassembled matrices

As well as providing symbolic field splitting, the ImplicitMatrixContext object is available to preconditioners. Since it contains a complete UFL description of the bilinear form, preconditioners can query or manipulate it as desired. As a particularly simple example, the class AssembledPC simply passes the UFL into assemble() to produce an explicit matrix during set up. It also sets up a new PETSc PC context acting on this assembled matrix so that the user can configure it at run-time via the options database. This allows the use of matrix-free actions in the Krylov solve, preconditioned using an assembled matrix.

Providing application context to preconditioners

Frequently, such custom preconditioners require some additional information that will not be fully available from the UFL description in ImplicitMatrixContext. For example, it is not possible to extract physical parameters such as the Reynolds number from a UFL bilinear form. In this case, the solver accepts a dictionary "appctx" as an optional keyword argument, the same argument may also be passed to assemble() in the case of preassembled solves. Firedrake passes that down into the ImplicitMatrixContext so that it is accessible to preconditioners.

Example usage

To demonstrate basic usage of matrix-free operators and preconditioners, we show a simple Poisson problem, introducing some of the additional solver options.

Poisson equation

It is what it is, a conforming discretization on a regular mesh using piecewise quadratic elements.

As usual we start by importing firedrake and setting up the problem.:

from firedrake import *

N = 128

mesh = UnitSquareMesh(N, N)

V = FunctionSpace(mesh, "CG", 2)

u = TrialFunction(V)
v = TestFunction(V)

a = inner(grad(u), grad(v)) * dx

x = SpatialCoordinate(mesh)
F = Function(V)
F.interpolate(sin(x[0]*pi)*sin(2*x[1]*pi))
L = F*v*dx

bcs = [DirichletBC(V, Constant(2.0), (1,))]

uu = Function(V)

With the setup out of the way, we now demonstrate various ways of configuring the solver. First, a direct solve with an assembled operator.:

solve(a == L, uu, bcs=bcs, solver_parameters={"ksp_type": "preonly",
                                              "pc_type": "lu"})

Next, we use unpreconditioned conjugate gradients using matrix-free actions. This is not very efficient due to the \(h^{-2}\) conditioning of the Laplacian, but demonstrates how to request an unassembled operator using the "mat_type" solver parameter.:

uu.assign(0)
solve(a == L, uu, bcs=bcs, solver_parameters={"mat_type": "matfree",
                                              "ksp_type": "cg",
                                              "pc_type": "none",
                                              "ksp_monitor": None})

Finally, we demonstrate the use of a AssembledPC preconditioner. This uses matrix-free actions but preconditions the Krylov iterations with an incomplete LU factorisation of the assembled operator.:

uu.assign(0)
solve(a == L, uu, bcs=bcs, solver_parameters={"mat_type": "matfree",
                                              "ksp_type": "cg",
                                              "ksp_monitor": None,

To use the assembled matrix for the preconditioner we select a "python" type:

"pc_type": "python",

and set its type, by providing the name of the class constructor to PETSc.:

"pc_python_type": "firedrake.AssembledPC",

Finally, we set the preconditioner type for the assembled operator:

"assembled_pc_type": "ilu"})

This demo is available as a runnable python file here.