Custom solvers¶
A solver in pops is a typed brick. It can be native C++, written in the Python DSL and
generated to C++, provided by an external C++ library, or specialized into problem.so. In
every case it runs in C++ and uses the shared HPC primitives (dot, norm, axpy, MPI
reductions, the scratch manager, the profiler); Python never iterates a Krylov loop.
Native solvers (today)¶
The matrix-free Krylov solvers are native C++ free functions in
include/pops/numerics/elliptic/linear/generic_krylov.hpp:
Solver |
Symbol |
|---|---|
Richardson |
|
CG |
|
BiCGStab |
|
GMRES |
|
They are named by descriptors (Typed bricks (pops.lib)): pops.solvers.CG(),
pops.solvers.BiCGStab(), pops.solvers.GMRES(), pops.solvers.Richardson(). pops.solvers
is the one public home for these descriptors. A compiled time Program drives them through
P.solve_linear(...) (Time programs); the elliptic field solve uses the geometric
multigrid (pops::GeometricMG).
Generated solvers (internal / experimental)¶
Internal / experimental API
The solver-generation DSL below lives in pops.codegen.solvers (Spec 5 criterion 19: a
solver-gen DSL, if any, lives in the codegen layer). It is INTERNAL / EXPERIMENTAL, not a stable
public API, and its surface may change or disappear without notice. There is no public @solver
decorator on pops / pops.lib / pops.solvers. Users do NOT author solvers: they CONFIGURE the
provided C++ solver descriptors in pops.solvers (the presets above) and let codegen / the
runtime drive them.
A solver can be written in the Python DSL and generated to C++ – it builds an IR, it does not compute in Python:
@pops.codegen.solvers.solver(name="richardson", signature="(A, b)")
def richardson(ctx, A, b, *, omega=0.5, tol=1e-8, max_iter=200):
x = ctx.zeros_like(b)
it = ctx.scalar_int(0)
# The convergence predicate is a BUILDER re-evaluated against the loop-updated x
# each pass; passing a pre-built Bool would freeze the test on the initial iterate.
def converging():
return ctx.logical_and(ctx.norm2(ctx.residual(A, x, b)) > tol,
it < ctx.scalar_int(max_iter))
with ctx.while_(converging):
r = ctx.residual(A, x, b) # r = b - A x
x = ctx.combine(x + omega * r) # x <- x + omega*r
it = it + ctx.scalar_int(1)
return x
pops.codegen.solvers.generate_solver_cpp(solver) lowers the IR to a self-contained C++ kernel:
template <class Op>
pops::KrylovResult richardson_solve(const Op& A, pops::MultiFab& x, const pops::MultiFab& b) {
// scratch fields allocated ONCE, before the loop
x.set_val(0); // warm start: zeros_like(b)
for (;; ++pops_iters) { // a REAL C++ loop; the predicate re-evaluates
A(v, x); // matrix-free A(x) (template Op, inlined)
pops::saxpy(r, 1.0, b); pops::saxpy(r, -1.0, v); // r = b - A x
if (!((std::sqrt(pops::dot(r, r)) > tol) && (pops_iters < max_iter))) break;
pops::saxpy(x, omega, r); // x <- x + omega*r
}
}
The operator A is a value-typed template parameter (the shape the native Krylov loops
take), so it inlines: no type-erased indirection in the kernel, no Python callback in the loop,
no heap allocation inside it, and no per-cell name dispatch (criterion 24.9). The kernel calls
the shared matrix-free primitives (pops::dot / pops::saxpy / pops::lincomb); a DSL solver that
maps onto a native scheme keeps the pops::*_solve free functions as its backend. Modes:
native, generated, library, specialized, auto.
Status
The native Krylov solvers, the pops.solvers descriptors, the internal / experimental solver
DSL (@pops.codegen.solvers.solver authoring + generate_solver_cpp C++ lowering), and
pops.compile_library (compiling a brick library to a real .so, see typed-bricks) exist. The
specialization modes and external C++ solver registration are follow-ups; the Program
solve_linear over the native Krylov solvers is the supported runtime path today.