# Copyright (c) Facebook, Inc. and its affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. # reference python implementations for C ops import torch from functorch._C import dim as _C from . import op_properties from .batch_tensor import _enable_layers from .tree_map import tree_flatten, tree_map DimList = _C.DimList import operator from functools import reduce # use dict to avoid writing C++ bindings for set pointwise = set(op_properties.pointwise) def prod(x): return reduce(operator.mul, x, 1) def _wrap_dim(d, N, keepdim): from . import Dim if isinstance(d, Dim): assert not keepdim, "cannot preserve first-class dimensions with keepdim=True" return d elif d >= 0: return d - N else: return d def _dims(d, N, keepdim, single_dim): from . import Dim if isinstance(d, (Dim, int)): return ltuple((_wrap_dim(d, N, keepdim),)) assert not single_dim, f"expected a single dimension or int but found: {d}" return ltuple(_wrap_dim(x, N, keepdim) for x in d) def _bind_dims_to_size(lhs_size, rhs, lhs_debug): from . import DimensionMismatchError not_bound = tuple((i, r) for i, r in enumerate(rhs) if not r.is_bound) if len(not_bound) == 1: idx, d = not_bound[0] rhs_so_far = prod(r.size for r in rhs if r.is_bound) if lhs_size % rhs_so_far != 0: rhs_s = tuple("?" if not r.is_bound else str(r.size) for r in rhs) raise DimensionMismatchError( f"inferred dimension does not evenly fit into larger dimension: {lhs_size} vs {rhs_s}" ) new_size = lhs_size // rhs_so_far d.size = new_size elif len(not_bound) > 1: rhs_s = tuple("?" if not r.is_bound else str(r.size) for r in rhs) raise DimensionMismatchError( f"cannot infer the size of two dimensions at once: {rhs} with sizes {rhs_s}" ) else: rhs_size = prod(r.size for r in rhs) if lhs_size != rhs_size: raise DimensionMismatchError( f"Dimension sizes to do not match ({lhs_size} != {rhs_size}) when matching {lhs_debug} to {rhs}" ) def _tensor_levels(inp): from . import _Tensor if isinstance(inp, _Tensor): return inp._tensor, llist(inp._levels), inp._has_device else: return inp, llist(range(-inp.ndim, 0)), True def _match_levels(v, from_levels, to_levels): view = [] permute = [] requires_view = False size = v.size() for t in to_levels: try: idx = from_levels.index(t) permute.append(idx) view.append(size[idx]) except ValueError: view.append(1) requires_view = True if permute != list(range(len(permute))): v = v.permute(*permute) if requires_view: v = v.view(*view) return v # make a single dimension positional but do not permute it, # used to do multi-tensor operators where the dim being acted on # should not physically move if possible def _positional_no_permute(self, dim, expand_dim=False): from . import Tensor ptensor, levels = self._tensor, llist(self._levels) try: idx = levels.index(dim) except ValueError: if not expand_dim: raise idx = 0 ptensor = ptensor.expand(dim.size, *ptensor.size()) levels.insert(0, 0) idx_batched = 0 for i in range(idx): if isinstance(levels[i], int): levels[i] -= 1 idx_batched += 1 levels[idx] = -idx_batched - 1 return Tensor.from_positional(ptensor, levels, self._has_device), idx_batched def seq(a, b): from . import Dim if isinstance(a, Dim) != isinstance(b, Dim): return False if isinstance(a, Dim): return a is b else: return a == b class isin: def __contains__(self, item): for x in self: if seq(item, x): return True return False def index(self, item): for i, x in enumerate(self): if seq(item, x): return i raise ValueError class llist(isin, list): pass class ltuple(isin, tuple): pass empty_dict = {} @classmethod def __torch_function__(self, orig, cls, args, kwargs=empty_dict): from . import _Tensor, Tensor, TensorLike from .delayed_mul_tensor import DelayedMulTensor if orig is torch.Tensor.__mul__: lhs, rhs = args if ( isinstance(lhs, _Tensor) and isinstance(rhs, _Tensor) and lhs.ndim == 0 and rhs.ndim == 0 ): return DelayedMulTensor(lhs, rhs) all_dims = llist() flat_args, unflatten = tree_flatten((args, kwargs)) device_holding_tensor = None for f in flat_args: if isinstance(f, _Tensor): if f._has_device: device_holding_tensor = f._batchtensor for d in f.dims: if d not in all_dims: all_dims.append(d) def unwrap(t): if isinstance(t, _Tensor): r = t._batchtensor if device_holding_tensor is not None and not t._has_device: r = r.to(device=device_holding_tensor.device) return r return t if orig in pointwise: result_levels = llist() arg_levels = llist() to_expand = [] for i, f in enumerate(flat_args): if isinstance(f, TensorLike): ptensor, levels, _ = _tensor_levels(f) if ( isinstance(f, _Tensor) and not f._has_device and device_holding_tensor is not None ): ptensor = ptensor.to(device=device_holding_tensor.device) flat_args[i] = ptensor for l in levels: if l not in result_levels: result_levels.append(l) to_expand.append((i, levels)) for i, levels in to_expand: flat_args[i] = _match_levels(flat_args[i], levels, result_levels) args, kwargs = unflatten(flat_args) result = orig(*args, **kwargs) def wrap(t): if isinstance(t, TensorLike): return Tensor.from_positional( t, result_levels, device_holding_tensor is not None ) return t return tree_map(wrap, result) else: def wrap(t): if isinstance(t, TensorLike): return Tensor.from_batched(t, device_holding_tensor is not None) return t with _enable_layers(all_dims): print(f"batch_tensor for {orig}") args, kwargs = unflatten(unwrap(f) for f in flat_args) result = orig(*args, **kwargs) # print("END", orig) return tree_map(wrap, result) def positional(self, *dims): from . import Dim, DimensionBindError, Tensor ptensor, levels = self._tensor, llist(self._levels) flat_dims = llist() view = [] needs_view = False ndim = self.ndim for d in dims: if isinstance(d, DimList): flat_dims.extend(d) view.extend(e.size for e in d) elif isinstance(d, Dim): flat_dims.append(d) view.append(d.size) elif isinstance(d, int): d = _wrap_dim(d, ndim, False) flat_dims.append(d) view.append(ptensor.size(d)) else: flat_dims.extend(d) view.append(prod(e.size for e in d)) needs_view = True permute = list(range(len(levels))) nflat = len(flat_dims) for i, d in enumerate(flat_dims): try: idx = levels.index(d) except ValueError as e: raise DimensionBindError( f"tensor of dimensions {self.dims} does not contain dim {d}" ) from e p = permute[idx] del levels[idx] del permute[idx] levels.insert(i, 0) permute.insert(i, p) ptensor = ptensor.permute(*permute) seen = 0 for i in range(len(levels) - 1, -1, -1): if isinstance(levels[i], int): seen += 1 levels[i] = -seen result = Tensor.from_positional(ptensor, levels, self._has_device) if needs_view: result = result.reshape(*view, *result.size()[len(flat_dims) :]) return result def _contains_dim(input): from . import Dim for i in input: if isinstance(i, Dim): return True def expand(self, *sizes): if not _contains_dim(sizes): return self.__torch_function__(torch.Tensor.expand, None, (self, *sizes)) dims = sizes sizes = [d.size for d in dims] + [-1] * self.ndim self = self.expand(*sizes) return self[dims] _not_present = object() def _getarg(name, offset, args, kwargs, default): if len(args) > offset: return args[offset] return kwargs.get(name, default) def _patcharg(name, offset, args, kwargs, value): if len(args) > offset: args[offset] = value else: kwargs[name] = value def _wrap( orig, dim_offset=0, keepdim_offset=1, dim_name="dim", single_dim=False, reduce=True ): from . import Dim, Tensor, TensorLike def fn(self, *args, **kwargs): dim = _getarg(dim_name, dim_offset, args, kwargs, _not_present) if dim is _not_present or (single_dim and not isinstance(dim, Dim)): with _enable_layers(self.dims): print(f"dim fallback batch_tensor for {orig}") return Tensor.from_batched( orig(self._batchtensor, *args, **kwargs), self._has_device ) keepdim = ( _getarg("keepdim", keepdim_offset, args, kwargs, False) if reduce else False ) t, levels = self._tensor, llist(self._levels) dims = _dims(dim, self._batchtensor.ndim, keepdim, single_dim) dim_indices = tuple(levels.index(d) for d in dims) if reduce and not keepdim: new_levels = [l for i, l in enumerate(levels) if i not in dim_indices] else: new_levels = levels if len(dim_indices) == 1: dim_indices = dim_indices[ 0 ] # so that dims that really only take a single argument work... args = list(args) _patcharg(dim_name, dim_offset, args, kwargs, dim_indices) def wrap(t): if isinstance(t, TensorLike): return Tensor.from_positional(t, new_levels, self._has_device) return t with _enable_layers(new_levels): print(f"dim used batch_tensor for {orig}") r = orig(t, *args, **kwargs) return tree_map(wrap, r) return fn def _def(name, *args, **kwargs): from . import _Tensor orig = getattr(torch.Tensor, name) setattr(_Tensor, name, _wrap(orig, *args, **kwargs)) no_slice = slice(None) _orig_getitem = torch.Tensor.__getitem__ class dim_tracker: def __init__(self): self.dims = llist() self.count = [] def record(self, d): if d not in self.dims: self.dims.append(d) self.count.append(1) def __getitem__(self, d): return self.count[self.dims.index(d)] def t__getitem__(self, input): from . import _Tensor, Dim, DimensionBindError, DimList, Tensor, TensorLike # * bail to original example if we have a single non-Dim tensor, or a non-tensor # * locate ... or an unbound tensor list, and determine its size, bind dim list # (remember that None does not count to the total dim count) # * bind simple dims and dim-packs to their sizes, count the number of uses of each dim, # produce the re-view if needed # * for each single-use dim index, replace with no_slice and mark that it will be added # (keep track of whether we have to call super) # * call super if needed # * if we have dims to bind, bind them (it will help if we eliminated ... and None before) # this handles bool indexing handling, as well as some other simple cases. is_simple = ( not isinstance(input, Dim) and not isinstance(input, (tuple, list)) and # WAR for functorch bug where zero time tensors in getitem are not handled correctly. not (isinstance(input, TensorLike) and input.ndim == 0) ) if is_simple: if isinstance(self, _Tensor): return _Tensor.__torch_function__(_orig_getitem, None, (self, input)) else: return _orig_getitem(self, input) # can further optimize this case if not isinstance(input, tuple): input = [input] else: input = list(input) dims_indexed = 0 expanding_object = None dimlists = [] for i, s in enumerate(input): if s is ... or isinstance(s, DimList) and not s.is_bound: if expanding_object is not None: msg = ( "at most one ... or unbound dimension list can exist in indexing list but" f" found 2 at offsets {i} and {expanding_object}" ) raise DimensionBindError(msg) expanding_object = i if isinstance(s, DimList): dims_indexed += len(s) if s.is_bound else 0 dimlists.append(i) elif s is not None and s is not ...: dims_indexed += 1 ndim = self.ndim if dims_indexed > ndim: raise IndexError( f"at least {dims_indexed} indices were supplied but the tensor only has {ndim} dimensions." ) if expanding_object is not None: expanding_ndims = ndim - dims_indexed obj = input[expanding_object] if obj is ...: input[expanding_object : expanding_object + 1] = [ no_slice ] * expanding_ndims else: obj.bind_len(expanding_ndims) # flatten the dimslists into the indexing for i in reversed(dimlists): input[i : i + 1] = input[i] dims_indexed = 0 requires_view = False size = self.size() view_sizes = [] dims_seen = dim_tracker() def add_dims(t): if not isinstance(t, _Tensor): return for d in t.dims: dims_seen.record(d) add_dims(self) dim_packs = [] for i, idx in enumerate(input): if idx is None: input[i] = no_slice view_sizes.append(1) requires_view = True else: sz = size[dims_indexed] if isinstance(idx, Dim): idx.size = sz dims_seen.record(idx) view_sizes.append(sz) elif isinstance(idx, (tuple, list)) and idx and isinstance(idx[0], Dim): for d in idx: dims_seen.record(idx) _bind_dims_to_size(sz, idx, f"offset {i}") view_sizes.extend(d.size for d in idx) requires_view = True dim_packs.append(i) else: add_dims(idx) view_sizes.append(sz) dims_indexed += 1 if requires_view: self = self.view(*view_sizes) for i in reversed(dim_packs): input[i : i + 1] = input[i] # currenty: # input is flat, containing either Dim, or Tensor, or something valid for standard indexing # self may have first-class dims as well. # to index: # drop the first class dims from self, they just become direct indices of their positions # figure out the dimensions of the indexing tensors: union of all the dims in the tensors in the index. # these dimensions will appear and need to be bound at the first place tensor occures if isinstance(self, _Tensor): ptensor_self, levels = self._tensor, list(self._levels) # indices to ptensor rather than self which has first-class dimensions input_it = iter(input) flat_inputs = [next(input_it) if isinstance(l, int) else l for l in levels] has_device = self._has_device to_pad = 0 else: ptensor_self, flat_inputs = self, input to_pad = ptensor_self.ndim - len(flat_inputs) has_device = True result_levels = [] index_levels = [] tensor_insert_point = None to_expand = {} requires_getindex = False for i, inp in enumerate(flat_inputs): if isinstance(inp, Dim) and dims_seen[inp] == 1: flat_inputs[i] = no_slice result_levels.append(inp) elif isinstance(inp, TensorLike): requires_getindex = True if tensor_insert_point is None: tensor_insert_point = len(result_levels) ptensor, levels, _ = _tensor_levels(inp) to_expand[i] = levels flat_inputs[i] = ptensor for l in levels: if l not in index_levels: index_levels.append(l) else: requires_getindex = True result_levels.append(0) if tensor_insert_point is not None: result_levels[tensor_insert_point:tensor_insert_point] = index_levels for i, levels in to_expand.items(): flat_inputs[i] = _match_levels(flat_inputs[i], levels, index_levels) if requires_getindex: result = _orig_getitem(ptensor_self, flat_inputs) else: result = ptensor_self next_positional = -1 if to_pad > 0: result_levels.extend([0] * to_pad) for i, r in enumerate(reversed(result_levels)): if isinstance(r, int): result_levels[-1 - i] = next_positional next_positional -= 1 return Tensor.from_positional(result, result_levels, has_device) # XXX - dim is optional and can be the outer-most dimension... def stack(tensors, new_dim, dim=0, out=None): if isinstance(dim, int): return torch.stack(tensors, dim, out).index(dim, new_dim) index = None if out is not None: out, index = _positional_no_permute(out, dim, expand_dim=True) ptensors = [] for t in tensors: pt, pi = _positional_no_permute(t, dim, expand_dim=True) if index is not None and pi != index: pt = pt.move_dim(pi, index) else: index = pi ptensors.append(pt) pr = torch.stack(ptensors, index, out=out) return pr.index((index, index + 1), (new_dim, dim)) _orig_split = torch.Tensor.split def split(self, split_size_or_sections, dim=0): from . import _Tensor, Dim if isinstance(split_size_or_sections, int) or any( isinstance(t, int) for t in split_size_or_sections ): if isinstance(dim, Dim): raise ValueError( "when dim is specified as a Dim object, split sizes must also be dimensions." ) return _orig_split(self, split_size_or_sections, dim=dim) if isinstance(dim, Dim): assert isinstance(self, _Tensor), f"Tensor does not have dimension {dim}" self, dim = _positional_no_permute(self, dim) size = self.size(dim) total_bound_size = 0 unbound = [] sizes = [] for i, d in enumerate(split_size_or_sections): if d.is_bound: sizes.append(d.size) total_bound_size += d.size else: sizes.append(0) unbound.append(i) if unbound: assert ( total_bound_size <= size ), f"result dimensions are larger than original: {total_bound_size} vs {size} ({split_size_or_sections})" remaining_size = size - total_bound_size chunk_size = -(-remaining_size // len(unbound)) for u in unbound: sz = min(chunk_size, remaining_size) split_size_or_sections[u].size = sz sizes[u] = sz remaining_size -= sz else: assert ( total_bound_size == size ), f"result dimensions do not match original: {total_bound_size} vs {size} ({split_size_or_sections})" return tuple( t.index(dim, d) for d, t in zip(split_size_or_sections, _orig_split(self, sizes, dim=dim)) )