Skip to content

feat[next-dace]: fold single iteration Map dimensions - #2799

Merged
havogt merged 5 commits into
GridTools:mainfrom
havogt:dace-fold-trivial-map-dimensions
Aug 17, 2026
Merged

havogt merged 5 commits into
GridTools:mainfrom
havogt:dace-fold-trivial-map-dimensions

Conversation

@havogt

@havogt havogt commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Description

A Map dimension such as i = c:c+1:1 can only ever take the value c, but the Memlets inside its scope still refer to i symbolically. MapFusionVertical compares producer and consumer subsets with Range.covers(), which does not know the Map range, so a producer writing a[c] is not seen to cover a consumer reading a[i - o] even though both denote the same element, and a legal fusion is rejected.

TrivialMapDimensionFolding replaces the parameter of such a dimension by its value inside the Map scope. On a Map with range 1:17, 80 over the surface level of an icon4py program:

                       before                                    after
MapEntry -> Tasklet    gtir_tmp_32[i_Cell - 1, i_K - 80]         gtir_tmp_32[i_Cell - 1, 0]
MapEntry -> Tasklet    gtir_tmp_38[i_Cell - 1, i_K - 80]         gtir_tmp_38[i_Cell - 1, 0]
Tasklet  -> MapExit    gtir_tmp_40[i_Cell - 1, i_K - 80]         gtir_tmp_40[i_Cell - 1, 0]

The dimension itself is kept, unlike DaCe's native TrivialMapElimination which removes it. The transformation runs in _gt_auto_process_top_level_maps immediately before vertical_map_fusion, restricted to top level Maps.

On compute_perturbed_quantities_and_interpolation the two Maps over the surface level then fuse into one, and nothing else in the program changes. It also removes a source of non-determinism: the same program compiled to a different number of kernels depending on what else was compiled in the same session.

ConstantPropagation does not cover this case, it propagates symbols assigned on interstate edges and a Map parameter is not one of those.

Testing

test_trivial_map_dimension_folding.py: folding rewrites the Memlets while the Map keeps its parameters and range, the SDFG computes the same result before and after, applying repeatedly yields a single application, and a Map without a single iteration dimension is left alone.

Requirements

  • All fixes and/or new features come with corresponding tests.
  • Important design decisions have been documented in the appropriate ADR inside the docs/development/ADRs/ folder.

Reviewed on the fork first, at havogt#73.

havogt and others added 4 commits August 10, 2026 15:31
A Map dimension `i = c:c+1:1` can only ever take the value `c`, but the Memlets
inside its scope keep referring to `i` symbolically. `MapFusionVertical` compares
producer and consumer subsets with `Range.covers()`, which does not know the Map
range, so a producer writing `a[c]` is not recognized as covering a consumer
reading `a[i - o]` and a legal fusion is rejected.

The new transformation folds the value into the scope and runs before the top
level fusion rounds. The dimension itself is kept, unlike DaCe's
`TrivialMapElimination` which removes it and thereby prevents the Map from being
scheduled on the GPU.

On `compute_perturbed_quantities_and_interpolation` this collapses the three
surface level kernels into one, 10 -> 9 kernels. It also removes a
non-determinism: the same program compiled to 10 or 11 kernels depending on
which other variants were compiled in the same session.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… folding

- drop the `rng[2] == 1` step check: with `start == end` the dimension has a
  single iteration for any step, so the step carries no information here.
- fold with a single `replace_dict()` instead of one `replace()` per parameter.
- shorten the note contrasting the transformation with `TrivialMapElimination`
  to what distinguishes them, the dimension being kept rather than removed.
- test the `other_subset` of every Memlet as well, not only the `subset`.
- drop the separate fixpoint test: the main test already applies the
  transformation repeatedly and asserts a single application.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@philip-paul-mueller philip-paul-mueller left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

only_toplevel_maps = dace_properties.Property(
dtype=bool,
default=False,
desc="Only process Maps that are on the top level.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
desc="Only process Maps that are on the top level.",
desc="See docs.",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Applied in 586c5b4.


# Only apply if a parameter is still referenced, otherwise the transformation
# would apply again and again on the same Map.
# NOTE: `include_entry` is needed because the uses are on the out edges of

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The same can be said for include_exit, I would drop the node entirely.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Right, dropped it in 586c5b4.

- point the property description at the class docstring
- drop the note on `include_entry`, it holds for `include_exit` just as much

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@havogt
havogt merged commit 6eca55d into GridTools:main Aug 17, 2026
24 checks passed
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.

2 participants