Skip to content

Faster code generation, serialization and library expansion of large SDFGs - #2679

Open
ThrudPrimrose wants to merge 19 commits into
mainfrom
sdfg-codegen-perf
Open

ThrudPrimrose wants to merge 19 commits into
mainfrom
sdfg-codegen-perf

Conversation

@ThrudPrimrose

@ThrudPrimrose ThrudPrimrose commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

Code generation, saving and library expansion of large SDFGs repeat whole-graph walks per element: a linear node_id scan per serialized edge, a dominator closure per allocated container, the full substitution dictionary handed to SymPy per range bound, a block-name ledger rebuild per new block, a deep copy of the SDFG into the history on the first expansion, and a recursive generator per nesting level in all_control_flow_regions. This computes each of those once per span or keeps it incrementally, without changing the generated code; symbol-side caching is left to the symbol registry (#2673).

…opying it per state

(cherry picked from commit d45befe)
…expression

Substituting the whole dictionary into every bound walks the expression once per entry; an SDFG specialized over 2878
offset symbols spent 575 ms per subset. Entries are kept when their key names a symbol of the expression or of a value
an already kept entry substitutes in, so chained substitutions resolve as before (3.7 ms per subset).

(cherry picked from commit 02c40f5)
expand_library_nodes asked defined_symbols of the same state once per expanded node, and
update_cfg_list rebuilt its member check as a list per nested SDFG.

(cherry picked from commit 797d492)
… not per edge

new_symbols(sdfg, ...) copied the symbols and every array dtype per assigning edge, 2.4 s per call on warpx_field_gather where each LoopToMap lift nesting a rebound symbol asks for it.

(cherry picked from commit 07a53df)
(cherry picked from commit 4acfe7c)
(ported from extended ead77f6)

(cherry picked from commit 34687ff)
…e per nested SDFG

It recursed into itself, and every level reset the whole tree again, so a call cost one full
rebuild per nested SDFG. The recursion now only points each nested SDFG at its parent, after the
single rebuild at the top.

(cherry picked from commit 8825be9, code only)
(cherry picked from commit cc942e8)
The recursive generator resumed once per nesting level for every region it yielded, and
reset_cfg_list runs this walk after most graph edits: 55M generator calls, 315 s of 1008 s
(cProfile) on lulesh's map lowering. Same regions, same order; the test pins it to the recursion.

(cherry picked from commit 95f5513)
(cherry picked from commit 4a9a007)
SubgraphView sorted its members by node_id, a linear scan per member, so a scope subgraph cost
members times graph size. For an OrderedDiGraph node_id is the position in nodes(), so filtering
nodes() gives the same order in one pass. sw4 rhs4sg canonicalize: SubgraphView.__init__
28.5 s -> 1.3 s CPU over 22294 views, same pass digest.

(cherry picked from commit dcb8bf3)
(cherry picked from commit 0f10523)
… it per name

`_ensure_unique_block_name` unioned `_labels` with `{s.label for s in self.nodes()}`
on every call, so naming one block walked every block in the region -- quadratic in
blocks per region, and every `add_state` pays it. The walk added nothing that could
not be recorded when the name arrives: `add_node` now records the label a block
enters with and the `ControlFlowBlock.label` setter records a rename in place, which
are the only two ways a name becomes live in a region (a branch appended to a
`ConditionalBlock` bypasses `add_node`, so `add_branch` records its own). A removal
still leaves its name in the ledger, as before. Membership only -- the set is never
iterated, so nothing order-dependent rides on it.

HPCAgent-Bench cloudsc parse, `to_sdfg(simplify=False)`, 4426 states / 1183 mapped
tasklets / 2271 descriptors, three alternating rounds on a contended box:
before 29.9 / 39.0 / 37.3 CPU s, after 26.1 / 32.3 / 34.1 CPU s -- mean 35.4 -> 30.8,
a 13% cut. Same work: 4484 `_ensure_unique_block_name` calls in both arms, and the
parsed SDFG's JSON is byte-identical with `guid`/`debuginfo` stripped on cloudsc plus
tsvc s3110, polybench 3mm and npbench hdiff.

(cherry picked from commit bae19b7)
(cherry picked from commit 71e9c61)
Serializing an edge looks up both endpoints with node_id, a linear scan, so
saving a large SDFG (e.g. the invalid-SDFG dump of h_psi) was quadratic.

(cherry picked from commit 9755b9e)
Every container needing a common (post)dominator recomputed the whole SDFG's dominators and their
transitive closure; QE h_psi's code generation spent over two hours there. One tree per SDFG answers
dominance from DFS intervals.

(cherry picked from commit d102614)
ThrudPrimrose added a commit that referenced this pull request Oct 12, 2026
Take the PR's dominator trees, ContextVar node-id index and cfg_list short-circuit over extended's
earlier copies of the same work; keep extended's CodegenAnalysisPipeline, states() iteration, the
expand_library_nodes predicate and its defined-symbol typing test. Drop the duplicated
nsdfg_argument_symbols method and cache field the textual merge produced.
The symbol-side optimizations leave this PR: memoized assignment-RHS names, the per-call
interstate type environment in defined_symbols, the library-expansion defined-symbols memo,
the nested-SDFG argument-symbol cache and the in-place symbol table in SDFGState.to_json.
Kept: dominator trees built once per SDFG for allocation lifetimes, Range.replace handing
SymPy only reachable substitutions, the frozen node_id index (now entered by region
serialization), the block-name ledger, the explicit-stack region walk, the linear cfg-list
merge, call-site debug info, subgraph-view ordering and history-free lowering.
@ThrudPrimrose
ThrudPrimrose marked this pull request as ready for review October 13, 2026 13:58
ThrudPrimrose added a commit that referenced this pull request Oct 14, 2026
Extended already carries the full codegen, serialization and expansion speedups #2679 splits out for main.
@ThrudPrimrose
ThrudPrimrose requested a review from tbennun October 15, 2026 12:31
@ThrudPrimrose

ThrudPrimrose commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator Author

I removed the symbol-related optimizations I think the rest are still good to have as optimizations for big Fortran SDFGs.

ThrudPrimrose added a commit that referenced this pull request Oct 16, 2026
Files owned by open PRs (#2443, #2677, #2679, #2680, #2690, #2691, #2693,
#2696) now equal main plus that PR's own diff; map fusion equals main plus
#2696. Removed: DemoteNonStructuralSymbols with tasklet_utils, narrowing,
optionals and the utils demote helpers (unused by dace-fortran);
ShortLoopUnroll and MakeTransientsPersistent (move to dace-fortran);
use_authority serialization; new-libnodes CI (general CI runs those tests).
ThrudPrimrose added a commit that referenced this pull request Oct 16, 2026
expand_library_nodes imported EXPANSION_DEFINED_SYMBOLS, which #2679's
state.py no longer defines; to_json froze node ids that #2679 already
freezes in ControlFlowRegion.
ThrudPrimrose added a commit that referenced this pull request Oct 16, 2026
Taking PR #2679's state.py dropped two FaCe fixes: SymbolResolver keeps
only the last state per SDFG (one entry per state costs gigabytes on QE
h_psi), and remove_node bounds-checks a deserialized start block index.
new_symbols(sdfg, ...) copied the symbols and every array dtype per
assigning edge: 2.4 s per call on warpx_field_gather.
# Conflicts:
#	dace/codegen/targets/framecode.py
#	tests/codegen/allocation_lifetime_test.py


def _get_dominator_and_postdominator(sdfg: SDFG, accesses: list[tuple[SDFGState, nodes.AccessNode]]):
class DominatorTree:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This both fixes allocations as well as improves speed by not recomputing the dominance all the time

Comment thread dace/sdfg/graph.py


@contextmanager
def frozen_node_ids() -> Iterator[None]:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Computing node ids is kind of expensive that is why I'm trying to cache them, helps a lot

Comment thread dace/sdfg/state.py
if dace.Config.get("compiler", "lineinfo") == "inspect":
caller = inspect.getframeinfo(inspect.stack()[2][0], context=0)
return dtypes.DebugInfo(caller.lineno, 0, caller.lineno, 0, caller.filename)
lineno, filename = caller_position(sys._getframe(2))

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_getframe is much faster getting the full stack all the time.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant