def calculate_COM(group): """ Determine a centre of mass (COM) for a group of definitely aliasing expressions, which is a set of bases spanning all iteration vectors. Return the COM as well as the vector distance of each aliasing expression from the COM. """ # Find the COM COM = [] for ofs in zip(*[i.offsets for i in group]): Tofs = LabeledVector.transpose(*ofs) entries = [] for k, v in Tofs: try: entries.append((k, int(np.mean(v, dtype=int)))) except TypeError: # At least an element in `v` has symbolic components. Even though # `analyze` guarantees that no accesses can be irregular, a symbol # might still be present as long as it's constant (i.e., known to # be never written to). For example: `A[t, x_m + 2, y, z]` # At this point, the only chance we have is that the symbolic entry # is identical across all elements in `v` if len(set(v)) == 1: entries.append((k, v[0])) else: raise ValueError COM.append(LabeledVector(entries)) # Calculate the distance from the COM distances = [] for i in group: assert len(COM) == len(i.offsets) distance = [o.distance(c) for o, c in zip(i.offsets, COM)] distance = [(l, set(i)) for l, i in LabeledVector.transpose(*distance)] # The distance of each Indexed from the COM must be uniform across all Indexeds if any(len(i) != 1 for l, i in distance): raise ValueError distances.append(LabeledVector([(l, i.pop()) for l, i in distance])) return COM, distances
def analyze(expr): """ Determine whether ``expr`` is a potential Alias and collect relevant metadata. A necessary condition is that all Indexeds in ``expr`` are affine in the access Dimensions so that the access offsets (or "strides") can be derived. For example, given the following Indexeds: :: A[i, j, k], B[i, j+2, k+3], C[i-1, j+4] All of the access functions all affine in ``i, j, k``, and the offsets are: :: (0, 0, 0), (0, 2, 3), (-1, 4) """ # No way if writing to a tensor or an increment if expr.lhs.is_Indexed or expr.is_Increment: return indexeds = retrieve_indexed(expr.rhs) if not indexeds: return bases = [] offsets = [] for i in indexeds: ii = IterationInstance(i) # There must not be irregular accesses, otherwise we won't be able to # calculate the offsets if ii.is_irregular: return # Since `ii` is regular (and therefore affine), it is guaranteed that `ai` # below won't be None base = [] offset = [] for e, ai in zip(ii, ii.aindices): if e.is_Number: base.append(e) else: base.append(ai) offset.append((ai, e - ai)) bases.append(tuple(base)) offsets.append(LabeledVector(offset)) return Candidate(expr.rhs, indexeds, bases, offsets)
def collect(exprs, min_storage, ignore_collected): """ Find groups of aliasing expressions. We shall introduce the following (loose) terminology: * A ``terminal`` is the leaf of a mathematical operation. Terminals can be numbers (n), literals (l), or Indexeds (I). * ``R`` is the relaxation operator := ``R(n) = n``, ``R(l) = l``, ``R(I) = J``, where ``J`` has the same base as ``I`` but with all offsets stripped away. For example, ``R(a[i+2,j-1]) = a[i,j]``. * A ``relaxed expression`` is an expression in which all of the terminals are relaxed. Now we define the concept of aliasing. We say that an expression A aliases an expression B if: * ``R(A) == R(B)`` * all pairwise Indexeds in A and B access memory locations at a fixed constant distance along each Dimension. For example, consider the following expressions: * 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] Out of the expressions above, the following 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 the distances along ``i`` differ (+2 and +0) """ # Find the potential aliases found = [] for expr in exprs: if expr.lhs.is_Indexed or expr.is_Increment: continue indexeds = retrieve_indexed(expr.rhs) bases = [] offsets = [] for i in indexeds: ii = IterationInstance(i) if ii.is_irregular: break base = [] offset = [] for e, ai in zip(ii, ii.aindices): if q_constant(e): base.append(e) else: base.append(ai) offset.append((ai, e - ai)) bases.append(tuple(base)) offsets.append(LabeledVector(offset)) if indexeds and len(bases) == len(indexeds): found.append(Candidate(expr, indexeds, bases, offsets)) # Create groups of aliasing expressions mapper = OrderedDict() unseen = list(found) while unseen: c = unseen.pop(0) group = [c] for u in list(unseen): # Is the arithmetic structure of `c` and `u` equivalent ? if not compare_ops(c.expr, u.expr): continue # Is `c` translated w.r.t. `u` ? if not c.translated(u): continue group.append(u) unseen.remove(u) group = Group(group) # Apply callback to heuristically discard groups if ignore_collected(group): continue if min_storage: k = group.dimensions_translated else: k = group.dimensions mapper.setdefault(k, []).append(group) aliases = Aliases() for _groups in list(mapper.values()): groups = list(_groups) while groups: # For each Dimension, determine the Minimum Intervals (MI) spanning # all of the Groups diameters # Example: x's largest_diameter=2 => [x[-2,0], x[-1,1], x[0,2]] # Note: Groups that cannot evaluate their diameter are dropped mapper = defaultdict(int) for g in list(groups): try: mapper.update({d: max(mapper[d], v) for d, v in g.diameter.items()}) except ValueError: groups.remove(g) intervalss = {d: make_rotations_table(d, v) for d, v in mapper.items()} # For each Group, find a rotation that is compatible with a given MI mapper = {} for d, intervals in intervalss.items(): for interval in list(intervals): found = {g: g.find_rotation_distance(d, interval) for g in groups} if all(distance is not None for distance in found.values()): # `interval` is OK ! mapper[interval] = found break if len(mapper) == len(intervalss): break # Try again with fewer groups smallest = len(min(groups, key=len)) groups = [g for g in groups if len(g) > smallest] for g in groups: c = g.pivot distances = defaultdict(int, [(i.dim, v[g]) for i, v in mapper.items()]) # Create the basis alias offsets = [LabeledVector([(l, v[l] + distances[l]) for l in v.labels]) for v in c.offsets] subs = {i: i.function[[l + v.fromlabel(l, 0) for l in b]] for i, b, v in zip(c.indexeds, c.bases, offsets)} alias = uxreplace(c.expr, subs) # All aliased expressions aliaseds = [i.expr for i in g] # Distance of each aliased expression from the basis alias distances = [] for i in g: distance = [o.distance(v) for o, v in zip(i.offsets, offsets)] distance = [(d, set(v)) for d, v in LabeledVector.transpose(*distance)] distances.append(LabeledVector([(d, v.pop()) for d, v in distance])) aliases.add(alias, list(mapper), aliaseds, distances) return aliases
def collect(extracted, ispace, minstorage): """ Find groups of aliasing expressions. We shall introduce the following (loose) terminology: * A ``terminal`` is the leaf of a mathematical operation. Terminals can be numbers (n), literals (l), or Indexeds (I). * ``R`` is the relaxation operator := ``R(n) = n``, ``R(l) = l``, ``R(I) = J``, where ``J`` has the same base as ``I`` but with all offsets stripped away. For example, ``R(a[i+2,j-1]) = a[i,j]``. * A ``relaxed expression`` is an expression in which all of the terminals are relaxed. Now we define the concept of aliasing. We say that an expression A aliases an expression B if: * ``R(A) == R(B)`` * all pairwise Indexeds in A and B access memory locations at a fixed constant distance along each Dimension. For example, consider the following expressions: * 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] Out of the expressions above, the following 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 the distances along ``i`` differ (+2 and +0) """ # Find the potential aliases found = [] for expr in extracted: assert not expr.is_Equality indexeds = retrieve_indexed(expr) bases = [] offsets = [] for i in indexeds: ii = IterationInstance(i) if ii.is_irregular: break base = [] offset = [] for e, ai in zip(ii, ii.aindices): if q_constant(e): base.append(e) else: base.append(ai) offset.append((ai, e - ai)) bases.append(tuple(base)) offsets.append(LabeledVector(offset)) if not indexeds or len(bases) == len(indexeds): found.append(Candidate(expr, ispace, indexeds, bases, offsets)) # Create groups of aliasing expressions mapper = OrderedDict() unseen = list(found) while unseen: c = unseen.pop(0) group = [c] for u in list(unseen): # Is the arithmetic structure of `c` and `u` equivalent ? if not compare_ops(c.expr, u.expr): continue # Is `c` translated w.r.t. `u` ? if not c.translated(u): continue group.append(u) unseen.remove(u) group = Group(group) if minstorage: k = group.dimensions_translated else: k = group.dimensions mapper.setdefault(k, []).append(group) aliases = AliasList() queue = list(mapper.values()) while queue: groups = queue.pop(0) while groups: # For each Dimension, determine the Minimum Intervals (MI) spanning # all of the Groups diameters # Example: x's largest_diameter=2 => [x[-2,0], x[-1,1], x[0,2]] # Note: Groups that cannot evaluate their diameter are dropped mapper = defaultdict(int) for g in list(groups): try: mapper.update( {d: max(mapper[d], v) for d, v in g.diameter.items()}) except ValueError: groups.remove(g) intervalss = { d: make_rotations_table(d, v) for d, v in mapper.items() } # For each Group, find a rotation that is compatible with a given MI mapper = {} for d, intervals in intervalss.items(): # Not all groups may access all dimensions # Example: `d=t` and groups=[Group(...[t, x]...), Group(...[time, x]...)] impacted = [g for g in groups if d in g.dimensions] for interval in list(intervals): found = { g: g.find_rotation_distance(d, interval) for g in impacted } if all(distance is not None for distance in found.values()): # `interval` is OK ! mapper[interval] = found break if len(mapper) == len(intervalss): break # Try again with fewer groups # Heuristic: first try retaining the larger ones smallest = len(min(groups, key=len)) fallback = groups groups, remainder = split(groups, lambda g: len(g) > smallest) if groups: queue.append(remainder) elif len(remainder) > 1: # No luck with the heuristic, e.g. there are two groups # and both have same `len` queue.append(fallback[1:]) groups = [fallback.pop(0)] else: break for g in groups: c = g.pivot distances = defaultdict(int, [(i.dim, v.get(g)) for i, v in mapper.items()]) # Create the basis alias offsets = [ LabeledVector([(l, v[l] + distances[l]) for l in v.labels]) for v in c.offsets ] subs = { i: i.function[[l + v.fromlabel(l, 0) for l in b]] for i, b, v in zip(c.indexeds, c.bases, offsets) } pivot = uxreplace(c.expr, subs) # All aliased expressions aliaseds = [extracted[i.expr] for i in g] # Distance of each aliased expression from the basis alias distances = [] for i in g: distance = [o.distance(v) for o, v in zip(i.offsets, offsets)] distance = [(d, set(v)) for d, v in LabeledVector.transpose(*distance)] distances.append( LabeledVector([(d, v.pop()) for d, v in distance])) # Compute the alias score na = len(aliaseds) nr = nredundants(ispace, pivot) score = estimate_cost(pivot, True) * ((na - 1) + nr) if score > 0: aliases.add(pivot, aliaseds, list(mapper), distances, score) return aliases