Skip to content

fix[next]: Deterministic dace auto optimize#2568

Open
tehrengruber wants to merge 6 commits intomainfrom
dace_toolchain_deterministic
Open

fix[next]: Deterministic dace auto optimize#2568
tehrengruber wants to merge 6 commits intomainfrom
dace_toolchain_deterministic

Conversation

@tehrengruber
Copy link
Copy Markdown
Contributor

@tehrengruber tehrengruber commented Apr 9, 2026

This PR is a first vertical slice to make auto_optimize deterministic. The general idea is to use ordered datastructures everywhere where order matters (e.g. when iterating over elements of a set) and provide an easy to follow workflow to debug and resolve indeterminism (see here). In many places this just means replacing set with OrderedSet from https://github.com/rspeer/ordered-set and using deterministic naming schemes. In addition to the changes here GridTools/dace#17 is needed, which also introduces an id property in all dace nodes. The id property is computed from a deterministic counter incremented whenever a new node is constructed. Together with the reset_node_id_counter context manager each node gets a unique id, which is stable across runs. This was initially meant as a way to order output from networkx algorithms used in dace that return sets, but surprisingly this was not needed for the stencil used to test this PR. Since this can be different for other stencils and since the id value is also very useful to quickly recognize that order has changed this is kept for now.

Open discussion points:

  • Is OrderedSet the right package?
  • How should we test this? Using icon4py or a fuzzer?
  • Guidelines for symbol name generation

The program used to test this is PR:
icon4py @ 0918c3d with model/atmosphere/dycore/tests/dycore/stencil_tests/test_compute_advection_in_vertical_momentum_equation.py::TestFusedVelocityAdvectionStencilVMomentum::test_TestFusedVelocityAdvectionStencilVMomentum[compile_time_domain]

Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

Generally looks okay, there are some improvements possible.

```
TODO: introduce new config var that prints the hash instead of hard-coding it.
- Execute the program in question twice and compare the output.
- Set a conditonal breakpoint in beginning of the `apply` method of the first pass where the SDFG
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.

apply() is something that is specific to the PatternTransformation.
Not all transformations use them (but the majority does).
The most general interface is given by Pass, whcih defines the apply_pass() method.
But as mentioned above, also functions can be transformations.

Comment on lines +47 to +50
with open(file) as f:
data = json.load(f)
sdfg = dace.SDFG.from_json(data)
print_sdfg_hash(sdfg)
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.

There is also dace.SDFG.from_file().

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 looked at this file and it looks okay, although the O(N) deletion is not nice.
One could rework the algorithm to get rid of them, however I would not do this.
There is PR#2531 which updates this file and also tries to avoid in determinism.
While it still uses sets it sorts the nodes ones in a deterministic way (depending on the node order upon input).

Comment on lines -205 to 214
first_map_params = set(first_map.params)
second_map_params = set(second_map.params)
# TODO(tehrengruber): The structure here looks a little funky. We just use an ordered set for
# now, but likely no sets are needed at all.
first_map_params = OrderedSet(first_map.params)
second_map_params = OrderedSet(second_map.params)
if first_map_params != second_map_params:
return None
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.

sets are needed here, at least for the check that follows because a Map with parameters ["i", "j"] is the same as one with parameters ["j", "i"].
However, you can do something like:

first_map_params = sorted(first_map.params)
second_map_params = sorted(second_map.params)

then you can also remove the sorted() calls bellow as well.

# Now we will reroute the edges went through the inner map, through the
# inner access node instead.
for old_inner_edge in list(
for old_inner_edge in list( # TODO(tehrengruber): Why all these list comprehensions everywhere?
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.

It is needed because you replace the edges (removing the old and adding a new one).
Furthermore, the by_connector() gives you back an iterator thus modyfing the edges will change your iteration source, which leads to a race condition.


- Enable printing each transformation step, e.g. using
```
dace.Config.set("progress", value=True)
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.

This will only give you like ~95% of the cases, as transformation can also run through other means that the patter matcher or can be simple functions that do things.

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.

In order to avoid editing the code and adding this line, you can export an environment variable:
export DACE_progress=1

(note the the upper/lower case unfortunately matters)


sdfg.apply_transformations_repeated(
transformation(
ignore_upstream_blocks=False,
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.

This should be True for the correct working.

sdfg=copy.deepcopy(node.sdfg),
inputs=set(node.in_connectors.keys()),
outputs=set(node.out_connectors.keys()),
# TODO(tehrengruber): What is the performance optimization from Philip about?
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 do you mean with my performance optimization?
What cold be a problem is the copying node.sdfg might also copy the surrounding SDFG, because nested SDFGs have a reference to their parent SDFG.

Comment on lines +85 to +86
inputs={k: None for k in node.in_connectors.keys()},
outputs={k: None for k in node.out_connectors.keys()},
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.

This should be equivelent since the data types should not change.

Suggested change
inputs={k: None for k in node.in_connectors.keys()},
outputs={k: None for k in node.out_connectors.keys()},
inputs=node.in_connectors.copy().
outputs=node.out_connectors.copy(),

It you want to play it safe ignore this suggestion.

**kwargs: Any,
) -> dace.SDFG:
with gtx_wfdcommon.dace_context(device_type=self.device_type):
with gtx_wfdcommon.dace_context(device_type=self.device_type), dace.sdfg.nodes.reset_node_id_counter():
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 kind of understand why it is used here, although I do not like it.

# NOTE: Each thread maintains its own set of configuration, i.e. `dace.Config` is
# a thread local variable. This means it is safe to set values that are different
# for each thread.
dace.Config.set("progress", value=True)
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.

This is not meant to be merged, right?


- Enable printing each transformation step, e.g. using
```
dace.Config.set("progress", value=True)
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.

In order to avoid editing the code and adding this line, you can export an environment variable:
export DACE_progress=1

(note the the upper/lower case unfortunately matters)

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