Preconditioner defaults: PETSc's GAMG cycle, node aggregation, and a flexible outer Krylov - #584
Conversation
…le outer Three defaults that deviated from what the design requires, none carrying a recorded reason. Closes #579 and #576. GAMG cycle. pc_mg_type was "additive" where PETSc's default for GAMG is "multiplicative". Additive applies the levels independently and sums the corrections, so no level sees what another has already removed. git log -S puts its arrival in December 2022, in example scripts, alongside pc_mg_levels=2 -- with one coarse level the two cycles nearly coincide, so it cost nothing where it was written. It was promoted into the solver defaults and the hierarchies got deeper. SolKz at 11 727 unknowns: 243-347 cycles per velocity solve against 74-118. Node aggregation. The velocity operator reached GAMG with block size 1, so it aggregated scalar degrees of freedom rather than nodes. PETSc infers this correctly wherever it can -- with no velocity BCs, or a full vector Dirichlet condition, the field IS and the fieldsplit sub-matrix both come out with block size 2. It collapses to 1 under a COMPONENT-WISE Dirichlet condition, because the field then carries one degree of freedom at a constrained wall node and two inside. That is free slip, so the case PETSc cannot infer is the ordinary one. Setting mat_block_size cannot change the answer -- a preconditioner alters the route and not the fixed point, and the two configurations agree to 2e-7 relative on a 1e-6 solve -- but on free-slip SolKz it is worth 3.48e9 flops against 1.60e9, and with the contrast concentrated in a band rather than spread smoothly, 2.61e11 against 4.44e9. The block size is recorded on the solver as _pc_block_size rather than only at the __init__ call sites, because _apply_preconditioner_options re-applies the bundle on every build and a bundle built without it lists mat_block_size as a stale key and deletes what __init__ set. SNES_MultiComponent keeps block size 1: its unknown is not mesh.dim components in general. Flexible outer Krylov. The Stokes outer ksp_type was never set, so it inherited PETSc's gmres while both fieldsplit sub-blocks are fgmres run to a tolerance. Inside the Schur factorisation the velocity sub-solve computes the search direction, so the operator the outer method applies differs between outer iterations, and GMRES's residual recurrence assumes it does not. Spiegelman notch at refinement 3: 983 velocity iterations per step and DIVERGED_LINEAR_SOLVE, against 58 for fgmres and 23 with a loosened inner tolerance. Raising the velocity iteration cap is inert -- byte-identical residuals -- so the failure is inconsistency, not a shortage of iterations. Set in __init__ and in the strategy setter, which also resets the fieldsplit block. The four call sites that open-coded the GAMG bundle key by key now read it from multigrid_options, which is that module's stated reason for existing and is how one value came to be wrong in four places at once. test_0203 asserted that a fresh solver's first velocity count undercounts the second by a strict margin. The contract is that the first count is a LOWER BOUND and says so; how short it falls depends on where PCSetUp lands relative to the first application of the block, which these defaults move. It is now tight on that problem, so the test asserts the bound rather than the size of the gap. Underworld development team with AI support from Claude Code
MatSetFromOptions hands mat_block_size to PetscLayoutSetBlockSize, which is a
hard error rather than a hint it may decline:
Arguments are incompatible
Local size 67 not compatible with block size 2
Whether it divides is a property of the particular COMBINATION of boundary
conditions, not of their kind. Constrained DOFs are absent from the field's
global section, so on a 3x3 P2 velocity field: no velocity BCs 98, full vector
Dirichlet on every wall 50, component-wise free slip 70 — all even — while
(0,0) on Bottom with (0,None) on the other three gives 67. Free slip is safe
by an accident of counting, which is why the measurements behind #579 never
met this and test_1010_stokesCart did.
_withdraw_block_size_if_not_node_blocked drops the option when the block is
not node-blocked, immediately before setFromOptions, which is the first point
the field decomposition exists. _apply_preconditioner_options re-pushes the
bundle on every build, so this runs on every build too.
The decision is collective because divisibility is MIXED across ranks: on a
6x6 box the per-rank velocity sizes are [127, 150] at np=2 and [72, 94, 111]
at np=3, so a rank-local decision would leave ranks disagreeing about the
options DB.
test_1022 pins both directions. The second case is the one that matters: a
gate that withdrew the option unconditionally would fix the crash and silently
undo #579, and the crash test alone cannot tell them apart.
test_1021's bundle-ownership test built `owned` from a list that never
included gamg_bundle(block_size=2) — the one bundle that sets the key — so
every bundle's stale list carried a key `owned` lacked. The sibling is now in
the list, which also brings it under the test.
Underworld development team with AI support from Claude Code
Adversarial review — solver option-ownership cluster (#584, #597, #548)Reviewed together: #584 and #597 both edit ClusterC1. There are two ownership mechanisms, and these PRs touch one each.
#584 extends the first ( The defect #597 fixes is that the latch outlived the option it was latched from. C2. #584 is red and the fix exists. Four failures, all C3. #593 also edits #5841. The measured win is narrower than the numbers imply. The 74–118 to 34–47 2. #597 — responses to our own earlier findingsThree findings were raised on this PR by us. Positions, so they are not left
#548Its existing review stands; nothing in this cluster changes it. Note only that Underworld development team with AI support from Claude Code |
The block-size failures, fixed, and the branch brought currentPushed The four PETSc error 75 failures. The commentary in
Free slip is safe by an accident of counting, and it is the case the
The decision is collective, because divisibility is mixed across ranks. On a The fifth failure was a test defect.
Two things for the PR text. The measured 74-118 to 34-47 cycle win is from Verified: full Underworld development team with AI support from Claude Code |
Two preconditioner defaults that deviate from PETSc's without a recorded reason, and one consequence of the first.
The GAMG cycle (#579)
pc_mg_typewasadditive. PETSc's default for GAMG ismultiplicative, and additive is a strictly weaker cycle — the levels do not see each other's corrections.Provenance, from
git log -S: it arrived in December 2022 in example scripts, alongsidepc_mg_levels = 2. With a single coarse level the two cycles very nearly coincide, so it cost nothing where it was written. It was promoted into the solver defaults, the hierarchies got deeper, and nobody revisited it. No comment ever gave a reason.Measured on SolKz, Taylor–Hood P2–P1, 11 727 unknowns, cycles per velocity solve: 243–347 additive against 74–118 multiplicative.
The four open-coded copies
utilities/multigrid_options.pysays of itself that "nobody writes a multigrid option value anywhere else", and that is the reason the module exists — hand-maintained copies are what let the smoother iteration count drift in #468. But four call sites inpetsc_generic_snes_solvers.pyxopen-coded the GAMG bundle key by key, which is how one value came to be wrong in four places at once.Those four now read the bundle. The module's claim about itself is true again.
The outer Krylov (#576)
The Stokes outer
ksp_typewas never set, so it inherited PETSc'sgmres, while both fieldsplit sub-blocks arefgmresrun to a tolerance.That is the wrong way round. Inside the Schur factorisation the velocity sub-solve computes the search direction, so the operator the outer method applies differs between outer iterations, and GMRES's residual recurrence assumes it does not. Measured on the Spiegelman notch at refinement 3: 983 velocity iterations per step and
DIVERGED_LINEAR_SOLVE, against 58 forfgmresand 23 with a loosened inner tolerance. Raising the velocity iteration cap is inert — byte-identical residuals — so the failure is inconsistency, not a shortage of iterations.Set explicitly in
__init__and in thestrategysetter, which also resets the fieldsplit block.Node aggregation
The velocity operator reached GAMG with block size 1 on a two-component field, so it aggregated scalars rather than nodes.
This is not an unconditional defect. PETSc infers the block size correctly wherever it can:
It collapses to 1 under a component-wise Dirichlet condition because the field then genuinely carries one degree of freedom at a constrained wall node and two inside — block size 1 is the correct description. Component-wise Dirichlet is free slip, so the one case PETSc cannot infer is the standard geodynamics boundary condition.
Setting
mat_block_sizeasks GAMG to aggregate on a pairing that does not align with nodes at the walls. That cannot change the answer — a preconditioner alters the route to the solution, not the solution — and measured on free-slip SolKz the two configurations agree to 2e-7 relative on a 1e-6 solve. What it changes is the cost:With the same contrast concentrated in a band rather than spread smoothly, a plain
stokes.solve()goes from 2.61e11 flops / 32.7 s to 4.44e9 / 1.8 s.PETSc honours
mat_block_sizeas an option, so this needs no hook and no post-hoc PC surgery — only that it is set before the matrix is built. It is recorded on the solver as_pc_block_size, not just at the__init__call sites, because_apply_preconditioner_optionsre-applies the bundle on every build and a bundle built without it listsmat_block_sizeas a stale key and deletes what__init__set.SNES_MultiComponentkeeps block size 1: its unknown is notmesh.dimcomponents in general, and claiming the wrong node size would cost rather than save.One test expectation changed
test_0203_solver_wallclock_guard::test_first_solve_admits_its_count_is_a_lower_boundasserted that a fresh solver's first velocity count strictly undercounts the second. The contract is that the first count is a lower bound and is flagged as one; how far short it falls depends on wherePCSetUplands relative to the first application of the block, which these defaults move. It is now tight on that problem (80 either way, still flagged incomplete), so the test asserts the bound rather than the size of the gap. Verified to pass ondevelopmentbefore the change and to fail only because of it.Still unaudited
pc_gamg_agg_nsmooths=2(PETSc: 1),pc_gamg_repartition=True(PETSc: False), and the geometric bundle'spc_mg_type="full"(PETSc PCMG:multiplicative) remain deviations without recorded measurements.Verification
./uw test: 1493 passed, 32 skipped, 2 xfailed. All three defaults confirmed live after./uw build, including thatstrategy=no longer reverts the outer.Underworld development team with AI support from Claude Code