Keep factorised contractions atomic in dual evaluation - #280
Conversation
sum_factorise searches every ordering of the contraction indices, so it
gave up past six of them. But a product of tensor product coefficient
evaluations has one set of indices per coefficient, and no factor carries
the indices of another, so the orderings that interleave them are never
worth searching.
Split the contraction into connected components of the graph joining the
indices that share a factor, and search each separately. Interpolating
f*f*f into a hexahedron CG1 space raised
NotImplementedError: Too many indices for sum factorisation!
as three coefficients contribute three indices each; it now factorises as
three independent contractions. This is also cheaper wherever it already
worked, as the search is over the orderings of each component rather than
of all the indices at once.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Ignoring the shape indices kept the number of contraction indices under the sum factorisation limit, at the cost of contracting them outermost. Keeping factorised contractions whole bounds that number directly, so the workaround no longer has anything to do: it was the only user of the ignore argument, and dropping both leaves every kernel we measured unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2c49f04 to
79a3d54
Compare
For this particular situation sum-factorization happens first in the source expression, and we run it again after we contract the quadrature weights to compute degrees of freedom. This is right because it is somehow preserving the mathematical structures, althoug it might not be optimal. The optimal approach is to factor everything in one go, but that will give you much harder problem, and you might pay a price, which is a considerably longer compile time. |
Well, actually, I don't think that the splitting should make things (asymptotically) suboptimal, since sum-factorization should be working independently on the connected components of the contraction graph. It does make sense to split the task in two stages. The only way I can explain the small regressions is that scalar operations (i.e. |
Co-authored-by: Pablo Brubeck <brubeck@protonmail.com>
Now that we split things into connected components are things actually that slow? I am hesitant to approve this PR because it feels hacky and I want to be confident that we actually need the hack in the first place. |
It shouldn't be that slow. But we can try to shut off the inner sum-factorization and see if we can do it in a single outer pass. |
|
And you might be right. This PR is hacky because the original code was very hacky. |
|
@connorjward I tried one single pass of
The moral of the story is that Flops (
Temporaries, as
Compile time (s, best of 3), and the largest connected contraction handed to
|
I don't understand how this can happen. Isn't the search space for the single pass just bigger than the other? And hence will reach at least the same optimum just slower? |
I don't have a good answer for this. I will get this properly fixed, but that needs its own PR. |
|
I think that there's much more going on behind the scenes and the transformations applied by sum-factorization do not consider the entire search space of possible transformations. We do not only split sum-factorization in two calls, but we essentially split |
connorjward
left a comment
There was a problem hiding this comment.
I think you've convinced me that there isn't an immediately better solution than this. If you create an issue and reference it in the code, with a big comment, then I am happy with this.
I just want to make sure we don't have what we usually have which is a block of magic code that makes no sense to anyone.
What if I open the PR that actually fixes this? |
Even better!
I read this as meaning that you'd be unwilling to do that right now. |
Co-authored-by: Pablo Brubeck <brubeck@protonmail.com>
Co-authored-by: Connor Ward <c.ward20@imperial.ac.uk>
* Test interpolation of products of coefficient evaluations A product of coefficient evaluations on a hexahedron contracts more indices than one sum factorisation can search, and raised NotImplementedError. Cover scalar powers, along with the vector and tensor expressions whose value indices contract the evaluations together. Companion to firedrakeproject/fiat#280. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Stacked on #269.
TLDR: interpolation was undoing sum-factorisation from the source expression and refactorising it after computing the degrees of freedom. This PR preserves the original sum-factorization by stopping early when traversing a contracted GEM expression.
TSFC hands FInAT's dual evaluation coefficient evaluations that are already sum factorised, and
traverse_productflattened them back into the surrounding contraction, undoing sum-factorization. That discarded the factorisation along with the subexpressions the factors shared, and multiplied the indices to search over, so a product of a few evaluations exceeded the sum factorisation limit.This PR makes contractions atomic: the new
gem.optimise.is_contractionpredicate is passed asstop_at, so an already factorised contraction is not flattened. Bounding the index count this way retires theignoreargument ofcontraction, along with the shape-index workaround that was its only user.Kernel flop counts for interpolation on a hexahedron, where error is
NotImplementedError: Too many indices for sum factorisation!: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 → DG3Tests are added for the GEM and FInAT paths.
AI declaration: written with Claude Code (Claude Opus 5).