Faster implicit pressure solves in OpenFOAM¶
pinneapple_simulation.accelerated_foam makes the pressure solve of an OpenFOAM run cheaper. It changes only
the initial guess of the linear solve. OpenFOAM's own solver (PCG, GAMG, ...) still iterates, with the same
tolerance and the same stopping test, so the converged fields pass the same convergence test as a plain run
(section 2 shows why an equal residual is not always an equal error, and compares at matched accuracy). Every
number on this page comes from a run made with the code in this package, and each one is compared with plain
OpenFOAM on the same case at the same settings.
Where the time goes¶
In a pressure-based solver (PISO in icoFoam/pisoFoam, PIMPLE in pimpleFoam/interFoam, SIMPLE in
simpleFoam) every corrector assembles and solves a Poisson-type equation for the pressure (p, or p_rgh
with gravity): fvm::laplacian(rAU, p) == fvc::div(phiHbyA). The matrix is symmetric and negative
definite, it is new at every corrector because rAU changes, and it is solved by PCG (DIC or FDIC
preconditioner) or GAMG. Momentum, turbulence and phase-fraction equations are much cheaper to solve. They are
diagonally dominant and need a few smoother sweeps. The pressure equation is elliptic, so its iteration count
grows with the mesh.
The logs of the runs below show this directly. On the 128 x 128 cavity with PCG-DIC, the pressure needed 34 239 iterations over 256 solves, while the velocity components needed 1 478 + 1 492 symmetric Gauss-Seidel sweeps over 128 solves each. At 256 x 256 the figures were 74 261 against 2 540 + 2 579. Each pressure iteration also costs more than a smoother sweep, because it adds a preconditioner application and two global reductions. Profiling studies by GPU-solver vendors also treat the pressure solve as the part worth offloading. One NVIDIA presentation reports that the pressure solve is still "about 35% of total" after offloading it to AmgX (OpenFOAM workshop 2020; we could not open the slides, so this figure comes from search excerpts). We found no peer-reviewed CPU breakdown for PISO/PIMPLE that we could verify, so this page relies on our own logs.
What exists, and what it costs to use¶
| Tool | What it does | Licence | Use here |
|---|---|---|---|
| PETSc4FOAM (gitlab.com/petsc/petsc4foam, upstream develop.openfoam.com/modules/external-solver) | ldu2csr conversion and PETSc/Hypre solvers behind solver petsc; (BoomerAMG, GPU back-ends, matrix caching) |
GPL-3.0-or-later (README and LICENSE) | Not used. It is a separate build against PETSc. |
| AmgX (github.com/NVIDIA/AMGX) | GPU algebraic multigrid and Krylov | BSD-3-Clause | Not used: there is no GPU here. |
| amgx4Foam (github.com/SimFlowCFD/foamExternalSolvers) | OpenFOAM to AmgX bridge | No LICENSE file in that repository; the Cineca original was not reachable | Not used: the licence is unknown. |
| Ginkgo (github.com/ginkgo-project/ginkgo) | Portable sparse linear algebra (CUDA/HIP/SYCL/OpenMP), including mixed precision | BSD-3-Clause | Not used. |
| OGL (github.com/hpsim/OGL; Olenik et al., Meccanica 2024, doi:10.1007/s11012-024-01806-1) | Ginkgo solvers as OpenFOAM lduMatrix solvers |
GPL-3.0 | Not used. |
| RapidCFD (github.com/Atizar/RapidCFD-dev) | OpenFOAM fork with everything on CUDA | GPLv3 | Not used. It is a fork, not a plugin. |
| pyamg | Smoothed-aggregation and classical AMG in Python | MIT | Optional, for the Python benchmarks |
None of these is a dependency of PINNeAPPle, and no code from them is copied into the repository. The
pinneWarmStart plugin is our own code. It is written against OpenFOAM's public lduMatrix::solver interface, the
same runtime-selection mechanism that PETSc4FOAM and OGL use. OpenFOAM is GPL-3.0-or-later: this repository
contains no OpenFOAM source, and the plugin binary you build links against your installed libOpenFOAM.so,
so that binary falls under OpenFOAM's licence if you distribute it.
Ideas from the literature, and which ones were tried. Learned multigrid and preconditioners: Greenfeld et
al. 2019 (arXiv:1902.10248), Luz et al. 2020
(arXiv:2003.05744), Li et al. ICML 2023
(PMLR 202), and Häusner et al. "Neural incomplete
factorization" (arXiv:2305.16368). Learned pressure predictions or initial
guesses: Tompson et al. 2017 (arXiv:1607.03597), CFD-GCN
(arXiv:2007.04439), and a neural initial guess for pressure-velocity coupling
(arXiv:2409.07175). Projection of a new right-hand side on earlier solutions:
Fischer 1998, CMAME 163:193-204, which PETSc implements as
KSPGUESSFISCHER. POD/ROM predictors as initial
guesses: Markovinović & Jansen 2006, IJNME 68:525-541. Mixed-precision iterative refinement: Carson & Higham
2018, SISC 40:A817. The arXiv and DOI pages could not be opened from the build environment, so these
references were checked through their abstracts and listings, not read in full.
We tried four of them, all measured below:
- Fischer projection on past solutions, in C++ inside OpenFOAM.
- POD of earlier runs.
- A learned graph-network correction of the guess.
- Mixed-precision CG.
We also tried AMG through pyamg, offline only.
What this package does¶
OpenFOAM pressure corrector
fvMatrix::solve -> lduMatrix::solver::New("pinneWarmStart")
1. x_prev = incoming p (what plain OpenFOAM starts from)
2. guess x0: projection on the last k solutions (C++, k mat-vecs)
or a Python guesser via named pipes (POD of earlier runs, learned network, ...)
3. safeguard: keep x0 only if sum|b - A x0| < sum|b - A x_prev|
4. inner OpenFOAM solver (PCG/DIC, GAMG, ...) from x0, same tolerance;
relTol measured against the residual of x_prev, so a run stops where a plain run would
5. store the solution; optionally write A, b, x_prev, x to disk
plugin/pinneWarmStart.C is the solver wrapper (about 500 lines). To use it, set this in fvSolution:
p
{
solver pinneWarmStart;
innerSolver PCG; // or GAMG, with its usual entries
preconditioner DIC;
tolerance 1e-6;
relTol 0.05;
guess projection; // none | projection | external
nSnapshots 8;
snapshots all; // all | converged: keep only solutions of relTol 0 solves
applyTo all; // all | converged: change the guess of relTol 0 solves only
dumpDir ""; // write every system (lduMatrix arrays) to this directory
pipeDir ""; // guess external: request/reply FIFOs served by Python
}
Also load the library from controlDict with libs ("<path>/libpinneWarmStart.so");.
The projection guess is the Galerkin projection on the span of the last k solutions and the current
field: x0 = X (X^T A X)^-1 X^T b, built with the matrix and right-hand side of the current solve. Because
x_prev is in the span, the A-norm error of x0 is never larger than that of x_prev. This is the default
guess that plain OpenFOAM uses. The cost is k matrix-vector products, k(k+1)/2 dot products and a k x k
solve, which is a few inner iterations' worth. The C++ code does a symmetric elimination with diagonal
pivoting that drops linearly dependent snapshots. It runs in parallel, because the reductions use
gSumProd/gSumMag and the history is kept per processor.
relTolReference previous (the default) handles the case where OpenFOAM's relTol is relative to the
initial residual. With a better guess, the same relTol would ask for a smaller absolute residual. That makes
the solve more accurate but not cheaper, and the run would no longer be comparable with a plain run. The
wrapper therefore turns relTol into the absolute tolerance max(tolerance, relTol * r(x_prev)), the point
where a plain run stops.
Building. build.build_plugin() compiles the wrapper with the installed compiler against
libOpenFOAM.so:
- With a full OpenFOAM installation (
$FOAM_SRC/OpenFOAM/lnInclude), it uses those headers.wmake libsoalso works withplugin/Make. - With a binary-only package such as Ubuntu's
openfoam(v1912), there are no headers. Use the source of the same version instead (apt-get source openfoamwith adeb-srcentry), passed asopenfoam_srcor$PINNEAPPLE_OPENFOAM_SRC. The headers are only read at build time.
Python side:
ldu: reads the dumped systems and converts them to SciPy CSR or PyTorch sparse matrices. It also computes OpenFOAM's normalised residual (normFactor).solvers: PCG with Jacobi, DIC (the same algorithm as OpenFOAM's), or pyamg AMG preconditioners, plus a PyTorch CG. The PyTorch CG runs in float64, or in mixed precision with float32 inner CG and float64 refinement. All of them use OpenFOAM's stopping test.warm_start:ProjectionWarmStart(the same algorithm as the C++ code) andPODWarmStart(a POD of earlier runs plus recent solutions).learned:LearnedWarmStart, the projection plus a graph-network correction. It is trained on dumped systems and safeguarded.guess_server: serves any of these guesses to a running solver through named pipes.case: writes the cavity case, runs and parses logs, and compares fields.benchmark: the comparisons on this page, includingtutorial_comparisonfor an installed tutorial.
FreeSurfaceFlow (numerical_solvers/free_surface.py) accepts pressure_solver= and libs=, so its
p_rgh solve can be warm-started.
Verification¶
Extraction is exact. A run with guess none; dumpDir systems; writes every pressure system as OpenFOAM
handed it to PCG. Re-solving those systems in Python with the DIC-PCG of solvers.pcg, starting from the
dumped x_prev, gives:
- the same iteration count as the OpenFOAM log, solve by solve;
- the same initial residual to about 1e-15 relative;
- a solution within about 1e-14 (relative L2) of OpenFOAM's.
test_openfoam_dump_is_reproduced_by_python checks this on a 16 x 16 cavity.
Fields are unchanged within solver tolerance. Every comparison runs three versions of the same case:
- plain OpenFOAM;
- the same inner solver wrapped by
pinneWarmStart; - a reference whose pressure is solved to
tolerance 1e-11; relTol 0(1e-12 for damBreak).
The tables report the relative L2 difference of the final fields between runs. "vs plain" is the warm-started run against plain OpenFOAM. "vs ref" measures each run against the tightly converged reference. The plain run's own "vs ref" value is the size of the error that the solver tolerance allows.
All runs used one core of the same machine (4-core VM, no GPU) with OpenFOAM v1912 from the Ubuntu 24.04 package. Wall times are the best of two runs of the solver process.
1. Lid-driven cavity, PISO (icoFoam), standard settings¶
Settings: Re = 100, Courant number 0.5, 128 time steps, 2 PISO correctors. Pressure tolerance 1e-6 with
relTol 0.05, and relTol 0 for pFinal. pinneWarmStart uses projection on the last 8 solutions around the
same inner solver.
| case | inner | run | p solves | p iterations | wall [s] | dp vs plain | dU vs plain | dp vs ref | dU vs ref |
|---|---|---|---|---|---|---|---|---|---|
| cavity 128x128 | PCG-DIC | plain | 256 | 34 239 | 13.11 | - | - | 2.8e-4 | 1.2e-7 |
| cavity 128x128 | PCG-DIC | warm | 256 | 11 197 | 7.19 | 2.6e-4 | 1.8e-7 | 3.8e-5 | 1.5e-7 |
| cavity 128x128 | GAMG | plain | 256 | 1 590 | 5.02 | - | - | 9.9e-4 | 1.2e-6 |
| cavity 128x128 | GAMG | warm | 256 | 501 | 4.32 | 9.8e-4 | 1.7e-6 | 1.8e-5 | 5.5e-7 |
| cavity 256x256 | PCG-DIC | plain | 256 | 74 261 | 112.8 | - | - | 3.2e-3 | 1.5e-7 |
| cavity 256x256 | PCG-DIC | warm | 256 | 30 041 | 61.3 | 3.1e-3 | 4.1e-7 | 5.8e-5 | 3.6e-7 |
| cavity 256x256 | GAMG | plain | 256 | 2 955 | 32.8 | - | - | 4.5e-3 | 1.3e-6 |
| cavity 256x256 | GAMG | warm | 256 | 979 | 25.9 | 4.4e-3 | 2.3e-6 | 3.9e-5 | 1.1e-6 |
How the speed-ups break down:
- Iterations: 3.1x fewer (PCG, 128), 2.5x fewer (PCG, 256), 3.2x fewer (GAMG, 128) and 3.0x fewer (GAMG, 256).
- Whole-run wall time: 1.83x and 1.84x with PCG, 1.16x and 1.27x with GAMG.
The pressure difference to plain OpenFOAM (2.6e-4 to 4.4e-3) is the same size as the plain run's own
distance to the tightly converged reference (2.8e-4 to 4.5e-3). That distance comes from relTol 0.05, not
from the warm start.
The warm-started run is 7-115x closer to the reference in pressure. This is because it stops at the same absolute residual as plain but starts from a much better point in the non-final correctors. The velocity agrees with the reference to 1e-6 or better in every run.
2. The same cavity with relTol 0 everywhere: matched accuracy¶
With relTol 0, both runs stop at the same absolute normalised residual. Even so, the warm-started solution
carries more algebraic error. The projection leaves error in smooth, low-eigenvalue modes, which the L1
residual under-weights.
In this closed cavity, most of that error is the pressure level (the near-null mode fixed only by pRefCell).
Re-solving the dumped 64 x 64 systems offline against exact solutions (spsolve) gives a relative error of
5e-7 for plain and 4e-6 for warm at the same residual, mostly in the mean level. The fair comparison is
therefore at equal accuracy: tighten the warm start's tolerance until its error against the reference is
no larger than plain's.
| case | run | p iterations | wall [s] | dp vs ref | dp vs ref (mean removed) | dU vs ref |
|---|---|---|---|---|---|---|
| cavity 64x64 | plain PCG-DIC, tol 1e-6 | 11 796 | 1.34 | 9.2e-7 | 9.2e-7 | 1.2e-8 |
| cavity 64x64 | warm, tol 1e-6 | 5 054 | 0.89 | 4.1e-5 | 8.2e-6 | 3.1e-8 |
| cavity 64x64 | warm, tol 1e-7 | 7 370 | 1.09 | 4.2e-6 | 5.2e-7 | 3.4e-9 |
| cavity 64x64 | warm, tol 1e-8 | 9 616 | 1.33 | 1.1e-6 | 9.8e-8 | 4.7e-10 |
| cavity 128x128 | plain PCG-DIC, tol 1e-6 | 46 844 | 17.52 | 1.5e-6 | 1.5e-6 | 5.3e-9 |
| cavity 128x128 | warm, tol 1e-6 | 11 435 | 7.92 | 4.6e-5 | 3.3e-6 | 1.1e-8 |
| cavity 128x128 | warm, tol 1e-7 | 19 782 | 10.35 | 2.2e-6 | 2.7e-7 | 9.9e-10 |
| cavity 128x128 | warm, tol 1e-8 | 29 227 | 13.36 | 2.5e-7 | 1.2e-8 | 1.2e-10 |
At equal or better accuracy in every field, the warm start is 1.31x faster on 128 x 128 (13.4 s vs 17.5 s, tolerance 1e-8 vs 1e-6). On 64 x 64 it gives nothing (1.33 s vs 1.34 s). At the same tolerance it is 2.2x faster on 128 x 128 (1.5x on 64 x 64), but with a 30x larger pressure error (still at the 1e-5 level) and the same velocity to 3e-8.
Recommendation: pair pinneWarmStart with a tolerance about 10-100x tighter than the plain run's when the
pressure itself is a quantity of interest. Settings with relTol > 0 in the non-final correctors, the usual
PISO/PIMPLE practice (table 1), do not need this.
3. Free surface: damBreak (interFoam, p_rgh), refined 2x¶
The case is the v1912 tutorial, refined 2x to 9 072 cells, run to t = 0.25 s. It uses PIMPLE with 3 correctors.
p_rgh uses PCG-DIC with tolerance 1e-7 and relTol 0.05, and relTol 0 for p_rghFinal. The reference
uses tolerance 1e-12 with relTol 0.
| run | p_rgh iterations | wall [s] | dp_rgh vs plain | dp_rgh vs ref | dU vs ref | dalpha vs ref |
|---|---|---|---|---|---|---|
| plain PCG-DIC | 23 355 | 9.24-10.03 | - | 1.5e-2 | 2.3e-2 | 7.8e-3 |
| warm, all solves | 31 964 | 12.32 | 1.0e-2 | 6.9e-3 | 1.0e-2 | 3.8e-3 |
warm, snapshots converged |
25 429 | 10.82 | 7.8e-3 | 8.5e-3 | 1.5e-2 | 4.3e-3 |
warm, snapshots converged; applyTo converged |
23 346 | 9.41 | 2.6e-7 | 1.5e-2 | 2.3e-2 | 7.8e-3 |
No gain here. The interface moves through the mesh and changes the density in the matrix from step to step, so old solutions are a poor basis. In the second corrector, the projected guess has about the same residual as the field it replaces, but its error is smoother: PCG needed 58 iterations where plain needed 12 for the same reduction (read from the logs).
Restricting the projection to the final solves (applyTo converged) makes the wrapper neutral (23 346 vs
23 355 iterations, fields identical to 3e-7). The overhead of the wrapper is then 1.8% of wall time (9.41 s vs 9.24 s).
The differences to the reference are at the 1e-2 level for every run, including plain. This transient is sensitive to the pressure tolerance itself.
4. Offline: Python solvers and guesses on the same systems¶
The systems are the 128 pressure systems of a 64 x 64 cavity (lid speed 1, relTol 0 everywhere), dumped by
guess none. Each is re-solved to tolerance 1e-6 starting from each guess. Times are Python times, which
are only comparable within this table: the NumPy DIC uses SciPy triangular solves and is far slower per
iteration than OpenFOAM's C++.
The POD basis comes from the 256 systems of two other runs, at lid speeds 0.8 and 1.2 (a two-point design of experiments), and keeps rank 9 for 99.9999% of the energy. The last 8 solutions of the current run are added to it.
The graph network has 4 layers and a width of 32. It was trained for 150 epochs (390 s on the CPU) on 128 systems of those two runs. Its mean training residual ratio went from 1.0 to 0.53.
| guess | solver | iterations | solve [s] | guess [s] | mean initial residual |
|---|---|---|---|---|---|
| previous field (OpenFOAM) | PCG-DIC | 11 796 | 31.8 | 0 | 1.9e-2 |
| previous field | PCG-Jacobi | 39 762 | 3.29 | 0 | 1.9e-2 |
| previous field | PCG-AMG (pyamg) | 695 | 3.72 | 0 | 1.9e-2 |
| previous field | Jacobi-CG, PyTorch f64 | 39 762 | 22.1 | 0 | 1.9e-2 |
| previous field | Jacobi-CG, PyTorch f32 + f64 refinement | 48 508 | 6.49 | 0 | 1.9e-2 |
| projection, last 8 | PCG-DIC | 2 743 | 8.92 | 0.12 | 1.2e-2 |
| projection, last 8 | PCG-AMG (pyamg) | 282 | 3.31 | 0.12 | 1.2e-2 |
| POD of 2 earlier runs + last 8 | PCG-DIC | 2 128 | 6.88 | 0.34 | 3.8e-4 |
| POD of 2 earlier runs + last 8 | PCG-AMG (pyamg) | 253 | 3.42 | 0.34 | 3.8e-4 |
| projection + learned correction | PCG-DIC | 2 683 | 7.85 | 1.99 | 8.7e-3 |
| projection + learned correction | PCG-AMG (pyamg) | 273 | 3.27 | 1.90 | 8.7e-3 |
What the table shows:
- Projection. It cuts DIC-PCG iterations 4.3x and AMG-PCG iterations 2.5x on the same systems. Every solution in this section agrees with OpenFOAM's to 7e-5 or better.
- POD of earlier runs. It removes another 22% of iterations relative to the projection alone. Its initial residual is 30x smaller.
- Learned correction. It removes 2% of DIC iterations relative to the projection alone, and its guess costs more than it saves. A local graph network acts like a learned smoother: it reduces the residual, but the iteration count is set by global, low-frequency error that it cannot see. We report it as no practical gain.
- AMG (pyamg). It cuts iterations about 17x relative to DIC. In Python, its setup on every new matrix dominates the time.
- Mixed-precision PyTorch CG. On the CPU it needs more iterations than float64 and is not faster per useful iteration. Without a GPU we make no GPU claim.
5. Python guesses inside a running OpenFOAM (guess external)¶
The 64 x 64 cavity from section 4, run by icoFoam with the guesses served through the pipes:
| run | p iterations | wall [s] | Python guess time [s] | dp vs ref | dU vs ref |
|---|---|---|---|---|---|
| plain PCG-DIC | 11 796 | 1.20 | - | 9.2e-7 | 1.2e-8 |
projection in C++ (guess projection) |
5 054 | 0.90 | - | 4.1e-5 | 3.1e-8 |
Python projection (guess external) |
4 999 | 1.34 | 0.29 | 3.4e-5 | 6.9e-8 |
| Python POD of earlier runs + last 8 | 4 237 | 1.35 | 0.43 | 3.9e-5 | 2.6e-8 |
| Python projection + learned correction | 5 088 | 3.82 | 2.71 | 1.9e-5 | 5.2e-8 |
The Python projection reproduces the C++ one (4 999 vs 5 054 iterations; the small difference comes from round-off in the elimination order). This confirms that the coupling loop works end to end. POD of earlier runs saves the most iterations, but on a 4 096-cell case the pipe and Python overhead outweighs the saving. These Python models belong inside the C++ wrapper, or on much larger meshes where an iteration costs more than a round trip.
Summary of measured speed-ups¶
| setting | pressure iterations | wall time (whole run) |
|---|---|---|
Cavity, PISO with relTol 0.05, PCG-DIC (128², 256²) |
3.1x, 2.5x fewer | 1.83x, 1.84x faster |
Cavity, PISO with relTol 0.05, GAMG (128², 256²) |
3.2x, 3.0x fewer | 1.16x, 1.27x faster |
Cavity, relTol 0, matched accuracy, PCG-DIC (128²) |
1.6x fewer | 1.31x faster |
Cavity, relTol 0, matched accuracy, PCG-DIC (64²) |
1.2x fewer | none |
damBreak, interFoam p_rgh |
none (worse unless restricted to final solves) | none |
Limits¶
- The gain comes from time correlation. The projection helps when consecutive pressure systems are alike, as
in transient PISO/PIMPLE runs with small time steps. For steady SIMPLE with
relTol 0.1-0.01, each solve is short and the right-hand side changes a lot, so expect little. - Equal residual does not mean equal error. At the same tolerance, a warm-started solution can have a larger algebraic error in smooth modes (section 2). Tighten the tolerance 10-100x when the pressure itself matters, and compare at matched accuracy, as done above.
- Multiphase cases with a moving interface (damBreak) did not benefit. Use
applyTo convergedthere, or leave the plain solver. - It works with any symmetric inner solver. With GAMG, iterations drop, but a GAMG cycle costs much more than the projection, so the wall-time gain is smaller (see the tables).
guess externalblocks the solver until the Python server answers. Start theGuessServerbefore the solver. For production use, prefer the in-process C++ projection; the pipes are for experimenting with Python models.- Systems with coupled interfaces (processor, cyclic) are handled by the C++ wrapper. In Python,
LduSystem.to_csrrefuses them because the interface coefficients are not in the dump. - No GPU was available, so the GPU paths (
torch_pcgon CUDA) were only run on the CPU. No GPU speed-up is claimed.
Reproducing¶
from pinneapple_simulation.accelerated_foam import build, benchmark
lib = build.build_plugin(openfoam_src="/path/to/openfoam-OpenFOAM-v1912.200626") # or with FOAM_SRC set
rows = benchmark.openfoam_comparison("work", lib, n=128, end_time=0.5, repeats=2)
print(benchmark.markdown_table(rows))
The tests are in tests/test_accelerated_foam.py. The OpenFOAM tests skip when icoFoam is missing or the
plugin cannot be built.