| """ |
| Python implementation of function wrapping functionality for functorch.dim. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import functools |
| from typing import Any, Optional, TYPE_CHECKING |
|
|
| import torch |
| from torch.utils._pytree import tree_map |
|
|
| from ._dim_entry import DimEntry |
| from ._enable_all_layers import EnableAllLayers |
| from ._tensor_info import TensorInfo |
|
|
|
|
| if TYPE_CHECKING: |
| from collections.abc import Callable |
|
|
|
|
| def handle_from_tensor(tensor: torch.Tensor) -> torch.Tensor: |
| """Handle tensor conversion for torch function integration.""" |
| return tensor |
|
|
|
|
| class WrappedOperator: |
| """ |
| This class wraps PyTorch operations to support first-class dimensions. |
| """ |
|
|
| def __init__( |
| self, orig: Callable, wrapper_implementation: Callable, dim_name: str = "dim" |
| ): |
| self.orig = orig |
| self.wrapper_implementation = wrapper_implementation |
| self.name = getattr(orig, "__name__", "") |
| self.doc = getattr(orig, "__doc__", None) |
| self.dim_name = dim_name |
|
|
| self.is_pointwise = False |
| self.dim_offset = 0 |
| self.keepdim_offset = 1 |
| self.single_dim = False |
| self.reduce = True |
|
|
| |
| if self.doc and self.dim_name: |
| self.doc = f"{self.doc}\nArgument '{self.dim_name}' can be either an integer or a torchdim.Dim object.\n" |
|
|
| def function(self) -> Callable: |
| """Create a wrapped function that calls our wrapper implementation.""" |
|
|
| def wrapped_func(*args: Any, **kwargs: Any) -> Any: |
| return self.wrapper_implementation(self, *args, **kwargs) |
|
|
| |
| functools.update_wrapper( |
| wrapped_func, self.orig, assigned=("__name__",), updated=() |
| ) |
| wrapped_func.__doc__ = self.doc |
|
|
| return wrapped_func |
|
|
|
|
| def _wrap_dim(dim: Any, ndim: int, keepdim: bool = False) -> DimEntry: |
| """Convert single dimension specification to DimEntry object.""" |
| from . import Dim |
|
|
| if isinstance(dim, Dim): |
| if keepdim: |
| raise ValueError("cannot preserve first-class dimensions with keepdim=True") |
| return DimEntry(dim) |
| elif isinstance(dim, int): |
| i = dim |
| while i >= 0: |
| i -= ndim |
| return DimEntry(i) |
| else: |
| return DimEntry() |
|
|
|
|
| def _wrap_dims(dim: Any, ndim: int, keepdim: bool = False) -> list[DimEntry]: |
| """Convert dimension specification to list of DimEntry objects.""" |
| de = _wrap_dim(dim, ndim, keepdim) |
| result = [] |
| if not de.is_none(): |
| result.append(de) |
| else: |
| for d in dim: |
| result.append(_wrap_dim(d, ndim, keepdim)) |
| return result |
|
|
|
|
| def patched_dim_method(wrapper: WrappedOperator, *args: Any, **kwargs: Any) -> Any: |
| """ |
| This is the core method that handles dimension-aware operations. |
| """ |
| if not args: |
| raise ValueError("Expected at least one argument (self)") |
|
|
| |
| dim_arg = kwargs.get(wrapper.dim_name) |
| if dim_arg is None and wrapper.dim_offset < len(args): |
| |
| dim_idx = wrapper.dim_offset + 1 |
| if dim_idx < len(args): |
| dim_arg = args[dim_idx] |
|
|
| |
| if dim_arg is None: |
| info = TensorInfo.create(args[0], ensure_batched=True, ensure_present=False) |
| if not info: |
| return wrapper.orig(*args, **kwargs) |
|
|
| with EnableAllLayers(info.levels) as guard: |
| if info.batchedtensor is None: |
| raise AssertionError("Expected batchedtensor to be non-None") |
| guard.inplace_update_layers(info.batchedtensor, info.levels) |
| new_args = list(args) |
| new_args[0] = handle_from_tensor(info.batchedtensor) |
| result = wrapper.orig(*new_args, **kwargs) |
| return guard.from_batched(result, info.has_device) |
|
|
| |
| info = TensorInfo.create(args[0]) |
| if not info: |
| return wrapper.orig(*args, **kwargs) |
|
|
| |
| keepdim = False |
| if wrapper.reduce: |
| keepdim_arg = kwargs.get("keepdim") |
| if keepdim_arg is None and wrapper.keepdim_offset < len(args): |
| keepdim_idx = wrapper.keepdim_offset + 1 |
| if keepdim_idx < len(args): |
| keepdim_arg = args[keepdim_idx] |
| if keepdim_arg is not None: |
| keepdim = bool(keepdim_arg) |
|
|
| |
| ndim = info.ndim() |
| dims = _wrap_dims(dim_arg, ndim, keepdim) |
|
|
| |
| dim_indices: list[int] = [] |
| seen = [False] * len(info.levels) |
|
|
| for d in dims: |
| midx = None |
| for i, level in enumerate(info.levels): |
| if level == d: |
| midx = i |
| break |
|
|
| if midx is None: |
| |
| for i, level in enumerate(info.levels): |
| if hasattr(level, "matches") and level.matches(d): |
| midx = i |
| break |
|
|
| if midx is None: |
| level_strs = [str(level) for level in info.levels] |
| raise ValueError( |
| f"Tensor with dimensions {level_strs} does not contain {d}" |
| ) |
|
|
| seen[midx] = True |
| dim_indices.append(midx) |
|
|
| |
| new_levels = [] |
| if wrapper.reduce and not keepdim: |
| for i, level in enumerate(info.levels): |
| if not seen[i]: |
| new_levels.append(level) |
| else: |
| new_levels = info.levels[:] |
|
|
| |
| if len(dim_indices) == 1: |
| py_indices: Any = dim_indices[0] |
| else: |
| py_indices = tuple(dim_indices) |
|
|
| |
| new_args = list(args) |
| new_kwargs = kwargs.copy() |
| if info.tensor is None: |
| raise AssertionError("Expected tensor to be non-None") |
| new_args[0] = handle_from_tensor(info.tensor) |
|
|
| |
| if wrapper.dim_name in new_kwargs: |
| new_kwargs[wrapper.dim_name] = py_indices |
| else: |
| dim_idx = wrapper.dim_offset + 1 |
| if dim_idx < len(new_args): |
| new_args = list(new_args) |
| new_args[dim_idx] = py_indices |
|
|
| |
| result = wrapper.orig(*new_args, **new_kwargs) |
|
|
| |
| def wrap_result(obj: Any) -> Any: |
| if isinstance(obj, torch.Tensor): |
| from . import Tensor |
|
|
| return Tensor.from_positional(obj, new_levels, info.has_device) |
| return obj |
|
|
| return tree_map(wrap_result, result) |
|
|
|
|
| def _wrap( |
| orig: Callable, |
| dim_offset: Optional[int] = None, |
| keepdim_offset: Optional[int] = None, |
| dim_name: Optional[str] = None, |
| single_dim: Optional[bool] = None, |
| reduce: Optional[bool] = None, |
| ) -> Callable: |
| """ |
| Wrap a PyTorch function to support first-class dimensions. |
| |
| Args: |
| orig: Original function to wrap |
| dim_offset: Offset for dimension argument (default: 0) |
| keepdim_offset: Offset for keepdim argument (default: 1) |
| dim_name: Name of dimension parameter (default: "dim") |
| single_dim: Whether function takes single dimension (default: False) |
| reduce: Whether function reduces dimensions (default: True) |
| """ |
| dim_name = dim_name or "dim" |
|
|
| wrapper = WrappedOperator(orig, patched_dim_method, dim_name) |
|
|
| if dim_offset is not None: |
| wrapper.dim_offset = dim_offset |
| if keepdim_offset is not None: |
| wrapper.keepdim_offset = keepdim_offset |
| if single_dim is not None: |
| wrapper.single_dim = single_dim |
| if reduce is not None: |
| wrapper.reduce = reduce |
|
|
| return wrapper.function() |
|
|
|
|
| def call_torch_function( |
| wrapper: WrappedOperator, |
| func: Callable, |
| types: tuple, |
| args: tuple = (), |
| kwargs: Optional[dict] = None, |
| ) -> Any: |
| """ |
| Handle __torch_function__ calls for wrapped operators. |
| """ |
| if kwargs is None: |
| kwargs = {} |
|
|
| |
| from . import _Tensor |
|
|
| |
| return _Tensor.__torch_function__(func, types, args, kwargs) |
|
|