Initial import: grid-bot — grid trading bot for BTC-USDT on Cifra Markets

This commit is contained in:
Kolp
2026-09-24 13:22:23 +07:00
commit 642cc11a9f
18968 changed files with 5683248 additions and 0 deletions
@@ -0,0 +1,39 @@
# 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.
import torch
from torch._functorch.deprecated import (
combine_state_for_ensemble,
functionalize,
grad,
grad_and_value,
hessian,
jacfwd,
jacrev,
jvp,
make_functional,
make_functional_with_buffers,
vjp,
vmap,
)
# utilities. Maybe these should go in their own namespace in the future?
from torch._functorch.make_functional import (
FunctionalModule,
FunctionalModuleWithBuffers,
)
# Was never documented
from torch._functorch.python_key import make_fx
# Top-level APIs. Please think carefully before adding something to the
# top-level namespace:
# - private helper functions should go into torch._functorch
# - very experimental things should go into functorch.experimental
# - compilation related things should go into functorch.compile
__version__ = torch.__version__
@@ -0,0 +1,8 @@
# This file has moved to under torch/_functorch. It is not public API.
# If you are not a PyTorch developer and you are relying on the following
# imports, please file an issue.
from torch._functorch.aot_autograd import (
aot_autograd_decompositions,
KNOWN_TYPES,
PytreeThunk,
)
@@ -0,0 +1,7 @@
# This file has moved to under torch/_functorch. It is not public API.
# If you are not a PyTorch developer and you are relying on the following
# imports, please file an issue.
from torch._functorch.eager_transforms import (
_assert_wrapped_functional,
_unwrap_functional_tensor,
)
@@ -0,0 +1,4 @@
# This file has moved to under torch/_functorch. It is not public API.
# If you are not a PyTorch developer and you are relying on the following
# imports, please file an issue.
from torch._functorch.make_functional import _swap_state
@@ -0,0 +1,16 @@
# This file has moved to under torch/_functorch. It is not public API.
# If you are not a PyTorch developer and you are relying on the following
# imports, please file an issue.
from torch._functorch.vmap import (
_add_batch_dim,
_broadcast_to_and_flatten,
_create_batched_inputs,
_get_name,
_process_batched_inputs,
_remove_batch_dim,
_unwrap_batched,
_validate_and_get_batch_size,
Tensor,
tree_flatten,
tree_unflatten,
)
@@ -0,0 +1,30 @@
from torch._functorch import config
from torch._functorch.aot_autograd import (
aot_function,
aot_module,
aot_module_simplified,
compiled_function,
compiled_module,
get_aot_compilation_context,
get_aot_graph_name,
get_graph_being_compiled,
make_boxed_compiler,
make_boxed_func,
)
from torch._functorch.compilers import (
debug_compile,
default_decompositions,
draw_graph_compile,
memory_efficient_fusion,
nnc_jit,
nop,
print_compile,
ts_compile,
)
from torch._functorch.fx_minifier import minifier
from torch._functorch.partitioners import (
default_partition,
draw_graph,
min_cut_rematerialization_partition,
)
from torch._functorch.python_key import pythonkey_decompose
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,130 @@
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Sequence
from . import Dim
import torch # noqa: TC002
# NB: The old code represented dimension was from as negative number, so we
# follow this convention even though it shouldn't be necessary now
class DimEntry:
# The dimension this is from the rhs, or a FCD
data: Dim | int
def __init__(self, data: Dim | int | None = None) -> None:
from . import Dim
if type(data) is int:
if data >= 0:
raise AssertionError(f"Expected negative int, got {data}")
elif data is None:
data = 0
else:
if not isinstance(data, Dim):
raise AssertionError(f"Expected Dim, got {type(data)}")
self.data = data
def __eq__(self, other: object) -> bool:
if not isinstance(other, DimEntry):
return False
# Use 'is' for Dim objects to avoid triggering __torch_function__
# Use '==' only for positional (int) comparisons
if self.is_positional() and other.is_positional():
# Both are positional (ints)
return self.data == other.data
elif not self.is_positional() and not other.is_positional():
# Both are Dim objects - use 'is' to avoid __eq__
return self.data is other.data
else:
# One is positional, one is Dim - they can't be equal
return False
def is_positional(self) -> bool:
return type(self.data) is int and self.data < 0
def is_none(self) -> bool:
# Use isinstance to check for Dim objects, avoid triggering __torch_function__
from . import Dim
if isinstance(self.data, Dim):
# This is a Dim object, it can't be "none" (which is represented by 0)
return False
else:
# This is an int or other type
return self.data == 0
def position(self) -> int:
if not isinstance(self.data, int):
raise AssertionError(f"Expected int, got {type(self.data)}")
return self.data
def dim(self) -> Dim:
if isinstance(self.data, int):
raise AssertionError("Expected Dim, got int")
return self.data
def __repr__(self) -> str:
return repr(self.data)
def ndim_of_levels(levels: Sequence[DimEntry]) -> int:
r = 0
for l in levels:
if l.is_positional():
r += 1
return r
def _match_levels(
tensor: torch.Tensor,
from_levels: list[DimEntry],
to_levels: list[DimEntry],
drop_levels: bool = False,
) -> torch.Tensor:
"""
Reshape a tensor to match target levels using as_strided.
Args:
tensor: Input tensor to reshape
from_levels: Current levels of the tensor
to_levels: Target levels to match
drop_levels: If True, missing dimensions are assumed to have stride 0
Returns:
Reshaped tensor
"""
if from_levels == to_levels:
return tensor
sizes = tensor.size()
strides = tensor.stride()
if not drop_levels:
if len(from_levels) > len(to_levels):
raise AssertionError("Cannot expand dimensions without drop_levels")
new_sizes = []
new_strides = []
for level in to_levels:
# Find index of this level in from_levels
try:
idx = from_levels.index(level)
except ValueError:
# Level not found in from_levels
if level.is_positional():
new_sizes.append(1)
else:
new_sizes.append(level.dim().size)
new_strides.append(0)
else:
new_sizes.append(sizes[idx])
new_strides.append(strides[idx])
return tensor.as_strided(new_sizes, new_strides, tensor.storage_offset())
@@ -0,0 +1,143 @@
from __future__ import annotations
from typing import Any, TYPE_CHECKING
import torch
from ._dim_entry import DimEntry
if TYPE_CHECKING:
from . import Dim, Tensor
class EnableAllLayers:
"""
RAII-style context manager for enabling functorch vmap layers.
It manages the creation and cleanup of functorch dynamic layers.
This is probably one of the more algorithmically important parts of first
class dims. Intuitively, FCD can be thought of as another way of using
vmap, where you don't actually have to vmap at the top level, instead the
vmaps are implicitly determined by inspecting the bound dimensions on the
FCD tensors involved in a compute (this is similar to our concept of
non-lexical modes that we spent a long time talking about years ago). But
under the hood you still need to actually enable the vmap mode. So once
FCD has determined all of the dims we are batching over, it needs to
enable all those layers so functorch can actually apply the batching
rules. Therefore enable all layers!
"""
levels_start: int
levels_to_dim: list[Dim]
def __init__(self, levels: list[DimEntry]):
"""
Initialize and push dynamic layers for all first-class dimensions.
Args:
levels: List of dimension entries to create layers for
"""
from . import Dim
self.levels_start = 0
self.levels_to_dim = []
for l in levels:
if not l.is_positional():
d = l.dim()
if not isinstance(d, Dim):
raise AssertionError(f"Expected Dim, got {type(d)}")
self.levels_to_dim.append(d)
# Sort by level for stable ordering
self.levels_to_dim.sort(key=lambda d: d._level)
def __enter__(self) -> EnableAllLayers: # noqa: PYI034
# Create functorch dynamic layers
for i, dim in enumerate(self.levels_to_dim):
batch_size = dim.size
level = torch._C._functorch._vmap_increment_nesting(batch_size, "different")
if i == 0:
self.levels_start = level
return self
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
"""Clean up dynamic layers in reverse order."""
to_remove = self.levels_start + len(self.levels_to_dim) - 1
for i in range(len(self.levels_to_dim)):
popped = torch._C._functorch._vmap_decrement_nesting()
if popped != to_remove - i:
raise AssertionError(f"Expected layer {to_remove - i}, got {popped}")
def from_batched(self, batchedtensor: torch.Tensor, has_device: bool) -> Tensor:
"""
Create a Tensor from a batched tensor by unwrapping functorch layers.
Args:
batchedtensor: Batched tensor from functorch operation
has_device: Whether tensor has device info
Returns:
Tensor with appropriate levels
"""
# Create positional levels for base dimensions
levels: list[DimEntry] = []
for i in range(-batchedtensor.dim(), 0):
levels.append(DimEntry(i))
tensor = batchedtensor
while torch._C._functorch.is_batchedtensor(tensor):
level = torch._C._functorch.maybe_get_level(tensor)
if level is None:
raise AssertionError("Expected level to be non-None")
if not (
level >= self.levels_start
and level < self.levels_start + len(self.levels_to_dim)
):
raise AssertionError(f"Level {level} out of range")
dim = DimEntry(self.levels_to_dim[level - self.levels_start])
bdim = torch._C._functorch.maybe_get_bdim(tensor)
if bdim is None:
raise AssertionError("Expected bdim to be non-None")
levels.insert(bdim, dim)
tensor = torch._C._functorch.get_unwrapped(tensor)
from . import Tensor
result = Tensor()
result._tensor = tensor
result._batchtensor = batchedtensor
result._has_device = has_device
result._levels = levels
return result
def inplace_update_layers(
self, batchtensor: torch.Tensor, levels: list[DimEntry]
) -> None:
"""
Update the levels of a batched tensor in place.
This requires the _maybe_unsafe_set_level binding that we'll add to functorch.
Args:
batchtensor: Batched tensor to update
levels: New levels to set
"""
# Check if tensor is batched
if not torch._C._functorch.is_batchedtensor(batchtensor):
return
impl = batchtensor
for i in reversed(range(len(self.levels_to_dim))):
if impl is None:
break
if any(l == DimEntry(self.levels_to_dim[i]) for l in levels):
# This is very interesting! The level on batch tensor is
# meaningless! We set it RIGHT before we go into vmap
torch._C._functorch._maybe_unsafe_set_level(impl, self.levels_start + i)
impl = torch._C._functorch.get_unwrapped(impl)
@@ -0,0 +1,564 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, TYPE_CHECKING
import torch
from ._dim_entry import _match_levels, DimEntry
from ._tensor_info import TensorInfo
if TYPE_CHECKING:
from . import Dim
def _safe_index(lst: list, item: Any) -> int | None:
"""
Helper function to find index of item in list.
For DimEntry objects, uses __eq__ comparison which properly handles
both positional and Dim entries.
Returns the index if found, None if not found.
"""
for i, list_item in enumerate(lst):
# Use == for DimEntry objects as they have proper __eq__ implementation
if isinstance(item, DimEntry) and isinstance(list_item, DimEntry):
if list_item == item:
return i
elif list_item is item:
return i
return None
@dataclass
class IndexingInfo:
can_call_original: bool = False
advanced_indexing: bool = False
self_tensor: torch.Tensor | None = None
flat_inputs: list[Any] = field(default_factory=list)
result_levels: list[DimEntry] = field(default_factory=list)
has_device: bool = False
def has_dims(obj: Any) -> bool:
"""
Check if an object has first-class dimensions.
This function checks if the object is either a Dim or a functorch Tensor
that has first-class dimensions, using the proper check_exact methods.
"""
from . import Dim, Tensor
return Dim.check_exact(obj) or Tensor.check_exact(obj)
def _bind_dims_to_size(sz: int, sd: int, dims: list, nsz: list, nsd: list) -> None:
"""
Bind dimensions to size and calculate proper strides for dim packs.
"""
from . import DimensionBindError
rhs_prod = 1
for i, dim in enumerate(dims):
if not dim.is_bound:
# Check for multiple unbound dimensions
for j in range(i + 1, len(dims)):
if not dims[j].is_bound:
raise DimensionBindError(
f"cannot infer the sizes of two dimensions at once {dim!r} and {dims[j]!r}"
)
rhs_prod *= dims[j].size
# Calculate the size for this unbound dimension
if sz % rhs_prod != 0:
tup = tuple(dim.size if dim.is_bound else "?" for dim in dims)
raise DimensionBindError(
f"inferred dimension does not evenly fit into larger dimension: {sz} vs {tup}"
)
inferred_size = sz // rhs_prod
dim.size = inferred_size
rhs_prod = sz
break
else:
rhs_prod *= dim.size
# Final validation that dimensions match
if rhs_prod != sz:
tup = tuple(dims)
raise DimensionBindError(
f"Dimension sizes to do not match ({sz} != {rhs_prod}) when matching dimension pack {tup}"
)
# Calculate new sizes and strides for each dimension in the pack
# First calculate all strides by iterating in reverse
new_strides = [0] * len(dims)
current_stride = sd
for i in reversed(range(len(dims))):
new_strides[i] = current_stride
current_stride *= dims[i].size
# Then append sizes and strides in forward order
for i in range(len(dims)):
nsz.append(dims[i].size)
nsd.append(new_strides[i])
def slice_to_tuple(flat_inputs: list) -> tuple:
return tuple(flat_inputs)
def extractIndices(index: Any, indices: list) -> bool:
if isinstance(index, tuple): # mpy::tuple_view::check
indices.extend(index)
return True
elif isinstance(index, torch.Tensor): # THPVariable_Check
indices.append(index)
return False
elif not hasattr(index, "__iter__") or isinstance(
index, (str, bytes)
): # !mpy::is_sequence
indices.append(index)
return False
# Handle sequence case (list)
if isinstance(index, list):
if len(index) >= 32:
indices.extend(index)
return True
# Check each item in the sequence
for item in index:
if (
isinstance(item, (torch.Tensor, slice))
or hasattr(item, "__iter__")
or item is ...
or item is None
or has_dims(item)
):
indices.extend(index)
return True
# If we got here, treat as single index
indices.append(index)
return False
# Default case
indices.append(index)
return False
def getitem(cls: Any, func: Any, types: Any, args: Any, kwargs: Any) -> Any:
self = args[0]
index = args[1]
iinfo = getsetitem(self, index, has_dims(self))
if iinfo.can_call_original:
# Call original tensor __getitem__ directly, bypassing __torch_function__
return torch.Tensor.__getitem__(self, index)
return invoke_getitem(iinfo)
def setitem(self: Any, index: Any, rhs: Any) -> None:
"""Set values in tensor using first-class dimensions."""
from . import DimensionBindError, TensorInfo
iinfo = getsetitem(self, index, has_dims(self) or has_dims(rhs))
if iinfo.can_call_original:
# Call original tensor __setitem__ directly, bypassing __torch_function__
torch._C.TensorBase.__setitem__(self, index, rhs)
return
# Handle RHS tensor with dimensions
rhs_info = TensorInfo.create(rhs, False, False)
if rhs_info:
# Check that rhs dimensions are compatible with result dimensions
for l in rhs_info.levels:
if not l.is_positional():
# Find this dimension in result levels
found = False
for result_level in iinfo.result_levels:
if (
not result_level.is_positional()
and result_level.dim() is l.dim()
):
found = True
break
if not found:
# Create tuple representation of result levels for error message
result_dims: list[int | Dim] = []
for rl in iinfo.result_levels:
if rl.is_positional():
result_dims.append(rl.position())
else:
result_dims.append(rl.dim())
raise DimensionBindError(
f"rhs of setitem contains dimension {l.dim()!r} which is not in the dimension on the left "
f"({tuple(result_dims)!r})"
)
# Match RHS tensor to result levels
if rhs_info.tensor is None:
raise AssertionError("Cannot match levels on None tensor")
matched_rhs = _match_levels(
rhs_info.tensor, rhs_info.levels, iinfo.result_levels
)
else:
matched_rhs = rhs
# For advanced indexing with dimensions, we need special handling
if iinfo.advanced_indexing:
# Use advanced indexing - the flat_inputs already contain matched tensors
tup = slice_to_tuple(iinfo.flat_inputs)
if iinfo.self_tensor is None:
raise RuntimeError("Cannot setitem on None tensor")
torch._C.TensorBase.__setitem__(iinfo.self_tensor, tup, matched_rhs)
else:
# Simple copy operation
if iinfo.self_tensor is None:
raise RuntimeError("Cannot copy to None tensor")
iinfo.self_tensor.copy_(matched_rhs)
def invoke_getitem(iinfo: IndexingInfo) -> Any:
if iinfo.advanced_indexing:
self_tensor = iinfo.self_tensor
tup = slice_to_tuple(iinfo.flat_inputs)
if self_tensor is None:
raise RuntimeError("Cannot getitem on None tensor")
rtensor = self_tensor[tup]
else:
rtensor = iinfo.self_tensor # type: ignore[assignment]
if rtensor is None:
raise RuntimeError("Cannot getitem on None tensor")
# rtensor is now guaranteed to be not None
# Create a Tensor with the proper dimensions using the class method
from . import Tensor
return Tensor.from_positional(rtensor, iinfo.result_levels, iinfo.has_device)
def getsetitem(self: Any, index: Any, tensors_have_dims: bool) -> IndexingInfo:
from . import DimList # Import DimList for type checking
can_call_original_getitem = not tensors_have_dims
input_list = []
if has_dims(index):
input_list.append(index)
else:
is_sequence = extractIndices(index, input_list)
# nothing about first class dims here, fallback to getitem
if can_call_original_getitem and not is_sequence:
return IndexingInfo(can_call_original=True)
# Calculate how many dimensions have been indexed in order to compute the
# size of ... or expand a potentially unbound dimension list.
dims_indexed = 0
expanding_object = -1
unbound_dim_list = None
dimlists = [] # Track DimList positions for later processing
def check_expanding(i: int) -> None:
nonlocal expanding_object
if expanding_object != -1:
from . import DimensionBindError
raise DimensionBindError(
f"at most one ... or unbound dimension list can exist in indexing list but found 2 at offsets "
f"{expanding_object} and {i}"
)
expanding_object = i
def is_dimpack(s: Any) -> bool:
from . import Dim
return (
isinstance(s, (tuple, list))
and len(s) > 0
and all(Dim.check_exact(item) for item in s)
)
has_dimpacks_or_none = False
for i, s in enumerate(input_list):
if has_dims(s):
can_call_original_getitem = False
dims_indexed += 1
elif s is ...:
check_expanding(i)
elif isinstance(s, DimList):
can_call_original_getitem = False
if not s.is_bound:
check_expanding(i)
unbound_dim_list = s
else:
dims_indexed += len(s._dims)
dimlists.append(i)
elif s is None:
has_dimpacks_or_none = True
elif is_dimpack(s):
can_call_original_getitem = False
has_dimpacks_or_none = True
dims_indexed += 1
else:
dims_indexed += 1
# Early return if we can use original getitem
if can_call_original_getitem:
return IndexingInfo(can_call_original=True)
self_info = TensorInfo.create(self, False, True)
total_dims = len(self_info.levels) # Total dimensions (positional + named)
if dims_indexed > total_dims:
raise ValueError(
f"at least {dims_indexed} indices were supplied but the tensor only has {total_dims} dimensions"
)
# Expand any unbound dimension list, or expand ... into individual : slices.
expanding_dims = total_dims - dims_indexed
if expanding_object != -1:
if unbound_dim_list is not None:
# Bind unbound dimension list to the expanding dimensions
unbound_dim_list.bind_len(expanding_dims)
else:
# Expand ... into slice(None) objects
no_slices = [slice(None)] * expanding_dims
input_list = (
input_list[:expanding_object]
+ no_slices
+ input_list[expanding_object + 1 :]
)
# Flatten out any dimensions stored in dimlist elements directly into the inputs
# Process in reverse order to maintain indices
for i in range(len(dimlists) - 1, -1, -1):
idx = dimlists[i]
# We added more elements to input because of ...
# so we need to also adjust the index to get back to where the
# dimlist existed
if (
unbound_dim_list is None
and expanding_object != -1
and idx > expanding_object
):
idx += expanding_dims
dl = input_list[idx]
# PRIVATE here naughty
input_list = input_list[:idx] + dl._dims + input_list[idx + 1 :]
return getsetitem_flat(self_info, input_list, [], [], has_dimpacks_or_none)
def getsetitem_flat(
self_info: TensorInfo,
input_list: list,
keys: list[DimEntry],
values: list,
has_dimpacks_or_none: bool,
) -> IndexingInfo:
from . import Dim
# Track dimension usage
seen_dims: list[Any] = []
seen_dims_nuses: list[int] = []
def add_dim(dim: Any) -> None:
# Use safe indexing to avoid triggering __torch_function__ on Dim objects
idx = _safe_index(seen_dims, dim)
if idx is not None:
seen_dims_nuses[idx] += 1
else:
seen_dims.append(dim)
seen_dims_nuses.append(1)
flat_inputs = []
tensor_inputs: list[Any] = []
device_holding_tensor = None
def append_flat_handle(handle: Any) -> None:
flat_inputs.append(handle)
tensor_inputs.append(None)
def append_tensor_input(ti: TensorInfo) -> None:
flat_inputs.append(None)
tensor_inputs.append(ti)
nonlocal device_holding_tensor
if ti.has_device and device_holding_tensor is None:
device_holding_tensor = ti.tensor
nsz = []
nsd = []
if self_info.tensor is None:
raise RuntimeError("Cannot get size/stride on None tensor")
sz = self_info.tensor.size()
sd = self_info.tensor.stride()
def append_size(i: int) -> None:
if has_dimpacks_or_none:
nsz.append(sz[i])
nsd.append(sd[i])
input_it = input_list[:]
def parse_nones() -> None:
nonlocal input_it
while input_it and input_it[0] is None:
append_flat_handle(slice(None))
nsz.append(1)
nsd.append(0)
input_it = input_it[1:]
def append_item(i: int, arg: Any) -> None:
if Dim.check_exact(arg):
d = arg
if d._size == -1:
d.size = sz[i]
add_dim(d)
append_size(i)
append_flat_handle(arg)
return
info = TensorInfo.create(arg, False, False)
if info:
append_size(i)
append_tensor_input(info)
for level in info.levels:
if not level.is_positional():
add_dim(level.dim())
return
if has_dimpacks_or_none:
if isinstance(arg, (tuple, list)) and all(Dim.check_exact(d) for d in arg):
# dim pack
dim_pack = list(arg)
for d in dim_pack:
add_dim(d)
append_flat_handle(d)
_bind_dims_to_size(sz[i], sd[i], dim_pack, nsz, nsd)
return
append_size(i)
append_flat_handle(arg)
# Match indexing expressions with tensor dimensions
for i, level in enumerate(self_info.levels):
# Use safe indexing to avoid triggering __torch_function__ on DimEntry comparisons
idx = _safe_index(keys, level)
if idx is not None:
append_item(i, values[idx])
else:
if level.is_positional():
parse_nones()
if not input_it:
append_flat_handle(slice(None))
append_size(i)
else:
arg = input_it[0]
input_it = input_it[1:]
append_item(i, arg)
else:
add_dim(level.dim())
append_flat_handle(level.dim())
append_size(i)
parse_nones()
# Restride tensor if needed
if has_dimpacks_or_none and nsz:
if self_info.tensor is None:
raise RuntimeError("Cannot restride None tensor")
self_tensor = self_info.tensor.as_strided(
nsz, nsd, self_info.tensor.storage_offset()
)
else:
self_tensor = self_info.tensor
# Determine result shape and indexing requirements
result_levels: list[Any] = []
index_levels = []
tensor_insert_point = -1
requires_getindex = False
def mark_tensor_index() -> None:
nonlocal tensor_insert_point
if tensor_insert_point == -1:
tensor_insert_point = len(result_levels)
elif tensor_insert_point != len(result_levels):
tensor_insert_point = 0
for i, inp in enumerate(flat_inputs):
if tensor_inputs[i] is not None:
requires_getindex = True
mark_tensor_index()
for level in tensor_inputs[i].levels:
if level not in index_levels:
index_levels.append(level)
elif Dim.check_exact(inp):
d = inp
# Use safe indexing to avoid triggering __torch_function__
dim_idx = _safe_index(seen_dims, d)
if dim_idx is None:
raise AssertionError(f"Dim {d} not found in seen_dims")
if seen_dims_nuses[dim_idx] == 1:
flat_inputs[i] = slice(None)
result_levels.append(DimEntry(d))
else:
requires_getindex = True
flat_inputs[i] = None
tensor_inputs[i] = TensorInfo(
d._get_range(), [DimEntry(d)], False, None
)
if DimEntry(d) not in index_levels:
index_levels.append(DimEntry(d))
mark_tensor_index()
else:
if inp != slice(None):
requires_getindex = True
if not isinstance(inp, int):
result_levels.append(DimEntry(-1))
# Insert indexing dimensions at first tensor use point
if tensor_insert_point != -1:
for level in reversed(index_levels):
result_levels.insert(tensor_insert_point, level)
# Match tensors to indexing shape
if requires_getindex:
for i in range(len(flat_inputs)):
if tensor_inputs[i] is not None:
t = tensor_inputs[i].tensor
if t is None:
raise AssertionError("TensorInfo should have valid tensor data")
if (
not tensor_inputs[i].has_device
and device_holding_tensor is not None
):
t = t.to(device_holding_tensor.device)
flat_inputs[i] = _match_levels(t, tensor_inputs[i].levels, index_levels)
# Number positional dimensions correctly
seen_positionals = 0
for i in reversed(range(len(result_levels))):
if result_levels[i].is_positional():
seen_positionals += 1
result_levels[i] = DimEntry(-seen_positionals)
return IndexingInfo(
can_call_original=False,
advanced_indexing=requires_getindex,
self_tensor=self_tensor,
flat_inputs=flat_inputs,
result_levels=result_levels,
has_device=self_info.has_device,
)
@@ -0,0 +1,215 @@
from __future__ import annotations
from typing import Any, TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Sequence
import torch # noqa: TC002
from ._dim_entry import _match_levels, DimEntry, ndim_of_levels
def _wrap_dim(arg: Any, orig_ndim: int, allow_none: bool = True) -> DimEntry:
"""
Convert various dimension representations to DimEntry.
Args:
arg: The argument to convert (Dim, int, or other)
orig_ndim: Original number of dimensions
allow_none: Whether to allow None values
Returns:
DimEntry representation of the dimension
"""
from . import Dim
if arg is None and allow_none:
return DimEntry() # None entry
elif isinstance(arg, Dim):
return DimEntry(arg)
elif isinstance(arg, int):
if arg < 0:
pos = arg
else:
pos = arg - orig_ndim
return DimEntry(pos)
else:
return DimEntry()
def order(
tensor_or_dim: torch.Tensor | Any, *dims: Any | Sequence[Any]
) -> torch.Tensor:
"""
Reorder the dimensions of a tensor or create a tensor from a dimension.
It allows reordering tensor dimensions using first-class dimensions and
positional indices.
Args:
tensor_or_dim: Input tensor with first-class dimensions, or a Dim object
*dims: Dimensions or sequences of dimensions specifying the new order
Returns:
Tensor with reordered dimensions
Examples:
>>> import torch
>>> from functorch.dim import dims
>>> batch, channel, height, width = dims(4)
>>> x = torch.randn(2, 3, 4, 5)[batch, channel, height, width]
>>> # Reorder to [height, width, batch, channel]
>>> y = order(x, height, width, batch, channel)
"""
from . import Dim, DimList, Tensor
# Handle first argument - tensor or dimension
if isinstance(tensor_or_dim, Tensor):
# First-class tensor
orig_levels = tensor_or_dim._levels[:]
data = tensor_or_dim._tensor
has_device = tensor_or_dim._has_device
elif isinstance(tensor_or_dim, Dim):
# Single dimension - create range tensor
orig_levels = [DimEntry(tensor_or_dim)]
data = tensor_or_dim._get_range()
has_device = False
else:
raise ValueError("First argument must be a Tensor or Dim object")
flat_positional_dims = []
to_flatten = [] # List of (start_index, length) pairs for flattening
levels = orig_levels[:]
orig_ndim = ndim_of_levels(levels)
def append_dim(d: DimEntry) -> None:
"""Add a dimension to the reordering, removing it from available levels."""
try:
idx = levels.index(d)
except ValueError:
idx = None
if idx is None:
if d.is_positional():
raise ValueError(
f"tensor has {orig_ndim} positional dimensions, but {d.position() + orig_ndim} specified, "
f"or it was specified twice"
)
else:
raise ValueError(
f"tensor does not contain dim {d.dim()} or it was specified twice"
)
levels[idx] = DimEntry()
flat_positional_dims.append(d)
n_new_positional = 0
# Process each dimension argument
for arg in dims:
entry = _wrap_dim(arg, orig_ndim, False)
if not entry.is_none():
append_dim(entry)
n_new_positional += 1
elif isinstance(arg, DimList):
# Handle DimList
for dim in arg._dims:
append_dim(DimEntry(dim))
n_new_positional += 1
else:
# Handle sequences of dimensions for flattening
n_new_positional += 1
if not hasattr(arg, "__iter__"):
raise ValueError("expected a Dim, List[Dim], or Sequence[Dim]")
# Convert to list to get length
seq = list(arg)
to_flatten.append((len(flat_positional_dims), len(seq)))
for item in seq:
entry = _wrap_dim(item, orig_ndim, False)
if entry.is_none():
raise ValueError("expected a Dim or int")
append_dim(entry)
# Build new level ordering
insert_point = -1
new_levels: list[DimEntry] = []
# Add remaining (non-reordered) levels, finding insertion point for new dimensions
for level in levels:
if level.is_none():
continue
if level.is_positional():
if insert_point == -1:
insert_point = len(new_levels)
new_levels.extend(flat_positional_dims)
new_levels.append(level)
# If no positional dimensions found, append new dims at the end
if insert_point == -1:
insert_point = len(new_levels)
new_levels.extend(flat_positional_dims)
# Match tensor to new level structure
if data is None:
raise AssertionError("Cannot reorder None tensor")
ndata = _match_levels(data, orig_levels, new_levels)
# Handle dimension flattening if requested
if to_flatten:
# Now build the reshape target
view_shape = []
sizes = ndata.size()
# Add dimensions before the reordered ones
for i in range(insert_point):
view_shape.append(sizes[i])
# Process flattening groups
i = 0
for start_idx, length in to_flatten:
# Add individual dims before this flattening group
while i < start_idx:
view_shape.append(sizes[insert_point + i])
i += 1
# Flatten the group
new_size = 1
for j in range(length):
new_size *= sizes[insert_point + i + j]
view_shape.append(new_size)
i += length
# Add remaining individual dims
while i < len(flat_positional_dims):
view_shape.append(sizes[insert_point + i])
i += 1
# Add dimensions after the reordered ones
for i in range(insert_point + len(flat_positional_dims), len(levels)):
view_shape.append(sizes[i])
# Update levels by removing flattened dimensions
n_to_remove = len(flat_positional_dims) - n_new_positional
if n_to_remove > 0:
# Remove flattened levels
new_levels = (
new_levels[:insert_point] + new_levels[insert_point + n_to_remove :]
)
ndata = ndata.reshape(view_shape)
# Renumber positional dimensions (negative indexing from the right)
seen = 0
for i in range(len(new_levels) - 1, -1, -1):
if new_levels[i].is_positional() or (
i >= insert_point and i < insert_point + n_new_positional
):
seen -= 1
new_levels[i] = DimEntry(seen)
result = Tensor.from_positional(ndata, new_levels, has_device)
return result # type: ignore[return-value]
@@ -0,0 +1,67 @@
import dis
from typing import Any
class _PyInstDecoder:
"""
Decodes Python bytecode instructions to extract variable names
"""
def __init__(self, code_object: Any, lasti: int) -> None:
self.code_object = code_object
self.instructions = list(dis.get_instructions(code_object))
self.offset = self._find_instruction_index(lasti)
def _find_instruction_index(self, lasti: int) -> int:
"""Find instruction index corresponding to lasti (byte offset)."""
# Find the instruction at or before lasti
# This should find the CALL instruction, not the next one
best_idx = 0
for i, instr in enumerate(self.instructions):
if instr.offset <= lasti:
best_idx = i
else:
break
return best_idx
def next(self) -> None:
"""Advance to the next instruction."""
self.offset += 1
def opcode(self) -> str | None:
"""Get the opcode name of the current instruction."""
if self.offset < len(self.instructions):
return self.instructions[self.offset].opname
return None
def oparg(self) -> int:
"""Get the argument of the current instruction."""
if self.offset < len(self.instructions):
return self.instructions[self.offset].arg or 0
return 0
def name(self) -> str | None:
"""
Extract variable name from current instruction.
"""
opname = self.opcode()
if not opname:
return None
names = None
if opname in ("STORE_NAME", "STORE_GLOBAL"):
names = self.code_object.co_names
elif opname == "STORE_FAST":
names = self.code_object.co_varnames
elif opname == "STORE_DEREF":
names = self.code_object.co_cellvars
if not names:
names = self.code_object.co_freevars
else:
return None
arg = self.oparg()
if names and 0 <= arg < len(names):
return names[arg]
return None
@@ -0,0 +1,69 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, TYPE_CHECKING
import torch
if TYPE_CHECKING:
from ._dim_entry import DimEntry
@dataclass
class TensorInfo:
tensor: torch.Tensor | None
levels: list[DimEntry]
has_device: bool
batchedtensor: torch.Tensor | None
def __post_init__(self) -> None:
from ._dim_entry import DimEntry
if not all(isinstance(l, DimEntry) for l in self.levels):
raise AssertionError("All levels must be DimEntry instances")
def ndim(self) -> int:
from ._dim_entry import ndim_of_levels
return ndim_of_levels(self.levels)
def __bool__(self) -> bool:
return self.tensor is not None
@staticmethod
def create(
h: Any, ensure_batched: bool = True, ensure_present: bool = True
) -> TensorInfo:
from . import Dim, DimEntry, Tensor
if Tensor.check_exact(h):
# functorch Tensor with first-class dimensions
return TensorInfo(
h._get_tensor(),
h._get_levels(),
h._get_has_device(),
h._get_batchtensor() if ensure_batched else None,
)
elif Dim.check_exact(h):
# For Dim objects, only get range/batchtensor if needed and dimension is bound
tensor = h._get_range() if h.is_bound else None
batchtensor = (
h._get_batchtensor() if ensure_batched and h.is_bound else None
)
return TensorInfo(
tensor,
[DimEntry(h)],
False,
batchtensor,
)
elif isinstance(h, torch.Tensor):
# Plain torch tensor - create positional levels
levels = []
for i in range(-h.dim(), 0):
levels.append(DimEntry(i))
return TensorInfo(h, levels, True, h)
else:
if ensure_present:
raise ValueError("expected a tensor object")
return TensorInfo(None, [], False, None)
@@ -0,0 +1,269 @@
"""
Python implementation of function wrapping functionality for functorch.dim.
"""
from __future__ import annotations
import functools
from typing import Any, 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
# Update docstring if we have a dim_name
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)
# Copy metadata using functools.update_wrapper for just __name__ and __doc__
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)")
# Get dimension argument
dim_arg = kwargs.get(wrapper.dim_name)
if dim_arg is None and wrapper.dim_offset < len(args):
# Try to get dim from positional args (accounting for self at index 0)
dim_idx = wrapper.dim_offset + 1
if dim_idx < len(args):
dim_arg = args[dim_idx]
# If no dimension argument provided, fall back to standard functorch handling
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)
# Handle dimension-aware operation
info = TensorInfo.create(args[0])
if not info:
return wrapper.orig(*args, **kwargs)
# Check for keepdim parameter
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)
# Wrap dimensions
ndim = info.ndim()
dims = _wrap_dims(dim_arg, ndim, keepdim)
# Convert dimensions to indices and validate
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:
# Try to match by position/name more flexibly
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)
# Determine new levels after reduction
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[:]
# Create dimension indices for the original function
if len(dim_indices) == 1:
py_indices: Any = dim_indices[0]
else:
py_indices = tuple(dim_indices)
# Update arguments
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)
# Update dimension argument
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
# Call original function
result = wrapper.orig(*new_args, **new_kwargs)
# Wrap results
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: int | None = None,
keepdim_offset: int | None = None,
dim_name: str | None = None,
single_dim: bool | None = None,
reduce: bool | None = 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: dict | None = None,
) -> Any:
"""
Handle __torch_function__ calls for wrapped operators.
"""
if kwargs is None:
kwargs = {}
# Import here to avoid circular imports
from . import _Tensor
# Use the torch function mechanism from _Tensor
return _Tensor.__torch_function__(func, types, args, kwargs)
@@ -0,0 +1,48 @@
# 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.
import os
import signal
import subprocess
from collections.abc import Generator
from contextlib import contextmanager
@contextmanager
def magic_trace(
output: str = "trace.fxt", magic_trace_cache: str = "/tmp/magic-trace"
) -> Generator[None, None, None]:
pid = os.getpid()
if not os.path.exists(magic_trace_cache):
print(f"Downloading magic_trace to: {magic_trace_cache}")
subprocess.run(
[
"wget",
"-O",
magic_trace_cache,
"-q",
"https://github.com/janestreet/magic-trace/releases/download/v1.0.2/magic-trace",
]
)
subprocess.run(["chmod", "+x", magic_trace_cache])
args = [magic_trace_cache, "attach", "-pid", str(pid), "-o", output]
p = subprocess.Popen(args, stderr=subprocess.PIPE, encoding="utf-8")
if p.stderr is None:
raise AssertionError("Expected stderr to be non-None")
while True:
x = p.stderr.readline()
print(x)
if "Attached" in x:
break
try:
yield
finally:
p.send_signal(signal.SIGINT)
r = p.wait()
if p.stderr is not None:
print(p.stderr.read())
p.stderr.close()
if r != 0:
raise ValueError(f"magic_trace exited abnormally: {r}")
@@ -0,0 +1,312 @@
# 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.
import torch
# pointwise operators can go through a faster pathway
tensor_magic_methods = ["add", ""]
pointwise_magic_methods_with_reverse = (
"add",
"sub",
"mul",
"floordiv",
"div",
"truediv",
"mod",
"pow",
"lshift",
"rshift",
"and",
"or",
"xor",
)
pointwise_magic_methods = (
*(x for m in pointwise_magic_methods_with_reverse for x in (m, "r" + m)),
"eq",
"gt",
"le",
"lt",
"ge",
"gt",
"ne",
"neg",
"pos",
"abs",
"invert",
"iadd",
"isub",
"imul",
"ifloordiv",
"idiv",
"itruediv",
"imod",
"ipow",
"ilshift",
"irshift",
"iand",
"ior",
"ixor",
"int",
"long",
"float",
"complex",
)
pointwise_methods = (*(f"__{m}__" for m in pointwise_magic_methods),)
pointwise = (
*(getattr(torch.Tensor, m) for m in pointwise_methods),
torch.nn.functional.dropout,
torch.where,
torch.Tensor.abs,
torch.abs,
torch.Tensor.acos,
torch.acos,
torch.Tensor.acosh,
torch.acosh,
torch.Tensor.add,
torch.add,
torch.Tensor.addcdiv,
torch.addcdiv,
torch.Tensor.addcmul,
torch.addcmul,
torch.Tensor.addr,
torch.addr,
torch.Tensor.angle,
torch.angle,
torch.Tensor.asin,
torch.asin,
torch.Tensor.asinh,
torch.asinh,
torch.Tensor.atan,
torch.atan,
torch.Tensor.atan2,
torch.atan2,
torch.Tensor.atanh,
torch.atanh,
torch.Tensor.bitwise_and,
torch.bitwise_and,
torch.Tensor.bitwise_left_shift,
torch.bitwise_left_shift,
torch.Tensor.bitwise_not,
torch.bitwise_not,
torch.Tensor.bitwise_or,
torch.bitwise_or,
torch.Tensor.bitwise_right_shift,
torch.bitwise_right_shift,
torch.Tensor.bitwise_xor,
torch.bitwise_xor,
torch.Tensor.ceil,
torch.ceil,
torch.celu,
torch.nn.functional.celu,
torch.Tensor.clamp,
torch.clamp,
torch.Tensor.clamp_max,
torch.clamp_max,
torch.Tensor.clamp_min,
torch.clamp_min,
torch.Tensor.copysign,
torch.copysign,
torch.Tensor.cos,
torch.cos,
torch.Tensor.cosh,
torch.cosh,
torch.Tensor.deg2rad,
torch.deg2rad,
torch.Tensor.digamma,
torch.digamma,
torch.Tensor.div,
torch.div,
torch.dropout,
torch.nn.functional.dropout,
torch.nn.functional.elu,
torch.Tensor.eq,
torch.eq,
torch.Tensor.erf,
torch.erf,
torch.Tensor.erfc,
torch.erfc,
torch.Tensor.erfinv,
torch.erfinv,
torch.Tensor.exp,
torch.exp,
torch.Tensor.exp2,
torch.exp2,
torch.Tensor.expm1,
torch.expm1,
torch.feature_dropout,
torch.Tensor.float_power,
torch.float_power,
torch.Tensor.floor,
torch.floor,
torch.Tensor.floor_divide,
torch.floor_divide,
torch.Tensor.fmod,
torch.fmod,
torch.Tensor.frac,
torch.frac,
torch.Tensor.frexp,
torch.frexp,
torch.Tensor.gcd,
torch.gcd,
torch.Tensor.ge,
torch.ge,
torch.nn.functional.gelu,
torch.nn.functional.glu,
torch.Tensor.gt,
torch.gt,
torch.Tensor.hardshrink,
torch.hardshrink,
torch.nn.functional.hardshrink,
torch.nn.functional.hardsigmoid,
torch.nn.functional.hardswish,
torch.nn.functional.hardtanh,
torch.Tensor.heaviside,
torch.heaviside,
torch.Tensor.hypot,
torch.hypot,
torch.Tensor.i0,
torch.i0,
torch.Tensor.igamma,
torch.igamma,
torch.Tensor.igammac,
torch.igammac,
torch.Tensor.isclose,
torch.isclose,
torch.Tensor.isfinite,
torch.isfinite,
torch.Tensor.isinf,
torch.isinf,
torch.Tensor.isnan,
torch.isnan,
torch.Tensor.isneginf,
torch.isneginf,
torch.Tensor.isposinf,
torch.isposinf,
torch.Tensor.isreal,
torch.isreal,
torch.Tensor.kron,
torch.kron,
torch.Tensor.lcm,
torch.lcm,
torch.Tensor.ldexp,
torch.ldexp,
torch.Tensor.le,
torch.le,
torch.nn.functional.leaky_relu,
torch.Tensor.lerp,
torch.lerp,
torch.Tensor.lgamma,
torch.lgamma,
torch.Tensor.log,
torch.log,
torch.Tensor.log10,
torch.log10,
torch.Tensor.log1p,
torch.log1p,
torch.Tensor.log2,
torch.log2,
torch.nn.functional.logsigmoid,
torch.Tensor.logical_and,
torch.logical_and,
torch.Tensor.logical_not,
torch.logical_not,
torch.Tensor.logical_or,
torch.logical_or,
torch.Tensor.logical_xor,
torch.logical_xor,
torch.Tensor.logit,
torch.logit,
torch.Tensor.lt,
torch.lt,
torch.Tensor.maximum,
torch.maximum,
torch.Tensor.minimum,
torch.minimum,
torch.nn.functional.mish,
torch.Tensor.mvlgamma,
torch.mvlgamma,
torch.Tensor.nan_to_num,
torch.nan_to_num,
torch.Tensor.ne,
torch.ne,
torch.Tensor.neg,
torch.neg,
torch.Tensor.nextafter,
torch.nextafter,
torch.Tensor.outer,
torch.outer,
torch.polar,
torch.Tensor.polygamma,
torch.polygamma,
torch.Tensor.positive,
torch.positive,
torch.Tensor.pow,
torch.pow,
torch.Tensor.prelu,
torch.prelu,
torch.nn.functional.prelu,
torch.Tensor.rad2deg,
torch.rad2deg,
torch.Tensor.reciprocal,
torch.reciprocal,
torch.Tensor.relu,
torch.relu,
torch.nn.functional.relu,
torch.nn.functional.relu6,
torch.Tensor.remainder,
torch.remainder,
torch.Tensor.round,
torch.round,
torch.rrelu,
torch.nn.functional.rrelu,
torch.Tensor.rsqrt,
torch.rsqrt,
torch.rsub,
torch.selu,
torch.nn.functional.selu,
torch.Tensor.sgn,
torch.sgn,
torch.Tensor.sigmoid,
torch.sigmoid,
torch.nn.functional.sigmoid,
torch.Tensor.sign,
torch.sign,
torch.Tensor.signbit,
torch.signbit,
torch.nn.functional.silu,
torch.Tensor.sin,
torch.sin,
torch.Tensor.sinc,
torch.sinc,
torch.Tensor.sinh,
torch.sinh,
torch.nn.functional.softplus,
torch.nn.functional.softshrink,
torch.Tensor.sqrt,
torch.sqrt,
torch.Tensor.square,
torch.square,
torch.Tensor.sub,
torch.sub,
torch.Tensor.tan,
torch.tan,
torch.Tensor.tanh,
torch.tanh,
torch.nn.functional.tanh,
torch.threshold,
torch.nn.functional.threshold,
torch.trapz,
torch.Tensor.true_divide,
torch.true_divide,
torch.Tensor.trunc,
torch.trunc,
torch.Tensor.xlogy,
torch.xlogy,
torch.rand_like,
)
@@ -0,0 +1,71 @@
# 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.
import functools
from collections.abc import Callable
from types import (
BuiltinMethodType,
FunctionType,
GetSetDescriptorType,
MethodDescriptorType,
WrapperDescriptorType,
)
from typing import Any
FUNC_TYPES = (
FunctionType,
MethodDescriptorType,
BuiltinMethodType,
WrapperDescriptorType,
)
PROPERTY_TYPES = (GetSetDescriptorType, property)
def _py_wrap_method(orig: Callable, __torch_function__: Callable) -> Callable:
def impl(*args: Any, **kwargs: Any) -> Any:
return __torch_function__(orig, None, args, kwargs)
# Copy metadata using functools.update_wrapper for just __name__ and __doc__
functools.update_wrapper(impl, orig, assigned=("__name__", "__doc__"), updated=())
return impl
def wrap_type(to_patch: Any, pattern: type, __torch_function__: Callable) -> None:
wrap_method = _py_wrap_method
all: dict[str, Any] = {}
for t in reversed(pattern.mro()[:-1]): # skip object
all.update(t.__dict__)
def wrap_attr(orig: Any) -> property:
return property(wrap_method(orig.__get__, __torch_function__))
for name, obj in all.items():
if name in (
"__dict__",
"__new__",
"__init__",
"__repr__",
"__weakref__",
"__doc__",
"__module__",
"__dir__",
):
continue
# skip things that have been overloaded
# things that come from object like `__eq__` still need to be patched, however.
if hasattr(to_patch, name) and getattr(to_patch, name) is not getattr(
object, name, None
):
continue
if isinstance(obj, FUNC_TYPES):
setattr(to_patch, name, wrap_method(obj, __torch_function__))
elif isinstance(obj, PROPERTY_TYPES):
setattr(to_patch, name, wrap_attr(obj))
@@ -0,0 +1,4 @@
from .rearrange import rearrange
__all__ = ["rearrange"]
@@ -0,0 +1,306 @@
"""Adapted from https://github.com/arogozhnikov/einops/blob/36c7bb16e57d6e57f8f3050f9e07abdf3f00469f/einops/parsing.py.
MIT License
Copyright (c) 2018 Alex Rogozhnikov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
from __future__ import annotations
import keyword
import warnings
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Collection, Mapping
_ellipsis: str = "\u2026" # NB, this is a single unicode symbol. String is used as it is not a list, but can be iterated
class AnonymousAxis:
"""Used by `ParsedExpression` to represent an axis with a size (> 1), but no associated identifier.
Note: Different instances of this class are not equal to each other, even if they have the same value.
"""
def __init__(self, value: str) -> None:
self.value = int(value)
if self.value < 1:
raise ValueError(
f"Anonymous axis should have positive length, not {self.value}"
)
def __repr__(self) -> str:
return f"{self.value}-axis"
class ParsedExpression:
"""Structure containing information about one side of an `einops`-style pattern (e.g. 'b c (h w)')."""
def __init__(
self,
expression: str,
*,
allow_underscore: bool = False,
allow_duplicates: bool = False,
) -> None:
"""Parse the expression and store relevant metadata.
Args:
expression (str): the `einops`-pattern to parse
allow_underscore (bool): whether to allow axis identifier names to begin with an underscore
allow_duplicates (bool): whether to allow an identifier to appear more than once in the expression
"""
self.has_ellipsis: bool = False
self.has_ellipsis_parenthesized: bool | None = None
self.identifiers: set[str | AnonymousAxis] = set()
# that's axes like 2, 3, 4 or 5. Axes with size 1 are exceptional and replaced with empty composition
self.has_non_unitary_anonymous_axes: bool = False
# composition keeps structure of composite axes, see how different corner cases are handled in tests
self.composition: list[list[str | AnonymousAxis] | str] = []
if "." in expression:
if "..." not in expression:
raise ValueError(
"Expression may contain dots only inside ellipsis (...)"
)
if str.count(expression, "...") != 1 or str.count(expression, ".") != 3:
raise ValueError(
"Expression may contain dots only inside ellipsis (...); only one ellipsis for tensor "
)
expression = expression.replace("...", _ellipsis)
self.has_ellipsis = True
bracket_group: list[str | AnonymousAxis] | None = None
def add_axis_name(x: str) -> None:
if x in self.identifiers:
if not (allow_underscore and x == "_") and not allow_duplicates:
raise ValueError(
f"Indexing expression contains duplicate dimension '{x}'"
)
if x == _ellipsis:
self.identifiers.add(_ellipsis)
if bracket_group is None:
self.composition.append(_ellipsis)
self.has_ellipsis_parenthesized = False
else:
bracket_group.append(_ellipsis)
self.has_ellipsis_parenthesized = True
else:
is_number = str.isdecimal(x)
if is_number and int(x) == 1:
# handling the case of anonymous axis of length 1
if bracket_group is None:
self.composition.append([])
else:
pass # no need to think about 1s inside parenthesis
return
is_axis_name, reason = self.check_axis_name_return_reason(
x, allow_underscore=allow_underscore
)
if not (is_number or is_axis_name):
raise ValueError(f"Invalid axis identifier: {x}\n{reason}")
axis_name: str | AnonymousAxis = AnonymousAxis(x) if is_number else x
self.identifiers.add(axis_name)
if is_number:
self.has_non_unitary_anonymous_axes = True
if bracket_group is None:
self.composition.append([axis_name])
else:
bracket_group.append(axis_name)
current_identifier = None
for char in expression:
if char in "() ":
if current_identifier is not None:
add_axis_name(current_identifier)
current_identifier = None
if char == "(":
if bracket_group is not None:
raise ValueError(
"Axis composition is one-level (brackets inside brackets not allowed)"
)
bracket_group = []
elif char == ")":
if bracket_group is None:
raise ValueError("Brackets are not balanced")
self.composition.append(bracket_group)
bracket_group = None
elif str.isalnum(char) or char in ["_", _ellipsis]:
if current_identifier is None:
current_identifier = char
else:
current_identifier += char
else:
raise ValueError(f"Unknown character '{char}'")
if bracket_group is not None:
raise ValueError(f"Imbalanced parentheses in expression: '{expression}'")
if current_identifier is not None:
add_axis_name(current_identifier)
@staticmethod
def check_axis_name_return_reason(
name: str, allow_underscore: bool = False
) -> tuple[bool, str]:
"""Check if the given axis name is valid, and a message explaining why if not.
Valid axes names are python identifiers except keywords, and should not start or end with an underscore.
Args:
name (str): the axis name to check
allow_underscore (bool): whether axis names are allowed to start with an underscore
Returns:
tuple[bool, str]: whether the axis name is valid, a message explaining why if not
"""
if not str.isidentifier(name):
return False, "not a valid python identifier"
elif name[0] == "_" or name[-1] == "_":
if name == "_" and allow_underscore:
return True, ""
return False, "axis name should should not start or end with underscore"
else:
if keyword.iskeyword(name):
warnings.warn(
f"It is discouraged to use axes names that are keywords: {name}",
RuntimeWarning,
)
if name == "axis":
warnings.warn(
"It is discouraged to use 'axis' as an axis name and will raise an error in future",
FutureWarning,
)
return True, ""
@staticmethod
def check_axis_name(name: str) -> bool:
"""Check if the name is a valid axis name.
Args:
name (str): the axis name to check
Returns:
bool: whether the axis name is valid
"""
is_valid, _ = ParsedExpression.check_axis_name_return_reason(name)
return is_valid
def parse_pattern(
pattern: str, axes_lengths: Mapping[str, int]
) -> tuple[ParsedExpression, ParsedExpression]:
"""Parse an `einops`-style pattern into a left-hand side and right-hand side `ParsedExpression` object.
Args:
pattern (str): the `einops`-style rearrangement pattern
axes_lengths (Mapping[str, int]): any additional length specifications for dimensions
Returns:
tuple[ParsedExpression, ParsedExpression]: a tuple containing the left-hand side and right-hand side expressions
"""
# adapted from einops.einops._prepare_transformation_recipe
# https://github.com/arogozhnikov/einops/blob/230ac1526c1f42c9e1f7373912c7f8047496df11/einops/einops.py
try:
left_str, right_str = pattern.split("->")
except ValueError:
raise ValueError("Pattern must contain a single '->' separator") from None
if _ellipsis in axes_lengths:
raise ValueError(f"'{_ellipsis}' is not an allowed axis identifier")
left = ParsedExpression(left_str)
right = ParsedExpression(right_str)
if not left.has_ellipsis and right.has_ellipsis:
raise ValueError(
f"Ellipsis found in right side, but not left side of a pattern {pattern}"
)
if left.has_ellipsis and left.has_ellipsis_parenthesized:
raise ValueError(
f"Ellipsis is parenthesis in the left side is not allowed: {pattern}"
)
return left, right
def validate_rearrange_expressions(
left: ParsedExpression, right: ParsedExpression, axes_lengths: Mapping[str, int]
) -> None:
"""Perform expression validations that are specific to the `rearrange` operation.
Args:
left (ParsedExpression): left-hand side expression
right (ParsedExpression): right-hand side expression
axes_lengths (Mapping[str, int]): any additional length specifications for dimensions
"""
for length in axes_lengths.values():
if (length_type := type(length)) is not int:
raise TypeError(
f"rearrange axis lengths must be integers, got: {length_type}"
)
if left.has_non_unitary_anonymous_axes or right.has_non_unitary_anonymous_axes:
raise ValueError("rearrange only supports unnamed axes of size 1")
difference = set.symmetric_difference(left.identifiers, right.identifiers)
if len(difference) > 0:
raise ValueError(
f"Identifiers only on one side of rearrange expression (should be on both): {difference}"
)
unmatched_axes = axes_lengths.keys() - left.identifiers
if len(unmatched_axes) > 0:
raise ValueError(
f"Identifiers not found in rearrange expression: {unmatched_axes}"
)
def comma_separate(collection: Collection[str | Collection[str]]) -> str:
"""Convert a collection of strings representing first class dims into a comma-separated string.
Args:
collection (Collection[Union[str, Collection[str]]]): the collection of strings to convert
Returns:
str: the comma-separated string
Examples:
>>> comma_separate(("d0",))
'd0'
>>> comma_separate(("d0", "d1", "d2", "d3"))
'd0, d1, d2, d3'
>>> comma_separate([("d1", "d4")])
'(d1, d4)'
>>> comma_separate([("d0",), (), ("d1",), ("d2",), ("d3", "d4")])
'(d0,), (), (d1,), (d2,), (d3, d4)'
"""
return ", ".join(
item
if isinstance(item, str)
else f"({comma_separate(item)}{',' if len(item) == 1 else ''})"
for item in collection
)
@@ -0,0 +1,210 @@
from __future__ import annotations
import functools
from typing import TYPE_CHECKING
import torch
from functorch.dim import dims # noqa: F401
from ._parsing import (
_ellipsis,
AnonymousAxis,
comma_separate,
parse_pattern,
validate_rearrange_expressions,
)
if TYPE_CHECKING:
from collections.abc import Callable, Sequence
__all__ = ["rearrange"]
@functools.lru_cache(256)
def _create_rearrange_callable(
tensor_ndim: int, pattern: str, **axes_lengths: int
) -> Callable[[torch.Tensor], torch.Tensor]:
r"""Translate an `einops`-style pattern into a callable that performs the rearrange using first-class dimensions.
Since the an equivalent result is computed for tensors with the same number of dimensions, with the same pattern and
specified axes lengths, this function can be memoized.
Args:
tensor_ndim (int): the number of dimensions in the tensor to rearrange
pattern (str): the `einops`-style rearrangement pattern
axes_lengths (int): any additional length specifications for dimensions
Returns:
Callable[[torch.Tensor], torch.Tensor]: a callable that performs the rearrangement
"""
left, right = parse_pattern(pattern, axes_lengths)
validate_rearrange_expressions(left, right, axes_lengths)
n_anon_dims = sum(not dim for dim in left.composition)
if left.has_ellipsis:
n_ellipsis_dims = tensor_ndim - (len(left.composition) - 1)
n_named_dims = len(left.identifiers) - 1
if (pattern_ndim := n_anon_dims + n_named_dims) > tensor_ndim:
raise ValueError(
f"Number of dimensions in pattern ({pattern_ndim}) must be less than or equal to the number of "
f"dimensions in the tensor ({tensor_ndim})"
)
else:
n_ellipsis_dims = 0
n_named_dims = len(left.identifiers)
if (pattern_ndim := len(left.composition)) != tensor_ndim:
raise ValueError(
f"Number of dimensions in pattern ({pattern_ndim}) must be equal to the number of dimensions in "
f"the tensor ({tensor_ndim})"
)
n_dims = n_named_dims + n_ellipsis_dims + n_anon_dims
if n_dims == 0:
# an identity rearrangement on a 0-dimension tensor
return lambda tensor: tensor
first_class_dims: tuple[str, ...] = tuple(f"d{i}" for i in range(n_dims))
identifier_dim_map: dict[str | AnonymousAxis, tuple[str, ...]] = {}
anon_axes: list[AnonymousAxis] = []
# map the left-hand side identifiers to strings representing first class dims
dims_i = 0
for dimension in left.composition:
if isinstance(dimension, list):
for identifier in dimension:
# non-unitary anon axes are not allowed in rearrange & unitary anon axes are represented as empty lists
if not isinstance(identifier, str):
raise AssertionError(f"Expected str, got {type(identifier)}")
identifier_dim_map[identifier] = (first_class_dims[dims_i],)
dims_i += 1
if not dimension:
# unitary anonymous axis
anon_axis = AnonymousAxis("1")
identifier_dim_map[anon_axis] = (first_class_dims[dims_i],)
anon_axes.append(anon_axis)
dimension.append(anon_axis)
dims_i += 1
elif dimension == _ellipsis:
identifier = _ellipsis
identifier_dim_map[identifier] = tuple(
first_class_dims[dims_i + j] for j in range(n_ellipsis_dims)
)
dims_i += n_ellipsis_dims
else:
raise ValueError(f"Unexpected dimension: {dimension}")
def composition_to_dims(
composition: Sequence[list[str | AnonymousAxis] | str],
) -> list[str | tuple[str, ...]]:
"""Convert a `ParsedExpression.composition` into a `Tensor.__getitem__` index of strings representing first
class dims."""
dim_composition: list[str | tuple[str, ...]] = []
for dimension in composition:
if isinstance(dimension, list):
dim_composition.append(
tuple(
dim
for identifier in dimension
for dim in identifier_dim_map[identifier]
)
)
elif dimension == _ellipsis:
dim_composition.extend(identifier_dim_map[_ellipsis])
else:
raise ValueError(f"Unexpected dimension: {dimension}")
return dim_composition
left_dims = composition_to_dims(left.composition)
right_dims = composition_to_dims(right.composition)
anon_dims = tuple(identifier_dim_map[axis][0] for axis in anon_axes)
specified_lengths = tuple(
(identifier_dim_map[axis][0], length) for axis, length in axes_lengths.items()
)
custom_rearrange_callable_name = "do_rearrange"
custom_rearrange_callable_code = (
(
f"def {custom_rearrange_callable_name}(tensor):\n"
f" {comma_separate(first_class_dims)} = dims({n_dims})\n"
)
+ (
"".join(
f" {dim}.size = {length}\n" for (dim, length) in specified_lengths
)
if specified_lengths
else ""
)
+ f" tensor = tensor[{comma_separate(left_dims)}].order({comma_separate(right_dims)})\n"
+ (
f" return tensor.sum({comma_separate([anon_dims])}, keepdim=False)\n"
if anon_dims
else " return tensor\n"
)
)
exec(custom_rearrange_callable_code)
return locals()[custom_rearrange_callable_name]
def rearrange(
tensor: torch.Tensor | list[torch.Tensor] | tuple[torch.Tensor, ...],
pattern: str,
**axes_lengths: int,
) -> torch.Tensor:
r"""A native implementation of `einops.rearrange`, a reader-friendly smart element reordering for multidimensional
tensors. This operation includes functionality of transpose (axes permutation), reshape (view), squeeze, unsqueeze,
stack, concatenate and other operations.
See: https://einops.rocks/api/rearrange/
Args:
tensor (Tensor or sequence of Tensor): the tensor(s) to rearrange
pattern (str): the rearrangement pattern
axes_lengths (int): any additional length specifications for dimensions
Returns:
Tensor: the rearranged tensor
Examples:
>>> # suppose we have a set of 32 images in "h w c" format (height-width-channel)
>>> images = torch.randn((32, 30, 40, 3))
>>> # stack along first (batch) axis, output is a single array
>>> rearrange(images, "b h w c -> b h w c").shape
torch.Size([32, 30, 40, 3])
>>> # concatenate images along height (vertical axis), 960 = 32 * 30
>>> rearrange(images, "b h w c -> (b h) w c").shape
torch.Size([960, 40, 3])
>>> # concatenated images along horizontal axis, 1280 = 32 * 40
>>> rearrange(images, "b h w c -> h (b w) c").shape
torch.Size([30, 1280, 3])
>>> # reordered axes to "b c h w" format for deep learning
>>> rearrange(images, "b h w c -> b c h w").shape
torch.Size([32, 3, 30, 40])
>>> # flattened each image into a vector, 3600 = 30 * 40 * 3
>>> rearrange(images, "b h w c -> b (c h w)").shape
torch.Size([32, 3600])
>>> # split each image into 4 smaller (top-left, top-right, bottom-left, bottom-right), 128 = 32 * 2 * 2
>>> rearrange(images, "b (h1 h) (w1 w) c -> (b h1 w1) h w c", h1=2, w1=2).shape
torch.Size([128, 15, 20, 3])
>>> # space-to-depth operation
>>> rearrange(images, "b (h h1) (w w1) c -> b h w (c h1 w1)", h1=2, w1=2).shape
torch.Size([32, 15, 20, 12])
"""
if not isinstance(tensor, torch.Tensor):
tensor = torch.stack(tensor)
rearrange_callable = _create_rearrange_callable(
tensor.ndim, pattern, **axes_lengths
)
return rearrange_callable(tensor)
@@ -0,0 +1,5 @@
# PyTorch forward-mode is not mature yet
from torch._functorch.apis import chunk_vmap
from torch._functorch.batch_norm_replacement import replace_all_batch_norm_modules_
from torch._functorch.eager_transforms import hessian, jacfwd, jvp
from torch.func import functionalize
@@ -0,0 +1,6 @@
from torch import cond # noqa: F401
from torch._higher_order_ops.map import ( # noqa: F401
_stack_pytree,
_unstack_pytree,
map,
)
@@ -0,0 +1 @@
from torch._ops import HigherOrderOperator # noqa: F401