def test_intersects_symbolic(): N, M = dace.symbol('N', positive=True), dace.symbol('M', positive=True) rng1 = subsets.Range([(0, N - 1, 1), (0, M - 1, 1)]) rng2 = subsets.Range([(0, 0, 1), (0, 0, 1)]) rng3_1 = subsets.Range([(N, N, 1), (0, 1, 1)]) rng3_2 = subsets.Range([(0, 1, 1), (M, M, 1)]) rng4 = subsets.Range([(N, N, 1), (M, M, 1)]) rng5 = subsets.Range([(0, 0, 1), (M, M, 1)]) rng6 = subsets.Range([(0, N, 1), (0, M, 1)]) rng7 = subsets.Range([(0, N - 1, 1), (N - 1, N, 1)]) ind1 = subsets.Indices([0, 1]) assert subsets.intersects(rng1, rng2) is True assert subsets.intersects(rng1, rng3_1) is False assert subsets.intersects(rng1, rng3_2) is False assert subsets.intersects(rng1, rng4) is False assert subsets.intersects(rng1, rng5) is False assert subsets.intersects(rng6, rng1) is True assert subsets.intersects(rng1, rng7) is None assert subsets.intersects(rng7, rng1) is None assert subsets.intersects(rng1, ind1) is None assert subsets.intersects(ind1, rng1) is None
def memlets_intersect(graph_a: SDFGState, group_a: List[nodes.AccessNode], inputs_a: bool, graph_b: SDFGState, group_b: List[nodes.AccessNode], inputs_b: bool) -> bool: """ Performs an all-pairs check for subset intersection on two groups of nodes. If group intersects or result is indeterminate, returns True as a precaution. :param graph_a: The graph in which the first set of nodes reside. :param group_a: The first set of nodes to check. :param inputs_a: If True, checks inputs of the first group. :param graph_b: The graph in which the second set of nodes reside. :param group_b: The second set of nodes to check. :param inputs_b: If True, checks inputs of the second group. :returns True if subsets intersect or result is indeterminate. """ # Set traversal functions src_subset = lambda e: (e.data.src_subset if e.data.src_subset is not None else e.data.dst_subset) dst_subset = lambda e: (e.data.dst_subset if e.data.dst_subset is not None else e.data.src_subset) if inputs_a: edges_a = [e for n in group_a for e in graph_a.out_edges(n)] subset_a = src_subset else: edges_a = [e for n in group_a for e in graph_a.in_edges(n)] subset_a = dst_subset if inputs_b: edges_b = [e for n in group_b for e in graph_b.out_edges(n)] subset_b = src_subset else: edges_b = [e for n in group_b for e in graph_b.in_edges(n)] subset_b = dst_subset # Simple all-pairs check for ea in edges_a: for eb in edges_b: result = subsets.intersects(subset_a(ea), subset_b(eb)) if result is True or result is None: return True return False
def test_intersects_constant(): rng1 = subsets.Range([(0, 4, 1)]) rng2 = subsets.Range([(3, 4, 1)]) rng3 = subsets.Range([(1, 5, 1)]) rng4 = subsets.Range([(5, 7, 1)]) ind1 = subsets.Indices([0]) ind2 = subsets.Indices([1]) ind3 = subsets.Indices([5]) assert subsets.intersects(rng1, rng2) is True assert subsets.intersects(rng1, rng3) is True assert subsets.intersects(rng1, rng4) is False assert subsets.intersects(ind1, rng1) is True assert subsets.intersects(rng1, ind2) is True assert subsets.intersects(rng1, ind3) is False
def can_be_applied(self, graph, candidate, expr_index, sdfg, strict=False): # Is this even a loop if not DetectLoop.can_be_applied(graph, candidate, expr_index, sdfg, strict): return False guard = graph.node(candidate[DetectLoop._loop_guard]) begin = graph.node(candidate[DetectLoop._loop_begin]) # Guard state should not contain any dataflow if len(guard.nodes()) != 0: return False # If loop cannot be detected, fail found = find_for_loop(graph, guard, begin, itervar=self.itervar) if not found: return False itervar, (start, end, step), (_, body_end) = found # We cannot handle symbols read from data containers unless they are # scalar for expr in (start, end, step): if symbolic.contains_sympy_functions(expr): return False # Find all loop-body states states = set([body_end]) to_visit = [begin] while to_visit: state = to_visit.pop(0) if state is body_end: continue for _, dst, _ in graph.out_edges(state): if dst not in states: to_visit.append(dst) states.add(state) write_set = set() for state in states: _, wset = state.read_and_write_sets() write_set |= wset # Get access nodes from other states to isolate local loop variables other_access_nodes = set() for state in sdfg.nodes(): if state in states: continue other_access_nodes |= set(n.data for n in state.data_nodes() if sdfg.arrays[n.data].transient) # Add non-transient nodes from loop state for state in states: other_access_nodes |= set(n.data for n in state.data_nodes() if not sdfg.arrays[n.data].transient) write_memlets = defaultdict(list) itersym = symbolic.pystr_to_symbolic(itervar) a = sp.Wild('a', exclude=[itersym]) b = sp.Wild('b', exclude=[itersym]) for state in states: for dn in state.data_nodes(): if dn.data not in other_access_nodes: continue # Take all writes that are not conflicted into consideration if dn.data in write_set: for e in state.in_edges(dn): if e.data.dynamic and e.data.wcr is None: # If pointers are involved, give up return False # To be sure that the value is only written at unique # indices per loop iteration, we want to match symbols # of the form "a*i+b" where a >= 1, and i is the iteration # variable. The iteration variable must be used. if e.data.wcr is None: dst_subset = e.data.get_dst_subset(e, state) if not _check_range(dst_subset, a, itersym, b, step): return False # End of check write_memlets[dn.data].append(e.data) # After looping over relevant writes, consider reads that may overlap for state in states: for dn in state.data_nodes(): if dn.data not in other_access_nodes: continue data = dn.data if data in write_memlets: # Import as necessary from dace.sdfg.propagation import propagate_subset for e in state.out_edges(dn): # If the same container is both read and written, only match if # it read and written at locations that will not create data races if e.data.dynamic and e.data.src_subset.num_elements() != 1: # If pointers are involved, give up return False src_subset = e.data.get_src_subset(e, state) if not _check_range(src_subset, a, itersym, b, step): return False pread = propagate_subset([e.data], sdfg.arrays[data], [itervar], subsets.Range([(start, end, step) ])) for candidate in write_memlets[data]: # Simple case: read and write are in the same subset if e.data.subset == candidate.subset: break # Propagated read does not overlap with propagated write pwrite = propagate_subset([candidate], sdfg.arrays[data], [itervar], subsets.Range([(start, end, step)])) if subsets.intersects(pread.subset, pwrite.subset) is False: break return False # Check that the iteration variable is not used on other edges or states # before it is reassigned prior_states = True for state in cfg.stateorder_topological_sort(sdfg): # Skip all states up to guard if prior_states: if state is begin: prior_states = False continue # We do not need to check the loop-body states if state in states: continue if itervar in state.free_symbols: return False # Don't continue in this direction, as the variable has # now been reassigned # TODO: Handle case of subset of out_edges if all(itervar in e.data.assignments for e in sdfg.out_edges(state)): break return True
def can_be_applied(graph, candidate, expr_index, sdfg, strict=False): in_array = graph.nodes()[candidate[RedundantSecondArray._in_array]] out_array = graph.nodes()[candidate[RedundantSecondArray._out_array]] in_desc = in_array.desc(sdfg) out_desc = out_array.desc(sdfg) # Ensure in degree is one (only one source, which is in_array) if graph.in_degree(out_array) != 1: return False # Make sure that the candidate is a transient variable if not out_desc.transient: return False # 1. Get edge e1 and extract/validate subsets for arrays A and B e1 = graph.edges_between(in_array, out_array)[0] a_subset, b1_subset = _validate_subsets(e1, sdfg.arrays) if strict: # In strict mode, make sure the memlet covers the removed array if not b1_subset: return False subset = copy.deepcopy(b1_subset) subset.squeeze() shape = [sz for sz in out_desc.shape if sz != 1] if any(m != a for m, a in zip(subset.size(), shape)): return False # NOTE: Library node check # The transformation must not apply in strict mode if out_array is # not a view, is input to a library node, and an access or a view # of in_desc is also output to the same library node. # The reason is that the application of the transformation will lead # to in_desc being both input and output of the library node. # We do not know if this is safe. # First find the true in_desc (in case in_array is a view). true_in_desc = in_desc if isinstance(in_desc, data.View): e = sdutil.get_view_edge(graph, in_array) if not e: return False true_in_desc = sdfg.arrays[e.dst.data] if not isinstance(out_desc, data.View): edges_to_check = [] for a in graph.out_edges(out_array): if isinstance(a.dst, nodes.LibraryNode): edges_to_check.append(a) elif (isinstance(a.dst, nodes.AccessNode) and isinstance(sdfg.arrays[a.dst.data], data.View)): for b in graph.out_edges(a.dst): edges_to_check.append(graph.memlet_path(b)[-1]) for a in edges_to_check: if isinstance(a.dst, nodes.LibraryNode): for b in graph.out_edges(a.dst): if isinstance(b.dst, nodes.AccessNode): desc = sdfg.arrays[b.dst.data] if isinstance(desc, data.View): e = sdutil.get_view_edge(graph, b.dst) if not e: return False desc = sdfg.arrays[e.dst.data] if desc is true_in_desc: return False # In strict mode, check if the state has two or more access nodes # for in_array and at least one of them is a write access. There # might be a RW, WR, or WW dependency. accesses = [ n for n in graph.nodes() if isinstance(n, nodes.AccessNode) and n.desc(sdfg) == in_desc and n is not in_array ] if len(accesses) > 0: if (graph.in_degree(in_array) > 0 or any(graph.in_degree(a) > 0 for a in accesses)): # We need to ensure that a data race will not happen if we # remove in_array. # First, we simplify the graph G = helpers.simplify_state(graph) # Loop over the accesses for a in accesses: subsets_intersect = False for e in graph.in_edges(a): _, subset = _validate_subsets(e, sdfg.arrays, dst_name=a.data) res = subsets.intersects(a_subset, subset) if res == True or res is None: subsets_intersect = True break if not subsets_intersect: continue try: has_bward_path = nx.has_path(G, a, in_array) except NodeNotFound: has_bward_path = nx.has_path(graph.nx, a, in_array) try: has_fward_path = nx.has_path(G, in_array, a) except NodeNotFound: has_fward_path = nx.has_path(graph.nx, in_array, a) # If there is no path between the access nodes # (disconnected components), then it is definitely # possible to have data races. Abort. if not (has_bward_path or has_fward_path): return False # If there is a forward path then a must not be a direct # successor of in_array. if has_fward_path and a in G.successors(in_array): for src, _ in G.in_edges(a): if src is in_array: continue if (nx.has_path(G, in_array, src) and src != out_array): continue return False # Make sure that both arrays are using the same storage location # and are of the same type (e.g., Stream->Stream) if in_desc.storage != out_desc.storage: return False if in_desc.location != out_desc.location: return False if type(in_desc) != type(out_desc): if isinstance(in_desc, data.View): # Case View -> Access # If the View points to the Access (and has a different shape?) # then we should (probably) not remove the Access. e = sdutil.get_view_edge(graph, in_array) if e and e.dst is out_array and in_desc.shape != out_desc.shape: return False # Check that the View's immediate ancestors are Accesses. # Otherwise, the application of the transformation will result # in an ambiguous View. view_ancestors_desc = [ e.src.desc(sdfg) if isinstance(e.src, nodes.AccessNode) else None for e in graph.in_edges(in_array) ] if any([ not desc or isinstance(desc, data.View) for desc in view_ancestors_desc ]): return False elif isinstance(out_desc, data.View): # Case Access -> View # If the View points to the Access and has the same shape, # it can be removed e = sdutil.get_view_edge(graph, out_array) if e and e.src is in_array and in_desc.shape == out_desc.shape: return True return False else: # Something else, for example, Stream return False else: # Two views connected to each other if isinstance(in_desc, data.View): return False # Find occurrences in this and other states occurrences = [] for state in sdfg.nodes(): occurrences.extend([ n for n in state.nodes() if isinstance(n, nodes.AccessNode) and n.desc(sdfg) == out_desc ]) for isedge in sdfg.edges(): if out_array.data in isedge.data.free_symbols: occurrences.append(isedge) if len(occurrences) > 1: return False # Check whether the data copied from the first datanode cover # the subsets of all the output edges of the second datanode. # We assume the following pattern: A -- e1 --> B -- e2 --> others # 2. Iterate over the e2 edges for e2 in graph.out_edges(out_array): # 2-a. Extract/validate subsets for array B and others try: b2_subset, _ = _validate_subsets(e2, sdfg.arrays) except NotImplementedError: return False # 2-b. Check where b1_subset covers b2_subset if not b1_subset.covers(b2_subset): return False # 2-c. Validate subsets in memlet tree # (should not be needed for valid SDGs) path = graph.memlet_tree(e2) for e3 in path: if e3 is not e2: try: _validate_subsets(e3, sdfg.arrays, src_name=out_array.data) except NotImplementedError: return False return True
def is_write_conflicted_with_reason(dfg, edge, datanode=None, sdfg_schedule=None): """ Detects whether a write-conflict-resolving edge can be emitted without using atomics or critical sections, returning the node or SDFG that caused the decision. :return: None if the conflict is nonatomic, otherwise returns the scope entry node or SDFG that caused the decision to be made. """ if edge.data.wcr_nonatomic or edge.data.wcr is None: return None # If it's an entire SDFG, it's probably write-conflicted if isinstance(dfg, SDFG): if datanode is None: return dfg in_edges = find_incoming_edges(datanode, dfg) if len(in_edges) != 1: return dfg if (isinstance(in_edges[0].src, nodes.ExitNode) and in_edges[0].src.map.schedule == dtypes.ScheduleType.Sequential): return None return dfg elif isinstance(dfg, gr.SubgraphView): dfg = dfg.graph # Traverse memlet path to determine conflicts. # If no conflicts will occur, write without atomics # (e.g., if the array has been defined in a non-parallel schedule context) while edge is not None: path = dfg.memlet_path(edge) for e in path: if (isinstance(e.dst, nodes.ExitNode) and e.dst.map.schedule != dtypes.ScheduleType.Sequential): if _check_map_conflicts(e.dst.map, e): # This map is parallel w.r.t. WCR # print('PAR: Continuing from map') continue # print('SEQ: Map is conflicted') return dfg.entry_node(e.dst) # Should never happen (no such thing as write-conflicting reads) if (isinstance(e.src, nodes.EntryNode) and e.src.map.schedule != dtypes.ScheduleType.Sequential): warnings.warn( 'Unexpected WCR path to have write-conflicting reads') return e.src sdfg = dfg.parent dst = path[-1].dst # Unexpected case if not isinstance(dst, nodes.AccessNode): warnings.warn('Unexpected WCR path to not end in access node') return dst if dfg.in_degree(dst) > 0: for x, y in itertools.combinations(dfg.in_edges(dst), 2): x, y = x.data.subset, y.data.subset if subsets.intersects(x, y): return dst # If this is a nested SDFG and the access leads outside if not sdfg.arrays[dst.data].transient: if sdfg.parent_nsdfg_node is not None: dfg = sdfg.parent nsdfg = sdfg.parent_nsdfg_node edge = next(iter(dfg.out_edges_by_connector(nsdfg, dst.data))) else: break else: # Memlet path ends here, transient. We can thus safely write here edge = None # print('PAR: Reached transient') return None return None