def fusion(sdfg: dace.SDFG, graph: dace.SDFGState, subgraph: Union[SubgraphView, List[SubgraphView]] = None, **kwargs): subgraph = graph if not subgraph else subgraph if not isinstance(subgraph, list): subgraph = [subgraph] map_fusion = SubgraphFusion(subgraph[0]) for (property, val) in kwargs.items(): setattr(map_fusion, property, val) for sg in subgraph: map_entries = helpers.get_outermost_scope_maps(sdfg, graph, sg) # remove map_entries and their corresponding exits from the subgraph # already before applying transformation if isinstance(sg, SubgraphView): for map_entry in map_entries: sg.nodes().remove(map_entry) if graph.exit_node(map_entry) in sg.nodes(): sg.nodes().remove(graph.exit_node(map_entry)) print(f"Subgraph Fusion on map entries {map_entries}") map_fusion.fuse(sdfg, graph, map_entries) if isinstance(sg, SubgraphView): sg.nodes().append(map_fusion._global_map_entry)
def apply(self, sdfg, map_base_variables=None): # get lowest scope map entries and expand subgraph = self.subgraph_view(sdfg) graph = subgraph.graph # next, get all the base maps and expand maps = helpers.get_outermost_scope_maps(sdfg, graph, subgraph) self.expand(sdfg, graph, maps, map_base_variables=map_base_variables)
def can_be_applied(self, sdfg: SDFG, subgraph: SubgraphView) -> bool: # get lowest scope maps of subgraph # grab first node and see whether all nodes are in the same graph # (or nested sdfgs therein) graph = subgraph.graph # next, get all the maps by obtaining a copy (for potential offsets) map_entries = helpers.get_outermost_scope_maps(sdfg, graph, subgraph) ranges = [dcpy(map_entry.range) for map_entry in map_entries] # offset if option is toggled if self.allow_offset == True: for r in ranges: r.offset(r.min_element(), negative=True) brng = helpers.common_map_base_ranges(ranges) # more than one outermost scoped map entry has to be availble if len(map_entries) <= 1: return False # check whether any parameters are in common if len(brng) == 0: return False # if option enabled, return false if any splits are introduced if self.permutation_only == True: for map_entry in map_entries: if len(map_entry.params) != len(brng): return False # if option enabled, check contiguity in the last contiguous dimension # if there is a map split ocurring with the last contiguous dimension being # in the *outer* map, we fail (-> bad data access pattern) if self.check_contiguity == True: reassignment = helpers.find_reassignment(map_entries, brng, offset=self.allow_offset) for map_entry in map_entries: no_common = sum( [1 for j in reassignment[map_entry] if j != -1]) if no_common != len(map_entry.params): # check every memlet for access for e in itertools.chain( graph.out_edges(map_entry), graph.in_edges(graph.exit_node(map_entry))): subset = dcpy(e.data.subset) subset.pop([i for i in range(subset.dims() - 1)]) for s in subset.free_symbols: if reassignment[map_entry][ map_entry.map.params.index(s)] != -1: warnings.warn( "MultiExpansion::Contiguity fusion violation detected" ) return False return True
def apply(self, sdfg): subgraph = self.subgraph_view(sdfg) graph = subgraph.graph scope_dict = graph.scope_dict() map_entries = helpers.get_outermost_scope_maps(sdfg, graph, subgraph, scope_dict) first_entry = next(iter(map_entries)) if self.allow_expansion: expansion = MultiExpansion() expansion.setup_match(subgraph, self.sdfg_id, self.state_id) expansion.permutation_only = not self.expansion_split if expansion.can_be_applied(sdfg, subgraph): expansion.apply(sdfg) sf = SubgraphFusion() sf.setup_match(subgraph, self.sdfg_id, self.state_id) if sf.can_be_applied(sdfg, self.subgraph_view(sdfg)): # set SubgraphFusion properties sf.debug = self.debug sf.transient_allocation = self.transient_allocation sf.schedule_innermaps = self.schedule_innermaps sf.apply(sdfg) self._global_map_entry = sf._global_map_entry return elif self.allow_tiling == True: st = StencilTiling() st.setup_match(subgraph, self.sdfg_id, self.state_id) if st.can_be_applied(sdfg, self.subgraph_view(sdfg)): # set StencilTiling properties st.debug = self.debug st.unroll_loops = self.stencil_unroll_loops st.strides = self.stencil_strides st.apply(sdfg) # StencilTiling: update nodes new_entries = st._outer_entries subgraph = helpers.subgraph_from_maps(sdfg, graph, new_entries) sf = SubgraphFusion() sf.setup_match(subgraph, self.sdfg_id, self.state_id) # set SubgraphFusion properties sf.debug = self.debug sf.transient_allocation = self.transient_allocation sf.schedule_innermaps = self.schedule_innermaps sf.apply(sdfg) self._global_map_entry = sf._global_map_entry return warnings.warn("CompositeFusion::Apply did not perform as expected")
def expand_maps(sdfg: dace.SDFG, graph: dace.SDFGState, subgraph: Union[SubgraphView, List[SubgraphView]] = None, **kwargs): subgraph = graph if not subgraph else subgraph if not isinstance(subgraph, list): subgraph = [subgraph] trafo_expansion = MultiExpansion(subgraph[0]) for (property, val) in kwargs.items(): setattr(trafo_expansion, property, val) for sg in subgraph: map_entries = helpers.get_outermost_scope_maps(sdfg, graph, sg) trafo_expansion.expand(sdfg, graph, map_entries)
def __init__(self, sdfg: SDFG, graph: SDFGState, subgraph: SubgraphView = None, condition_function: Callable = None): self._sdfg = sdfg self._graph = graph self._subgraph = subgraph self._scope_children = graph.scope_children() # optional function to decide whether an enumerated # subgraph should be yielded or not self._condition_function = condition_function # get map related information self._map_entries = helpers.get_outermost_scope_maps( sdfg, graph, subgraph) self._max_length = len(self._map_entries)
def can_be_applied(sdfg: SDFG, subgraph: SubgraphView) -> bool: # get lowest scope maps of subgraph # grab first node and see whether all nodes are in the same graph # (or nested sdfgs therein) graph = subgraph.graph for node in subgraph.nodes(): if node not in graph.nodes(): return False # next, get all the maps maps = helpers.get_outermost_scope_maps(sdfg, graph, subgraph) brng = helpers.common_map_base_ranges(maps) # if leq than one map found -> fail if len(maps) <= 1: return False # see whether they have common parameters; if not -> fail if len(brng) == 0: return False return True
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 can_be_applied(sdfg: SDFG, subgraph: SubgraphView) -> bool: ''' Fusible if 1. Maps have the same access sets and ranges in order 2. Any nodes in between two maps are AccessNodes only, without WCR There is at most one AccessNode only on a path between two maps, no other nodes are allowed 3. The exiting memlets' subsets to an intermediate edge must cover the respective incoming memlets' subset into the next map. Also, as a limitation, the union of all exiting memlets' subsets must be contiguous. ''' # get graph graph = subgraph.graph for node in subgraph.nodes(): if node not in graph.nodes(): return False # next, get all the maps map_entries = helpers.get_outermost_scope_maps(sdfg, graph, subgraph) map_exits = [graph.exit_node(map_entry) for map_entry in map_entries] maps = [map_entry.map for map_entry in map_entries] # 1. basic checks: # 1.1 we need to have at least two maps if len(maps) <= 1: return False ''' # 1.2 Special Case: If we can establish a valid permutation, we can # skip check 1.3 permutation = self.find_permutation ''' # 1.3 check whether all maps are the same base_map = maps[0] for map in maps: if map.get_param_num() != base_map.get_param_num(): return False if not all( [p1 == p2 for (p1, p2) in zip(map.params, base_map.params)]): return False if not map.range == base_map.range: return False # 1.3 check whether all map entries have the same schedule schedule = map_entries[0].schedule if not all([entry.schedule == schedule for entry in map_entries]): return False # 2. check intermediate feasiblility # see map_fusion.py for similar checks # with the restrictions below being more relaxed # 2.1 do some preparation work first: # calculate all out_nodes and intermediate_nodes # definition see in apply() node_config = SubgraphFusion.get_adjacent_nodes(sdfg, graph, map_entries) _, intermediate_nodes, out_nodes = node_config # 2.2 topological feasibility: if not SubgraphFusion.check_topo_feasibility( sdfg, graph, map_entries, intermediate_nodes, out_nodes): return False # 2.3 memlet feasibility # For each intermediate node, look at whether inner adjacent # memlets of the exiting map cover inner adjacent memlets # of the next entering map. # We also check for any WCRs on the fly. for node in intermediate_nodes: upper_subsets = set() lower_subsets = set() # First, determine which dimensions of the memlet ranges # change with the map, we do not need to care about the other dimensions. try: dims_to_discard = SubgraphFusion.get_invariant_dimensions( sdfg, graph, map_entries, map_exits, node) except NotImplementedError: return False # find upper_subsets for in_edge in graph.in_edges(node): in_in_edge = graph.memlet_path(in_edge)[-2] # first check for WCRs if in_edge.data.wcr: # check whether the WCR is actually produced at # this edge or further up in the memlet path. If not, # we can still fuse! subset_params = set( [str(s) for s in in_in_edge.data.subset.free_symbols]) if any([ p not in subset_params for p in in_edge.src.map.params ]): return False if in_edge.src in map_exits: subset_to_add = dcpy(in_in_edge.data.subset\ if in_in_edge.data.data == node.data\ else in_in_edge.data.other_subset) subset_to_add.pop(dims_to_discard) upper_subsets.add(subset_to_add) else: raise NotImplementedError("Nodes between two maps to be" "fused with *incoming* edges" "from outside the maps are not" "allowed yet.") # find lower_subsets for out_edge in graph.out_edges(node): if out_edge.dst in map_entries: # cannot use memlet tree here as there could be # not just one map succedding. Do it manually for oedge in graph.out_edges(out_edge.dst): if oedge.src_conn[3:] == out_edge.dst_conn[2:]: subset_to_add = dcpy(oedge.data.subset \ if oedge.data.data == node.data \ else oedge.data.other_subset) subset_to_add.pop(dims_to_discard) lower_subsets.add(subset_to_add) # We assume that upper_subsets are contiguous # Check for this. try: contiguous_upper = find_contiguous_subsets(upper_subsets) if len(contiguous_upper) > 1: return False except TypeError: warnings.warn( 'Could not determine whether subset is continuous.' 'Exiting Check with False.') return False # now take union of upper subsets upper_iter = iter(upper_subsets) union_upper = next(upper_iter) for subs in upper_iter: union_upper = subsets.union(union_upper, subs) if not union_upper: # something went wrong using union -- we'd rather abort return False # finally check coverage # every lower subset must be completely covered by union_upper for lower_subset in lower_subsets: if not union_upper.covers(lower_subset): return False return True
def apply(self, sdfg, do_not_override=None, **kwargs): subgraph = self.subgraph_view(sdfg) graph = subgraph.graph map_entries = helpers.get_outermost_scope_maps(sdfg, graph, subgraph) self.fuse(sdfg, graph, map_entries, do_not_override, **kwargs)