Preserve finite element linear maps during factorisation - #284
Open
pbrubeck wants to merge 1 commit into
Open
Conversation
This was referenced Aug 20, 2026
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>
pbrubeck
force-pushed
the
pbrubeck/structured-codegen
branch
from
August 26, 2026 11:42
4a22302 to
5f42006
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.
Goal: to inject finite element structure down to the C level.
Generate code that separates the pullbacks from the quadrature loop.
Stacked on #282. Needs firedrakeproject/firedrake#5362 to pass the argument
indices; without it everything here is inert, which the baseline below relies on.
What changes
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_monomialsnow takes the argument axes, distributes onlythe 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. That exposes that
test and trial apply the same map without inspecting the element family, and
materialises the map once. This is generalised code motion: the basis
transformation is evaluated once and both axes index its result.
Removing
ComponentTensors happens incollect_monomials, not inside thecollector. The collector rebuilds nodes, and the maps identified here have to
be the very nodes it then classifies.
estimate_costgives a caller a cost for a whole GEM DAG, so preserving amap is compared against expanding it rather than assumed better.
has_linear_mapsanswers whether the comparison is worth making, for onetraversal instead of a whole collection pass.
flop_countcounts a preserved map. It reaches scheduling as aComponentTensorassignment rather than a loop nest, so the multiindexextents that no enclosing
Forsupplies are counted here. It previouslyraised
ValueError, andcount_flopsturned that into a silent zero.Literalcompared and hashed without its dtype, so tables holding the samenumbers at different dtypes were interchangeable wherever GEM memoizes on node
identity, which the map sharing above relies on.
Benchmarks
Re-run against
mainfor every case below. The bilinear form isinner(u, v)*dx + inner(d(u), d(v))*dx1, withd=gradfor CG and Q,divfor RT, andcurlfor NCE,dx1forcing a separate quadrature degreefor the derivative term, on simplices and on an extruded quadrilateral mesh.
tsfc (s)is the TSFC compile time;build (s)is the isolated cold-cache Cbuild (compiler and linker only, no TSFC, no PyOP2);
kernel (s)is theisolated per-call kernel time: the compiled kernel is called directly,
bypassing PyOP2's Python wrapper and the local-to-global indirection, and
averaged over calls made in one second.
array temps/entries/largestcount mutable loopy temporaries with a shape, their total entries, and the
largest single temporary.
mainis measured against the real, unpatchedfiredrake, since it cannot import a tsfc that expects the gem API this PR
adds; the PR side is measured against the firedrake commit that adds exactly
that API (
0bf4a4007).Bilinear form
Matrix-free action
Raviart--Thomas is again where the pullback is worth preserving, and now
correctly measured against
div, notcurl: 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 only
moves at degree 1, where preserving the map is cheaper than expanding it
(-6.3% in 2D, -9.8% in 3D); at degree 3 and 5 the cost comparison below picks
expansion instead, so flops are unchanged.
The tensor-product rows move only by what #282 already changed. Q is
flop-for-flop identical between
mainand this PR; NCE carries a small,degree-independent +33 flops that is unrelated to this PR's mechanism (which
needs an argument axis NCE's tensor-product tabulation does not expose) --
it is an artifact of comparing against two different firedrake commits, one
of which
main's FIAT cannot avoid (see above).Zany elements, bilinear form
Zany elements, matrix-free action
Argyris, Guzman--Neilan in 2D, and Johnson--Mercier are unchanged by this PR
alone: none of their sums span exactly one argument axis yet, so
has_linear_mapsfinds nothing to preserve. Guzman--Neilan in 3D is the onezany case this PR alone moves: flops fall 3.3%, AST lines fall 30.2% (1,803
-> 1,258), and the isolated build time falls 26.1%. Johnson--Mercier and
Argyris need #281's padded transformation before there is a map worth this
PR's cost comparison preserving; see #281.
The cost comparison is doing real work
estimate_costis not a formality: it accepts preservation where it pays andrejects it where it does not. Costing both plans for the same form gives
CG degree 3 is why the earlier
len(free_indices) > 1heuristic was not enough:preserving its map costs more arithmetic than distributing it, and only a cost
model sees that. On the RT rows the estimate matches the kernel's own
flop_countexactly.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,
and covers that preserving the map leaves strictly fewer monomials than
distributing it; that COFFEE materialises one tensor for both axes; that the
preserved and expanded forms evaluate to the same matrix; that
has_linear_mapsneeds argument axes to find anything; and thatestimate_costcounts the contraction.
Validation
pytest test/ --ignore=test/FIAT/regression(2378 passed, 26 skipped, 31 xfailed, 1 xpassed)tests/tsfcand the three zany regression suites inPreserve finite element pullbacks when collecting monomials firedrake#5362 (437 passed)
AI assistance
Claude Code was used for implementation, benchmarking, and drafting this
section. The human contributor remains responsible for understanding,
validating, and maintaining the changes.