Multigrid#
gko::solver::Multigrid builds a hierarchy of progressively coarser representations
of a system matrix and uses cheap smoother iterations on each level combined with
exact (or near-exact) solves on the coarsest level to attack errors at all
frequencies. It is most effective on elliptic and diagonally-dominant problems where
the low-frequency error modes (the ones Krylov methods struggle with) can be
captured by a coarse-grid representation. In practice, Ginkgo’s Multigrid is most
often used as a preconditioner inside a Krylov solver, not as the outermost
iteration.
The architecture#
A multigrid solve is a recursive structure of four ingredients per level:
finest level (level 0) A_0 <-- system matrix
|
smooth (pre)
|
restrict --> A_1 = R A_0 P
|
smooth (pre)
|
restrict --> A_2 ...
...
coarsest level
(direct solve, by default)
...
prolong <-- correction
|
smooth (post)
|
prolong <-- correction
|
smooth (post)
finest level (output)
Smoothers (
pre_smoother,post_smoother, optionalmid_smoother) attack high-frequency errors that the coarse grid cannot represent. Defaults to one step of iterative refinement with a scalar Jacobi preconditioner if not specified (with scalar Jacobi as the local solver ofSchwarzin distributed runs).Restriction (
R) maps the residual from a fine level to a coarse level.Prolongation (
P) maps a correction from a coarse level back to the fine level.Coarsening (the
mg_levelfactories) computesR,P, andA_{i+1} = R A_i Pfor every level. This is the algebraic component that makes Ginkgo an algebraic multigrid (AMG) library; see Coarsening methods below.Coarsest solver (
coarsest_solver) handles the smallest-level system. Defaults to a direct LU solver if not specified.
Each pair of consecutive levels is represented by a gko::multigrid::MultigridLevel
implementation. The interface exposes four operators:
auto fine = level->get_fine_op(); // A_i
auto restrict = level->get_restrict_op(); // R
auto coarse = level->get_coarse_op(); // A_{i+1}
auto prolong = level->get_prolong_op(); // P
This is the contract every coarsening method satisfies. A custom coarsening
is a LinOp that also inherits gko::multigrid::EnableMultigridLevel<ValueType>
and calls set_multigrid_level(prolong, coarse, restrict) from generate().
Cycle types#
The cycle parameter controls how the recursion descends and ascends through the
hierarchy. At every non-coarsest level, the cycle does pre-smooth → restrict → one
or more coarse-grid sub-cycles → prolong → post-smooth. The three available cycle
types differ only in the number and shape of the coarse-grid sub-cycles:
|
Coarse-grid sub-cycles per level |
Cost per outer iteration |
|---|---|---|
|
One V-recursive sub-cycle. |
Cheapest, typical default. |
|
Two sub-cycles: first an F-recursive call, then a V-recursive call. |
Between V and W. |
|
Two W-recursive sub-cycles. |
Most expensive but often the most effective per iteration on stiff problems. |
The exact branching is in MultigridState::run_cycle: the V case takes the early-exit end_of_cycle path after a single recursive call, while F and W make a second recursive call before prolongation.
Key factory parameters#
The Multigrid::build() interface exposes a sizeable parameter surface. Most users
need to set only a small handful: the defaults are intended to work on standard
elliptic problems out of the box.
Parameter |
Type |
Purpose |
|---|---|---|
|
vector of |
Coarsening factories; see Coarsening methods. Required; generation fails without at least one entry. |
|
vector of |
Smoother factory per level (or one shared). Empty vector means “use the default smoother”: one step of iterative refinement with scalar Jacobi (with scalar Jacobi as the local solver of |
|
|
When |
|
|
Behaviour of the mid-smoother in F and W cycles: |
|
|
Maximum number of coarsening steps (excluding the coarsest solver). |
|
|
Stop coarsening once the matrix has at most this many rows. |
|
vector of |
Solver factory for the coarsest level. The default is a direct LU solver in single-process runs, GMRES with Jacobi on DPC++ and GMRES with a Schwarz preconditioner (Jacobi local solver) in distributed runs. |
|
|
V, W, or F cycle. |
|
scalar |
Tune the default smoother (they apply wherever no smoother factory is given). |
|
vector of |
Stopping criteria of the multigrid iteration. Required, also when |
|
|
What to assume about the initial |
|
|
Pick which |
mg_level needs at least one factory. The smoother and coarsest-solver vectors
(pre_smoother, post_smoother, mid_smoother, coarsest_solver) follow one
selection rule:
Empty → use the default for that role.
One entry → that single factory is used at every level.
Multiple entries →
level_selector(formg_leveland the smoothers) orsolver_selector(forcoarsest_solver), or its default, picks one per level.
This gives fine-grained per-level control without forcing the user to write a selector function for the common cases.
Coarsening methods#
The coarsening method is the algebraic component that turns a fine matrix into a
coarse one. It is what makes a multigrid solver algebraic: pure geometric multigrid
would expect the user to supply the hierarchy from a known mesh. Ginkgo
ships five coarsening implementations, each deriving from gko::LinOp and
gko::multigrid::EnableMultigridLevel (a custom level does the same):
Class |
Approach |
|---|---|
|
Aggregation by parallel graph matching (the AmgX algorithm). The standard AMG-style coarsening; works on most algebraic problems. Supports distributed matrices; see the Pgm page for the cross-rank aggregation protocol. Supports |
|
Classical C/F splitting by a parallel modified independent set with direct interpolation; strong connections chosen by |
|
Ruge–Stüben classical coarsening for M-matrices: greedy C/F splitting on the host and classical interpolation. Coarsens non-distributed matrices; see the Rs page. |
|
Index-based coarsening that keeps every |
|
User-specified coarse rows. Useful when you have external knowledge of which DOFs should survive coarsening (e.g., from a hierarchical mesh); UniformCoarsening, Pmis and Rs pick the coarse rows automatically. Coarsens non-distributed matrices. |
All of them produce four operators at each level (fine, coarse, restrict,
prolong) satisfying the MultigridLevel contract. They are passed to Multigrid
through the mg_level parameter.
Construction#
A typical multigrid-as-preconditioner setup. Multigrid requires mg_level and
criteria, also when it is used as a preconditioner:
auto solver =
gko::solver::Cg<double>::build()
.with_preconditioner(
gko::solver::Multigrid::build()
.with_mg_level(gko::multigrid::Pgm<double, int>::build())
.with_criteria(gko::stop::max_iters(1u)))
.with_criteria(gko::stop::max_iters(1000u),
gko::stop::relative_residual_norm(1e-8))
.on(exec)
->generate(A);
solver->apply(b, x);
Here max_iters(1u) makes the preconditioner apply one multigrid cycle per outer
iteration. A factory created with .on(exec) is a std::unique_ptr; wrap it in
gko::share before passing it to with_mg_level, or pass the builder directly as above.
For a stand-alone multigrid solve (rare in practice but supported), give
Multigrid real stopping criteria and skip the outer Krylov solver:
auto solver_factory =
gko::solver::Multigrid::build()
.with_mg_level(gko::multigrid::Pgm<double, int>::build())
.with_criteria(gko::stop::max_iters(100u),
gko::stop::relative_residual_norm(1e-8))
.on(exec);
auto solver = solver_factory->generate(A);
solver->apply(b, x);
Reusing the hierarchy#
When a sequence of matrices shares one sparsity pattern, generate_reuse() records
the hierarchy of the first matrix in a reuse object and recomputes only what depends
on the values for later matrices. Levels are reused while they reproduce their coarse
sizes; below the first level that comes out different, everything is generated anew.
Pgm keeps its aggregates, so the first matrix should be representative of the later
ones. Among the coarsenings, Pgm implements reuse; levels from the other
coarsenings are generated again.
The reuse object belongs to the Multigrid factory. To use it inside a Krylov solver,
generate the Multigrid with its own factory’s generate_reuse() and hand the result
to the solver through with_generated_preconditioner(...) or set_preconditioner(...);
calling generate_reuse() on the Krylov solver’s factory performs a full rebuild.
auto factory = gko::solver::Multigrid::build()
.with_mg_level(gko::multigrid::Pgm<double, int>::build())
.with_criteria(gko::stop::max_iters(1u))
.on(exec);
auto reuse_data = factory->create_empty_reuse_data();
auto mg = factory->generate_reuse(A, *reuse_data);
// ... change the values of A, keeping its sparsity pattern ...
mg = factory->generate_reuse(A, *reuse_data);
Attention
The reuse interface (create_empty_reuse_data(), generate_reuse()) is
experimental and may change.
When multigrid helps (and when it does not)#
Multigrid shines when:
The problem is elliptic or strongly diagonally dominant (Poisson, diffusion, structural mechanics with reasonable conditioning).
The matrix has natural locality: neighbours in the graph correspond to spatially nearby unknowns.
You have time to tune cycle type, smoother, and coarsening for the specific PDE and discretisation; a poorly-tuned multigrid can be slower than CG-Jacobi.
Multigrid struggles when:
The matrix has long-range coupling or no natural mesh structure (e.g., problems derived from highly irregular graphs).
The problem is strongly non-symmetric or indefinite: the smoother and coarse solver assumptions break down.
In Ginkgo specifically, Multigrid is most often used as a preconditioner inside
CG, FCG, or GMRES. Used that way, even an imperfectly-tuned multigrid can outperform
simpler preconditioners on the right class of problems.
See also
Solver taxonomy: multigrid in context of the other families.
Preconditioner taxonomy: the preconditioners that
build_smootherwraps for the smoother slots.LinOp and composition: the factory pattern multigrid uses.
Publications: Mixed Precision Algebraic Multigrid on GPUs, Three-Precision Algebraic Multigrid on GPUs
API reference:
gko::solver::Multigrid,gko::multigrid::MultigridLevel