Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion ci/cscs-ci-dace-determinism.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ build_cscs_amd_rocm:
USE_MPI: 0 # TODO(havogt): to workaround the libfabric hook injecting incompatible libraries
SLURM_JOB_NUM_NODES: 1
SLURM_TIMELIMIT: 60
allow_failure: true
artifacts:
when: always
paths:
Expand Down
95 changes: 95 additions & 0 deletions docs/development/dace_codegen_reproducability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Debugging indeterministic behavior of dace transformations

- 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)

```
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.

hash changes with condition `sdfg.hash_sdfg() == <last equal hash>`.
Note: In case running the previous passes takes a long time it makes sense to serialize the SDFG
to json (`sdfg.to_json("sdfg(1|2).json")`) and loading it again (see debug script below) to
ease debugging. In rare cases the serializing and deserializing the sdfg changes the hash. In such
cases this trick doesn't work and the first location where the hash changes might not be the exact
location where the indeterministic behavior is. It helps to use a different hash, e.g.
`content_hash`, but this should be solved in general.
Note: It makes sense to also place a breakpoint after `DaceTranslator.generate_sdfg` to recognize
when all executions finished.
- When the location is found it is usually easy to spot the origin of the indeterminism. Often
there is a set operation or a symbol is named in an indeterministic way. Use ordered sets and
deterministic symbol names.

## Appendix

__Debugging sdfg autooptimize__

Usage `python debug_auto_optimize_sdfg.py sdfg1.json`

```python
import pickle
import sys

import dace
import json

from dace import SDFG

from gt4py.next.program_processors.runners.dace import (
lowering as gtx_dace_lowering,
sdfg_args as gtx_dace_args,
transformations as gtx_transformations,
)
from dace.utils import print_sdfg_hash

file = sys.argv[1]

with open(file) as f:
data = json.load(f)
sdfg = dace.SDFG.from_json(data)
print_sdfg_hash(sdfg)
Comment on lines +47 to +50

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().


gtx_transformations.gt_auto_optimize(
sdfg,
gpu=False,
constant_symbols={},
unit_strides_kind=None,
)
```

__Debugging single sdfg transform__

Usage `python debug_single_sdfg_transform.py sdfg1.json`

```python
import pickle
import sys

import dace
import json

from dace import SDFG

from gt4py.next.program_processors.runners.dace import (
lowering as gtx_dace_lowering,
sdfg_args as gtx_dace_args,
transformations as gtx_transformations,
)
from dace.utils import print_sdfg_hash

transformation = gtx_transformations.MoveDataflowIntoIfBody
file = sys.argv[1]

