Preserve finite element pullbacks when collecting monomials - #5362
Draft
pbrubeck wants to merge 12 commits into
Draft
Preserve finite element pullbacks when collecting monomials#5362pbrubeck wants to merge 12 commits into
pbrubeck wants to merge 12 commits into
Conversation
Expanding a pullback exposes scalar factorisation across its entries;
preserving it exposes a physical basis that both argument axes share.
Neither dominates: preserving wins on the Piola mapped families, where
the geometry otherwise crosses the element tensor contraction twice, and
expanding wins on Lagrange at moderate degree, where the entries carry
enough structure to fold.
Factorisation is therefore parameterised on that choice and run twice,
and the cheaper plan by estimate_cost is kept. The second run is skipped
when no sum spans exactly one argument axis, which is every tensor
product cell here: sum factorisation has already split the basis into one
dimensional factors and contracted the Jacobian into the per point
geometry, so no mapped tabulation exists to share and the search would
cost compile time for an identical plan.
Preserved maps must survive finalisation to be shared at all, so
spectral mode keeps its ComponentTensors.
Measured on inner(u, v)*dx + inner(d(u), d(v))*dx, d the family's
derivative, against the same tree without this change:
RT tetrahedra degree 5 12,965,694 -> 8,711,463 flops, 33% fewer
RT tetrahedra degree 3 374,589 -> 255,933 flops, 32% fewer
RT triangles degree 5 253,051 -> 192,564 flops, 24% fewer
CG tetrahedra degree 1 432 -> 390 flops, 10% fewer
Q, NCE hexahedra unchanged, no map to preserve
Scalar temporaries fall 31% and AST lines 11% on RT tetrahedra, and no
case regresses on flops. Compile time rises by up to 17% where a second
plan is built, and is unchanged elsewhere.
Fewer operations do not yet make a faster kernel. The RT bilinear kernel
runs 11% slower on triangles and 4.5% slower on tetrahedra at degree 3,
reproducibly: each shared map becomes its own ComponentTensor, and
scheduling gives each one its own loop, so one fused loop over the basis
becomes three over the same extent. Stacking the maps that share an
extent into one tensor is what this needs next.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A preserved linear map is materialised as a ComponentTensor, and the loopy backend minted a fresh iname for each one, so several maps over one extent became several loops where expansion emits a single fused nest. That fission cost 7-11% of the bilinear kernel. Reuse the iname between tabulations that the schedule places side by side. Only adjacent ones: impero interleaves statements that depend on a tabulation, and one iname can not sit both inside and outside such a statement, which loopy reports as a scheduling cycle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pbrubeck
force-pushed
the
pbrubeck/zany-matvec
branch
from
August 20, 2026 15:01
39f1e41 to
eee9def
Compare
pbrubeck
force-pushed
the
pbrubeck/zany-matvec
branch
from
August 20, 2026 15:10
eee9def to
a458257
Compare
pbrubeck
commented
Aug 20, 2026
The TSFC changes here need the GEM changes in the FIAT stack firedrakeproject/fiat#282 -> #284 -> #281 -> #286, whose head carries all four. Install it over the one pyproject.toml resolves from main, so that CI exercises both halves together. Revert this commit once the FIAT stack lands. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pbrubeck
force-pushed
the
pbrubeck/zany-matvec
branch
from
August 22, 2026 15:40
e4536ab to
5bc40c9
Compare
GEM no longer factors reductions through indirect gathers inside optimise_monomial_sum, which the recursive sum_factorise calls at every level. Apply the traversal once to each finished assignment instead, so plan costing still sees its effect. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A basis transformation is now a contraction against a Delta, and delta_elimination only inspects top-level product factors, so the Delta inside a preserved linear map never reaches it. Cancel those before monomial collection, which recovers the gather the transformation used to build directly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dham
marked this pull request as draft
August 25, 2026 15:29
# Conflicts: # tests/tsfc/test_sum_factorisation.py
…edrake
The install step ends with firedrake-clean, which imports Firedrake, and so
imports the tsfc that needs the GEM changes in the FIAT stack. Swapping the
stack in from a step after that one leaves firedrake-clean to run against the
FIAT that pyproject.toml resolved from main:
ImportError: cannot import name 'eliminate_deltas' from 'gem.optimise'
The job never reached firedrake-check. Install the stack inline instead,
after the Firedrake install and before firedrake-clean, which is what
pbrubeck/form-interp-tsfc does with its own siblings.
Revert this commit once the FIAT stack lands.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013NoTXWyj2fVdJTHnMDFB4k
The docs job installs Firedrake itself rather than going through .github/actions/install, so the stack this branch needs never reached it, and it failed the same way the test jobs did before 34c5f58: ImportError: cannot import name 'eliminate_deltas' from 'gem.optimise' raised by firedrake-clean at the end of its own install step. Give it its own copy of the install, in the same place relative to firedrake-clean. Revert this along with 34c5f58 once the FIAT stack lands. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013NoTXWyj2fVdJTHnMDFB4k
FIAT renamed eliminate_deltas to cancel_nested_deltas, and moved the cost model to gem.cost. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013NoTXWyj2fVdJTHnMDFB4k
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
This is the TSFC half of the FIAT stack that ends at firedrakeproject/fiat#286.
It does two things. TSFC now costs two factorisation plans and keeps the
cheaper one, instead of choosing by a rule. And Loopy now puts adjacent
shared tabulations in one loop, instead of one loop each.
Raviart--Thomas in 3D loses a third of its operations. Q and NCE do not change.
What this does
Cost the two plans (
tsfc/spectral.py).A pullback reaches TSFC as a sum over one argument axis. There are two ways to
handle it.
Neither wins everywhere. So
flattenbuilds both plans and keeps the cheaperone by
estimate_cost. It skips the second plan whenhas_linear_mapssaysthere is no map to keep, so the extra work is only paid where it can help.
On a CG degree 3 Laplacian in 2D, keeping the map costs 8,305 operations against
8,125 for expanding it. Only a cost model rejects it there.
Fuse adjacent tabulations (
tsfc/loopy.py).One kept map per
ComponentTensorgave one loop each, so a single fused nestbecame three loops over the same extent. TSFC now reuses one iname per bound
index and fuses them back.
The iname is reused only between adjacent tabulations. Impero can place
other statements between two tabulations that depend on each other, and one
iname cannot be both inside and outside such a statement.
statement_blocktherefore resets the memo on any statement that is not a
ComponentTensorEvaluate.Follow the GEM module split. FIAT moved the cost model to
gem.costandrenamed
eliminate_deltastocancel_nested_deltas.Effect
Raviart--Thomas is where keeping the pullback pays. On
inner(u, v)*dx + inner(curl(u), curl(v))*dx,mainagainst this PR:Arithmetic falls 32% at degree 3 and 33% at degree 5 in 3D. There are 31% fewer
scalar temporaries and 20% fewer lines of C.
Q and NCE do not change. On a tensor-product cell no sum depends on exactly one
argument index, so the second plan is never built.
On time this reaches parity, not a speedup. Best of four interleaved
repetitions:
-fno-fast-mathPyOP2 compiles with
-ffast-math. gcc may then turna*x + a*yintoa*(x + y)and hoist the sum out of the loop. That is the same saving thatkeeping the map makes structurally. Forbid the reassociation and the two
separate:
mainloses 7.0% and 9.2%, and this branch loses 2.4% and 0.6%.So the counted 32-33% is real, and the C compiler was already collecting most
of it. What this PR adds is a third fewer operations to schedule, and a
representation that does not depend on gcc choosing to reassociate.
Validation
tests/tsfcand the zany helmholtz, interpolate and projection regressionsuites: 437 passed
test_impero_loopy_flop_counts.pycovers theComponentTensorcase thatcount_flopsused to turn into a silent zerotest_sum_factorisation.pycovers the plan selectionNotes
A DROP BEFORE MERGE commit adds a step to
.github/actions/install/action.yml. It installs the head of the FIAT stackover the one
pyproject.tomlresolves frommain, so CI exercises both halvestogether. Revert it once they land.
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.