Recognise repeated and joined contractions when sum factorising - #282
Open
pbrubeck wants to merge 10 commits into
Open
Recognise repeated and joined contractions when sum factorising#282pbrubeck wants to merge 10 commits into
pbrubeck wants to merge 10 commits into
Conversation
pbrubeck
commented
Aug 19, 2026
| subtree holding every factor that carries it, which is the earliest | ||
| its reduction is legal. Unlike a search over orderings of the | ||
| indices, this can reduce an index over part of the product and | ||
| multiply the rest in afterwards. |
Author
There was a problem hiding this comment.
This is the algorithmic realisation of splitting sum-factorization into subsets of factors. This models the splitting in dual_evaluation.
Before, we were tackling all indices at once and searching over all possible permutations = indices!.
Now we traverse a tree, by appending factors one by one. The cost is reduced to 3^factors.
pbrubeck
force-pushed
the
pbrubeck/single-pass-sharing
branch
from
August 19, 2026 14:45
ff49423 to
8c90307
Compare
This was referenced Aug 20, 2026
pbrubeck
added a commit
to firedrakeproject/firedrake
that referenced
this pull request
Aug 20, 2026
The TSFC changes here need the GEM changes in the FIAT stack firedrakeproject/fiat#282 -> #284 -> #281, whose head carries all three. 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
added a commit
to firedrakeproject/firedrake
that referenced
this pull request
Aug 22, 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
commented
Aug 24, 2026
| # Factorise over the new contraction with Qi, keeping whole the | ||
| # contractions that fn already factorised | ||
| evaluation = gem.optimise.contraction(evaluation, stop_at=is_contraction) | ||
| evaluation = gem.optimise.contraction(evaluation) |
Author
There was a problem hiding this comment.
Suggested change
| evaluation = gem.optimise.contraction(evaluation) |
pbrubeck
commented
Aug 24, 2026
| # a minimal memory footprint, although the operation count | ||
| # does appear to be minimal. | ||
| evaluation = gem.optimise.contraction(evaluation, stop_at=is_contraction) | ||
| evaluation = gem.optimise.contraction(evaluation) |
Author
There was a problem hiding this comment.
Suggested change
| evaluation = gem.optimise.contraction(evaluation) |
Coefficient evaluations reach FInAT's dual evaluation already sum factorised by TSFC. Flattening them back into the surrounding contraction discards that factorisation, along with the subexpressions the factors share, and multiplies the indices to search over: a product of a few evaluations, or evaluations coupled through a value index, then exceeds what one exhaustive search can handle. Pass the new gem.optimise.is_contraction predicate as stop_at, so that traverse_product keeps each factorised contraction whole. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drop the inner sum_factorise from dual evaluation so only the outer gem.optimise.contraction remains, and raise the index limit that one pass now has to cover.
Flattening a contraction renames its indices apart, so a coefficient evaluation used more than once in a product becomes that many independent contractions and is evaluated once per use. Count the occurrences of each contraction in the product tree and keep whole the ones that occur more than once. Expanding a product only pays where factorising the expanded form eliminates the sharing the expansion introduced, which multilinearity guarantees; a product of repeated evaluations is not multilinear in them, so expanding it can only lose sharing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Searching orderings of the contraction indices has to multiply every factor carrying an index before it can reduce that index, so it cannot reduce an index over part of a product and multiply the rest in after. Plan the product tree instead, by dynamic programming over subsets of the factors, reducing each index at the smallest subtree that holds every factor carrying it. This costs 3^factors rather than indices factorial, and plans the contractions a tensor value index joins together without the ordering search blowing up. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sets of indices iterate in hash order, so the reduction order, and with it the generated kernel, varied between runs and broke idempotency. Break the ties on index count. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ranking plans by storage as well as operations, whether as a tiebreak or added into one score, selects exactly the same plans: the storage a kernel declares follows from the loop nest the schedule builds, which a cost over the expression alone cannot see. Planning the product tree also leaves the ordering search unreachable for these contractions, so its limit goes back to six. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bound on a single connected contraction now applies to the ordering search the planner falls back on, and an unrestricted contraction keeps a repeated evaluation whole by itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nothing passes it now that a repeated contraction is recognised from the expression itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bound on the factors a product tree is planned for sat in a module global while the bound on the indices an ordering search takes is a literal at its test. Put them together, and write the two docstrings this file gained in the :arg: style the rest of it uses. Cover a connected contraction of more factors than the planner takes but few enough indices for the ordering search, which is the shape that keeps both bounds live. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pbrubeck
force-pushed
the
pbrubeck/single-pass-sharing
branch
from
August 26, 2026 11:42
951b80a to
e218260
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.
Fixes #283 by recasting sum-factorization as a DP tree traversal with cost growing
with the number of factors as opposed to contraction indices, and falling back to
the original index combinatorial search when there are too many factors.
TLDR: #280 kept an already factorised contraction whole by asking the caller to
pass
stop_at. This PR drops that second pass and gets the same result from theexpression itself, by not flattening a contraction that is used more than once
and by planning the product tree instead of searching orderings of the indices.
What changes
Flattening a contraction renames its indices apart. That is what makes it
sound, but it also means a coefficient evaluation used three times becomes
three independent contractions, and is computed three times. So don't flatten
a contraction that occurs more than once in the product. Expanding a product
only pays where factorising the expanded form recovers the sharing that
expansion broke, which multilinearity guarantees; a product of repeated
evaluations is not multilinear in them, so there is nothing to recover.
Plan the product tree, not the index ordering. Searching orderings has to
multiply every factor carrying an index before it can reduce that index, so it
can never reduce over part of a product and multiply the rest in afterwards.
Build the tree by dynamic programming over subsets of the factors, reducing
each index at the smallest subtree that holds every factor carrying it. That
costs
3^factorsrather thanindices!, and plans the contractions that atensor value index joins together.
With the tree planned, dual evaluation no longer needs the inner
sum_factorise, so only onegem.optimise.contractionremains, and the boundon the ordering search stays where it was.
Recognising a repeated contraction from the expression retires the
stop_atargument of
contractionand theis_contractionpredicate Keep factorised contractions atomic in dual evaluation #280 added for it,which nothing passes any more.
traverse_productandtraverse_sumkeep theirown
stop_at, whichgem.refactorisestill uses.Interpolation on a hexahedron
Kernel flop counts and temporaries, as
count / entries / largest.f*f*f, CG1 → CG1f*f*f*f, CG1 → CG1f*f, CG4 → DG3dot(u, u)*u, vector CG4 → vector CG4dot(A, A), tensor CG2 → tensor CG3inner(A, A), tensor CG2 → CG2grad(f)[0], CG4 → CG4div(u), vector CG4 → DG3Every case that raised
NotImplementedError: Too many indices for sum factorisation!on the base of #280 still compiles here.dot(A, A)costs 0.078 s more to compile. It is the one case here whose valueindex joins the evaluations into a contraction of seven indices, so it is the
one case that reaches the planner with enough factors to plan.
Helmholtz on an extruded hexahedral mesh
inner(u, v)*dx + inner(d(u), d(v))*dx, withd=gradfor CG andcurlforNCE. This is the cost of the planner where nothing needed fixing.
Compile time here is within run to run variation either way. The NCE forms cost
33 flops more at every degree. The planner finds cheaper
trees for three subproblems of those forms, 70 against 90 operations each, but costs
each connected subproblem on its own while the subproblems share terms, so the
cheaper trees drop a shared subexpression. Keeping the whole expression in view
when costing a plan is left for later.
Tests
test_sum_factorise.pycovers the bound, now on the ordering search the plannerfalls back on; an unrestricted contraction keeping a repeated evaluation whole;
and a contraction joined by a value index into more indices than an ordering
search can take. Each was checked to fail when the change it covers is reverted.
AI declaration: written with Claude Code (Claude Opus 5).