Multigrid: skip re-evaluation of unrefined nodes - #5288
Conversation
fac303d to
af6c7df
Compare
af6c7df to
1a70084
Compare
| # An adaptive refinement leaves most of the mesh alone, and the nodes | ||
| # it preserves are copied rather than evaluated. | ||
| node_subset = utils.transfer_node_subset(Vc, Vf) | ||
| op2.par_loop(kernel, node_subset, *kernel_args) | ||
| utils.prolong_preserved_nodes(coarse, fine) |
There was a problem hiding this comment.
This is the main part of the PR
646da29 to
5625a7c
Compare
| # cell's count. op2.Map cannot hold the -1 that marks a slot as empty, | ||
| # and the macro-cell kernel reading through this map integrates over | ||
| # every slot alike. Point all of an empty slot's nodes at a single one |
There was a problem hiding this comment.
I'm not sure how accurate this is.
There was a problem hiding this comment.
It is certainly confusing.
macro-cell kernel
Huh?
empty slot's nodes
unclear
There was a problem hiding this comment.
Context: DG injection integrates on a macro-cell formed by all fine children, which for an adaptive mesh are not uniform, so we need a ragged array with empty slots
There was a problem hiding this comment.
I am asking about op2.Map not being able to hold a negative value
|
|
||
| def _copied_nodes(mh, V): | ||
| """Count the nodes of ``V`` that the transfers copy rather than evaluate.""" | ||
| from firedrake.mg.utils import transfer_node_subset |
| op2.par_loop(kernel, fine.node_set, *kernel_args) | ||
| # An adaptive refinement leaves most of the mesh alone, and the nodes | ||
| # it preserves are copied rather than evaluated. | ||
| node_subset = utils.transfer_node_subset(Vc, Vf) |
There was a problem hiding this comment.
| node_subset = utils.transfer_node_subset(Vc, Vf) | |
| changed_node_subset = utils.transfer_node_subset(Vc, Vf) |
clearer?
| # of the cell's own children's nodes: the slot then describes a | ||
| # degenerate cell sitting on the patch it belongs to, whose Jacobian | ||
| # determinant, and so whose contribution, vanishes. | ||
| if not valid.all(): |
There was a problem hiding this comment.
I've just had to fix this in pyop3. Please remove this branch because it causes rank divergence. All ranks should just do it
There was a problem hiding this comment.
What do you mean? Have you fixed this bug or a different one?
| # cell's count. op2.Map cannot hold the -1 that marks a slot as empty, | ||
| # and the macro-cell kernel reading through this map integrates over | ||
| # every slot alike. Point all of an empty slot's nodes at a single one |
There was a problem hiding this comment.
It is certainly confusing.
macro-cell kernel
Huh?
empty slot's nodes
unclear
| if Vc.extruded or Vf.extruded: | ||
| # The plex of an extruded mesh is the flat base one, whose points | ||
| # carry a whole column of nodes that the Sections cannot tell apart. | ||
| return None |
There was a problem hiding this comment.
Why can't we do extruded?
| kernel accumulated from the remaining fine nodes. | ||
|
|
||
| """ | ||
| from firedrake.halo import _get_mtype |
|
|
||
|
|
||
| def restrict_preserved_nodes(fine_dual, coarse_dual): | ||
| """Add what the nodes an adaptive refinement preserved contribute to the coarse dual. |
There was a problem hiding this comment.
I don't understand why we're adding instead of copying
There was a problem hiding this comment.
I think we should be able to copy because the operator is the identity on these nodes.
| CHKERR(DMPlexGetTransitiveClosure(fine_dm.dm, fine_point[child], PETSC_TRUE, | ||
| &fine_size, &fine_closure)) | ||
| # A cell with one child that the transform nonetheless changed would | ||
| # have a closure of its own shape; leave it to the transfer kernel. |
There was a problem hiding this comment.
I don't understand this
There was a problem hiding this comment.
I think the rule to detect preserved points is more subtle. A cell might be left unrefined but still changed by the transform?
Adaptive refinement only touches part of a mesh, so most fine cells are exact copies of a coarse one. Detect those copied cells via a PETSc SF over the points an adaptive refine_sbr transform preserves, and have prolong/restrict copy their nodes' values directly instead of running them through the transfer kernel, which is both cheaper and exact where evaluation would otherwise be approximate. Also pads the macro-cell coarse-to-fine node map's empty slots (where a coarse cell has fewer fine children than the busiest one in the hierarchy) with a degenerate cell of zero measure, so the map stays rectangular without the kernel double-counting real contributions.
- Drop the temporary FIAT install step now that fiat#267 is merged. - preserved_points computes nfine/ncoarse via num_owned_cells instead of taking nfine as an argument, asserting the passed-in array shape against it rather than trusting it as the source of truth.
Follow ASD-STE100: short sentences, one idea each, active voice, and the subject named up front rather than buried in a relative clause. The AGENTS.md rules this follows are in #5338. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1c2b268 to
994901b
Compare
Description
AI-assisted (Claude Code)
An adaptive refinement leaves most of the mesh alone. Prolongation, restriction and injection
previously treated every fine node as if the whole mesh had changed: locate the coarse cell holding
it, pull it back to the coarse reference cell, and evaluate the coarse basis there. On a cell the
refinement did not touch, that reproduces the coarse dof it started from, to within roundoff — so
this PR skips the re-evaluation and copies those nodes' values directly.
Main changes
firedrake/cython/mgimpl.pyx:preserved_pointsfinds, for a coarse/fine plex pair produced byadaptive refinement, which fine points are exact copies of a coarse point (the cells the refinement
left alone).
firedrake/mg/utils.py:preserved_node_sfbuilds thePETSc.SFpairing the coarse and fine nodesthat correspond to those preserved points (via
Section.distributeSection/createSectionSF,trimmed to owned fine nodes only);
transfer_node_subsetgives the complementarySubsetof finenodes the transfer kernels must still visit;
prolong_preserved_nodes/restrict_preserved_nodesdo the copy/reduce for the nodes the SF accounts for.
firedrake/mg/interface.py:prolong/restrictrun theirop2.par_looponly overtransfer_node_subset's nodes, then call the preserved-node copy/reduce for the rest.tests/firedrake/multigrid/test_adaptive_multigrid.py: asserts the transferkernels are actually skipped on preserved nodes (
_copied_nodes), acrossfiredrake/netgenmeshes,
square/cube, and multiple process counts.Changes since the last review
real fix: the DG injection kernel now stops at each coarse cell's actual child count instead of
padding the map. @connorjward's point about
if not valid.all():causing rank divergence ishandled there too, by dropping the branch entirely rather than by filling padding.
fdkhelper #5338, with the ASD-STE100 rule and two prose anti-patterns.test_adaptive_multigrid.pyandfiredrake/mg/utils.py(
transfer_node_subset,coarse_cell_to_fine_node_map,complex_mode,_get_mtype). Checkedthat the
firedrake.haloone does not make the import circular.threads on
preserved_node_sf,_preserved_point_sfand themgimpl.pyxhelpers.Testing
test_adaptive_multigrid.pypasses at 1, 2 and 4 processes (91/80/58).test_grid_transfer.pypasses at 170 / 0 failed, matching
main.