Recognise repeated and joined contractions when sum factorising - #282
Open
pbrubeck wants to merge 13 commits into
Open
Recognise repeated and joined contractions when sum factorising#282pbrubeck wants to merge 13 commits into
pbrubeck wants to merge 13 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
repeated_contractions re-implemented the rule that traverse_product already encodes, and dropped its treatment of a reciprocal. Put the rule in _product_descent and let both walks use it. Name the factor bound _MAX_PLANNED_FACTORS and keep the reasoning for its value beside the number. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013NoTXWyj2fVdJTHnMDFB4k
…sharing main gained a greedy fallback for a connected contraction too large to search (#292), while this branch plans a product tree for one with few enough factors. Keep both: plan when the factor count allows it, and let the ordering search below that fall back on the greedy order rather than raising. `test_too_many_indices_in_one_contraction` asserted the NotImplementedError that #292 removed, and #292's own `test_greedy_contraction` builds two factors, which the planner now takes before the greedy path can see it. They become one test over the shape that still reaches greedy: more factors than the planner takes, and more indices than the search can afford. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A1NNAv95LANgpBX61ozPE2
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
Sum factorisation now plans the product tree instead of searching every
order of the contraction indices. It also keeps a contraction whole when the
expression uses it more than once, so the kernel evaluates it once.
Forms that used to fail with
Too many indices for sum factorisation!nowcompile. Flops fall on the cases that were doing redundant work, and stay equal
everywhere else.
Fixes #283.
What this does
Plan the product tree.
A search over index orders must multiply every factor that carries an index
before it can sum that index away. It can never sum over part of a product and
multiply the rest in later.
This PR builds the tree by dynamic programming over subsets of the factors. It
sums each index at the smallest subtree that holds every factor carrying that
index. The search costs
3^factorsinstead ofindices!. A search over ordersstays as a fallback for very wide products.
Keep a repeated contraction whole.
Breaking a contraction up renames its indices. A coefficient evaluation used
three times therefore becomes three separate contractions, and the kernel
computes it three times. This PR finds such a contraction in the expression and
leaves it alone.
Tidy-up. One helper now holds the rule for what a product-tree walk descends
into. Two walks used to carry their own copy of it.
Interpolation on a hexahedron
Kernel flops and temporaries, as
count / entries / largest.f*f*f, CG1 -> CG1f*f*f*f, CG1 -> CG1f*f, CG4 -> DG3dot(u, u)*u, vector CG4dot(A, A), tensor CG2 -> CG3inner(A, A), tensor CG2 -> CG2grad(f)[0], CG4 -> CG4div(u), vector CG4 -> DG3dot(A, A)costs 0.078 s more to compile. Its value index joins the evaluationsinto a contraction of seven indices. It is the only case here wide enough for
the planner to do real work.
Helmholtz on an extruded hexahedral mesh
inner(u, v)*dx + inner(d(u), d(v))*dx, withd=gradfor CG andcurlforNCE. These forms need no repair, so this table shows what the planner costs.
Compile time moves less than run-to-run noise. The NCE forms cost 33 more flops
at every degree. The planner finds cheaper trees for three parts of those forms,
70 operations against 90 each. It costs each connected part on its own, so it
does not see that the parts share terms, and it drops one shared subexpression.
Costing the whole expression at once is future work.
Tests
test_sum_factorise.pycovers the fallback bound, a contraction that keeps arepeated evaluation whole, and a contraction joined by a value index into more
indices than an order search can take. Each test was checked to fail when its
change is reverted.
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.