Preserve finite element linear maps during factorisation - #284
Open
pbrubeck wants to merge 4 commits into
Open
Conversation
This was referenced Aug 20, 2026
pbrubeck
force-pushed
the
pbrubeck/structured-codegen
branch
from
August 26, 2026 11:42
4a22302 to
5f42006
Compare
A pullback reaches TSFC as a sum over one argument axis: the physical gradient, divergence or curl of one basis function. Monomial collection distributed over that sum, so the geometry was pushed through the middle of the element tensor contraction and each argument axis mapped its own tabulation. collect_monomials now takes the multilinear axes. It distributes only the sums spanning several of them, which separate form monomials, and keeps the one-axis sums atomic. COFFEE renames each argument axis to a canonical index, which exposes that test and trial apply the same map without inspecting the element family, and materialises that map once. Removing ComponentTensors happens once, in collect_monomials, rather than inside the collector: the collector rebuilds nodes, and the maps identified here have to be the nodes it then classifies. estimate_cost gives a caller a cost for a whole GEM DAG, so that preserving a map can be compared against expanding it rather than assumed better. has_linear_maps answers whether the comparison is worth making, for one traversal instead of a whole collection pass. A preserved map reaches scheduling as a ComponentTensor assignment rather than a loop nest, so flop_count counts the multiindex extents that no enclosing For supplies. It previously raised, and count_flops turned that into a silent zero. Literal compared and hashed without its dtype, so tables holding the same numbers at different dtypes were interchangeable wherever GEM memoizes on node identity, which the map sharing above now relies on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The factorisation cost model repeated, as an isinstance ladder, the per-node costs that gem.flop_count already held as a dispatch table: the free negation, the unit-cost arithmetic group, the Power heuristic, and the dense linear algebra counts. Put those constants in gem.cost as node_cost. The two counters differ only in how they weigh a node: flop_count multiplies by the enclosing loop extents, and estimate_cost by the node's own free indices. Both now read the same table, so tuning one rule changes both. Rename _distribute_sum to distribute_sum; it is a primitive other modules use. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013NoTXWyj2fVdJTHnMDFB4k
pbrubeck
force-pushed
the
pbrubeck/structured-codegen
branch
from
August 29, 2026 15:21
5f42006 to
8da7d4e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TLDR
A pullback reaches TSFC as a sum over one argument axis. That sum is the
finite element map. This PR keeps such a sum whole instead of multiplying it
out, and shares one map between the test and trial axes.
The compiler costs both plans and keeps the cheaper one. Raviart--Thomas gets up
to 24% fewer flops in 3D. Nothing else regresses.
Base: #282. Needs firedrakeproject/firedrake#5362 to pass the argument indices.
What this does
Keep a one-axis sum whole. Monomial collection used to distribute over every
sum. That pushed the geometry into the middle of the element tensor contraction,
and each argument axis then mapped its own tabulation.
collect_monomialsnowtakes the argument axes. It distributes only the sums that span several of them,
because those separate the terms of the form. It leaves a one-axis sum whole.
Share one map between argument axes. COFFEE renames each argument axis to a
canonical index. Test and trial then look identical if they apply the same map,
whatever their element family. The compiler evaluates that map once, and both
axes read its result.
Choose by cost, not by rule.
estimate_costscores a whole GEM DAG, so thecompiler compares keeping a map against expanding it.
has_linear_mapsanswersin one walk whether the comparison is worth making at all.
Count a kept map. A kept map reaches scheduling as a
ComponentTensorassignment, not as a loop nest.
flop_countnow counts the index extents thatno enclosing loop supplies. It used to raise
ValueError, whichcount_flopsturned into a silent zero.
Tell literal tables apart by dtype.
Literalcompared and hashed without itsdtype. Two tables holding the same numbers at different dtypes were therefore
interchangeable. Map sharing relies on that comparison being right.
Tidy-up. The per-node cost constants now live in one module,
gem.cost.gem.flop_countandestimate_costread the same table. They weigh itdifferently, and that is the only difference between them.
The cost comparison earns its place
Both plans, costed on the same form:
CG degree 3 is the case a fixed rule cannot get right. Keeping its map costs
more arithmetic than expanding it. Only a cost model sees that. On the RT rows
the estimate matches the kernel's own
flop_countexactly.Benchmarks
Measured on this branch against
main. The form isinner(u, v)*dx + inner(d(u), d(v))*dx1, withd=gradfor CG and Q,divfor RT and
curlfor NCE.dx1forces a separate quadrature degree for thederivative term.
tsfc (s)is the TSFC compile time.build (s)is the cold-cache C buildalone: compiler and linker, no TSFC and no PyOP2.
kernel (s)is the compiledkernel called directly, without the PyOP2 wrapper, averaged over one second of
calls.
array temps,entriesandlargestcount mutable Loopy temporariesthat have a shape, their total entries, and the biggest single one.
Bilinear form
Matrix-free action
Raviart--Thomas is where keeping the map pays. Arithmetic falls 10.4%, 13.8%
and 18.1% at degree 1, 3 and 5 in 2D, and 9.3%, 19.7% and 24.3% in 3D. CG moves
only at degree 1, where keeping the map is cheaper than expanding it: -6.3% in
2D and -9.8% in 3D. At degree 3 and 5 the cost comparison picks expansion, so
the flops do not change.
Q is identical to
main, flop for flop. NCE carries 33 more flops at everydegree. That number does not grow with the degree. NCE's tensor-product
tabulation exposes no single argument axis, so this mechanism does not act on
it.
Zany elements, bilinear form
Zany elements, matrix-free action
Guzman--Neilan in 3D is the one zany case this branch moves. Flops fall 3.3%,
AST lines fall 30.2%, and the isolated build time falls 26.1%. In the other zany
cases no sum spans exactly one argument axis, so
has_linear_mapsfinds nothingto keep.
Tests
test_sum_factorise.pybuilds a Laplacian element tensor from a mapped gradienttable, where test and trial apply the same map over their own argument index. It
covers that keeping the map leaves strictly fewer monomials than distributing
it, that COFFEE materialises one tensor for both axes, that the two forms
evaluate to the same matrix, that
has_linear_mapsneeds argument axes to findanything, and that
estimate_costcounts the contraction.Validation
test/FIAT/regression: 2378 passed, 26 skipped, 31 xfailed, 1 xpassedmake linttests/tsfcand the three zany regression suites in Cost and fuse preserved pullbacks, and cancel indirect Deltas firedrake#5362: 437 passedAI assistance
Claude Code was used for implementation, benchmarking, and drafting this
section. The human contributor remains responsible for understanding,
validating, and maintaining the changes.