with open(file) as f:
data = json.load(f)
sdfg = dace.SDFG.from_json(data)
print_sdfg_hash(sdfg)

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.

),
validate=False,
validate_all=True,
)
```
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ dependencies = [
'toolz>=0.12.1',
'typing-extensions>=4.12.0',
'versioningit>=3.1.1',
'xxhash>=3.5.0'
'xxhash>=3.5.0',
'ordered-set>=4.0.0'
]
description = 'Python library for generating high-performance implementations of stencil kernels for weather and climate modeling from a domain-specific language (DSL)'
dynamic = ['version']
Expand Down Expand Up @@ -479,6 +480,7 @@ url = 'https://gridtools.github.io/pypi/'
# dace = {index = "gridtools"}
[tool.uv.sources]
atlas4py = {index = "test.pypi"}
dace = {git = "https://github.com/GridTools/dace", branch = "dace_toolchain_deterministic"}

# -- versioningit --
[tool.versioningit]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from dace.transformation import dataflow as dace_dataflow
from dace.transformation.auto import auto_optimize as dace_aoptimize
from dace.transformation.passes import analysis as dace_analysis
from dace.utils import print_sdfg_hash

from gt4py.next import common as gtx_common, utils as gtx_utils
from gt4py.next.program_processors.runners.dace import (
Expand Down Expand Up @@ -254,12 +255,14 @@ def gt_auto_optimize(
# Initial Cleanup
# NOTE: The initial simplification stage must be synchronized with the one that
# `gt_substitute_compiletime_symbols()` performs!
print_sdfg_hash(sdfg)
gtx_transformations.gt_simplify(
sdfg=sdfg,
validate=False,
skip=gtx_transformations.constants._GT_AUTO_OPT_INITIAL_STEP_SIMPLIFY_SKIP_LIST,
validate_all=validate_all,
)
print_sdfg_hash(sdfg)

if constant_symbols:
gtx_transformations.gt_substitute_compiletime_symbols(
Expand All @@ -271,6 +274,7 @@ def gt_auto_optimize(
validate=False,
validate_all=validate_all,
)
print_sdfg_hash(sdfg)

# Demote the fields.
# Actually they should probably be at the very start of this function, however,
Expand Down Expand Up @@ -304,10 +308,12 @@ def gt_auto_optimize(
skip=gtx_transformations.constants._GT_AUTO_OPT_INITIAL_STEP_SIMPLIFY_SKIP_LIST,
validate_all=validate_all,
)
print_sdfg_hash(sdfg)

gtx_transformations.gt_reduce_distributed_buffering(
sdfg, validate=False, validate_all=validate_all
)
print_sdfg_hash(sdfg)

# Process top level Maps
sdfg = _gt_auto_process_top_level_maps(
Expand All @@ -317,12 +323,14 @@ def gt_auto_optimize(
optimization_hooks=optimization_hooks,
validate_all=validate_all,
)
print_sdfg_hash(sdfg)

# We now ensure that point wise computations are properly double buffered,
# this ensures that rule 3 of ADR-18 is maintained.
# TODO(phimuell): Figuring out if it is important to do it before the inner
# Map optimization. I think it is, especially when we apply `LoopBlocking`.
gtx_transformations.gt_create_local_double_buffering(sdfg)
print_sdfg_hash(sdfg)

# Optimize the interior of the Maps:
sdfg = _gt_auto_process_dataflow_inside_maps(
Expand All @@ -336,6 +344,7 @@ def gt_auto_optimize(
validate_all=validate_all,
uids=uids,
)
print_sdfg_hash(sdfg)

# Configure the Maps:
# Will also perform the GPU transformation.
Expand Down Expand Up @@ -395,6 +404,7 @@ def gt_auto_optimize(
gpu_block_size_spec=gpu_block_size_spec if gpu_block_size_spec else None,
validate_all=validate_all,
)
print_sdfg_hash(sdfg)

# Transients
sdfg = _gt_auto_post_processing(
Expand All @@ -409,6 +419,7 @@ def gt_auto_optimize(
gpu_memory_pool=gpu_memory_pool,
validate_all=validate_all,
)
print_sdfg_hash(sdfg)

# Canonicalize the SDFG. This ensures that the code generator will see SDFGs
# that conform to the historical expected version.
Expand Down

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).

Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,7 @@ def _check_for_data_and_symbol_conflicts(
self,
sdfg: dace.SDFG,
state: dace.SDFGState,
relocatable_dataflow: set[dace_nodes.Node],
relocatable_dataflow: OrderedSet[dace_nodes.Node],
if_block: dace_nodes.NestedSDFG,
enclosing_map: dace_nodes.MapEntry,
) -> bool:
Expand Down Expand Up @@ -724,7 +724,7 @@ def _filter_relocatable_dataflow(
non_relocatable_dataflow: dict[str, OrderedSet[dace_nodes.Node]],
connector_usage_location: dict[str, tuple[dace.SDFGState, dace_nodes.AccessNode]],
enclosing_map: dace_nodes.MapEntry,
) -> set[dace_nodes.Node]:
) -> OrderedSet[dace_nodes.Node]:
"""Compute the final set of the relocatable nodes.

The function expects the dataflow that is upstream of every connector
Expand All @@ -749,8 +749,8 @@ def _filter_relocatable_dataflow(
"""

# These are the nodes that can not be relocated anyway.
all_non_relocatable_dataflow: set[dace_nodes.Node] = functools.reduce(
lambda s1, s2: s1.union(s2), non_relocatable_dataflow.values(), set()
all_non_relocatable_dataflow: OrderedSet[dace_nodes.Node] = functools.reduce(
lambda s1, s2: s1.union(s2), non_relocatable_dataflow.values(), OrderedSet()
)

# While we can relocate nodes that are needed by multiple connectors, we can
Expand Down Expand Up @@ -782,8 +782,8 @@ def _filter_relocatable_dataflow(
# process all of them together. We do this because a node can be associated to
# multiple connectors and as such data dependencies can show up. We will,
# after the filtering distribute them back.
nodes_proposed_for_reloc: set[dace_nodes.Node] = functools.reduce(
lambda s1, s2: s1.union(s2), raw_relocatable_dataflow.values(), set()
nodes_proposed_for_reloc: OrderedSet[dace_nodes.Node] = functools.reduce(
lambda s1, s2: s1.union(s2), raw_relocatable_dataflow.values(), OrderedSet()
)

# Filtering out all nodes that can not be relocated anyway.
Expand Down Expand Up @@ -874,8 +874,8 @@ def _partition_if_block(
if len(if_block.out_connectors.keys()) == 0:
return None

input_names: set[str] = set(if_block.in_connectors.keys())
output_names: set[str] = set(if_block.out_connectors.keys())
input_names: OrderedSet[str] = OrderedSet(if_block.in_connectors.keys())
output_names: OrderedSet[str] = OrderedSet(if_block.out_connectors.keys())

# If data is used as input and output we ignore it.
# TODO(phimuell): Think if this case can be handled.
Expand All @@ -895,7 +895,7 @@ def _partition_if_block(
connector_usage_location: dict[str, tuple[dace.SDFGState, dace_nodes.AccessNode]] = {}

# This is the dataflow that can not be relocated.
non_relocatable_connectors: set[str] = set()
non_relocatable_connectors: OrderedSet[str] = OrderedSet()

# Now inspect all states.
for _, if_branch in inner_if_block.branches:
Expand Down Expand Up @@ -943,7 +943,7 @@ def _partition_if_block(
# In addition to the non relocatable connectors that were found above, we also
# mark all connectors that were not found as non relocatable.
non_relocatable_connectors.update(
conn for conn in input_names if conn not in connector_usage_location
[conn for conn in input_names if conn not in connector_usage_location]
)

# We require that at least one non relocatable dataflow is there, this is for
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1036,7 +1036,9 @@ def apply(

# 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?
graph.out_edges_by_connector(map_entry, "OUT_" + connector_name)
):
# We now modify the downstream data. This is because we no longer refer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ def split_node(
# NOTE: Turning them into a string is the best solution is probably the only way
# to achieve some stability. The only downside is that the order now depends
# on the specialization level that is used, i.e. if we have numbers or symbols.
# TODO(tehrengruber): Is this still needed?

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.

If split_description is stable we can remove the sort.

split_description = sorted(split_description, key=lambda split: str(split))

desc_to_split = node_to_split.desc(sdfg)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def set_dace_config(
# 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?


# We rely on dace cache to avoid recompiling the SDFG.
# Note that the workflow step with the persistent `FileCache` store
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from __future__ import annotations

import dataclasses
import json
from typing import Any, Optional

import dace
Expand All @@ -29,6 +30,35 @@
from gt4py.next.type_system import type_specifications as ts


def remove_guid(data: Any) -> Any:
"""
Recursively traverse a dict and remove all keys named 'guid'.

Args:
data: A dictionary, list, or other data structure to traverse

Returns:
The data structure with all 'guid' keys removed
"""
if isinstance(data, dict):
return {key: remove_guid(value) for key, value in data.items() if key != "guid"}
elif isinstance(data, list):
return [remove_guid(item) for item in data]
elif isinstance(data, tuple):
return tuple(remove_guid(item) for item in data)
elif isinstance(data, (str, int, float, bool)) or data is None:
return data
else:
raise RuntimeError("Unsupported data type")


def remove_guid_and_save_to_file(sdfg: dace.SDFG, filename: str) -> None:
cleaned_data = remove_guid(sdfg.to_json())

with open(filename, "w") as f:
json.dump(cleaned_data, f, indent=2)


def find_constant_symbols(
ir: itir.Program,
sdfg: dace.SDFG,
Expand Down Expand Up @@ -364,7 +394,10 @@ def generate_sdfg(
*args: Any,
**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(),
):
return self._generate_sdfg_without_configuring_dace(*args, **kwargs)

def _generate_sdfg_without_configuring_dace(
Expand Down
7 changes: 4 additions & 3 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading