Factor scalar maps through basis transformations - #286
Draft
pbrubeck wants to merge 12 commits into
Draft
Conversation
pbrubeck
force-pushed
the
pbrubeck/coffee-scalar-factor
branch
from
August 22, 2026 14:38
e7cc67c to
6a06910
Compare
pbrubeck
force-pushed
the
pbrubeck/coffee-scalar-factor
branch
from
August 22, 2026 15:39
6a06910 to
b4f9db3
Compare
pbrubeck
force-pushed
the
pbrubeck/coffee-scalar-factor
branch
3 times, most recently
from
August 23, 2026 11:21
3a9882c to
d8b8e3f
Compare
pbrubeck
force-pushed
the
pbrubeck/coffee-scalar-factor
branch
from
August 24, 2026 17:01
2eb8724 to
7aba6e7
Compare
pbrubeck
force-pushed
the
pbrubeck/coffee-scalar-factor
branch
from
August 26, 2026 11:42
7aba6e7 to
a096411
Compare
pbrubeck
force-pushed
the
pbrubeck/coffee-scalar-factor
branch
from
August 29, 2026 09:49
a096411 to
4e650b6
Compare
optimise_monomial_sum runs inside the recursive sum_factorise, so calling the reduction placement there walked the same subtrees once per recursion level: 754 calls over 75,497 nodes for a Johnson-Mercier 3D matrix, which the pass never improves. Leave the traversal to the caller, memoise it on the node rather than its id, and skip costing an IndexSum that offers no indirect gather to factor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sharing the result between structurally equal subexpressions, as the other rewrites in this module do. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The tie in _plan_contraction ranks indices by their position in sum_indices, which spectral.py fixes to quadrature source order and _independent_contractions preserves into each subproblem. A plan therefore depends only on the contraction it is planning. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Indexed(ComponentTensor(Indexed(C, kk), jj), ii) rewrites kk in place, which
only reaches an index that appears in kk as itself. An index reached through
a VariableIndex is a free index of the lookup, not a key of kk, so the rewrite
left it behind while the caller went on believing it had been renamed.
That is how make_renamer separates two sums that bind the same index, so the
two sums stayed joined:
(sum_k a_k) * (sum_k b_k) ==> sum_k a_k b_k
Nothing exercised this until a basis transformation became a gather through a
lookup table. Assembling a diagonal indexes both arguments with the test
index, so a mapped tabulation meets itself in a product, and the shared row
index turned the square of a sum into a sum of squares. The matrix-free
diagonal of an HCT-red biharmonic form came out 5.4 times too large, and the
multigrid built on it took 20 iterations where 16 were expected.
Leave the pattern alone when jj is hidden in a VariableIndex, so that
replace_indices, which does substitute inside the lookup, handles it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013NoTXWyj2fVdJTHnMDFB4k
inline_temporaries now offers a ComponentTensor over an IndexSum for inlining,
so that a consumer can index straight through the view rather than read a
temporary. Only Indexed can do that: loopy lowers Indexed(ComponentTensor(e))
by reusing the temporary behind e.
Inverse, Solve and FlexiblyIndexed read the tensor whole, and a ComponentTensor
carries no name to give them. Slate reaches all three -- A.solve(A*C) puts a
matrix-vector product under a Solve -- and dropping its Evaluate left codegen
with a node it cannot name:
AttributeError: 'ComponentTensor' object has no attribute 'name'
AssertionError: cannot generate expression from gem.gem.ComponentTensor
That accounted for 42 failures across tests/firedrake/slate. Keep a reduction
view inlinable only where an Indexed consumes it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013NoTXWyj2fVdJTHnMDFB4k
make_sum takes a sequence of summands. Passing a Counter worked only because iterating a Counter yields its keys. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013NoTXWyj2fVdJTHnMDFB4k
pbrubeck
force-pushed
the
pbrubeck/coffee-scalar-factor
branch
from
August 29, 2026 15:21
4e650b6 to
9941617
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 padded basis transformation stores the same coefficient many times, and writes
each copy into the kernel as its own instruction. This PR stores each distinct
coefficient once and looks the rest up through a small table.
It also factors a reduction through a repeated indirect lookup, so the
kernel sums over the reference table once and then gathers.
Johnson--Mercier on tetrahedra drops from 1,993 lines of C to 358, and its cold
C build time falls 68%. Flops fall on the elements this PR touches.
Base: #281. Needs firedrakeproject/firedrake#5362.
What this does
Intern equal coefficients.
MappedTabulationcollects the distinct symboliccoefficients of the transformation. Two read-only tables then pick the reference
column and the coefficient for each padded entry. The 3D Guzman--Neilan matrix
no longer holds a mutable 16-by-9 coefficient matrix, and its distinct
coefficient workspace has length 43.
Give an additive map an order-free key. COFFEE keys an additive linear map
on its set of summands, not on the order they were built in. It then moves a
uniform multiplicity out into the scalar part of the monomial. A diagonal strain
component simplifies from
0.5 (Gii + Gii)toGii. An off-diagonal componentkeeps its multiplicity.
Factor a reduction through an indirect lookup. For an indirect column
c(i),linearity gives
The kernel computes a dense reference residual once per table row, then gathers
it through the padded map. A cost model decides when this is worth doing.
Factor common scalars, but only inside coefficients. Sums that carry free
indices stay with the contraction planner.
Count an empty contraction. A contraction of an empty product contributes the
size of its index space. This is the identity
sum_i 1 = dim(i). It comes upwhen a selector makes one part of a contraction independent of an index.
Nothing here branches on element family, cell dimension, degree or form operator.
Benchmarks
Measured on this branch against
main. The forms and columns are the same asin #281.
Zany elements, bilinear form
Zany elements, matrix-free action
Interning removes real arithmetic where the map has repeated coefficients:
Guzman--Neilan in 3D falls 10.6%, Johnson--Mercier falls 0.1% in 2D and 8.5% in
3D. AST lines fall further than in #281: Argyris to 203 lines, 52.9% below
main; Guzman--Neilan in 3D to 440, down 75.6%; Johnson--Mercier in 3D to 358,down 82.0%. The isolated build time improves in every matrix and action case,
by 12-68%.
The action still costs more arithmetic than
main, up to +98.0% forJohnson--Mercier in 3D. An action has no second argument axis to share
coefficients across, so interning has nothing to remove there.
Elements this PR does not touch
Q and NCE flops do not change. Interning only touches the scalar coefficients
inside a basis-transformation matrix, and Q and NCE have none.
NCE compile time rises 32-66%. This is the one measured cost on a case the PR
does not otherwise touch. The new cost comparison walks NCE's much larger
expression DAG and finds nothing to rewrite.
Bernstein control
Degree-4 Bernstein Laplacians are structurally identical to
main.Validation
make lintbenchmarks
AI assistance
OpenAI Codex and Claude Code were used for implementation, benchmarking, and
drafting this section. The human contributor remains responsible for
understanding, validating, and maintaining the changes.