From 605c774973fa0a2974e96817be74c25ed4d4e752 Mon Sep 17 00:00:00 2001 From: Till Ehrengruber Date: Mon, 20 Jul 2026 11:44:55 +0200 Subject: [PATCH 1/5] Use ordered sets --- dace/sdfg/graph.py | 3 +- dace/sdfg/scope.py | 7 +- dace/sdfg/state.py | 6 +- .../dataflow/map_fusion_vertical.py | 17 +- .../passes/analysis/analysis.py | 158 ++++++++++-------- .../passes/array_elimination.py | 3 +- requirements.txt | 1 + setup.py | 2 +- 8 files changed, 108 insertions(+), 89 deletions(-) diff --git a/dace/sdfg/graph.py b/dace/sdfg/graph.py index f107e2caac..1af788c74b 100644 --- a/dace/sdfg/graph.py +++ b/dace/sdfg/graph.py @@ -8,6 +8,7 @@ from dace.dtypes import deduplicate import dace.serialize from typing import Any, Callable, Generic, Iterable, List, Optional, Sequence, TypeVar, Union +from ordered_set import OrderedSet class NodeNotFoundError(Exception): @@ -215,7 +216,7 @@ def __getitem__(self, node: NodeT) -> Iterable[NodeT]: def all_edges(self, *nodes: NodeT) -> Iterable[Edge[EdgeT]]: """Returns an iterable to incoming and outgoing Edge objects.""" - result = set() + result = OrderedSet() for node in nodes: result.update(self.in_edges(node)) result.update(self.out_edges(node)) diff --git a/dace/sdfg/scope.py b/dace/sdfg/scope.py index cd139aaa17..f5a397d96b 100644 --- a/dace/sdfg/scope.py +++ b/dace/sdfg/scope.py @@ -8,6 +8,7 @@ from dace.config import Config from dace.sdfg import nodes as nd from dace.sdfg.state import StateSubgraphView +from ordered_set import OrderedSet ScopeDictType = Dict[nd.Node, List[nd.Node]] @@ -62,16 +63,16 @@ def _scope_subgraph(graph, entry_node, include_entry, include_exit) -> ScopeSubg raise TypeError("Received {}: should be dace.nodes.EntryNode".format(type(entry_node).__name__)) node_to_children = graph.scope_children() if include_exit: - children_nodes = set(node_to_children[entry_node]) + children_nodes = OrderedSet(node_to_children[entry_node]) else: - children_nodes = set(n for n in node_to_children[entry_node] if not isinstance(n, nd.ExitNode)) + children_nodes = OrderedSet(n for n in node_to_children[entry_node] if not isinstance(n, nd.ExitNode)) map_nodes = [node for node in children_nodes if isinstance(node, nd.EntryNode)] while len(map_nodes) > 0: next_map_nodes = [] # Traverse children map nodes for map_node in map_nodes: # Get child map subgraph (1 level) - more_nodes = set(node_to_children[map_node]) + more_nodes = OrderedSet(node_to_children[map_node]) # Unionize children_nodes with new nodes children_nodes |= more_nodes # Add nodes of the next level to next_map_nodes diff --git a/dace/sdfg/state.py b/dace/sdfg/state.py index f9b288b3ae..efb5253fdd 100644 --- a/dace/sdfg/state.py +++ b/dace/sdfg/state.py @@ -1791,9 +1791,11 @@ def add_nested_sdfg( sdfg.update_cfg_list([]) # Make dictionary of autodetect connector types from set - if isinstance(inputs, (set, collections.abc.KeysView)): + if isinstance(inputs, set) or isinstance(outputs, set): + warnings.warn("Using sets as inputs is discouraged as it leads to indeterministic behavior.") + if isinstance(inputs, (set, collections.abc.KeysView, collections.abc.Set)): inputs = {k: None for k in inputs} - if isinstance(outputs, (set, collections.abc.KeysView)): + if isinstance(outputs, (set, collections.abc.KeysView, collections.abc.Set)): outputs = {k: None for k in outputs} s = nd.NestedSDFG( diff --git a/dace/transformation/dataflow/map_fusion_vertical.py b/dace/transformation/dataflow/map_fusion_vertical.py index 89883425cb..92b0b109d5 100644 --- a/dace/transformation/dataflow/map_fusion_vertical.py +++ b/dace/transformation/dataflow/map_fusion_vertical.py @@ -8,6 +8,7 @@ from dace.sdfg import SDFG, SDFGState, graph, nodes, propagation from dace.transformation.dataflow import map_fusion_helper as mfhelper from dace.sdfg.type_inference import infer_expr_type +from ordered_set import OrderedSet @properties.make_properties @@ -405,9 +406,9 @@ def partition_first_outputs( param_repl: Dict[str, str], ) -> Union[ Tuple[ - Set[graph.MultiConnectorEdge[dace.Memlet]], - Set[graph.MultiConnectorEdge[dace.Memlet]], - Set[graph.MultiConnectorEdge[dace.Memlet]], + OrderedSet[graph.MultiConnectorEdge[dace.Memlet]], + OrderedSet[graph.MultiConnectorEdge[dace.Memlet]], + OrderedSet[graph.MultiConnectorEdge[dace.Memlet]], ], None, ]: @@ -447,9 +448,9 @@ def partition_first_outputs( `require_all_intermediates` and by `self.require_exclusive_intermediates`. """ # The three outputs set. - pure_outputs: Set[graph.MultiConnectorEdge[dace.Memlet]] = set() - exclusive_outputs: Set[graph.MultiConnectorEdge[dace.Memlet]] = set() - shared_outputs: Set[graph.MultiConnectorEdge[dace.Memlet]] = set() + pure_outputs: Set[graph.MultiConnectorEdge[dace.Memlet]] = OrderedSet() + exclusive_outputs: Set[graph.MultiConnectorEdge[dace.Memlet]] = OrderedSet() + shared_outputs: Set[graph.MultiConnectorEdge[dace.Memlet]] = OrderedSet() # Set of intermediate nodes that we have already processed. processed_inter_nodes: Set[nodes.Node] = set() @@ -703,7 +704,7 @@ def partition_first_outputs( def handle_intermediate_set( self, - intermediate_outputs: Set[graph.MultiConnectorEdge[dace.Memlet]], + intermediate_outputs: OrderedSet[graph.MultiConnectorEdge[dace.Memlet]], state: dace.SDFGState, sdfg: SDFG, first_map_exit: nodes.MapExit, @@ -870,7 +871,7 @@ def handle_intermediate_set( # the input connectors on the MapEntry, such that we know where we # have to reroute inside the Map. # NOTE: Assumes that Map (if connected is the direct neighbour). - conn_names: Set[str] = set() + conn_names: OrderedSet[str] = OrderedSet() for inter_node_out_edge in state.out_edges(inter_node): if inter_node_out_edge.dst == second_map_entry: assert inter_node_out_edge.dst_conn.startswith("IN_") diff --git a/dace/transformation/passes/analysis/analysis.py b/dace/transformation/passes/analysis/analysis.py index 5fb0acaa93..6518673591 100644 --- a/dace/transformation/passes/analysis/analysis.py +++ b/dace/transformation/passes/analysis/analysis.py @@ -16,6 +16,7 @@ from typing import Dict, Iterable, List, Set, Tuple, Any, Optional, Union import networkx as nx from networkx.algorithms import shortest_paths as nxsp +from ordered_set import OrderedSet from dace.transformation.passes.analysis import loop_analysis @@ -43,7 +44,7 @@ def should_reapply(self, modified: ppl.Modifies) -> bool: def depends_on(self): return [ControlFlowBlockReachability] - def apply_pass(self, top_sdfg: SDFG, pipeline_res: Dict) -> Dict[int, Dict[SDFGState, Set[SDFGState]]]: + def apply_pass(self, top_sdfg: SDFG, pipeline_res: Dict) -> Dict[int, Dict[SDFGState, OrderedSet[SDFGState]]]: """ :return: A dictionary mapping each state to its other reachable states. """ @@ -52,9 +53,9 @@ def apply_pass(self, top_sdfg: SDFG, pipeline_res: Dict) -> Dict[int, Dict[SDFGS cf_block_reach_dict = ControlFlowBlockReachability().apply_pass(top_sdfg, {}) else: cf_block_reach_dict = pipeline_res[ControlFlowBlockReachability.__name__] - reachable: Dict[int, Dict[SDFGState, Set[SDFGState]]] = {} + reachable: Dict[int, Dict[SDFGState, OrderedSet[SDFGState]]] = {} for sdfg in top_sdfg.all_sdfgs_recursive(): - result: Dict[SDFGState, Set[SDFGState]] = defaultdict(set) + result: Dict[SDFGState, OrderedSet[SDFGState]] = defaultdict(OrderedSet) for state in sdfg.states(): for reached in cf_block_reach_dict[state.parent_graph.cfg_id][state]: if isinstance(reached, SDFGState): @@ -88,10 +89,10 @@ def should_reapply(self, modified: ppl.Modifies) -> bool: def _region_closure( self, region: ControlFlowRegion, - block_reach: Dict[int, Dict[ControlFlowBlock, Set[ControlFlowBlock]]], - cached_closures: dict[int, Set[ControlFlowBlock]], + block_reach: Dict[int, Dict[ControlFlowBlock, OrderedSet[ControlFlowBlock]]], + cached_closures: dict[int, OrderedSet[ControlFlowBlock]], ) -> Set[ControlFlowBlock]: - closure: Set[ControlFlowBlock] = set() + closure: Set[SDFGState] = OrderedSet() if isinstance(region, LoopRegion): # Any point inside the loop may reach any other point inside the loop again. # TODO(later): This is an overapproximation. A branch terminating in a break is excluded from this. @@ -114,7 +115,7 @@ def _region_closure( pivot = pivot.parent_graph return closure - def apply_pass(self, top_sdfg: SDFG, _) -> Dict[int, Dict[ControlFlowBlock, Set[ControlFlowBlock]]]: + def apply_pass(self, top_sdfg: SDFG, _) -> Dict[int, Dict[ControlFlowBlock, OrderedSet[ControlFlowBlock]]]: """ :return: For each control flow region, a dictionary mapping each control flow block to its other reachable control flow blocks. @@ -122,13 +123,13 @@ def apply_pass(self, top_sdfg: SDFG, _) -> Dict[int, Dict[ControlFlowBlock, Set[ top_sdfg.reset_cfg_list() single_level_reachable: Dict[int, Dict[ControlFlowBlock, - Set[ControlFlowBlock]]] = defaultdict(lambda: defaultdict(set)) + OrderedSet[ControlFlowBlock]]] = defaultdict(lambda: defaultdict(set)) for cfg in top_sdfg.all_control_flow_regions(recursive=True): # In networkx this is currently implemented naively for directed graphs. # The implementation below is faster # tc: nx.DiGraph = nx.transitive_closure(sdfg.nx) for n, v in reachable_nodes(cfg.nx): - reach = set() + reach = OrderedSet() for nd in v: reach.add(nd) if isinstance(nd, AbstractControlFlowRegion): @@ -140,11 +141,11 @@ def apply_pass(self, top_sdfg: SDFG, _) -> Dict[int, Dict[ControlFlowBlock, Set[ if self.contain_to_single_level: return single_level_reachable - reachable: Dict[int, Dict[ControlFlowBlock, Set[ControlFlowBlock]]] = {} - cached_closures: dict[int, Set[ControlFlowBlock]] = {} + reachable: Dict[int, Dict[ControlFlowBlock, OrderedSet[ControlFlowBlock]]] = {} + cached_closures: dict[int, OrderedSet[ControlFlowBlock]] = {} for sdfg in top_sdfg.all_sdfgs_recursive(): for cfg in sdfg.all_control_flow_regions(): - result: Dict[ControlFlowBlock, Set[ControlFlowBlock]] = defaultdict(set) + result: Dict[ControlFlowBlock, OrderedSet[ControlFlowBlock]] = defaultdict(OrderedSet) for block in cfg.nodes(): for reached in single_level_reachable[block.parent_graph.cfg_id][block]: if isinstance(reached, AbstractControlFlowRegion): @@ -179,11 +180,11 @@ def _single_shortest_path_length_no_self(adj, source): seen = {} # level (number of hops) when seen in BFS level = 0 # the current level - nextlevel = set(firstlevel) # set of nodes to check at next level + nextlevel = OrderedSet(firstlevel) # set of nodes to check at next level n = len(adj) while nextlevel: thislevel = nextlevel # advance to next level - nextlevel = set() # and start a new set (fringe) + nextlevel = OrderedSet() # and start a new set (fringe) found = [] for v in thislevel: if v not in seen: @@ -225,9 +226,9 @@ def should_reapply(self, modified: ppl.Modifies) -> bool: return modified & ppl.Modifies.States | ppl.Modifies.Edges | ppl.Modifies.Symbols | ppl.Modifies.Nodes def apply(self, region: ControlFlowRegion, - _) -> Dict[Union[ControlFlowBlock, Edge[InterstateEdge]], Tuple[Set[str], Set[str]]]: + _) -> Dict[Union[ControlFlowBlock, Edge[InterstateEdge]], Tuple[OrderedSet[str], OrderedSet[str]]]: adesc = set(region.sdfg.arrays.keys()) - result: Dict[ControlFlowBlock, Tuple[Set[str], Set[str]]] = {} + result: Dict[ControlFlowBlock, Tuple[OrderedSet[str], OrderedSet[str]]] = {} for block in region.nodes(): # No symbols may be written to inside blocks. result[block] = (block.free_symbols, set()) @@ -254,7 +255,7 @@ def should_reapply(self, modified: ppl.Modifies) -> bool: # If access nodes were modified, reapply return modified & ppl.Modifies.AccessNodes - def _get_loop_region_readset(self, loop: LoopRegion, arrays: Set[str]) -> Set[str]: + def _get_loop_region_readset(self, loop: LoopRegion, arrays: OrderedSet[str]) -> OrderedSet[str]: readset = set() exprs = {loop.loop_condition.as_string} update_stmt = loop_analysis.get_update_assignment(loop) @@ -267,15 +268,15 @@ def _get_loop_region_readset(self, loop: LoopRegion, arrays: Set[str]) -> Set[st readset |= (symbolic.free_symbols_and_functions(expr) | symbolic.arrays(expr)) & arrays return readset - def apply_pass(self, top_sdfg: SDFG, _) -> Dict[ControlFlowBlock, Tuple[Set[str], Set[str]]]: + def apply_pass(self, top_sdfg: SDFG, _) -> Dict[ControlFlowBlock, Tuple[OrderedSet[str], OrderedSet[str]]]: """ :return: A dictionary mapping each control flow block to a tuple of its (read, written) data descriptors. """ - result: Dict[ControlFlowBlock, Tuple[Set[str], Set[str]]] = {} + result: Dict[ControlFlowBlock, Tuple[OrderedSet[str], OrderedSet[str]]] = {} for sdfg in top_sdfg.all_sdfgs_recursive(): - arrays: Set[str] = set(sdfg.arrays.keys()) + arrays: OrderedSet[str] = OrderedSet(sdfg.arrays.keys()) for block in sdfg.all_control_flow_blocks(): - readset, writeset = set(), set() + readset, writeset = OrderedSet(), OrderedSet() if isinstance(block, SDFGState): for anode in block.data_nodes(): if block.in_degree(anode) > 0: @@ -302,7 +303,7 @@ def apply_pass(self, top_sdfg: SDFG, _) -> Dict[ControlFlowBlock, Tuple[Set[str] # Edges that read from arrays add to both ends' access sets anames = sdfg.arrays.keys() for e in sdfg.all_interstate_edges(): - fsyms = e.data.free_symbols & anames + fsyms = sorted(e.data.free_symbols & anames) if fsyms: result[e.src][0].update(fsyms) result[e.dst][0].update(fsyms) @@ -325,14 +326,14 @@ def should_reapply(self, modified: ppl.Modifies) -> bool: # If anything was modified, reapply return modified & ppl.Modifies.AccessNodes - def apply_pass(self, top_sdfg: SDFG, _) -> Dict[int, Dict[str, Set[SDFGState]]]: + def apply_pass(self, top_sdfg: SDFG, _) -> Dict[int, Dict[str, OrderedSet[SDFGState]]]: """ :return: A dictionary mapping each data descriptor name to states where it can be found in. """ - top_result: Dict[int, Dict[str, Set[SDFGState]]] = {} + top_result: Dict[int, Dict[str, OrderedSet[SDFGState]]] = {} for sdfg in top_sdfg.all_sdfgs_recursive(): - result: Dict[str, Set[SDFGState]] = defaultdict(set) + result: Dict[str, OrderedSet[SDFGState]] = defaultdict(OrderedSet) for state in sdfg.states(): for anode in state.data_nodes(): result[anode.data].add(state) @@ -340,9 +341,9 @@ def apply_pass(self, top_sdfg: SDFG, _) -> Dict[int, Dict[str, Set[SDFGState]]]: # Edges that read from arrays add to both ends' access sets anames = sdfg.arrays.keys() for e in sdfg.all_interstate_edges(): - fsyms = e.data.free_symbols & anames + fsyms = sorted(e.data.free_symbols & anames) for access in fsyms: - result[access].update({e.src, e.dst}) + result[access].update((e.src, e.dst)) top_result[sdfg.cfg_id] = result return top_result @@ -374,18 +375,18 @@ def should_reapply(self, modified: ppl.Modifies) -> bool: # If anything was modified, reapply return modified & ppl.Modifies.AccessNodes & ppl.Modifies.CFG - def apply_pass(self, sdfg: SDFG, _) -> Dict[SDFG, Set[str]]: + def apply_pass(self, sdfg: SDFG, _) -> Dict[SDFG, OrderedSet[str]]: """ :return: A dictionary mapping SDFGs to a `set` of strings containing the name of the data descriptors that are only used once. """ # TODO(pschaad): Should we index on cfg or the SDFG itself. - exclusive_data: Dict[SDFG, Set[str]] = {} + exclusive_data: Dict[SDFG, OrderedSet[str]] = {} for nsdfg in sdfg.all_sdfgs_recursive(): exclusive_data[nsdfg] = self._find_single_use_data_in_sdfg(nsdfg) return exclusive_data - def _find_single_use_data_in_sdfg(self, sdfg: SDFG) -> Set[str]: + def _find_single_use_data_in_sdfg(self, sdfg: SDFG) -> OrderedSet[str]: """Scans an SDFG and computes the data that is only used once in the SDFG. The rules used to classify data descriptors are outlined above. The function @@ -396,8 +397,8 @@ def _find_single_use_data_in_sdfg(self, sdfg: SDFG) -> Set[str]: # If we encounter a data descriptor for the first time we immediately # classify it as single use. We will undo this decision as soon as # learn that it is used somewhere else. - single_use_data: Set[str] = set() - previously_seen: Set[str] = set() + single_use_data: OrderedSet[str] = OrderedSet() + previously_seen: OrderedSet[str] = OrderedSet() for state in sdfg.states(): for dnode in state.data_nodes(): @@ -437,17 +438,19 @@ def modifies(self) -> ppl.Modifies: def should_reapply(self, modified: ppl.Modifies) -> bool: return modified & ppl.Modifies.AccessNodes - def apply_pass(self, top_sdfg: SDFG, - _) -> Dict[int, Dict[str, Dict[SDFGState, Tuple[Set[nd.AccessNode], Set[nd.AccessNode]]]]]: + def apply_pass( + self, top_sdfg: SDFG, + _) -> Dict[int, Dict[str, Dict[SDFGState, Tuple[OrderedSet[nd.AccessNode], OrderedSet[nd.AccessNode]]]]]: """ :return: A dictionary mapping each data descriptor name to a dictionary keyed by states with all access nodes that use that data descriptor. """ - top_result: Dict[int, Dict[str, Set[nd.AccessNode]]] = dict() + top_result: Dict[int, Dict[str, OrderedSet[nd.AccessNode]]] = dict() for sdfg in top_sdfg.all_sdfgs_recursive(): - result: Dict[str, Dict[SDFGState, Tuple[Set[nd.AccessNode], Set[nd.AccessNode]]]] = defaultdict( - lambda: defaultdict(lambda: [set(), set()])) + result: Dict[str, Dict[SDFGState, + Tuple[OrderedSet[nd.AccessNode], OrderedSet[nd.AccessNode]]]] = defaultdict( + lambda: defaultdict(lambda: [OrderedSet(), OrderedSet()])) for state in sdfg.states(): for anode in state.data_nodes(): if state.in_degree(anode) > 0: @@ -510,15 +513,16 @@ def _find_dominating_write(self, sym: str, read: Union[ControlFlowBlock, Edge[In return write_isedge def apply(self, region, pipeline_results) -> SymbolScopeDict: - result: SymbolScopeDict = defaultdict(lambda: defaultdict(lambda: set())) + result: SymbolScopeDict = defaultdict(lambda: defaultdict(lambda: OrderedSet())) idom = nx.immediate_dominators(region.nx, region.start_block) all_doms = cfg_analysis.all_dominators(region, idom) - b_reach: Dict[ControlFlowBlock, - Set[ControlFlowBlock]] = pipeline_results[ControlFlowBlockReachability.__name__][region.cfg_id] + b_reach: Dict[ControlFlowBlock, OrderedSet[ControlFlowBlock]] = pipeline_results[ + ControlFlowBlockReachability.__name__][region.cfg_id] symbol_access_sets: Dict[Union[ControlFlowBlock, Edge[InterstateEdge]], - Tuple[Set[str], Set[str]]] = pipeline_results[SymbolAccessSets.__name__][region.cfg_id] + Tuple[OrderedSet[str], + OrderedSet[str]]] = pipeline_results[SymbolAccessSets.__name__][region.cfg_id] for read_loc, (reads, _) in symbol_access_sets.items(): for sym in reads: @@ -552,7 +556,7 @@ def apply(self, region, pipeline_results) -> SymbolScopeDict: other_accesses.update(accesses) other_accesses.add(write) to_remove.add((sym, write)) - result[sym][write] = set() + result[sym][write] = OrderedSet() for sym, write in to_remove: del result[sym][write] @@ -583,9 +587,10 @@ def _find_dominating_write(self, desc: str, block: ControlFlowBlock, read: Union[nd.AccessNode, InterstateEdge], - access_nodes: Dict[SDFGState, Tuple[Set[nd.AccessNode], Set[nd.AccessNode]]], + access_nodes: Dict[SDFGState, Tuple[OrderedSet[nd.AccessNode], + OrderedSet[nd.AccessNode]]], idom_dict: Dict[ControlFlowRegion, Dict[ControlFlowBlock, ControlFlowBlock]], - access_sets: Dict[ControlFlowBlock, Tuple[Set[str], Set[str]]], + access_sets: Dict[ControlFlowBlock, Tuple[OrderedSet[str], OrderedSet[str]]], no_self_shadowing: bool = False) -> Optional[Tuple[SDFGState, nd.AccessNode]]: if isinstance(read, nd.AccessNode): state: SDFGState = block @@ -652,16 +657,18 @@ def apply_pass(self, top_sdfg: SDFG, pipeline_results: Dict[str, Any]) -> Dict[i """ top_result: Dict[int, WriteScopeDict] = dict() - access_sets: Dict[ControlFlowBlock, Tuple[Set[str], Set[str]]] = pipeline_results[AccessSets.__name__] + access_sets: Dict[ControlFlowBlock, Tuple[OrderedSet[str], + OrderedSet[str]]] = pipeline_results[AccessSets.__name__] for sdfg in top_sdfg.all_sdfgs_recursive(): - result: WriteScopeDict = defaultdict(lambda: defaultdict(lambda: set())) + result: WriteScopeDict = defaultdict(lambda: defaultdict(lambda: OrderedSet())) idom_dict: Dict[ControlFlowRegion, Dict[ControlFlowBlock, ControlFlowBlock]] = {} - all_doms_transitive: Dict[ControlFlowBlock, Set[ControlFlowBlock]] = defaultdict(lambda: set()) + all_doms_transitive: Dict[ControlFlowBlock, + OrderedSet[ControlFlowBlock]] = defaultdict(lambda: OrderedSet()) for cfg in sdfg.all_control_flow_regions(): if isinstance(cfg, ConditionalBlock): idom_dict[cfg] = {b: b for _, b in cfg.branches} - all_doms = {b: set([b]) for _, b in cfg.branches} + all_doms = {b: OrderedSet([b]) for _, b in cfg.branches} else: idom_dict[cfg] = nx.immediate_dominators(cfg.nx, cfg.start_block) all_doms = cfg_analysis.all_dominators(cfg, idom_dict[cfg]) @@ -673,15 +680,16 @@ def apply_pass(self, top_sdfg: SDFG, pipeline_results: Dict[str, Any]) -> Dict[i all_doms_transitive[k].add(cfg) all_doms_transitive[k].update(all_doms_transitive[cfg]) - access_nodes: Dict[str, Dict[SDFGState, Tuple[Set[nd.AccessNode], Set[nd.AccessNode]]]] = pipeline_results[ - FindAccessNodes.__name__][sdfg.cfg_id] + access_nodes: Dict[str, Dict[SDFGState, Tuple[OrderedSet[nd.AccessNode], + OrderedSet[nd.AccessNode]]]] = pipeline_results[ + FindAccessNodes.__name__][sdfg.cfg_id] block_reach: Dict[ControlFlowBlock, - Set[ControlFlowBlock]] = pipeline_results[ControlFlowBlockReachability.__name__] + OrderedSet[ControlFlowBlock]] = pipeline_results[ControlFlowBlockReachability.__name__] anames = sdfg.arrays.keys() for desc in sdfg.arrays: - desc_states_with_nodes = set(access_nodes[desc].keys()) + desc_states_with_nodes = OrderedSet(access_nodes[desc].keys()) for state in desc_states_with_nodes: for read_node in access_nodes[desc][state][0]: write = self._find_dominating_write(desc, state, read_node, access_nodes, idom_dict, @@ -713,7 +721,7 @@ def apply_pass(self, top_sdfg: SDFG, pipeline_results: Dict[str, Any]) -> Dict[i # If any write A is dominated by another write B and any reads in B's scope are also reachable by A, # then merge A and its scope into B's scope. - to_remove = set() + to_remove = OrderedSet() for write, accesses in result[desc].items(): if write is None: continue @@ -730,7 +738,7 @@ def apply_pass(self, top_sdfg: SDFG, pipeline_results: Dict[str, Any]) -> Dict[i other_accesses.update(accesses) other_accesses.add(write) to_remove.add(write) - result[desc][write] = set() + result[desc][write] = OrderedSet() for write in to_remove: del result[desc][write] top_result[sdfg.cfg_id] = result @@ -752,14 +760,14 @@ def modifies(self) -> ppl.Modifies: def should_reapply(self, modified: ppl.Modifies) -> bool: return modified & ppl.Modifies.Memlets - def apply_pass(self, top_sdfg: SDFG, _) -> Dict[int, Dict[str, Set[Memlet]]]: + def apply_pass(self, top_sdfg: SDFG, _) -> Dict[int, Dict[str, OrderedSet[Memlet]]]: """ :return: A dictionary mapping each data descriptor name to a set of memlets. """ - top_result: Dict[int, Dict[str, Set[Memlet]]] = dict() + top_result: Dict[int, Dict[str, OrderedSet[Memlet]]] = dict() for sdfg in top_sdfg.all_sdfgs_recursive(): - result: Dict[str, Set[Memlet]] = defaultdict(set) + result: Dict[str, OrderedSet[Memlet]] = defaultdict(OrderedSet) for state in sdfg.states(): for anode in state.data_nodes(): for e in state.all_edges(anode): @@ -794,17 +802,17 @@ def modifies(self) -> ppl.Modifies: def should_reapply(self, modified: ppl.Modifies) -> bool: return modified & ppl.Modifies.Memlets - def apply_pass(self, top_sdfg: SDFG, _) -> Dict[int, Dict[str, Set[Union[Memlet, nd.CodeNode]]]]: + def apply_pass(self, top_sdfg: SDFG, _) -> Dict[int, Dict[str, OrderedSet[Union[Memlet, nd.CodeNode]]]]: """ :return: A dictionary mapping each data descriptor name to a set of memlets. """ - top_result: Dict[int, Dict[str, Set[Union[Memlet, nd.CodeNode]]]] = dict() + top_result: Dict[int, Dict[str, OrderedSet[Union[Memlet, nd.CodeNode]]]] = dict() for sdfg in top_sdfg.all_sdfgs_recursive(): - result: Dict[str, Set[Memlet]] = defaultdict(set) - reference_descs = set(k for k, v in sdfg.arrays.items() if isinstance(v, dt.Reference)) + result: Dict[str, OrderedSet[Memlet]] = defaultdict(OrderedSet) + reference_descs = OrderedSet(k for k, v in sdfg.arrays.items() if isinstance(v, dt.Reference)) for state in sdfg.states(): - code_sources: Dict[str, Set[nd.CodeNode]] = defaultdict(set) + code_sources: Dict[str, OrderedSet[nd.CodeNode]] = defaultdict(OrderedSet) for anode in state.data_nodes(): if anode.data not in reference_descs: continue @@ -883,21 +891,22 @@ def should_reapply(self, modified: ppl.Modifies) -> bool: # If anything was modified, reapply return modified & ppl.Modifies.Everything - def _derive_parameter_datasize_constraints(self, sdfg: SDFG, invariants: Dict[str, Set[str]]) -> None: - handled = set() + def _derive_parameter_datasize_constraints(self, sdfg: SDFG, invariants: Dict[str, OrderedSet[str]]) -> None: + handled = OrderedSet() for arr in sdfg.arrays.values(): for dim in arr.shape: if isinstance(dim, symbolic.symbol) and not dim in handled: ds = str(dim) if ds not in invariants: - invariants[ds] = set() + invariants[ds] = OrderedSet() invariants[ds].add(f'{ds} > 0') if self.assume_max_data_size is not None: invariants[ds].add(f'{ds} <= {self.assume_max_data_size}') handled.add(ds) - def apply_pass(self, sdfg: SDFG, _) -> Tuple[Dict[str, Set[str]], Dict[str, Set[str]], Dict[str, Set[str]]]: - invariants: Dict[str, Set[str]] = {} + def apply_pass(self, sdfg: SDFG, + _) -> Tuple[Dict[str, OrderedSet[str]], Dict[str, OrderedSet[str]], Dict[str, OrderedSet[str]]]: + invariants: Dict[str, OrderedSet[str]] = {} self._derive_parameter_datasize_constraints(sdfg, invariants) return {}, invariants, {} @@ -927,9 +936,9 @@ def __init__(self): def depends_on(self): return [ControlFlowBlockReachability] - def _propagate_in_cfg(self, cfg: ControlFlowRegion, reachable: Dict[ControlFlowBlock, Set[ControlFlowBlock]], + def _propagate_in_cfg(self, cfg: ControlFlowRegion, reachable: Dict[ControlFlowBlock, OrderedSet[ControlFlowBlock]], starting_executions: int, starting_dynamic_executions: bool): - visited_blocks: Set[ControlFlowBlock] = set() + visited_blocks: OrderedSet[ControlFlowBlock] = OrderedSet() traversal_q: deque[Tuple[ControlFlowBlock, int, bool, List[str]]] = deque() traversal_q.append((cfg.start_block, starting_executions, starting_dynamic_executions, [])) while traversal_q: @@ -1081,11 +1090,11 @@ def should_reapply(self, modified: ppl.Modifies) -> bool: def depends_on(self): return [] - def apply_pass(self, top_sdfg: SDFG, pipeline_res: Dict) -> Set[nd.AccessNode]: + def apply_pass(self, top_sdfg: SDFG, pipeline_res: Dict) -> OrderedSet[nd.AccessNode]: """ :return: A set of access nodes, which are unique writes in conditional blocks. """ - cond_unique = set() + cond_unique = OrderedSet() for cfb in top_sdfg.all_control_flow_blocks(recursive=True): if not isinstance(cfb, ConditionalBlock): continue @@ -1101,12 +1110,15 @@ def apply_pass(self, top_sdfg: SDFG, pipeline_res: Dict) -> Set[nd.AccessNode]: for st in br.all_states(): for an in st.data_nodes(): array_name = an.data - write_subsets = set(e.data.dst_subset for e in st.in_edges(an)) + write_subsets = OrderedSet(e.data.dst_subset for e in st.in_edges(an)) wss = str(write_subsets) if array_name not in access_write_branch: access_write_branch[array_name] = {} if wss not in access_write_branch[array_name]: - access_write_branch[array_name][wss] = {"branches": set(), "access_nodes": set()} + access_write_branch[array_name][wss] = { + "branches": OrderedSet(), + "access_nodes": OrderedSet() + } access_write_branch[array_name][wss]["branches"].add(br) access_write_branch[array_name][wss]["access_nodes"].add(an) diff --git a/dace/transformation/passes/array_elimination.py b/dace/transformation/passes/array_elimination.py index 5583f704ee..7fa23c41e7 100644 --- a/dace/transformation/passes/array_elimination.py +++ b/dace/transformation/passes/array_elimination.py @@ -13,6 +13,7 @@ SqueezeViewRemove, UnsqueezeViewRemove, RemoveSliceView) from dace.transformation.passes import analysis as ap from dace.transformation.transformation import SingleStateTransformation +from ordered_set import OrderedSet @properties.make_properties @@ -56,7 +57,7 @@ def apply_pass(self, sdfg: SDFG, pipeline_results: Dict[str, Any]) -> Optional[S return None for state in reversed(state_order): # Find all data descriptors that will no longer be used after this state - removable_data: Set[str] = set( + removable_data: OrderedSet[str] = OrderedSet( s for s in access_sets if state in access_sets[s] and not (access_sets[s] & reachable[state]) - {state}) # Find duplicate access nodes as an ordered list diff --git a/requirements.txt b/requirements.txt index 6877c07e0e..f424ba69b6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,4 @@ packaging==24.1 ply==3.11 PyYAML==6.0.2 sympy==1.13.3 +ordered-set==4.1.0 diff --git a/setup.py b/setup.py index 90f8e8d945..b1e747bea8 100644 --- a/setup.py +++ b/setup.py @@ -69,7 +69,7 @@ include_package_data=True, install_requires=[ 'numpy', 'networkx >= 2.5, <= 3.5', 'astunparse', 'sympy >= 1.9', 'pyyaml', 'ply', 'fparser >= 0.1.3, != 0.2.3', - 'dill', 'pyreadline;platform_system=="Windows"', 'packaging', 'typing-extensions' + 'dill', 'pyreadline;platform_system=="Windows"', 'packaging', 'typing-extensions', 'ordered-set >= 4.0.0' ] + cmake_requires, extras_require={ 'ml': ['onnx', 'torch', 'onnxsim', 'onnxscript', 'onnxruntime', 'protobuf', 'ninja'], From d2d54580e679ca62352a1e8c3ac06007e0107cfd Mon Sep 17 00:00:00 2001 From: Till Ehrengruber Date: Mon, 20 Jul 2026 12:11:47 +0200 Subject: [PATCH 2/5] Use ordered sets --- dace/transformation/helpers.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/dace/transformation/helpers.py b/dace/transformation/helpers.py index 75530224d0..8b0688d025 100644 --- a/dace/transformation/helpers.py +++ b/dace/transformation/helpers.py @@ -5,6 +5,7 @@ import itertools import warnings from networkx import MultiDiGraph +from ordered_set import OrderedSet from dace.properties import CodeBlock from dace.sdfg.state import AbstractControlFlowRegion, ConditionalBlock, ControlFlowBlock, ControlFlowRegion, LoopRegion, ReturnBlock @@ -820,7 +821,7 @@ def isolate_nested_sdfg( # a backwards search starting from the nodes that serves as input to the nested # SDFG. It is important that these nodes, that serves as input to the nested # SDFG are also belonging to this set. But they are only added if they needed. - pre_nodes: Set[nodes.Node] = set() + pre_nodes: OrderedSet[nodes.Node] = OrderedSet() to_visit: List[nodes.Node] = [] for iedge in state.in_edges(nsdfg_node): input_node: nodes.AccessNode = iedge.src @@ -840,7 +841,7 @@ def isolate_nested_sdfg( # as input to the nested SDFG and the nested SDFG itself. # Note that the AccessNodes serving as input and output of the nested SDFG # belonging to the pre and post set, respectively, as well. - middle_nodes: Set[nodes.Node] = {nsdfg_node} + middle_nodes: OrderedSet[nodes.Node] = OrderedSet((nsdfg_node, )) for iedge in state.in_edges(nsdfg_node): if (not isinstance(iedge.src, nodes.AccessNode)) or isinstance(iedge.src.desc(state.sdfg), data.View): if test_if_applicable: @@ -865,10 +866,8 @@ def isolate_nested_sdfg( # These are the nodes that belongs to the Post State. There are two reasons why a # node belongs to the set of post nodes. # The first is that the node does not belong to any other set. - post_nodes: Set[nodes.Node] = { - node - for node in state.nodes() if (node not in pre_nodes) and (node not in middle_nodes) - } + post_nodes: OrderedSet[nodes.Node] = OrderedSet( + node for node in state.nodes() if (node not in pre_nodes) and (node not in middle_nodes)) # The second reason, are read dependencies, for this we have to look at the incoming # edges and add any node that we need. From eef321e42b28d469bb71ff966e6f818d4f5879fc Mon Sep 17 00:00:00 2001 From: Till Ehrengruber Date: Mon, 20 Jul 2026 12:16:16 +0200 Subject: [PATCH 3/5] Use ordered sets --- dace/sdfg/state.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dace/sdfg/state.py b/dace/sdfg/state.py index efb5253fdd..9c422ab903 100644 --- a/dace/sdfg/state.py +++ b/dace/sdfg/state.py @@ -1713,9 +1713,11 @@ def add_tasklet( debuginfo = _get_debug_info(debuginfo or self._default_lineinfo) # Make dictionary of autodetect connector types from set - if isinstance(inputs, (set, collections.abc.KeysView)): + if isinstance(inputs, set) or isinstance(outputs, set): + warnings.warn("Using sets as inputs is discouraged as it leads to indeterministic behavior.") + if isinstance(inputs, (set, collections.abc.KeysView, collections.abc.Set)): inputs = {k: None for k in inputs} - if isinstance(outputs, (set, collections.abc.KeysView)): + if isinstance(outputs, (set, collections.abc.KeysView, collections.abc.Set)): outputs = {k: None for k in outputs} tasklet = nd.Tasklet( From 418cef481bd8bc3f1499ff330fd593d1ebcfd5c4 Mon Sep 17 00:00:00 2001 From: Till Ehrengruber Date: Mon, 20 Jul 2026 12:47:42 +0200 Subject: [PATCH 4/5] Fix format --- dace/transformation/helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dace/transformation/helpers.py b/dace/transformation/helpers.py index 8b0688d025..9d2264c07f 100644 --- a/dace/transformation/helpers.py +++ b/dace/transformation/helpers.py @@ -866,8 +866,8 @@ def isolate_nested_sdfg( # These are the nodes that belongs to the Post State. There are two reasons why a # node belongs to the set of post nodes. # The first is that the node does not belong to any other set. - post_nodes: OrderedSet[nodes.Node] = OrderedSet( - node for node in state.nodes() if (node not in pre_nodes) and (node not in middle_nodes)) + post_nodes: OrderedSet[nodes.Node] = OrderedSet(node for node in state.nodes() + if (node not in pre_nodes) and (node not in middle_nodes)) # The second reason, are read dependencies, for this we have to look at the incoming # edges and add any node that we need. From d8dce0c444ad4a906477878d527e522870f0183b Mon Sep 17 00:00:00 2001 From: Till Ehrengruber Date: Thu, 6 Aug 2026 13:40:04 +0200 Subject: [PATCH 5/5] Fix format --- setup.py | 118 +++++++++++++++++++++++++++---------------------------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/setup.py b/setup.py index 45bfbaee7d..f25ab09c5b 100644 --- a/setup.py +++ b/setup.py @@ -47,62 +47,62 @@ with open(os.path.join(dace_path, "version.py"), "r") as fp: version = fp.read().strip().split(' ')[-1][1:-1] -setup( - name='dace', - version=version, - url='https://github.com/spcl/dace', - author='SPCL @ ETH Zurich', - author_email='talbn@inf.ethz.ch', - description='Data-Centric Parallel Programming Framework', - long_description=long_description, - long_description_content_type='text/markdown', - classifiers=[ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: BSD License", - "Operating System :: OS Independent", - ], - python_requires='>=3.10, <3.15', - packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]), - package_data={ - '': [ - '*.yml', 'codegen/CMakeLists.txt', 'codegen/tools/*.cpp', 'external/moodycamel/*.h', - 'external/moodycamel/LICENSE.md' - ] + runtime_files + cub_files + viewer_files + library_files + cmake_files - }, - include_package_data=True, - install_requires=[ - 'numpy', 'networkx >= 2.5, <= 3.5', 'astunparse', 'sympy >= 1.9', 'pyyaml', 'ply', 'fparser >= 0.1.3, != 0.2.3', - 'dill', 'pyreadline;platform_system=="Windows"', 'packaging', 'typing-extensions', 'ml_dtypes', 'ordered-set >= 4.0.0' - ] + cmake_requires + ninja_requires, - extras_require={ - 'ml': ['onnx', 'torch', 'onnxsim', 'onnxscript', 'onnxruntime', 'protobuf', 'ninja'], - 'testing': [ - 'coverage', - 'pytest-cov', - 'scipy', - 'absl-py', - 'opt_einsum', - 'pymlir', - 'click', - 'ipykernel', - 'nbconvert', - 'pytest-timeout', - ], - 'ml-testing': [ - 'coverage', 'pytest-cov', 'scipy', 'absl-py', 'opt_einsum', 'pymlir', 'click', 'ipykernel', 'nbconvert', - 'pytest-timeout', 'transformers == 4.50', 'jax <= 0.6.2', 'efficientnet_pytorch' - ], - 'docs': ['jinja2<3.2.0', 'sphinx-autodoc-typehints', 'sphinx-rtd-theme>=0.5.1'], - 'linting': ['pre-commit==4.1.0', 'yapf==0.43.0'], - }, - entry_points={ - 'console_scripts': [ - 'dacelab = dace.cli.dacelab:main', - 'sdfv = dace.cli.sdfv:main', - 'sdfgcc = dace.cli.sdfgcc:main', - 'sdfg-diff = dace.cli.sdfg_diff:main', - 'fcfd = dace.cli.fcdc:main', - 'daceprof = dace.cli.daceprof:main', - 'dace-external-transformation-registry = dace.cli.external_transformation_registry:main', - ], - }) +setup(name='dace', + version=version, + url='https://github.com/spcl/dace', + author='SPCL @ ETH Zurich', + author_email='talbn@inf.ethz.ch', + description='Data-Centric Parallel Programming Framework', + long_description=long_description, + long_description_content_type='text/markdown', + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + ], + python_requires='>=3.10, <3.15', + packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]), + package_data={ + '': [ + '*.yml', 'codegen/CMakeLists.txt', 'codegen/tools/*.cpp', 'external/moodycamel/*.h', + 'external/moodycamel/LICENSE.md' + ] + runtime_files + cub_files + viewer_files + library_files + cmake_files + }, + include_package_data=True, + install_requires=[ + 'numpy', 'networkx >= 2.5, <= 3.5', 'astunparse', 'sympy >= 1.9', 'pyyaml', 'ply', + 'fparser >= 0.1.3, != 0.2.3', 'dill', 'pyreadline;platform_system=="Windows"', 'packaging', + 'typing-extensions', 'ml_dtypes', 'ordered-set >= 4.0.0' + ] + cmake_requires + ninja_requires, + extras_require={ + 'ml': ['onnx', 'torch', 'onnxsim', 'onnxscript', 'onnxruntime', 'protobuf', 'ninja'], + 'testing': [ + 'coverage', + 'pytest-cov', + 'scipy', + 'absl-py', + 'opt_einsum', + 'pymlir', + 'click', + 'ipykernel', + 'nbconvert', + 'pytest-timeout', + ], + 'ml-testing': [ + 'coverage', 'pytest-cov', 'scipy', 'absl-py', 'opt_einsum', 'pymlir', 'click', 'ipykernel', 'nbconvert', + 'pytest-timeout', 'transformers == 4.50', 'jax <= 0.6.2', 'efficientnet_pytorch' + ], + 'docs': ['jinja2<3.2.0', 'sphinx-autodoc-typehints', 'sphinx-rtd-theme>=0.5.1'], + 'linting': ['pre-commit==4.1.0', 'yapf==0.43.0'], + }, + entry_points={ + 'console_scripts': [ + 'dacelab = dace.cli.dacelab:main', + 'sdfv = dace.cli.sdfv:main', + 'sdfgcc = dace.cli.sdfgcc:main', + 'sdfg-diff = dace.cli.sdfg_diff:main', + 'fcfd = dace.cli.fcdc:main', + 'daceprof = dace.cli.daceprof:main', + 'dace-external-transformation-registry = dace.cli.external_transformation_registry:main', + ], + })