def can_be_applied(graph, candidate, expr_index, sdfg, permissive=False): # Check the edges between the entries of the two maps. outer_map_entry: nodes.MapEntry = graph.nodes()[candidate[ MapCollapse._outer_map_entry]] inner_map_entry: nodes.MapEntry = graph.nodes()[candidate[ MapCollapse._inner_map_entry]] # Check that inner map range is independent of outer range map_deps = set() for s in inner_map_entry.map.range: map_deps |= set(map(str, symlist(s))) if any(dep in outer_map_entry.map.params for dep in map_deps): return False # Check that the destination of all the outgoing edges # from the outer map's entry is the inner map's entry. for _src, _, dest, _, _ in graph.out_edges(outer_map_entry): if dest != inner_map_entry: return False # Check that the source of all the incoming edges # to the inner map's entry is the outer map's entry. for src, _, _, dst_conn, memlet in graph.in_edges(inner_map_entry): if src != outer_map_entry: return False # Check that dynamic input range memlets are independent of # first map range if dst_conn is not None and not dst_conn.startswith('IN_'): memlet_deps = set() for s in memlet.subset: memlet_deps |= set(map(str, symlist(s))) if any(dep in outer_map_entry.map.params for dep in memlet_deps): return False # Check the edges between the exits of the two maps. inner_map_exit = graph.exit_node(inner_map_entry) outer_map_exit = graph.exit_node(outer_map_entry) # Check that the destination of all the outgoing edges # from the inner map's exit is the outer map's exit. for _src, _, dest, _, _ in graph.out_edges(inner_map_exit): if dest != outer_map_exit: return False # Check that the source of all the incoming edges # to the outer map's exit is the inner map's exit. for src, _, _dest, _, _ in graph.in_edges(outer_map_exit): if src != inner_map_exit: return False if not permissive: if inner_map_entry.map.schedule != outer_map_entry.map.schedule: return False return True
def can_be_applied(graph, candidate, expr_index, sdfg, permissive=False): # TODO: Assuming that the subsets on the edges between the two map # entries/exits are the union of separate inner subsets, is it possible # that inverting these edges breaks the continuity of union? What about # the opposite? # Check the edges between the entries of the two maps. outer_map_entry = graph.nodes()[candidate[ MapInterchange.outer_map_entry]] inner_map_entry = graph.nodes()[candidate[ MapInterchange.inner_map_entry]] # Check that inner map range is independent of outer range map_deps = set() for s in inner_map_entry.map.range: map_deps |= set(map(str, symlist(s))) if any(dep in outer_map_entry.map.params for dep in map_deps): return False # Check that the destination of all the outgoing edges # from the outer map's entry is the inner map's entry. for e in graph.out_edges(outer_map_entry): if e.dst != inner_map_entry: return False # Check that the source of all the incoming edges # to the inner map's entry is the outer map's entry. for e in graph.in_edges(inner_map_entry): if e.src != outer_map_entry: return False # Check that dynamic input range memlets are independent of # first map range if e.dst_conn and not e.dst_conn.startswith('IN_'): memlet_deps = set() for s in e.data.subset: memlet_deps |= set(map(str, symlist(s))) if any(dep in outer_map_entry.map.params for dep in memlet_deps): return False # Check the edges between the exits of the two maps. inner_map_exit = graph.exit_node(inner_map_entry) outer_map_exit = graph.exit_node(outer_map_entry) # Check that the destination of all the outgoing edges # from the inner map's exit is the outer map's exit. for e in graph.out_edges(inner_map_exit): if e.dst != outer_map_exit: return False # Check that the source of all the incoming edges # to the outer map's exit is the inner map's exit. for e in graph.in_edges(outer_map_exit): if e.src != inner_map_exit: return False return True
def __call__(self, *args, **kwargs): """ Convenience function that parses, compiles, and runs a DaCe program. """ binaryobj = self.compile(*args) # Add named arguments to the call kwargs.update({aname: arg for aname, arg in zip(self.argnames, args)}) # Update arguments with symbols in data shapes kwargs.update({ sym: symbolic.symbol(sym).get() for arg in args for sym in (symbolic.symlist(arg.descriptor.shape) if hasattr( arg, 'descriptor') else []) }) # Update arguments with symbol values for aname in self.argnames: if aname in binaryobj.sdfg.arrays: sym_shape = binaryobj.sdfg.arrays[aname].shape for sym in (sym_shape): if symbolic.issymbolic(sym): try: kwargs[str(sym)] = sym.get() except: pass return binaryobj(**kwargs)
def update_resolved_symbol(self, sym): """ Notifies an array that a symbol has been resolved so that it can be resized. """ self.resize( [symbolic.eval(s, 0) for s in self.descriptor.shape], refcheck=False) self._symlist = symbolic.symlist(self.descriptor.shape)
def ndarray(shape, dtype=numpy.float64, *args, **kwargs): """ Returns a numpy ndarray where all symbols have been evaluated to numbers and types are converted to numpy types. """ repldict = {sym: sym.get() for sym in symbolic.symlist(shape).values()} new_shape = [ int(s.subs(repldict) if symbolic.issymbolic(s) else s) for s in shape ] new_dtype = dtype.type if isinstance(dtype, dtypes.typeclass) else dtype return numpy.ndarray(shape=new_shape, dtype=new_dtype, *args, **kwargs)
def __new__(cls, shape, dtype=types.float32, materialize_func=None, allow_conflicts=False, *args, **kwargs): """ Initializes a DaCe ND-array. @param shape: The array shape (may contain symbols). @param dtype: The array data type. @param materialize_func: An optional string that contains a method to materialize array contents on demand. If not None, the array is not allocated within the DaCe program. @param allow_conflicts: If True, suppresses warnings on conflicting array writes in DaCe programs without a matching conflict resolution memlet. """ # Avoiding import loops from dace import data tmpshape = shape shape = [symbolic.eval(s, 0) for s in shape] kwargs.update({'dtype': dtype.type}) res = numpy.ndarray.__new__(cls, shape, *args, **kwargs) res._symlist = symbolic.symlist(tmpshape) for _, sym in res._symlist.items(): sym._arrays_to_update.append(res) if not isinstance(dtype, types.typeclass): dtype = types.typeclass(dtype.type) res.descriptor = data.Array( dtype, tmpshape, materialize_func=materialize_func, transient=False, allow_conflicts=allow_conflicts) return res
def replace_func(element, dyn_syms, retparams): # Resolve all symbols using the retparams-dict for x in dyn_syms: target = sp.functions.Min(retparams[x] * (retparams[x] - 1) / 2, 0) bstr = str(element) element = symbolic.pystr_to_symbolic(bstr) element = element.subs(x, target) # Add the classic sum formula; going upwards # To not have hidden elements that get added again later, we # also replace the values in the other itvars... for k, v in retparams.items(): newv = symbolic.pystr_to_symbolic(str(v)) tarsyms = symbolic.symlist(target).keys() if x in tarsyms: continue tmp = newv.subs(x, target) if tmp != v: retparams[k] = tmp return element
def expand(self, sdfg, graph, map_entries, map_base_variables=None): """ Expansion into outer and inner maps for each map in a specified set. The resulting outer maps all have same range and indices, corresponding variables and memlets get changed accordingly. The inner map contains the leftover dimensions :param sdfg: Underlying SDFG :param graph: Graph in which we expand :param map_entries: List of Map Entries(Type MapEntry) that we want to expand :param map_base_variables: Optional parameter. List of strings If None, then expand() searches for the maximal amount of equal map ranges and pushes those and their corresponding loop variables into the outer loop. If specified, then expand() pushes the ranges belonging to the loop iteration variables specified into the outer loop (For instance map_base_variables = ['i','j'] assumes that all maps have common iteration indices i and j with corresponding correct ranges) """ maps = [entry.map for entry in map_entries] if not map_base_variables: # find the maximal subset of variables to expand # greedy if there exist multiple ranges that are equal in a map map_base_ranges = helpers.common_map_base_ranges(maps) reassignments = helpers.find_reassignment(maps, map_base_ranges) ##### first, regroup and reassign # create params_dict for every map # first, let us define the outer iteration variable names, # just take the first map and their indices at common ranges map_base_variables = [] for rng in map_base_ranges: for i in range(len(maps[0].params)): if maps[0].range[i] == rng and maps[0].params[ i] not in map_base_variables: map_base_variables.append(maps[0].params[i]) break params_dict = {} if self.debug: print("MultiExpansion::Map_base_variables:", map_base_variables) print("MultiExpansion::Map_base_ranges:", map_base_ranges) for map in maps: # for each map create param dict, first assign identity params_dict_map = {param: param for param in map.params} # now look for the correct reassignment # for every element neq -1, need to change param to map_base_variables[] # if param already appears in own dict, do a swap # else we just replace it for i, reassignment in enumerate(reassignments[map]): if reassignment == -1: # nothing to do pass else: current_var = map.params[i] current_assignment = params_dict_map[current_var] target_assignment = map_base_variables[reassignment] if current_assignment != target_assignment: if target_assignment in params_dict_map.values(): # do a swap key1 = current_var for key, value in params_dict_map.items(): if value == target_assignment: key2 = key value1 = params_dict_map[key1] value2 = params_dict_map[key2] params_dict_map[key1] = key2 params_dict_map[key2] = key1 else: # just reassign params_dict_map[current_var] = target_assignment # done, assign params_dict_map to the global one params_dict[map] = params_dict_map for map, map_entry in zip(maps, map_entries): map_scope = graph.scope_subgraph(map_entry) params_dict_map = params_dict[map] for firstp, secondp in params_dict_map.items(): if firstp != secondp: replace(map_scope, firstp, '__' + firstp + '_fused') for firstp, secondp in params_dict_map.items(): if firstp != secondp: replace(map_scope, '__' + firstp + '_fused', secondp) # now also replace the map variables inside maps for i in range(len(map.params)): map.params[i] = params_dict_map[map.params[i]] if self.debug: print("MultiExpansion::Params replaced") else: # just calculate map_base_ranges # do a check whether all maps correct map_base_ranges = [] map0 = maps[0] for var in map_base_variables: index = map0.params.index(var) map_base_ranges.append(map0.range[index]) for map in maps: for var, rng in zip(map_base_variables, map_base_ranges): assert map.range[map.params.index(var)] == rng # then expand all the maps for map, map_entry in zip(maps, map_entries): if map.get_param_num() == len(map_base_variables): # nothing to expand, continue continue map_exit = graph.exit_node(map_entry) # create two new maps, outer and inner params_outer = map_base_variables ranges_outer = map_base_ranges init_params_inner = [] init_ranges_inner = [] for param, rng in zip(map.params, map.range): if param in map_base_variables: continue else: init_params_inner.append(param) init_ranges_inner.append(rng) params_inner = init_params_inner ranges_inner = subsets.Range(init_ranges_inner) inner_map = nodes.Map(label = map.label + '_inner', params = params_inner, ndrange = ranges_inner, schedule = dtypes.ScheduleType.Sequential \ if self.sequential_innermaps \ else dtypes.ScheduleType.Default) map.label = map.label + '_outer' map.params = params_outer map.range = ranges_outer # create new map entries and exits map_entry_inner = nodes.MapEntry(inner_map) map_exit_inner = nodes.MapExit(inner_map) # analogously to Map_Expansion for edge in graph.out_edges(map_entry): graph.remove_edge(edge) graph.add_memlet_path(map_entry, map_entry_inner, edge.dst, src_conn=edge.src_conn, memlet=edge.data, dst_conn=edge.dst_conn) dynamic_edges = dynamic_map_inputs(graph, map_entry) for edge in dynamic_edges: # Remove old edge and connector graph.remove_edge(edge) edge.dst._in_connectors.remove(edge.dst_conn) # Propagate to each range it belongs to path = [] for mapnode in [map_entry, map_entry_inner]: path.append(mapnode) if any(edge.dst_conn in map(str, symbolic.symlist(r)) for r in mapnode.map.range): graph.add_memlet_path(edge.src, *path, memlet=edge.data, src_conn=edge.src_conn, dst_conn=edge.dst_conn) for edge in graph.in_edges(map_exit): graph.remove_edge(edge) graph.add_memlet_path(edge.src, map_exit_inner, map_exit, memlet=edge.data, src_conn=edge.src_conn, dst_conn=edge.dst_conn)
def apply(self, sdfg: dace.SDFG): # Extract the map and its entry and exit nodes. graph = sdfg.nodes()[self.state_id] map_entry = graph.nodes()[self.subgraph[MapExpansion._map_entry]] map_exit = graph.exit_node(map_entry) current_map = map_entry.map # Create new maps new_maps = [ nodes.Map(current_map.label + '_' + str(param), [param], subsets.Range([param_range]), schedule=dtypes.ScheduleType.Sequential) for param, param_range in zip(current_map.params[1:], current_map.range[1:]) ] current_map.params = [current_map.params[0]] current_map.range = subsets.Range([current_map.range[0]]) # Create new map entries and exits entries = [nodes.MapEntry(new_map) for new_map in new_maps] exits = [nodes.MapExit(new_map) for new_map in new_maps] # Create edges, abiding by the following rules: # 1. If there are no edges coming from the outside, use empty memlets # 2. Edges with IN_* connectors replicate along the maps # 3. Edges for dynamic map ranges replicate until reaching range(s) for edge in graph.out_edges(map_entry): graph.remove_edge(edge) graph.add_memlet_path(map_entry, *entries, edge.dst, src_conn=edge.src_conn, memlet=edge.data, dst_conn=edge.dst_conn) # Modify dynamic map ranges dynamic_edges = dace.sdfg.dynamic_map_inputs(graph, map_entry) for edge in dynamic_edges: # Remove old edge and connector graph.remove_edge(edge) edge.dst.remove_in_connector(edge.dst_conn) # Propagate to each range it belongs to path = [] for mapnode in [map_entry] + entries: path.append(mapnode) if any(edge.dst_conn in map(str, symbolic.symlist(r)) for r in mapnode.map.range): graph.add_memlet_path(edge.src, *path, memlet=edge.data, src_conn=edge.src_conn, dst_conn=edge.dst_conn) # Create new map exits for edge in graph.in_edges(map_exit): graph.remove_edge(edge) graph.add_memlet_path(edge.src, *exits[::-1], map_exit, memlet=edge.data, src_conn=edge.src_conn, dst_conn=edge.dst_conn)
def infer_symbols_from_datadescriptor(sdfg: SDFG, args: Dict[str, Any], exclude: Optional[Set[str]] = None) -> \ Dict[str, Any]: """ Infers the values of SDFG symbols (not given as arguments) from the shapes and strides of input arguments (e.g., arrays). :param sdfg: The SDFG that is being called. :param args: A dictionary mapping from current argument names to their values. This may also include symbols. :param exclude: An optional set of symbols to ignore on inference. :return: A dictionary mapping from symbol names that are not in ``args`` to their inferred values. :raise ValueError: If symbol values are ambiguous. """ exclude = exclude or set() exclude = set(symbolic.symbol(s) for s in exclude) equations = [] symbols = set() # Collect equations and symbols from arguments and shapes for arg_name, arg_val in args.items(): if arg_name in sdfg.arrays: desc = sdfg.arrays[arg_name] if not hasattr(desc, 'shape') or not hasattr(arg_val, 'shape'): continue symbolic_values = list(desc.shape) + list( getattr(desc, 'strides', [])) given_values = list(arg_val.shape) given_strides = [] if hasattr(arg_val, 'strides'): # NumPy arrays use bytes in strides factor = getattr(arg_val, 'itemsize', 1) given_strides = [s // factor for s in arg_val.strides] given_values += given_strides for sym_dim, real_dim in zip(symbolic_values, given_values): repldict = {} for sym in symbolic.symlist(sym_dim).values(): newsym = symbolic.symbol('__SOLVE_' + str(sym)) if str(sym) in args: exclude.add(newsym) else: symbols.add(newsym) exclude.add(sym) repldict[sym] = newsym # Replace symbols with __SOLVE_ symbols so as to allow # the same symbol in the called SDFG if repldict: sym_dim = sym_dim.subs(repldict) equations.append(sym_dim - real_dim) if len(symbols) == 0: return {} # Solve for all at once results = sympy.solve(equations, *symbols, dict=True, exclude=exclude) if len(results) > 1: raise ValueError('Ambiguous values for symbols in inference. ' 'Options: %s' % str(results)) if len(results) == 0: raise ValueError('Cannot infer values for symbols in inference.') result = results[0] if not result: raise ValueError('Cannot infer values for symbols in inference.') # Remove __SOLVE_ prefix return {str(k)[8:]: v for k, v in result.items()}
def apply(self, sdfg: dace.SDFG): # Extract the map and its entry and exit nodes. graph = sdfg.node(self.state_id) map_entry = self.map_entry(sdfg) map_exit = graph.exit_node(map_entry) current_map = map_entry.map # Create new maps new_maps = [ nodes.Map(current_map.label + '_' + str(param), [param], subsets.Range([param_range]), schedule=dtypes.ScheduleType.Sequential) for param, param_range in zip(current_map.params[1:], current_map.range[1:]) ] current_map.params = [current_map.params[0]] current_map.range = subsets.Range([current_map.range[0]]) # Create new map entries and exits entries = [nodes.MapEntry(new_map) for new_map in new_maps] exits = [nodes.MapExit(new_map) for new_map in new_maps] # Create edges, abiding by the following rules: # 1. If there are no edges coming from the outside, use empty memlets # 2. Edges with IN_* connectors replicate along the maps # 3. Edges for dynamic map ranges replicate until reaching range(s) for edge in graph.out_edges(map_entry): graph.remove_edge(edge) graph.add_memlet_path(map_entry, *entries, edge.dst, src_conn=edge.src_conn, memlet=edge.data, dst_conn=edge.dst_conn) # Modify dynamic map ranges dynamic_edges = dace.sdfg.dynamic_map_inputs(graph, map_entry) for edge in dynamic_edges: # Remove old edge and connector graph.remove_edge(edge) edge.dst.remove_in_connector(edge.dst_conn) # Propagate to each range it belongs to path = [] for mapnode in [map_entry] + entries: path.append(mapnode) if any(edge.dst_conn in map(str, symbolic.symlist(r)) for r in mapnode.map.range): graph.add_memlet_path(edge.src, *path, memlet=edge.data, src_conn=edge.src_conn, dst_conn=edge.dst_conn) # Create new map exits for edge in graph.in_edges(map_exit): graph.remove_edge(edge) graph.add_memlet_path(edge.src, *exits[::-1], map_exit, memlet=edge.data, src_conn=edge.src_conn, dst_conn=edge.dst_conn) from dace.sdfg.scope import ScopeTree scope = None queue: List[ScopeTree] = graph.scope_leaves() while len(queue) > 0: tnode = queue.pop() if tnode.entry == entries[-1]: scope = tnode break elif tnode.parent is not None: queue.append(tnode.parent) else: raise ValueError('Cannot find scope in state') consolidate_edges(sdfg, scope) return [map_entry] + entries
def infer_symbols_from_shapes(sdfg: SDFG, args: Dict[str, Any], exclude: Optional[Set[str]] = None) -> \ Dict[str, Any]: """ Infers the values of SDFG symbols (not given as arguments) from the shapes of input arguments (e.g., arrays). :param sdfg: The SDFG that is being called. :param args: A dictionary mapping from current argument names to their values. This may also include symbols. :param exclude: An optional set of symbols to ignore on inference. :return: A dictionary mapping from symbol names that are not in ``args`` to their inferred values. :raise ValueError: If symbol values are ambiguous. """ exclude = exclude or set() exclude = set(symbolic.symbol(s) for s in exclude) equations = [] symbols = set() # Collect equations and symbols from arguments and shapes for arg_name, arg_val in args.items(): if arg_name in sdfg.arrays: desc = sdfg.arrays[arg_name] if not hasattr(desc, 'shape') or not hasattr(arg_val, 'shape'): continue symbolic_shape = desc.shape given_shape = arg_val.shape for sym_dim, real_dim in zip(symbolic_shape, given_shape): repldict = {} for sym in symbolic.symlist(sym_dim).values(): newsym = symbolic.symbol('__SOLVE_' + str(sym)) if str(sym) in args: exclude.add(newsym) else: symbols.add(newsym) exclude.add(sym) repldict[sym] = newsym # Replace symbols with __SOLVE_ symbols so as to allow # the same symbol in the called SDFG if repldict: sym_dim = sym_dim.subs(repldict) equations.append(sym_dim - real_dim) if len(symbols) == 0: return {} # Solve for all at once results = sympy.solve(equations, *symbols, dict=True, exclude=exclude) if len(results) > 1: raise ValueError('Ambiguous values for symbols in inference. ' 'Options: %s' % str(results)) if len(results) == 0: raise ValueError('Cannot infer values for symbols in inference.') result = results[0] if not result: raise ValueError('Cannot infer values for symbols in inference.') # Fast path (unnecessary) # # For each symbol in each dimension, try to solve an equation # for sym_dim, real_dim in zip(symbolic_shape, given_shape): # for sym in symbolic.symlist(sym_dim): # if sym in inferred_syms and symval != inferred_syms[sym]: # raise ValueError('Ambiguous value for symbol %s in argument ' # '%s: can be either %d or %d' % ( # sym, arg_name, inferred_syms[sym], symval)) # Remove __SOLVE_ prefix return {str(k)[8:]: v for k, v in result.items()}
def apply(self, sdfg): graph = sdfg.nodes()[self.state_id] subgraph = self.subgraph_view(sdfg) map_entries = helpers.get_outermost_scope_maps(sdfg, graph, subgraph) result = StencilTiling.topology(sdfg, graph, map_entries) (children_dict, parent_dict, sink_maps) = result # next up, calculate inferred ranges for each map # for each map entry, this contains a tuple of dicts: # each of those maps from data_name of the array to # inferred outer ranges. An inferred outer range is created # by taking the union of ranges of inner subsets corresponding # to that data and substituting this subset by the min / max of the # parametrized map boundaries # finally, from these outer ranges we can easily calculate # strides and tile sizes required for every map inferred_ranges = defaultdict(dict) # create array of reverse topologically sorted map entries # to iterate over topo_reversed = [] queue = set(sink_maps.copy()) while len(queue) > 0: element = next(e for e in queue if not children_dict[e] - set(topo_reversed)) topo_reversed.append(element) queue.remove(element) for parent in parent_dict[element]: queue.add(parent) # main loop # first get coverage dicts for each map entry # for each map, contains a tuple of two dicts # each of those two maps from data name to outer range coverage = {} for map_entry in map_entries: coverage[map_entry] = StencilTiling.coverage_dicts( sdfg, graph, map_entry, outer_range=True) # we have a mapping from data name to outer range # however we want a mapping from map parameters to outer ranges # for this we need to find out how all array dimensions map to # outer ranges variable_mapping = defaultdict(list) for map_entry in topo_reversed: map = map_entry.map # first find out variable mapping for e in itertools.chain( graph.out_edges(map_entry), graph.in_edges(graph.exit_node(map_entry))): mapping = [] for dim in e.data.subset: syms = set() for d in dim: syms |= symbolic.symlist(d).keys() if len(syms) > 1: raise NotImplementedError( "One incoming or outgoing stencil subset is indexed " "by multiple map parameters. " "This is not supported yet.") try: mapping.append(syms.pop()) except KeyError: # just append None if there is no map symbol in it. # we don't care for now. mapping.append(None) if e.data in variable_mapping: # assert that this is the same everywhere. # else we might run into problems assert variable_mapping[e.data.data] == mapping else: variable_mapping[e.data.data] = mapping # now do mapping data name -> outer range # and from that infer mapping variable -> outer range local_ranges = {dn: None for dn in coverage[map_entry][1].keys()} for data_name, cov in coverage[map_entry][1].items(): local_ranges[data_name] = subsets.union( local_ranges[data_name], cov) # now look at proceeding maps # and union those subsets -> could be larger with stencil indent for child_map in children_dict[map_entry]: if data_name in coverage[child_map][0]: local_ranges[data_name] = subsets.union( local_ranges[data_name], coverage[child_map][0][data_name]) # final assignent: combine local_ranges and variable_mapping # together into inferred_ranges inferred_ranges[map_entry] = {p: None for p in map.params} for data_name, ranges in local_ranges.items(): for param, r in zip(variable_mapping[data_name], ranges): # create new range from this subset and assign rng = subsets.Range((r, )) if param: inferred_ranges[map_entry][param] = subsets.union( inferred_ranges[map_entry][param], rng) # get parameters -- should all be the same params = next(iter(map_entries)).map.params.copy() # define reference range as inferred range of one of the sink maps self.reference_range = inferred_ranges[next(iter(sink_maps))] if self.debug: print("StencilTiling::Reference Range", self.reference_range) # next up, search for the ranges that don't change invariant_dims = [] for idx, p in enumerate(params): different = False if self.reference_range[p] is None: invariant_dims.append(idx) warnings.warn( f"StencilTiling::No Stencil pattern detected for parameter {p}" ) continue for m in map_entries: if inferred_ranges[m][p] != self.reference_range[p]: different = True break if not different: invariant_dims.append(idx) warnings.warn( f"StencilTiling::No Stencil pattern detected for parameter {p}" ) # during stripmining, we will create new outer map entries # for easy access self._outer_entries = set() # with inferred_ranges constructed, we can begin to strip mine for map_entry in map_entries: # Retrieve map entry and exit nodes. map = map_entry.map stripmine_subgraph = { StripMining._map_entry: graph.nodes().index(map_entry) } sdfg_id = sdfg.sdfg_id last_map_entry = None original_schedule = map_entry.schedule self.tile_sizes = [] self.tile_offset_lower = [] self.tile_offset_upper = [] # strip mining each dimension where necessary removed_maps = 0 for dim_idx, param in enumerate(map_entry.map.params): # get current_node tile size if dim_idx >= len(self.strides): tile_stride = symbolic.pystr_to_symbolic(self.strides[-1]) else: tile_stride = symbolic.pystr_to_symbolic( self.strides[dim_idx]) trivial = False if dim_idx in invariant_dims: self.tile_sizes.append(tile_stride) self.tile_offset_lower.append(0) self.tile_offset_upper.append(0) else: target_range_current = inferred_ranges[map_entry][param] reference_range_current = self.reference_range[param] min_diff = symbolic.SymExpr(reference_range_current.min_element()[0] \ - target_range_current.min_element()[0]) max_diff = symbolic.SymExpr(target_range_current.max_element()[0] \ - reference_range_current.max_element()[0]) try: min_diff = symbolic.evaluate(min_diff, {}) max_diff = symbolic.evaluate(max_diff, {}) except TypeError: raise RuntimeError("Symbolic evaluation of map " "ranges failed. Please check " "your parameters and match.") self.tile_sizes.append(tile_stride + max_diff + min_diff) self.tile_offset_lower.append( symbolic.pystr_to_symbolic(str(min_diff))) self.tile_offset_upper.append( symbolic.pystr_to_symbolic(str(max_diff))) # get calculated parameters tile_size = self.tile_sizes[-1] dim_idx -= removed_maps # If map or tile sizes are trivial, skip strip-mining map dimension # special cases: # if tile size is trivial AND we have an invariant dimension, skip if tile_size == map.range.size()[dim_idx] and ( dim_idx + removed_maps) in invariant_dims: continue # trivial map: we just continue if map.range.size()[dim_idx] in [0, 1]: continue if tile_size == 1 and tile_stride == 1 and ( dim_idx + removed_maps) in invariant_dims: trivial = True removed_maps += 1 # indent all map ranges accordingly and then perform # strip mining on these. Offset inner maps accordingly afterwards range_tuple = (map.range[dim_idx][0] + self.tile_offset_lower[-1], map.range[dim_idx][1] - self.tile_offset_upper[-1], map.range[dim_idx][2]) map.range[dim_idx] = range_tuple stripmine = StripMining(sdfg_id, self.state_id, stripmine_subgraph, 0) stripmine.tiling_type = 'ceilrange' stripmine.dim_idx = dim_idx stripmine.new_dim_prefix = self.prefix if not trivial else '' # use tile_stride for both -- we will extend # the inner tiles later stripmine.tile_size = str(tile_stride) stripmine.tile_stride = str(tile_stride) outer_map = stripmine.apply(sdfg) outer_map.schedule = original_schedule # apply to the new map the schedule of the original one map_entry.schedule = self.schedule # if tile stride is 1, we can make a nice simplification by just # taking the overapproximated inner range as inner range # this eliminates the min/max in the range which # enables loop unrolling if tile_stride == 1: map_entry.range[dim_idx] = tuple( symbolic.SymExpr(el._approx_expr) if isinstance( el, symbolic.SymExpr) else el for el in map_entry.range[dim_idx]) # in map_entry: enlarge tiles by upper and lower offset # doing it this way and not via stripmine strides ensures # that the max gets changed as well old_range = map_entry.range[dim_idx] map_entry.range[dim_idx] = ((old_range[0] - self.tile_offset_lower[-1]), (old_range[1] + self.tile_offset_upper[-1]), old_range[2]) # We have to propagate here for correct outer volume and subset sizes _propagate_node(graph, map_entry) _propagate_node(graph, graph.exit_node(map_entry)) # usual tiling pipeline if last_map_entry: new_map_entry = graph.in_edges(map_entry)[0].src mapcollapse_subgraph = { MapCollapse._outer_map_entry: graph.node_id(last_map_entry), MapCollapse._inner_map_entry: graph.node_id(new_map_entry) } mapcollapse = MapCollapse(sdfg_id, self.state_id, mapcollapse_subgraph, 0) mapcollapse.apply(sdfg) last_map_entry = graph.in_edges(map_entry)[0].src # add last instance of map entries to _outer_entries if last_map_entry: self._outer_entries.add(last_map_entry) # Map Unroll Feature: only unroll if conditions are met: # Only unroll if at least one of the inner map ranges is strictly larger than 1 # Only unroll if strides all are one if self.unroll_loops and all(s == 1 for s in self.strides) and any( s not in [0, 1] for s in map_entry.range.size()): l = len(map_entry.params) if l > 1: subgraph = { MapExpansion.map_entry: graph.nodes().index(map_entry) } trafo_expansion = MapExpansion(sdfg.sdfg_id, sdfg.nodes().index(graph), subgraph, 0) trafo_expansion.apply(sdfg) maps = [map_entry] for _ in range(l - 1): map_entry = graph.out_edges(map_entry)[0].dst maps.append(map_entry) for map in reversed(maps): # MapToForLoop subgraph = { MapToForLoop._map_entry: graph.nodes().index(map) } trafo_for_loop = MapToForLoop(sdfg.sdfg_id, sdfg.nodes().index(graph), subgraph, 0) trafo_for_loop.apply(sdfg) nsdfg = trafo_for_loop.nsdfg # LoopUnroll guard = trafo_for_loop.guard end = trafo_for_loop.after_state begin = next(e.dst for e in nsdfg.out_edges(guard) if e.dst != end) subgraph = { DetectLoop._loop_guard: nsdfg.nodes().index(guard), DetectLoop._loop_begin: nsdfg.nodes().index(begin), DetectLoop._exit_state: nsdfg.nodes().index(end) } transformation = LoopUnroll(0, 0, subgraph, 0) transformation.apply(nsdfg) elif self.unroll_loops: warnings.warn( "Did not unroll loops. Either all ranges are equal to " "one or range difference is symbolic.") self._outer_entries = list(self._outer_entries)
def can_be_applied(sdfg, subgraph) -> bool: # get highest scope maps graph = subgraph.graph map_entries = set( helpers.get_outermost_scope_maps(sdfg, graph, subgraph)) # 1.1: There has to be more than one outermost scope map entry if len(map_entries) <= 1: return False # 1.2: check basic constraints: # - all parameters have to be the same (this implies same length) # - no parameter permutations here as ambiguity is very high then # - same strides everywhere first_map = next(iter(map_entries)) params = dcpy(first_map.map.params) strides = first_map.map.range.strides() schedule = first_map.map.schedule for map_entry in map_entries: if map_entry.map.params != params: return False if map_entry.map.range.strides() != strides: return False if map_entry.map.schedule != schedule: return False # 1.3: check whether all map entries only differ by a const amount first_entry = next(iter(map_entries)) for map_entry in map_entries: for r1, r2 in zip(map_entry.map.range, first_entry.map.range): if len((r1[0] - r2[0]).free_symbols) > 0: return False if len((r1[1] - r2[1]).free_symbols) > 0: return False # get intermediate_nodes, out_nodes from SubgraphFusion Transformation node_config = SubgraphFusion.get_adjacent_nodes( sdfg, graph, map_entries) (_, intermediate_nodes, out_nodes) = node_config # 1.4: check topological feasibility if not SubgraphFusion.check_topo_feasibility( sdfg, graph, map_entries, intermediate_nodes, out_nodes): return False # 1.5 nodes that are both intermediate and out nodes # are not supported in StencilTiling if len(intermediate_nodes & out_nodes) > 0: return False # get coverages for every map entry coverages = {} memlets = {} for map_entry in map_entries: coverages[map_entry] = StencilTiling.coverage_dicts( sdfg, graph, map_entry) memlets[map_entry] = StencilTiling.coverage_dicts( sdfg, graph, map_entry, outer_range=False) # get DAG neighbours for each map dag_neighbors = StencilTiling.topology(sdfg, graph, map_entries) (children_dict, _, sink_maps) = dag_neighbors # 1.6: we now check coverage: # each outgoing coverage for a data memlet has to # be exactly equal to the union of incoming coverages # of all chidlren map memlets of this data # important: # 1. it has to be equal and not only cover it in order to # account for ranges too long # 2. we check coverages by map parameter and not by # array, this way it is even more general # 3. map parameter coverages are checked for each # (map_entry, children of this map_entry) - pair for map_entry in map_entries: # get coverage from current map_entry map_coverage = coverages[map_entry][1] # final mapping map_parameter -> coverage will be stored here param_parent_coverage = {p: None for p in map_entry.params} param_children_coverage = {p: None for p in map_entry.params} for child_entry in children_dict[map_entry]: # get mapping data_name -> coverage for (data_name, cov) in map_coverage.items(): parent_coverage = cov children_coverage = None if data_name in coverages[child_entry][0]: children_coverage = subsets.union( children_coverage, coverages[child_entry][0][data_name]) # extend mapping map_parameter -> coverage # by the previous mapping for i, (p_subset, c_subset) in enumerate( zip(parent_coverage, children_coverage)): # transform into subset p_subset = subsets.Range((p_subset, )) c_subset = subsets.Range((c_subset, )) # get associated parameter in memlet params1 = symbolic.symlist( memlets[map_entry][1][data_name][i]).keys() params2 = symbolic.symlist( memlets[child_entry][0][data_name][i]).keys() if params1 != params2: return False params = params1 if len(params) > 1: # this is not supported return False try: symbol = next(iter(params)) param_parent_coverage[symbol] = subsets.union( param_parent_coverage[symbol], p_subset) param_children_coverage[symbol] = subsets.union( param_children_coverage[symbol], c_subset) except StopIteration: # current dim has no symbol associated. # ignore and continue warnings.warn( f"In map {map_entry}, there is a " "dimension belonging to {data_name} " "that has no map parameter associated.") pass # 1.6: parameter mapping must be the same if param_parent_coverage != param_children_coverage: return False # 1.7: we want all sink maps to have the same range size assert len(sink_maps) > 0 first_sink_map = next(iter(sink_maps)) if not all([ map.range.size() == first_sink_map.range.size() for map in sink_maps ]): return False return True
def free_symbols(self) -> Set[str]: result = set() for dim in self.ranges: for d in dim: result |= symbolic.symlist(d).keys() return result
def free_symbols(self) -> Set[str]: result = set() for dim in self.indices: result |= symbolic.symlist(dim).keys() return result
def free_symbols(self): result = {} for dim in self.ranges: for d in dim: result.update(symbolic.symlist(d)) return result
def reduce_iteration_count(begin, end, step, rparams: dict): # There are different rules when expanding depending on where the expand # should happen if isinstance(begin, int): start_syms = [] else: start_syms = symbolic.symlist(begin).keys() if isinstance(end, int): end_syms = [] else: end_syms = symbolic.symlist(end).keys() if isinstance(step, int): step_syms = [] else: step_syms = symbolic.symlist(step).keys() def intersection(lista, listb): return [x for x in lista if x in listb] start_dyn_syms = intersection(start_syms, rparams.keys()) end_dyn_syms = intersection(end_syms, rparams.keys()) step_dyn_syms = intersection(step_syms, rparams.keys()) def replace_func(element, dyn_syms, retparams): # Resolve all symbols using the retparams-dict for x in dyn_syms: target = sp.functions.Min( retparams[x] * (retparams[x] - 1) / 2, 0) bstr = str(element) element = symbolic.pystr_to_symbolic(bstr) element = element.subs( x, target) # Add the classic sum formula; going upwards # To not have hidden elements that get added again later, we # also replace the values in the other itvars... for k, v in retparams.items(): newv = symbolic.pystr_to_symbolic(str(v)) tarsyms = symbolic.symbols_in_sympy_expr(target).keys() if x in tarsyms: continue tmp = newv.subs(x, target) if tmp != v: retparams[k] = tmp return element if len(start_dyn_syms) > 0: pass begin = replace_func(begin, start_dyn_syms, rparams) if len(end_dyn_syms) > 0: pass end = replace_func(end, end_dyn_syms, rparams) if len(step_dyn_syms) > 0: pass print("Dynamic step symbols %s!" % str(step)) raise NotImplementedError return begin, end, step
def free_symbols(self): result = {} for dim in self.indices: result.update(symbolic.symlist(dim)) return result