def estimate_memory(handle, mode='realistic'): """ Estimate the number of memory reads and writes. :param handle: a SymPy expression or an iterator of SymPy expressions. :param mode: Mode for computing the estimate: Estimate ``mode`` might be any of: :: * ideal: Also known as "compulsory traffic", which is the minimum number of read/writes to be performed (ie, models an infinite cache). * ideal_with_stores: Like ideal, but a data item which is both read. and written is counted twice (ie both load an store are counted). * realistic: Assume that all datasets, even the time-independent ones, need to be re-read at each time iteration. """ assert mode in ['ideal', 'ideal_with_stores', 'realistic'] def access(symbol): assert isinstance(symbol, Indexed) # Irregular accesses (eg A[B[i]]) are counted as compulsory traffic if any(i.atoms(Indexed) for i in symbol.indices): return symbol else: return symbol.base try: # Is it a plain SymPy object ? iter(handle) except TypeError: handle = [handle] if mode in ['ideal', 'ideal_with_stores']: filter = lambda s: t in s.atoms() else: filter = lambda s: s reads = set(flatten([retrieve_indexed(e.rhs) for e in handle])) writes = set(flatten([retrieve_indexed(e.lhs) for e in handle])) reads = set([access(s) for s in reads if filter(s)]) writes = set([access(s) for s in writes if filter(s)]) if mode == 'ideal': return len(set(reads) | set(writes)) else: return len(reads) + len(writes)
def extract(cls, expr): """ Compute the stencil of ``expr``. """ assert expr.is_Equality # Collect all indexed objects appearing in /expr/ indexed = list(retrieve_indexed(expr.lhs)) indexed += list(retrieve_indexed(expr.rhs)) indexed += flatten([retrieve_indexed(i) for i in e.indices] for e in indexed) # Enforce deterministic ordering dims = [] for e in indexed: d = [] for a in e.indices: found = [ idx for idx in a.free_symbols if isinstance(idx, Dimension) ] d.extend([idx for idx in found if idx not in d]) dims.append(tuple(d)) stencil = Stencil([(i, set()) for i in partial_order(dims)]) # Determine the points accessed along each dimension for e in indexed: for a in e.indices: if isinstance(a, Dimension): stencil[a].update([0]) d = None off = [0] for idx in a.args: if isinstance(idx, Dimension): d = idx elif idx.is_integer: off += [idx] if d is not None: stencil[d].update(off) return stencil
def is_index(self, key): """ Return True if ``key`` is used as array index in an expression of the TemporariesGraph, False otherwise. """ if key not in self: return False match = key.base.label if self[key].is_tensor else key for i in self.extract(key, readby=True): for e in retrieve_indexed(i): if any(match in idx.free_symbols for idx in e.indices): return True return False
def tensors(self): """ Return all occurrences of the tensors in ``self`` keyed by function. """ mapper = {} for v in self.values(): handle = retrieve_indexed(v) for i in handle: found = mapper.setdefault(i.base.function, []) if i not in found: # Not using sets to preserve order found.append(i) return mapper
def q_indirect(expr): """ Return True if ``indexed`` has indirect accesses, False otherwise. Examples ======== a[i] --> False a[b[i]] --> True """ from devito.dse.search import retrieve_indexed if not q_indexed(expr): return False return any(retrieve_indexed(i) for i in expr.indices)
def extract(cls, expr): """ Compute the stencil of ``expr``. """ assert expr.is_Equality # Collect all indexed objects appearing in /expr/ terminals = retrieve_terminals(expr, mode='all') indexeds = [i for i in terminals if q_indexed(i)] indexeds += flatten([retrieve_indexed(i) for i in e.indices] for e in indexeds) # Enforce deterministic dimension ordering... dims = OrderedDict() for e in terminals: if isinstance(e, Dimension): dims[(e, )] = e elif q_indexed(e): d = [] for a in e.indices: found = [ i for i in a.free_symbols if isinstance(i, Dimension) ] d.extend([i for i in found if i not in d]) dims[tuple(d)] = e # ... giving higher priority to TimeData objects; time always go first dims = sorted(list(dims), key=lambda i: not (isinstance(dims[i], Dimension) or dims[i].base.function.is_TimeData)) stencil = Stencil([(i, set()) for i in partial_order(dims)]) # Determine the points accessed along each dimension for e in indexeds: for a in e.indices: if isinstance(a, Dimension): stencil[a].update([0]) d = None off = [0] for i in a.args: if isinstance(i, Dimension): d = i elif i.is_integer: off += [i] if d is not None: stencil[d].update(off) return stencil
def create_alias(expr, offsets): """ Create an aliasing expression of ``expr`` by replacing the offsets of each indexed object in ``expr`` with the new values in ``offsets``. ``offsets`` is an ordered sequence of tuples with as many elements as the number of indexed objects in ``expr``. """ indexeds = retrieve_indexed(expr, mode='all') assert len(indexeds) == len(offsets) mapper = {} for indexed, ofs in zip(indexeds, offsets): base = indexed.base dimensions = base.function.indices assert len(dimensions) == len(ofs) mapper[indexed] = indexed.func(base, *[sum(i) for i in zip(dimensions, ofs)]) return expr.xreplace(mapper)
def time_invariant(self, expr=None): """ Check if ``expr`` is time invariant. ``expr`` may be an expression ``e`` explicitly tracked by the TemporariesGraph or even a generic subexpression of ``e``. If no ``expr`` is provided, then time invariance of the entire TemporariesGraph is assessed. """ if expr is None: return all(self.time_invariant(v) for v in self.values()) if any(i in expr.free_symbols for i in self.time_indices): return False queue = [expr.rhs] if expr.is_Equality else [expr] while queue: item = queue.pop() for i in retrieve_indexed(item): if any(j in i.free_symbols for j in self.time_indices): return False temporaries = [i for i in item.free_symbols if i in self] queue.extend( [self[i].rhs for i in temporaries if self[i].rhs != item]) return True
def collect_aliases(exprs): """ Determine groups of aliasing expressions in ``exprs``. An expression A aliases an expression B if both A and B apply the same operations to the same input operands, with the possibility for indexed objects to index into locations at a fixed constant offset in each dimension. For example: :: exprs = (a[i+1] + b[i+1], a[i+1] + b[j+1], a[i] + c[i], a[i+2] - b[i+2], a[i+2] + b[i], a[i-1] + b[i-1]) The following expressions in ``exprs`` alias to ``a[i] + b[i]``: :: a[i+1] + b[i+1] : same operands and operations, distance along i = 1 a[i-1] + b[i-1] : same operands and operations, distance along i = -1 Whereas the following do not: :: a[i+1] + b[j+1] : because at least one index differs a[i] + c[i] : because at least one of the operands differs a[i+2] - b[i+2] : because at least one operation differs a[i+2] + b[i] : because distance along ``i`` differ (+2 and +0) """ ExprData = namedtuple('ExprData', 'dimensions offsets') # Discard expressions that surely won't alias to anything candidates = OrderedDict() for expr in exprs: indexeds = retrieve_indexed(expr, mode='all') if indexeds and not any(q_indirect(i) for i in indexeds): handle = calculate_offsets(indexeds) if handle: candidates[expr.rhs] = ExprData(*handle) aliases = OrderedDict() mapper = OrderedDict() unseen = list(candidates) while unseen: # Find aliasing expressions handle = unseen.pop(0) group = [handle] for e in list(unseen): if compare(handle, e) and\ is_translated(candidates[handle].offsets, candidates[e].offsets): group.append(e) unseen.remove(e) mapper.update([(i, group) for i in group]) if len(group) == 1: continue # Create an alias for the group of aliasing expressions, as well as # any metadata needed by the caller offsets = [tuple(candidates[e].offsets) for e in group] COM, distances = calculate_COM(offsets) alias = create_alias(handle, COM) aliases[alias] = Alias(alias, group, distances, candidates[handle].dimensions) # Heuristically attempt to relax the aliases offsets # to maximize the likelyhood of loop fusion grouped = OrderedDict() for i in aliases.values(): grouped.setdefault(i.dimensions, []).append(i) for dimensions, group in grouped.items(): ideal_anti_stencil = Stencil.union(*[i.anti_stencil for i in group]) for i in group: if i.anti_stencil.subtract(ideal_anti_stencil).empty: aliases[i.alias] = i.relax(ideal_anti_stencil) return mapper, aliases
def collect(exprs): """ Determine groups of aliasing expressions in ``exprs``. An expression A aliases an expression B if both A and B apply the same operations to the same input operands, with the possibility for indexed objects to index into locations at a fixed constant offset in each dimension. For example: :: exprs = (a[i+1] + b[i+1], a[i+1] + b[j+1], a[i] + c[i], a[i+2] - b[i+2], a[i+2] + b[i], a[i-1] + b[i-1]) The following expressions in ``exprs`` alias to ``a[i] + b[i]``: :: a[i+1] + b[i+1] : same operands and operations, distance along i = 1 a[i-1] + b[i-1] : same operands and operations, distance along i = -1 Whereas the following do not: :: a[i+1] + b[j+1] : because at least one index differs a[i] + c[i] : because at least one of the operands differs a[i+2] - b[i+2] : because at least one operation differs a[i+2] + b[i] : because distance along ``i`` differ (+2 and +0) """ ExprData = namedtuple('ExprData', 'dimensions offsets') # Discard expressions: # - that surely won't alias to anything # - that are non-scalar candidates = OrderedDict() for expr in exprs: if q_indexed(expr): continue indexeds = retrieve_indexed(expr.rhs, mode='all') if indexeds and not any(q_indirect(i) for i in indexeds): handle = calculate_offsets(indexeds) if handle: candidates[expr.rhs] = ExprData(*handle) aliases = OrderedDict() mapper = OrderedDict() unseen = list(candidates) while unseen: # Find aliasing expressions handle = unseen.pop(0) group = [handle] for e in list(unseen): if compare(handle, e) and\ is_translated(candidates[handle].offsets, candidates[e].offsets): group.append(e) unseen.remove(e) mapper.update([(i, group) for i in group]) # Try creating a basis for the aliasing expressions' offsets offsets = [tuple(candidates[e].offsets) for e in group] try: COM, distances = calculate_COM(offsets) except DSEException: # Ignore these potential aliases and move on continue alias = create_alias(handle, COM) # In circumstances in which an expression has repeated coefficients, e.g. # ... + 0.025*a[...] + 0.025*b[...], # We may have found a common basis (i.e., same COM, same alias) at this point v = aliases.setdefault(alias, Alias(alias, candidates[handle].dimensions)) v.extend(group, distances) # Heuristically attempt to relax the aliases offsets # to maximize the likelyhood of loop fusion grouped = OrderedDict() for i in aliases.values(): grouped.setdefault(i.dimensions, []).append(i) for dimensions, group in grouped.items(): ideal_anti_stencil = Stencil.union(*[i.anti_stencil for i in group]) for i in group: if i.anti_stencil.subtract(ideal_anti_stencil).empty: aliases[i.alias] = i.relax(ideal_anti_stencil) return mapper, aliases