Skip to content

Place indirect contractions once per assignment - #5438

Open
pbrubeck wants to merge 3 commits into
pbrubeck/zany-matvecfrom
pbrubeck/indirect-contractions
Open

Place indirect contractions once per assignment#5438
pbrubeck wants to merge 3 commits into
pbrubeck/zany-matvecfrom
pbrubeck/indirect-contractions

Conversation

@pbrubeck

@pbrubeck pbrubeck commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

TLDR

This is the TSFC half of firedrakeproject/fiat#286. It places the indirect
contraction pass once per finished assignment
, and lowers the view it
leaves behind without copying it
.

Stacked on #5362, which carries the TSFC halves of fiat#284 and fiat#281.
Needs firedrakeproject/fiat#286.

What this does

Place the pass once per assignment (tsfc/spectral.py,
tsfc/coffee_mode.py).

tabulate_indirect_contractions rewrites a contraction that reads a table
through a map into one tabulation over the table's rows, followed by a gather.
It used to run at the tail of gem.coffee.optimise_monomial_sum, which
sum_factorise calls recursively, so it re-walked the same subtrees once per
level of recursion: 754 calls over 75,497 nodes for a Johnson--Mercier matrix
in 3D.

It now runs once on each finished assignment, in _factorise and in
optimise_expressions. Placing it there also keeps it inside plan costing, so
the two plans #5362 compares are costed with the tabulation applied.

Lower the view without a copy (tsfc/loopy.py).

The rewrite leaves its result behind a ComponentTensor view of a temporary
that code generation has already emitted. Indexing that view used to
materialise a second array and copy into it. _expression_indexed now reads
the existing temporary through the view's own indices.

Effect

Flops are unchanged; this is placement, not a new rewrite. TSFC compile time
falls 1.4-2.4x on the zany elements, and the copy the view lowering removes is
one array per tabulated contraction.

The rewrite itself is what recovers the Johnson--Mercier and Argyris actions
that #5362 alone regresses -- see the tables there. Without this PR the pass is
present in GEM but never reaches a whole assignment.

Benchmarks

Measured 2026-09-08 against today's main, with the whole stack installed
(fiat#282 -> #284 -> #281 -> #286 paired with the two firedrake PRs). Minimum of
25 timed assemblies, three interleaved repetitions. NCE degree 3 on a prism is
the control: the stack cannot touch it, and it is flat, so the harness is
honest.

Action

element dim flops main flops stack assemble main assemble stack
Argyris 2 6,508 3,938 0.890 ms 0.796 ms 11% faster
Johnson--Mercier 2 3,753 3,129 1.151 ms 1.151 ms unchanged
Johnson--Mercier 3 44,248 32,806 8.348 ms 10.191 ms 22% slower
Guzman--Neilan 2 3,549 2,586 1.044 ms 0.965 ms 8% faster
Guzman--Neilan 3 106,236 51,296 26.882 ms 7.806 ms 3.4x faster
NCE degree 3 (control) 3 110,019 110,052 1.608 ms 1.621 ms 1.01

Bilinear form

element dim flops main flops stack assemble main assemble stack
Argyris 2 38,611 53,286 3.532 ms 4.053 ms 15% slower
Johnson--Mercier 2 21,660 21,645 3.384 ms 3.543 ms 5% slower
Johnson--Mercier 3 686,329 494,893 53.466 ms 50.410 ms 6% faster
Guzman--Neilan 2 10,041 10,963 2.300 ms 2.506 ms 9% slower
Guzman--Neilan 3 384,767 344,078 47.364 ms 43.073 ms 9% faster
NCE degree 3 (control) 3 1,639,009 1,639,042 27.664 ms 27.733 ms 1.00

The assembled action vectors agree with main to 12 digits on Johnson--Mercier
and Guzman--Neilan in 2D and 3D, so the Guzman--Neilan win is real work and not
dropped work.

Generated code

The stack replaces unrolled scalar arithmetic with tables and loops, so it moves
storage out of working temporaries and into read-only tables. Working
temporaries are the ones that matter, and they fall.

element dim working elements main working stack C lines main C lines stack
Argyris (action) 2 63 113 387 352
Johnson--Mercier (action) 3 798 224 2,127 771
Johnson--Mercier (matrix) 3 4,704 4,676 2,315 364
Guzman--Neilan (action) 3 144 91 1,559 851
Guzman--Neilan (matrix) 3 288 347 1,803 432

AI assistance

Claude Code was used for the split, the benchmarking and this description. The
human contributor remains responsible for understanding, validating and
maintaining the changes.

🤖 Generated with Claude Code

https://claude.ai/code/session_01A1NNAv95LANgpBX61ozPE2

pbrubeck and others added 3 commits September 8, 2026 17:20
Tabulating an indirect contraction leaves the result behind a
ComponentTensor view of a temporary that code generation has already
emitted.  Read that temporary through the view's own indices instead of
materialising the view as a second array.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M6QZ26z2p9o7sYzxZXV1B3
`tabulate_indirect_contractions` was called at the tail of
`optimise_monomial_sum`, which sum factorisation calls recursively, so it
re-walked the same subtrees once per level of recursion.  Apply it once to
each finished assignment instead, which keeps plan costing aware of the
tabulation without the repeated traversal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M6QZ26z2p9o7sYzxZXV1B3
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M6QZ26z2p9o7sYzxZXV1B3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

base:main Run this PR using a main (dev) build LLM used An LLM was used in the production of this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant