Padded basis transformation for better codegen - #281
Open
pbrubeck wants to merge 4 commits into
Open
Conversation
pbrubeck
force-pushed
the
pbrubeck/zany-matvec
branch
2 times, most recently
from
August 19, 2026 09:08
0080000 to
4969bd6
Compare
pbrubeck
force-pushed
the
pbrubeck/zany-matvec
branch
from
August 20, 2026 15:01
0b2cfe8 to
51ee6d4
Compare
pbrubeck
added a commit
to firedrakeproject/firedrake
that referenced
this pull request
Aug 20, 2026
This branch needs the linear-map preservation added in firedrakeproject/fiat#281. Revert this commit once that lands on FIAT main. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pbrubeck
force-pushed
the
pbrubeck/zany-matvec
branch
from
August 22, 2026 15:39
51ee6d4 to
c3eb76b
Compare
A padded basis transformation tabulates each facet as an IndexSum, so selecting one by a variable facet index reached _select_expression with a type it could not factorise. Rewrite the summands over one shared multiindex and select inside the reduction, which the equal extents on every facet make well defined. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The padded transformation was applied by building its row-padded gather directly, which fixed the orientation at construction: contracting the coefficient against a mapped tabulation then costs one gather per quadrature point, six times the symmetric test-side scatter. Represent M instead as a rank-2 expression, an interned entry summed over the padded row against a Delta selecting its column. Cancelling that Delta reproduces the gather, so the mat-mat is unchanged, while contracting M's own axes first pulls a coefficient back to the reference basis once per cell. Guzman-Neilan 3D action: 147180 -> 51920 flops, largest working temporary 144 -> 24 entries. Delta now propagates the free indices of a VariableIndex operand, and substitution folds a variable index that has become constant. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MappedTabulation minted a fresh index on every call, so the tabulations of different derivative orders contracted over distinct indices of equal extent. Expressions that are structurally equal then hash apart, and the scheduler gives each its own loop nest. Reuse one index per instance for the reference basis and for the padded row. Equal tabulations now share a subexpression, and their loops fuse without any change to the scheduler. The row index of a tabulation stays per call, since a ComponentTensor binds it and sharing it only forces redundant materialisation. Guzman-Neilan 3D action: 51920 -> 51296 flops, 17 -> 15 array temporaries; 2D action: 2676 -> 2586 flops. Four groups of sibling loops over equal extents collapse to one loop each, and the Argyris and Johnson-Mercier actions lose theirs likewise. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pbrubeck
force-pushed
the
pbrubeck/zany-matvec
branch
from
August 26, 2026 11:42
c6ccec2 to
a780820
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.
This PR vectorizes the sparse basis transformation.
Stacked on #284, which keeps the transformation whole through monomial
collection. Needs firedrakeproject/firedrake#5362.
The original sparse approach generated individual instructions for each row of the sparse-dense matrix product, which were poorly handled in loopy. Each row became its own temporary: Johnson--Mercier on tetrahedra declared 655 arrays of length
num_quadrature_points, one per basis function and tabulation, where a loop over the basis index needs 24 arrays of shape (42, nqp).In order to generate good loopy kernels, rows are padded so that they have same number of entries. A ragged index expresses this exactly, but loopy forbids one: it builds every iteration domain as an isl set whose bounds must be quasi-affine in the enclosing indices, and a row length read from a table is not.
Padding alone is not enough.
collect_monomialsdistributes sums to reach a sum-of-products normal form, and the padded transformation reaches it as anIndexSumover the nonzeros of one basis row. Distributing over that sum expands the map into the Cartesian product of its entries before any loop is placed, and Johnson--Mercier on tetrahedra collects 3240 monomials for what is one linear operand. #284 keeps such a sum atomic and shares it between the argument axes, and costs that against expanding it; this PR is what makes the sum worth preserving.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; the zany forms are
(hess u, hess v)for Argyris(
hess = sym(grad(grad(.)))),(eps u, eps v) + (div u, div v)forGuzman--Neilan 1st kind H1 (
eps = sym(grad(.))), and(sym(u), sym(v)) + (div u, div v)*dx1for Johnson--Mercier.tsfc (s)isthe TSFC compile time;
build (s)is the isolated cold-cache C build(compiler and linker only, no TSFC, no PyOP2);
kernel (s)is the isolatedper-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 mutableloopy temporaries with a shape, their total entries, and the largest single
temporary.
mainis measured against the real, unpatched firedrake, since itcannot import a tsfc that expects gem API this PR's stack adds; the PR side
is measured against the firedrake commit that adds exactly the gem API this
PR's stack needs.
Bilinear form
Matrix-free action
CG, RT, Q and NCE are unaffected by this PR beyond what #284 already
contributes, and match #284's own table exactly: RT falls 10.4%/13.8%/18.1%
at degree 1/3/5 in 2D and 9.3%/19.7%/24.3% in 3D (now correctly measured
against
div, notcurl); CG only moves at degree 1 (-6.3% in 2D, -9.8% in3D); Q and NCE are unchanged, since this PR's padded transformation only
applies to physically-mapped (zany) elements.
Zany elements, bilinear form
Zany elements, matrix-free action
The gain is in code generation. Arithmetic rises on every zany case (Argyris
+40.5%; Guzman--Neilan +26.8%/+8.6% in 2D/3D; Johnson--Mercier
+21.0%/+24.3%), and every action rises too (up to +128% for Guzman--Neilan in
3D), while AST lines fall 48-79% for the matrix and 18-65% for the action.
The largest temporary grows wherever the padded
(ndof, nqp)table replacesthe per-row vectors
maindeclares -- up to 2.7x for Guzman--Neilan in 3D --and is unchanged for Johnson--Mercier, whose largest temporary already has
that shape. Isolated build time falls everywhere: 24-72% for the matrix,
18-74% for the action.
Validation
python -m pytest -q test/ --ignore=test/FIAT/regression(2379 passed, 26 skipped, 31 xfailed, 1 xpassed)tests/tsfcand the three zany regression suites in Preserve 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.