Optimise generic GEM sum factorisation - #276
Draft
pbrubeck wants to merge 31 commits into
Draft
Conversation
pbrubeck
commented
Aug 7, 2026
This was referenced Aug 7, 2026
pbrubeck
force-pushed
the
pbrubeck/optimise-sum-factor
branch
from
August 13, 2026 21:10
2c0c56f to
43f1be4
Compare
MappedTabulation contracted each sparse row over a number of entries that varied with the row. A loop nest cannot express that bound, so the lowering either failed to build a convex iteration domain or ran every row to the longest one, and physically mapped elements assembled wrong values. The transformation is now held in compressed sparse row form and applied as one rectangular contraction, with rows shorter than the longest padded by zero coefficients. The basis axis stays a single loop, so the map reaches the quadrature contraction as a linear map over the reference tabulation. Literal compared and hashed without its dtype, so an unsigned index literal and a floating point value literal holding the same number were interchangeable wherever GEM memoizes on node identity. Literal now separates them, which Mardal--Tai--Winther needs in order to compile. Comparison counts as one floating point operation again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Compiling the Johnson--Mercier mass-plus-divergence form spends all of its time in code generation: a cold-cache assembly of the tetrahedral form runs for 9.5 s and executes in 0.03 s. Four changes cut the compile from 2.36 s to 1.77 s without altering a single generated kernel. Memoize `_contraction_component`. A loop-ordering search re-plans the same components once per candidate ordering, so the subset dynamic program ran 33291 times over 2402 distinct arguments. `Index` compares by identity, so equal factor tuples share index objects and the plan is reusable. Its two per-call `lru_cache` closures become dict memos, which no longer rebuild a `functools` wrapper on every entry. Return early from `sum_factorise` when nothing is contracted. Monomial collection builds each `rest` through it, always with no contraction indices and at most two factors, so the planner cost 20% of compilation to form a product. Without contraction indices the jagged, constant-index and distribution branches are all inert and the plan is exactly an association of the factors. Add `has_arithmetic`. Sharing linear maps asks only whether an expression performs arithmetic, but paid `estimate_cost` for a storage model it discards; deciding that needs one short-circuiting traversal. Delete `sort_monomials`. It reordered a list local to `find_optimal_atomics`, whose set cover is now solved exactly by branch and bound, so the result no longer depends on the order in which atomics are numbered. The search truncation that would reinstate that dependence is never reached: the largest observed search visits 67 of 65536 permitted states. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Johnson--Mercier compilation spends most of its time building and comparing GEM nodes, so two allocations on the hot path cost more than the algebra around them. Neither change alters a generated kernel: flop counts are identical across 24 forms on triangles and tetrahedra, and the assembled Johnson--Mercier matrix norm is unchanged. Return the children tuple directly from `_cons_args` when a node carries no non-child data. 82% of the 295353 calls in a tetrahedral compile come from `Sum` and `Product`, which built and unpacked two empty generators to rebuild a tuple they already had. `_arguments` now hands back `children` itself, so `is_equal` compares tuples without allocating. Accumulate monomials onto a plain dict. `MonomialSum.monomials` defaulted to `Zero`, so summing onto an absent key constructed a `Zero` for `Sum` to fold straight back away: 55593 of the 184985 node constructions in a tetrahedral compile existed only to be discarded. Reading with `get` leaves 143. TSFC compilation of the tetrahedral form drops from 1.77 s to 1.43 s. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 19, 2026
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.
Motivation
Chapter 4 of Luporini's thesis treats finite element kernel optimization as the coordination of sharing elimination, reduction pre-evaluation, factorization, and generalized loop-invariant code motion. Section 4.11 leaves several relevant directions open: systematically optimizing expressions outside the linear basis loops, exploiting redundancies in basis functions, relaxing restrictions on code motion, replacing a fixed memory threshold with a better cost model, and extending the method to jagged loop nests.
This PR addresses those limitations at the GEM level, where finite element and contraction structure are still explicit. GEM chooses the loop domain in which each reduction is evaluated; COFFEE then eliminates scalar sharing inside the resulting loop nest. Keeping those decisions separate prevents scalar algebra from obscuring a legal contraction and prevents a contraction rewrite from losing scalar factorization opportunities.
Johnson--Mercier is a useful target even though it is not sum-factorizable. Its physical basis transformation followed by mass and divergence contractions stresses exactly the boundary this PR changes: reference tabulation, sparse basis recombination, quadrature contraction, and scalar sharing.
Mathematical model
A normalized contraction is represented by a hypergraph whose factors are vertices and whose contracted indices connect the factors in which they occur. Independent connected components are planned separately. Within a component, subset dynamic programming chooses a product tree. An index is reduced at the first subtree containing every factor incident on that index, which is its earliest legal code motion.
Plans are ordered lexicographically by exact operation count, peak live intermediate storage, and total materialized storage. Rectangular and jagged iteration domains are counted from their actual index domains. This replaces an architecture-specific temporary-size threshold with explicit mathematical costs.
Physical basis recombination is kept as a transformation of the reference tabulation. The transformation is stored in compressed sparse row form and applied as one rectangular contraction, so the basis axis reaches the quadrature contraction as a single loop rather than as one expression per basis function. Rows shorter than the longest are padded with zero coefficients, and the generated kernel selects no basis function through an
ifbranch. ExplicitComponentTensorvalue loops allow several mapped outputs to share scalar work without materializing that work as arrays.Changes
gem.contractionas the single planner for associative scalar trees and indexed tensor contractions;Literal;Johnson--Mercier code generation
The target is
for degree-one Johnson--Mercier elements on a triangle and tetrahedron. The comparison is
mainagainst this PR together with firedrakeproject/firedrake#5335. Compile time, kernel run time, and cold-cache assemble time are pinned-core averages over repeated runs, with the kernel timed by calling the compiled cell loop directly rather than throughassemble; the remaining values are deterministic properties of the generated Loopy kernel.The eight writable 2D arrays are six mapped basis outputs, one geometry vector, and the coefficients of the basis transformation. The generated basis-map loop computes shared scalar expressions once and writes those six outputs directly; the element-tensor loop then performs the quadrature contraction.
Reproduce each side by checking out both repositories at either
mainor theirpbrubeck/optimise-sum-factorbranches and running:The script lives in firedrakeproject/firedrake#5335 and prints copyable Markdown.
Validation
python -m pydocstyle .python -m pytest -q test/(2457 passed, 26 skipped, 31 xfailed)python -m pytest -q test/gem/test_simplify.py test/gem/test_sum_factorise.py(30 passed)mainat 1,782,525 FLOPs, 22 scalar temporaries, 5 arrays, and 13,454 stored valuesAI assistance
OpenAI Codex and Claude Code were used for implementation, refactoring, testing, benchmarking, and drafting this PR. The human contributor remains responsible for understanding, validating, and maintaining the changes.