Skip to content

fix[next]: deterministic declaration order in gtfn codegen - #2779

Merged
havogt merged 6 commits into
GridTools:mainfrom
havogt:havogt/gtfn-deterministic-tag-order
Aug 13, 2026
Merged

fix[next]: deterministic declaration order in gtfn codegen#2779
havogt merged 6 commits into
GridTools:mainfrom
havogt:havogt/gtfn-deterministic-tag-order

Conversation

@havogt

@havogt havogt commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

The gtfn lowering collected dimensions, cartesian offsets and shift offsets into sets and iterated them while building the generated source.

The fix replaces the set by an OrderedSet at the three collectors. Only the one in _get_domains is backed by observed diffs; the other two are the same defect on inputs the test runs did not reach, namely several cartesian dimensions and more than one connectivity offset in a stencil.

The gtfn lowering collected dimensions, cartesian offsets and shift offsets into
`set`s and then iterated them to build the generated source. Set iteration order
for these elements varies with `PYTHONHASHSEED`, so the same program lowered in
two processes could emit the same declarations in a different order.

The build cache is keyed on a fingerprint of the generated source, so this made
affected programs miss the cache on every fresh process.

Observed on the icon4py dycore and diffusion benchmarks: across three runs, 5-6
of 67 programs differed, always as a pure permutation of the dimension tag block

    -    using Edge_t   = gtfn::unstructured::dim::horizontal;
    +    using Vertex_t = gtfn::unstructured::dim::horizontal;

and one program produced three distinct fingerprints in three runs. Only programs
with two or more distinct horizontal domains are affected.

Derive the order from the IR instead, at all three collection sites:

- `_get_domains` drove the observed differences.
- `_collect_offset_definitions` feeds the same `offset_definitions` dict, so it
  has the same defect on cartesian grids, which this workload never exercised.
- `_collect_offset_or_axis_node` returns the shift offsets its caller turns into
  the generated `connectivities` list.

@edopao edopao 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

)
)
return result
return list(result)

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.

What about returning just an iterable (result.keys()) as in _get_domains()?

@tehrengruber tehrengruber 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.

I would just use OrderedSet which we have as a requirement anyway and is a drop-in replacement for set.

OrderedSet is already a requirement and is a drop-in replacement for set, so it
says what is meant more directly than threading order through a dict. Returning
the OrderedSet also lets `_collect_offset_or_axis_node` hand back an iterable
rather than a materialized list, matching `_get_domains`.
@havogt

havogt commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

Switched all three sites to OrderedSet — it is a genuine drop-in (add / remove /
discard / update all behave), and it states the intent far more directly than
threading order through a dict. The diff is now setOrderedSet and .to_set()
.to_list(), which reads as the minimal change it should have been.

@edopao on returning an iterable: _collect_offset_or_axis_node now returns the
OrderedSet itself rather than a materialized list, which is the OrderedSet
analogue of your result.keys() suggestion and keeps it symmetric with _get_domains.

Verified after the rewrite: the two regression tests still fail against main and pass
with the fix, and pre-commit (ruff, mypy, tach) is clean.

@havogt

havogt commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

Comments tightened to a single one on _get_domains, noting that it covers the other
collectors too, and no longer mentioning PYTHONHASHSEED. With OrderedSet the intent
is legible from the type, so the other two sites are now bare.

On whether this over-orders: I checked, and all three do reach the generated code.

site order flows to
_get_domains offset_definitions insertion order → dimension tag declarations
_collect_offset_definitions the same dict → the same declarations
_collect_offset_or_axis_node connectivities.append(...) → the emitted connectivities list

Worth being explicit about the evidence, though: only _get_domains is backed by
observed diffs. The other two are the same defect on inputs the icon4py dycore /
diffusion workload does not reach — _collect_offset_definitions needs several
cartesian dimensions (unstructured only ever inserts the vertical one there), and
_collect_offset_or_axis_node needs a stencil with more than one connectivity offset.

So: correct as far as I can tell, but two thirds of it is reasoned rather than measured.
Happy to split those two out if you would rather they came with their own evidence.

havogt added 2 commits August 13, 2026 09:41
An ordering assertion only fails when the unordered iteration happens to differ
from the expected order, so it is flaky by construction and gives little
confidence in exchange.
@havogt

havogt commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

Dropped the tests. An ordering assertion only fails when the unordered iteration happens
to differ from the expected order, so it is flaky by construction and buys little
confidence — the same reason #2780 ships without one.

The PR is now source-only: setOrderedSet at the three collectors, one comment.

@havogt
havogt marked this pull request as ready for review August 13, 2026 07:44
@havogt
havogt requested a review from edopao August 13, 2026 07:44
@tehrengruber

Copy link
Copy Markdown
Contributor

The description is not correct:

The build cache is keyed on a fingerprint of the generated source
(strict_fingerprinter(ExtensionSource) in otf/compilation/cache.py), so affected
programs miss the cache on every fresh process.

The translation cache side-steps this.

I'm noticing wrong descriptions quite a bit recently and I fear that this deteriorate human and agentic performance. At least I frequently resort to commit messages (let aside their importance for review).

@havogt

havogt commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

I usually cleanup the description before merge, but I am not sure what's better for review. The details (with maybe partially wrong reasoning, which might highlight also wrong implementation) or strip to the very concise high level description that I would usually write. Happy to discuss. (And with the last sentence you don't know who wrote this. ;) )

@havogt
havogt merged commit 6ec5244 into GridTools:main Aug 13, 2026
24 checks passed
@havogt
havogt deleted the havogt/gtfn-deterministic-tag-order branch August 13, 2026 08:50
havogt added a commit to havogt/gt4py that referenced this pull request Aug 13, 2026
…k pass

`_eliminate()` uses the `FindAccessNodes` result instead of scanning every state.
The pass returns a `set` per state and its iteration order decides in which order
the new AccessNodes are inserted, so the loop sorts by `state.node_id`, see GridTools#2779
and GridTools#2780. `_accesses_region()` uses the shared `maybe_intersecting()` and the
removal of `T` is validated under the GT4Py debug flag rather than DaCe's.

The class docstring described the requirement on `G` as a window between the
definition of `T` and the write back. It has to hold from the definition onwards,
which is also what serves a consumer that reads after the write back; that was
enforced but left to be inferred. It is now stated as the simplification it is.

The comment on the subset size was wrong: `Range.size()` does divide by the step.
The check holds because the source subset equals `Range.from_array()` and so has
unit steps, which the comment now says.
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.

3 participants