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,7 @@
from torch._C import FileCheck as FileCheck
from . import _utils
# pyrefly: ignore [deprecated]
from ._comparison import assert_allclose, assert_close as assert_close
from ._creation import make_tensor as make_tensor
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,276 @@
"""
This module contains tensor creation utilities.
"""
import collections.abc
import functools
import math
import warnings
from typing import cast
import torch
_INTEGRAL_TYPES = [
torch.uint8,
torch.int8,
torch.int16,
torch.int32,
torch.int64,
torch.uint16,
torch.uint32,
torch.uint64,
]
_FLOATING_TYPES = [torch.float16, torch.bfloat16, torch.float32, torch.float64]
_FLOATING_8BIT_TYPES = [
torch.float8_e4m3fn,
torch.float8_e5m2,
torch.float8_e4m3fnuz,
torch.float8_e5m2fnuz,
]
_COMPLEX_TYPES = [torch.complex32, torch.complex64, torch.complex128]
_BOOLEAN_OR_INTEGRAL_TYPES = [torch.bool, *_INTEGRAL_TYPES]
_FLOATING_OR_COMPLEX_TYPES = [*_FLOATING_TYPES, *_COMPLEX_TYPES]
def _uniform_random_(t: torch.Tensor, low: float, high: float) -> torch.Tensor:
# uniform_ requires to-from <= std::numeric_limits<scalar_t>::max()
# Work around this by scaling the range before and after the PRNG
if high - low >= torch.finfo(t.dtype).max:
return t.uniform_(low / 2, high / 2).mul_(2)
else:
return t.uniform_(low, high)
def make_tensor(
*shape: int | torch.Size | list[int] | tuple[int, ...],
dtype: torch.dtype,
device: str | torch.device,
low: float | None = None,
high: float | None = None,
requires_grad: bool = False,
noncontiguous: bool = False,
exclude_zero: bool = False,
memory_format: torch.memory_format | None = None,
) -> torch.Tensor:
r"""Creates a tensor with the given :attr:`shape`, :attr:`device`, and :attr:`dtype`, and filled with
values uniformly drawn from ``[low, high)``.
If :attr:`low` or :attr:`high` are specified and are outside the range of the :attr:`dtype`'s representable
finite values then they are clamped to the lowest or highest representable finite value, respectively.
If ``None``, then the following table describes the default values for :attr:`low` and :attr:`high`,
which depend on :attr:`dtype`.
+---------------------------+------------+----------+
| ``dtype`` | ``low`` | ``high`` |
+===========================+============+==========+
| boolean type | ``0`` | ``2`` |
+---------------------------+------------+----------+
| unsigned integral type | ``0`` | ``10`` |
+---------------------------+------------+----------+
| signed integral types | ``-9`` | ``10`` |
+---------------------------+------------+----------+
| floating types | ``-9`` | ``9`` |
+---------------------------+------------+----------+
| complex types | ``-9`` | ``9`` |
+---------------------------+------------+----------+
Args:
shape (Tuple[int, ...]): Single integer or a sequence of integers defining the shape of the output tensor.
dtype (:class:`torch.dtype`): The data type of the returned tensor.
device (Union[str, torch.device]): The device of the returned tensor.
low (Optional[Number]): Sets the lower limit (inclusive) of the given range. If a number is provided it is
clamped to the least representable finite value of the given dtype. When ``None`` (default),
this value is determined based on the :attr:`dtype` (see the table above). Default: ``None``.
high (Optional[Number]): Sets the upper limit (exclusive) of the given range. If a number is provided it is
clamped to the greatest representable finite value of the given dtype. When ``None`` (default) this value
is determined based on the :attr:`dtype` (see the table above). Default: ``None``.
.. deprecated:: 2.1
Passing ``low==high`` to :func:`~torch.testing.make_tensor` for floating or complex types is deprecated
since 2.1 and will be removed in 2.3. Use :func:`torch.full` instead.
requires_grad (Optional[bool]): If autograd should record operations on the returned tensor. Default: ``False``.
noncontiguous (Optional[bool]): If `True`, the returned tensor will be noncontiguous. This argument is
ignored if the constructed tensor has fewer than two elements. Mutually exclusive with ``memory_format``.
exclude_zero (Optional[bool]): If ``True`` then zeros are replaced with the dtype's small positive value
depending on the :attr:`dtype`. For bool and integer types zero is replaced with one. For floating
point types it is replaced with the dtype's smallest positive normal number (the "tiny" value of the
:attr:`dtype`'s :func:`~torch.finfo` object), and for complex types it is replaced with a complex number
whose real and imaginary parts are both the smallest positive normal number representable by the complex
type. Default ``False``.
memory_format (Optional[torch.memory_format]): The memory format of the returned tensor. Mutually exclusive
with ``noncontiguous``.
Raises:
ValueError: If ``requires_grad=True`` is passed for integral `dtype`
ValueError: If ``low >= high``.
ValueError: If either :attr:`low` or :attr:`high` is ``nan``.
ValueError: If both :attr:`noncontiguous` and :attr:`memory_format` are passed.
TypeError: If :attr:`dtype` isn't supported by this function.
Examples:
>>> # xdoctest: +SKIP
>>> # xdoctest: +REQUIRES(env:TORCH_DOCTEST_CUDA)
>>> from torch.testing import make_tensor
>>> # Creates a float tensor with values in [-1, 1)
>>> make_tensor((3,), device="cpu", dtype=torch.float32, low=-1, high=1)
>>> # xdoctest: +SKIP
tensor([ 0.1205, 0.2282, -0.6380])
>>> # Creates a bool tensor on CUDA
>>> make_tensor((2, 2), device="cuda", dtype=torch.bool)
tensor([[False, False],
[False, True]], device='cuda:0')
"""
def modify_low_high(
low: float | None,
high: float | None,
*,
lowest_inclusive: float,
highest_exclusive: float,
default_low: float,
default_high: float,
) -> tuple[float, float]:
"""
Modifies (and raises ValueError when appropriate) low and high values given by the user (input_low, input_high)
if required.
"""
def clamp(a: float, l: float, h: float) -> float:
return min(max(a, l), h)
low = low if low is not None else default_low
high = high if high is not None else default_high
if any(isinstance(value, float) and math.isnan(value) for value in [low, high]):
raise ValueError(
f"`low` and `high` cannot be NaN, but got {low=} and {high=}"
)
elif low == high and dtype in _FLOATING_OR_COMPLEX_TYPES:
warnings.warn(
"Passing `low==high` to `torch.testing.make_tensor` for floating or complex types "
"is deprecated since 2.1 and will be removed in 2.3. "
"Use `torch.full(...)` instead.",
FutureWarning,
stacklevel=3,
)
elif low >= high:
raise ValueError(f"`low` must be less than `high`, but got {low} >= {high}")
elif high < lowest_inclusive or low >= highest_exclusive:
raise ValueError(
f"The value interval specified by `low` and `high` is [{low}, {high}), "
f"but {dtype} only supports [{lowest_inclusive}, {highest_exclusive})"
)
low = clamp(low, lowest_inclusive, highest_exclusive)
high = clamp(high, lowest_inclusive, highest_exclusive)
if dtype in _BOOLEAN_OR_INTEGRAL_TYPES:
# 1. `low` is ceiled to avoid creating values smaller than `low` and thus outside the specified interval
# 2. Following the same reasoning as for 1., `high` should be floored. However, the higher bound of
# `torch.randint` is exclusive, and thus we need to ceil here as well.
return math.ceil(low), math.ceil(high)
return low, high
if len(shape) == 1 and isinstance(shape[0], collections.abc.Sequence):
shape = shape[0] # type: ignore[assignment]
shape = cast(tuple[int, ...], tuple(shape))
if noncontiguous and memory_format is not None:
raise ValueError(
f"The parameters `noncontiguous` and `memory_format` are mutually exclusive, "
f"but got {noncontiguous=} and {memory_format=}"
)
if requires_grad and dtype in _BOOLEAN_OR_INTEGRAL_TYPES:
raise ValueError(
f"`requires_grad=True` is not supported for boolean and integral dtypes, but got {dtype=}"
)
noncontiguous = noncontiguous and functools.reduce(lambda x, y: x * y, shape, 1) > 1
if noncontiguous:
# Double the size of the shape in the last dimension, so that we have
# non-identical values when we make the non-contiguous operation.
shape = cast(tuple[int, ...], (*shape[:-1], 2 * shape[-1]))
if dtype is torch.bool:
low, high = cast(
tuple[int, int],
modify_low_high(
low,
high,
lowest_inclusive=0,
highest_exclusive=2,
default_low=0,
default_high=2,
),
)
result = torch.randint(low, high, shape, device=device, dtype=dtype)
elif dtype in _BOOLEAN_OR_INTEGRAL_TYPES:
low, high = cast(
tuple[int, int],
modify_low_high(
low,
high,
lowest_inclusive=torch.iinfo(dtype).min,
highest_exclusive=torch.iinfo(dtype).max
# In theory, `highest_exclusive` should always be the maximum value + 1. However, `torch.randint`
# internally converts the bounds to an int64 and would overflow. In other words: `torch.randint` cannot
# sample 2**63 - 1, i.e. the maximum value of `torch.int64` and we need to account for that here.
+ (1 if dtype is not torch.int64 else 0),
# This is incorrect for `torch.uint8`, but since we clamp to `lowest`, i.e. 0 for `torch.uint8`,
# _after_ we use the default value, we don't need to special case it here
default_low=-9,
default_high=10,
),
)
result = torch.randint(low, high, shape, device=device, dtype=dtype)
elif dtype in _FLOATING_OR_COMPLEX_TYPES:
low, high = modify_low_high(
low,
high,
lowest_inclusive=torch.finfo(dtype).min,
highest_exclusive=torch.finfo(dtype).max,
default_low=-9,
default_high=9,
)
result = torch.empty(shape, device=device, dtype=dtype)
_uniform_random_(
torch.view_as_real(result) if dtype in _COMPLEX_TYPES else result, low, high
)
elif dtype in _FLOATING_8BIT_TYPES:
low, high = modify_low_high(
low,
high,
lowest_inclusive=torch.finfo(dtype).min,
highest_exclusive=torch.finfo(dtype).max,
default_low=-9,
default_high=9,
)
result = torch.empty(shape, device=device, dtype=torch.float32)
_uniform_random_(result, low, high)
result = result.to(dtype)
else:
raise TypeError(
f"The requested dtype '{dtype}' is not supported by torch.testing.make_tensor()."
" To request support, file an issue at: https://github.com/pytorch/pytorch/issues"
)
if noncontiguous:
# Offset by 1 to also catch offsetting issues
result = result[..., 1::2]
elif memory_format is not None:
result = result.clone(memory_format=memory_format)
if exclude_zero:
result[result == 0] = (
1 if dtype in _BOOLEAN_OR_INTEGRAL_TYPES else torch.finfo(dtype).tiny
)
if dtype in _FLOATING_OR_COMPLEX_TYPES:
result.requires_grad = requires_grad
return result
@@ -0,0 +1,473 @@
# mypy: ignore-errors
import collections
import torch
from torch.testing._internal.common_utils import TEST_WITH_ROCM
from torch.testing._internal.common_utils import TestCase
class AutocastTestLists:
def _rnn_cell_args(self, n, num_chunks, is_lstm, dev, dtype):
input = (torch.randn((n, n), device=dev, dtype=torch.float32),)
hx = ((torch.randn((n, n), device=dev, dtype=torch.float32),
torch.randn((n, n), device=dev, dtype=torch.float32)) if is_lstm else
torch.randn((n, n), device=dev, dtype=torch.float32),)
weights = (torch.randn((num_chunks * n, n), device=dev, dtype=torch.float32), # weight_ih
torch.randn((num_chunks * n, n), device=dev, dtype=torch.float32), # weight_hh
torch.randn((num_chunks * n), device=dev, dtype=torch.float32), # bias_ih
torch.randn((num_chunks * n), device=dev, dtype=torch.float32)) # bias_hh
# returns args as a tuple
return input + hx + weights
# Supplies ops and arguments for test_autocast_* in test/test_cuda.py
def __init__(self, dev):
super().__init__()
n = 8
# Utility arguments, created as one-element tuples
pointwise0_fp16 = (torch.randn(n, dtype=torch.float16, device=dev),)
pointwise1_fp16 = (torch.randn(n, dtype=torch.float16, device=dev),)
pointwise2_fp16 = (torch.randn(n, dtype=torch.float16, device=dev),)
mat0_fp16 = (torch.randn((n, n), dtype=torch.float16, device=dev),)
mat1_fp16 = (torch.randn((n, n), dtype=torch.float16, device=dev),)
mat2_fp16 = (torch.randn((n, n), dtype=torch.float16, device=dev),)
dimsets = ((n, n, n), (n, n, n, n), (n, n, n, n, n))
conv_args_fp32 = [(torch.randn(dimset, dtype=torch.float32, device=dev),
torch.randn(dimset, dtype=torch.float32, device=dev))
for dimset in dimsets]
bias_fp32 = (torch.randn((n,), dtype=torch.float32, device=dev),)
element0_fp32 = (torch.randn(1, dtype=torch.float32, device=dev),)
pointwise0_fp32 = (torch.randn(n, dtype=torch.float32, device=dev),)
pointwise1_fp32 = (torch.randn(n, dtype=torch.float32, device=dev),)
mat0_fp32 = (torch.randn((n, n), dtype=torch.float32, device=dev),)
mat1_fp32 = (torch.randn((n, n), dtype=torch.float32, device=dev),)
mat2_fp32 = (torch.randn((n, n), dtype=torch.float32, device=dev),)
mat3_fp32 = (torch.randn((n, n), dtype=torch.float32, device=dev),)
# The lists below organize ops that autocast needs to test.
# self.list_name corresponds to test_autocast_list_name in test/test_cuda.py.
# Each op is associated with a tuple of valid arguments.
# In addition, cudnn conv ops are not supported on ROCm and hence will
# be skipped by passing TEST_WITH_ROCM flag to those ops in self.torch_fp16 list.
# Some ops implement built-in type promotion. These don't need autocasting,
# but autocasting relies on their promotion, so we include tests to double-check.
self.torch_expect_builtin_promote = [
("eq", pointwise0_fp32 + pointwise1_fp16, torch.bool),
("ge", pointwise0_fp32 + pointwise1_fp16, torch.bool),
("gt", pointwise0_fp32 + pointwise1_fp16, torch.bool),
("le", pointwise0_fp32 + pointwise1_fp16, torch.bool),
("lt", pointwise0_fp32 + pointwise1_fp16, torch.bool),
("ne", pointwise0_fp32 + pointwise1_fp16, torch.bool),
("add", pointwise0_fp32 + pointwise1_fp16, torch.float32),
("div", pointwise0_fp32 + pointwise1_fp16, torch.float32),
("mul", pointwise0_fp32 + pointwise1_fp16, torch.float32),
("cat", (pointwise0_fp16 + pointwise1_fp32,), torch.float32),
("equal", pointwise0_fp32 + pointwise1_fp16, torch.float32),
("stack", (pointwise0_fp16 + pointwise1_fp32,), torch.float32),
]
self.methods_expect_builtin_promote = [
("__eq__", pointwise0_fp32 + pointwise1_fp16, torch.bool),
("__ge__", pointwise0_fp32 + pointwise1_fp16, torch.bool),
("__gt__", pointwise0_fp32 + pointwise1_fp16, torch.bool),
("__le__", pointwise0_fp32 + pointwise1_fp16, torch.bool),
("__lt__", pointwise0_fp32 + pointwise1_fp16, torch.bool),
("__ne__", pointwise0_fp32 + pointwise1_fp16, torch.bool),
("__add__", pointwise0_fp32 + pointwise1_fp16, torch.float32),
("__div__", pointwise0_fp32 + pointwise1_fp16, torch.float32),
("__mul__", pointwise0_fp32 + pointwise1_fp16, torch.float32),
]
# The remaining lists organize ops that autocast treats explicitly.
self.torch_fp16 = [
# deprecated _convolution
("_convolution", conv_args_fp32[1] + bias_fp32 + ((1, 1), (0, 0), (1, 1), False,
(0, 0), 1, False, True, True)),
# the current _convolution
("_convolution", conv_args_fp32[1] + bias_fp32 + ((1, 1), (0, 0), (1, 1), False,
(0, 0), 1, False, True, True, True)),
("conv1d", conv_args_fp32[0]),
("conv2d", conv_args_fp32[1]),
("conv3d", conv_args_fp32[2]),
("conv_tbc", conv_args_fp32[0] + bias_fp32),
("conv_transpose1d", conv_args_fp32[0]),
("conv_transpose2d", conv_args_fp32[1]),
("conv_transpose3d", conv_args_fp32[2]),
("convolution", conv_args_fp32[1] + bias_fp32 + ((1, 1), (0, 0), (1, 1), False, (0, 0), 1)),
("cudnn_convolution", conv_args_fp32[1] + ((0, 0), (1, 1), (1, 1), 1, False, True, True), TEST_WITH_ROCM),
("cudnn_convolution_transpose", conv_args_fp32[1] + ((0, 0), (0, 0), (1, 1),
(1, 1), 1, False, True, True), TEST_WITH_ROCM),
("prelu", pointwise0_fp32 + element0_fp32),
("addmm", mat1_fp32 + mat2_fp32 + mat3_fp32),
("addmv", pointwise0_fp32 + mat2_fp32 + pointwise1_fp32),
("addr", mat0_fp32 + pointwise0_fp32 + pointwise1_fp32),
("matmul", mat0_fp32 + mat1_fp32),
("einsum", "bkhd,bqhd->bqkh", mat0_fp32 + mat1_fp32),
("mm", mat0_fp32 + mat1_fp32),
("mv", mat0_fp32 + pointwise0_fp32),
("chain_matmul", mat0_fp32 + mat1_fp32 + mat2_fp32),
("addbmm", mat0_fp32 + (torch.randn((n, n, n), device=dev, dtype=torch.float32),
torch.randn((n, n, n), device=dev, dtype=torch.float32))),
("baddbmm", (torch.randn((n, n, n), device=dev, dtype=torch.float32),
torch.randn((n, n, n), device=dev, dtype=torch.float32),
torch.randn((n, n, n), device=dev, dtype=torch.float32))),
("bmm", (torch.randn((n, n, n), device=dev, dtype=torch.float32),
torch.randn((n, n, n), device=dev, dtype=torch.float32))),
# _thnn_fused_lstm_cell and _thnn_fused_gru_cell are not Python-exposed as far as I can tell.
# ("_thnn_fused_lstm_cell", mat0_fp32 + mat1_fp32 + mat2_fp32 + pointwise0_fp32 + pointwise1_fp32),
# ("_thnn_fused_gru_cell", mat0_fp32 + mat1_fp32 + mat2_fp32 + pointwise0_fp32 + pointwise1_fp32),
("lstm_cell", self._rnn_cell_args(n, num_chunks=4, is_lstm=True, dev=dev, dtype=torch.float32)),
("gru_cell", self._rnn_cell_args(n, num_chunks=3, is_lstm=False, dev=dev, dtype=torch.float32)),
("rnn_tanh_cell", self._rnn_cell_args(n, num_chunks=1, is_lstm=False, dev=dev, dtype=torch.float32)),
("rnn_relu_cell", self._rnn_cell_args(n, num_chunks=1, is_lstm=False, dev=dev, dtype=torch.float32)),
]
self.torch_fp32 = [
("acos", (pointwise0_fp16[0].clamp(-.9, 0.9),)),
("asin", (pointwise0_fp16[0].clamp(-.9, 0.9),)),
("cosh", pointwise0_fp16),
("erfinv", (pointwise0_fp16[0].clamp(-.9, .9),)),
("exp", pointwise0_fp16),
("expm1", pointwise0_fp16),
("log", (pointwise0_fp16[0].clamp(0.1, 100.0),)),
("log10", (pointwise0_fp16[0].clamp(0.1, 100.0),)),
("log2", (pointwise0_fp16[0].clamp(0.1, 100.0),)),
("log1p", (pointwise0_fp16[0].clamp(-0.9, 100.0),)),
("reciprocal", pointwise0_fp16),
("rsqrt", (pointwise0_fp16[0].clamp(0.0, 100.0),)),
("sinh", pointwise0_fp16),
("tan", (pointwise0_fp16[0].clamp(-3.1 / 2, 3.1 / 2),)),
("pow", ((pointwise0_fp16[0] + 1.).clamp(0.0, 100.0),) + pointwise1_fp16),
("pow", ((pointwise0_fp16[0] + 1.).clamp(0.0, 100.0),) + (1.7,)),
# ("pow", (1.7,) + pointwise0_fp16), # This variant has a backend, but is not documented in the API.
("softmax", pointwise0_fp16 + (0,)),
("log_softmax", pointwise0_fp16 + (0,)),
("layer_norm", pointwise0_fp16 + ((pointwise0_fp16[0].numel(),),)),
("rms_norm", pointwise0_fp16 + ((pointwise0_fp16[0].numel(),),)),
("group_norm", mat0_fp16 + (1,)),
("norm", pointwise0_fp16),
("norm", pointwise0_fp16, {"dim": 0}),
# these need magma
# ("norm", mat0_fp16, {"p": "nuc"}),
# ("norm", mat0_fp16, {"p": "nuc", "dim": 0}),
("norm", pointwise0_fp16, {"p": 1}),
("norm", pointwise0_fp16, {"p": 1, "dim": 0}),
("cosine_similarity", mat0_fp16 + mat1_fp16),
("poisson_nll_loss", mat0_fp16 + mat1_fp16 + (True, False, 1.e-8, torch.nn._reduction.get_enum('mean'))),
("cosine_embedding_loss", (torch.tensor([[1, 2, 3]], device=dev, dtype=torch.float16),
torch.tensor([[1, 3, 4]], device=dev, dtype=torch.float16),
torch.tensor([1], device=dev, dtype=torch.int))),
("hinge_embedding_loss", mat0_fp16 + (torch.ones(n, device=dev, dtype=torch.int),)),
("kl_div", mat0_fp16 + (torch.rand((n, n), device=dev, dtype=torch.float16),)),
("margin_ranking_loss", mat0_fp16 + mat1_fp16 + (torch.ones((n,), device=dev, dtype=torch.float16),)),
("triplet_margin_loss", mat0_fp16 + mat1_fp16 + mat2_fp16),
("binary_cross_entropy_with_logits", mat0_fp16 + (torch.rand((n, n), device=dev, dtype=torch.float16),)),
("cumprod", pointwise0_fp16 + (0,)),
("cumsum", pointwise0_fp16 + (0,)),
("dist", pointwise0_fp16 + pointwise1_fp16),
("pdist", mat0_fp16),
("cdist", mat0_fp16 + mat1_fp16),
("prod", pointwise0_fp16),
("prod", pointwise0_fp16 + (0,)),
("renorm", mat0_fp16 + (2, 0, 1.0)),
("sum", pointwise0_fp16),
("sum", mat0_fp16 + (1,)),
("logsumexp", mat0_fp16 + (1,)),
]
self.torch_need_autocast_promote = [
("addcdiv", pointwise0_fp32 + pointwise1_fp16 + (pointwise2_fp16[0].clamp(0.1, 100),)),
("addcmul", pointwise0_fp32 + pointwise1_fp16 + pointwise2_fp16),
("atan2", pointwise0_fp32 + (pointwise1_fp16[0].clamp(0.1, 100),)),
("bilinear", (torch.randn((1, 2), dtype=torch.float16, device=dev),
torch.randn((1, 2), dtype=torch.float32, device=dev),
torch.randn((1, 2, 2), dtype=torch.float16, device=dev),
torch.randn((1,), dtype=torch.float32, device=dev))),
("cross", (torch.randn(3, dtype=torch.float32, device=dev),
torch.randn(3, dtype=torch.float16, device=dev))),
("dot", pointwise0_fp16 + pointwise1_fp32),
("vdot", pointwise0_fp16 + pointwise1_fp32),
("grid_sampler", (torch.randn((2, 3, 33, 22), dtype=torch.float16, device=dev),
torch.randn((2, 22, 11, 2), dtype=torch.float32, device=dev),
0, 0, False)),
("index_put", pointwise0_fp32 + ((torch.tensor([1], device=dev, dtype=torch.long),),
torch.randn(1, device=dev, dtype=torch.float16))),
("index_put", pointwise0_fp16 + ((torch.tensor([1], device=dev, dtype=torch.long),),
torch.randn(1, device=dev, dtype=torch.float32))),
("tensordot", (torch.randn((2, 2, 2), dtype=torch.float32, device=dev),
torch.randn((2, 2, 2), dtype=torch.float16, device=dev))),
("scatter_add", (torch.zeros(2, 2, 2, dtype=torch.float32, device=dev),
0,
torch.randint(0, 2, (2, 2, 2), device=dev),
torch.randn((2, 2, 2), dtype=torch.float16, device=dev))),
("scatter_add", (torch.zeros(2, 2, 2, dtype=torch.float16, device=dev),
0,
torch.randint(0, 2, (2, 2, 2), device=dev),
torch.randn((2, 2, 2), dtype=torch.float32, device=dev))),
]
self.nn_fp16 = [
("linear", mat0_fp32 + mat1_fp32 + mat2_fp32),
]
self.nn_fp32 = [
("softplus", pointwise0_fp16),
("nll_loss", (torch.rand((n, n), device=dev, dtype=torch.float),
torch.zeros((n,), device=dev, dtype=torch.long))),
("nll_loss2d", (torch.rand((n, n, n, n), device=dev, dtype=torch.half),
torch.zeros((n, n, n), device=dev, dtype=torch.long))),
("l1_loss", mat0_fp16 + mat1_fp16),
("smooth_l1_loss", mat0_fp16 + mat1_fp16),
("mse_loss", mat0_fp16 + mat1_fp16),
("multilabel_margin_loss", mat0_fp16 + (torch.ones((n, n), device=dev, dtype=torch.long),)),
("soft_margin_loss", mat0_fp16 + (torch.ones((n, n), device=dev, dtype=torch.long),)),
("multi_margin_loss", mat0_fp16 + (torch.ones((n,), device=dev, dtype=torch.long),)),
]
self.linalg_fp16 = [
("linalg_vecdot", mat0_fp32 + mat0_fp32),
("linalg_multi_dot", (mat0_fp32 + mat1_fp32 + mat2_fp32,)),
]
self.methods_fp16 = [
("__matmul__", mat0_fp32 + mat1_fp32)
]
self.methods_fp32 = [
("__pow__", (torch.rand(n, device=dev, dtype=torch.float16), 1.5)),
]
self.banned = [
("binary_cross_entropy", (torch.rand((n, n), device=dev, dtype=torch.float32),
torch.rand((n, n), device=dev, dtype=torch.float32)), torch._C._nn),
]
class AutocastCPUTestLists:
# Supplies ops and arguments for test_autocast_* in test/test_cpu.py
def __init__(self, dev):
super().__init__()
n = 8
# Utility arguments, created as one-element tuples
pointwise0_bf16 = (torch.randn(n, dtype=torch.bfloat16, device=dev),)
pointwise1_bf16 = (torch.randn(n, dtype=torch.bfloat16, device=dev),)
mat0_bf16 = (torch.randn((n, n), dtype=torch.bfloat16, device=dev),)
mat1_bf16 = (torch.randn((n, n), dtype=torch.bfloat16, device=dev),)
mat2_bf16 = (torch.randn((n, n), dtype=torch.bfloat16, device=dev),)
pointwise0_fp16 = (torch.randn(n, dtype=torch.float16, device=dev),)
pointwise1_fp16 = (torch.randn(n, dtype=torch.float16, device=dev),)
dummy_dimsets = ((n,), (n, n), (n, n, n), (n, n, n, n), (n, n, n, n, n))
dummy_bf16 = [(torch.randn(dimset, dtype=torch.bfloat16, device=dev),)
for dimset in dummy_dimsets]
dimsets = ((n, n, n), (n, n, n, n), (n, n, n, n, n))
conv_args_fp32 = [(torch.randn(dimset, dtype=torch.float32, device=dev),
torch.randn(dimset, dtype=torch.float32, device=dev))
for dimset in dimsets]
element0_fp32 = (torch.randn(1, dtype=torch.float32, device=dev),)
pointwise0_fp32 = (torch.randn(n, dtype=torch.float32, device=dev),)
pointwise1_fp32 = (torch.randn(n, dtype=torch.float32, device=dev),)
mat0_fp32 = (torch.randn((n, n), dtype=torch.float32, device=dev),)
mat1_fp32 = (torch.randn((n, n), dtype=torch.float32, device=dev),)
mat2_fp32 = (torch.randn((n, n), dtype=torch.float32, device=dev),)
mat3_fp32 = (torch.randn((n, n), dtype=torch.float32, device=dev),)
dummy_fp32 = [ # noqa: F841
(torch.randn(dimset, dtype=torch.float32, device=dev),)
for dimset in dummy_dimsets
]
# The lists below organize ops that autocast needs to test.
# self.list_name corresponds to test_autocast_list_name in test/test_cpu.py.
# Each op is associated with a tuple of valid arguments.
# Some ops implement built-in type promotion. These don't need autocasting,
# but autocasting relies on their promotion, so we include tests to double-check.
self.torch_expect_builtin_promote = [
("eq", pointwise0_fp32 + pointwise1_bf16, pointwise0_fp32 + pointwise1_fp16, torch.bool),
("ge", pointwise0_fp32 + pointwise1_bf16, pointwise0_fp32 + pointwise1_fp16, torch.bool),
("gt", pointwise0_fp32 + pointwise1_bf16, pointwise0_fp32 + pointwise1_fp16, torch.bool),
("le", pointwise0_fp32 + pointwise1_bf16, pointwise0_fp32 + pointwise1_fp16, torch.bool),
("lt", pointwise0_fp32 + pointwise1_bf16, pointwise0_fp32 + pointwise1_fp16, torch.bool),
("ne", pointwise0_fp32 + pointwise1_bf16, pointwise0_fp32 + pointwise1_fp16, torch.bool),
("add", pointwise0_fp32 + pointwise1_bf16, pointwise0_fp32 + pointwise1_fp16, torch.float32),
("div", pointwise0_fp32 + pointwise1_bf16, pointwise0_fp32 + pointwise1_fp16, torch.float32),
("mul", pointwise0_fp32 + pointwise1_bf16, pointwise0_fp32 + pointwise1_fp16, torch.float32),
]
self.methods_expect_builtin_promote = [
("__eq__", pointwise0_fp32 + pointwise1_bf16, pointwise0_fp32 + pointwise1_fp16, torch.bool),
("__ge__", pointwise0_fp32 + pointwise1_bf16, pointwise0_fp32 + pointwise1_fp16, torch.bool),
("__gt__", pointwise0_fp32 + pointwise1_bf16, pointwise0_fp32 + pointwise1_fp16, torch.bool),
("__le__", pointwise0_fp32 + pointwise1_bf16, pointwise0_fp32 + pointwise1_fp16, torch.bool),
("__lt__", pointwise0_fp32 + pointwise1_bf16, pointwise0_fp32 + pointwise1_fp16, torch.bool),
("__ne__", pointwise0_fp32 + pointwise1_bf16, pointwise0_fp32 + pointwise1_fp16, torch.bool),
("__add__", pointwise0_fp32 + pointwise1_bf16, pointwise0_fp32 + pointwise1_fp16, torch.float32),
("__div__", pointwise0_fp32 + pointwise1_bf16, pointwise0_fp32 + pointwise1_fp16, torch.float32),
("__mul__", pointwise0_fp32 + pointwise1_bf16, pointwise0_fp32 + pointwise1_fp16, torch.float32),
]
# The remaining lists organize ops that autocast treats explicitly.
self.torch_16 = [
("conv1d", conv_args_fp32[0]),
("conv2d", conv_args_fp32[1]),
("conv3d", conv_args_fp32[2]),
("bmm", (torch.randn((n, n, n), device=dev, dtype=torch.float32),
torch.randn((n, n, n), device=dev, dtype=torch.float32))),
("mm", mat0_fp32 + mat1_fp32),
("matmul", mat0_fp32 + mat1_fp32),
("baddbmm", (torch.randn((n, n, n), device=dev, dtype=torch.float32),
torch.randn((n, n, n), device=dev, dtype=torch.float32),
torch.randn((n, n, n), device=dev, dtype=torch.float32))),
("addmm", mat1_fp32 + mat2_fp32 + mat3_fp32),
("_addmm_activation", mat1_fp32 + mat2_fp32 + mat3_fp32, {"beta": 1, "alpha": 1, "use_gelu": True}),
("addbmm", mat0_fp32 + (torch.randn((n, n, n), device=dev, dtype=torch.float32),
torch.randn((n, n, n), device=dev, dtype=torch.float32))),
("conv_tbc", (torch.randn((10, 7, 3), device=dev, dtype=torch.float32),
torch.randn((5, 3, 5), device=dev, dtype=torch.float32),
torch.randn(5, device=dev, dtype=torch.float32),
0)),
("conv_transpose1d", conv_args_fp32[0]),
("conv_transpose2d", conv_args_fp32[1]),
("conv_transpose3d", conv_args_fp32[2]),
("prelu", pointwise0_fp32 + element0_fp32),
("_native_multi_head_attention", (torch.randn((n, n, n), device=dev, dtype=torch.float32),
torch.randn((n, n, n), device=dev, dtype=torch.float32),
torch.randn((n, n, n), device=dev, dtype=torch.float32),
n, 4, torch.randn((3 * n, n), device=dev, dtype=torch.float32),
torch.randn((3 * n), device=dev, dtype=torch.float32),
torch.randn((n, n), device=dev, dtype=torch.float32),
torch.randn((n), device=dev, dtype=torch.float32))),
]
self.torch_fp32 = [
("poisson_nll_loss", mat0_bf16 + mat1_bf16 + (True, False, 1.e-8, torch.nn._reduction.get_enum('mean'))),
("cosine_embedding_loss", (torch.tensor([[1, 2, 3]], device=dev, dtype=torch.bfloat16),
torch.tensor([[1, 3, 4]], device=dev, dtype=torch.bfloat16),
torch.tensor([1], device=dev, dtype=torch.int))),
("hinge_embedding_loss", mat0_bf16 + (torch.ones(n, device=dev, dtype=torch.int),)),
("margin_ranking_loss", mat0_bf16 + mat1_bf16 + (torch.ones((n,), device=dev, dtype=torch.bfloat16),)),
("triplet_margin_loss", mat0_bf16 + mat1_bf16 + mat2_bf16),
("binary_cross_entropy_with_logits", mat0_bf16 + (torch.rand((n, n), device=dev, dtype=torch.bfloat16),)),
]
self.nn_16 = [
("linear", mat0_fp32 + mat1_fp32, {}),
]
self.nn_fp32 = [
("avg_pool3d", dummy_bf16[3], {"kernel_size": (3, 3, 3), "stride": (1, 1, 1)}),
("binary_cross_entropy", (torch.rand((n, n), device=dev, dtype=torch.bfloat16),) +
(torch.rand((n, n), device=dev, dtype=torch.bfloat16),)),
("reflection_pad1d", dummy_bf16[2], {"padding": (3, 3)}),
("nll_loss", (torch.rand((n, n), device=dev, dtype=torch.bfloat16),
torch.zeros((n,), device=dev, dtype=torch.long))),
("nll_loss2d", (torch.rand((n, n, n, n), device=dev, dtype=torch.bfloat16),
torch.zeros((n, n, n), device=dev, dtype=torch.long))),
("l1_loss", mat0_bf16 + mat1_bf16),
("smooth_l1_loss", mat0_bf16 + mat1_bf16),
("mse_loss", mat0_bf16 + mat1_bf16),
("multilabel_margin_loss", mat0_bf16 + (torch.ones((n, n), device=dev, dtype=torch.long),)),
("soft_margin_loss", mat0_bf16 + (torch.ones((n, n), device=dev, dtype=torch.long),)),
("multi_margin_loss", mat0_bf16 + (torch.ones((n,), device=dev, dtype=torch.long),)),
("huber_loss", mat0_bf16 + mat1_bf16),
]
self.torch_need_autocast_promote = [
("cat", (pointwise0_bf16 + pointwise1_fp32,), (pointwise0_fp16 + pointwise1_fp32,)),
("stack", (pointwise0_bf16 + pointwise1_fp32,), (pointwise0_fp16 + pointwise1_fp32,)),
]
class TestAutocast(TestCase):
def args_maybe_kwargs(self, op_with_args):
if len(op_with_args) == 2:
return op_with_args[0], op_with_args[1], {}
else:
return op_with_args[0], op_with_args[1], op_with_args[2]
def _run_autocast_outofplace(
self,
op,
args,
run_as_type,
device,
out_type=None,
module=torch,
add_kwargs=None,
amp_dtype=torch.bfloat16,
):
# helper to cast args
def cast(val, to_type):
if isinstance(val, torch.Tensor):
return val.to(to_type) if val.is_floating_point() else val
elif isinstance(val, collections.abc.Iterable):
return type(val)(cast(v, to_type) for v in val)
else:
return val
if add_kwargs is None:
add_kwargs = {}
self.assertFalse(torch.is_autocast_enabled(device_type=device))
with torch.amp.autocast(device_type=device, dtype=amp_dtype):
self.assertTrue(torch.is_autocast_enabled(device_type=device))
out_type = out_type if out_type is not None else run_as_type
output = output_method = None
# Try module.* variant, if requested:
if module is not None and hasattr(module, op):
output = getattr(module, op)(*args, **add_kwargs)
if isinstance(output, torch.Tensor):
self.assertTrue(
out_type == output.dtype,
f"autocast for torch.{op} produced {output.dtype}, should produce {out_type}",
)
# Try Tensor.* variant:
if hasattr(torch.Tensor, op):
output_method = getattr(args[0], op)(*args[1:], **add_kwargs)
if isinstance(output_method, torch.Tensor):
self.assertTrue(
out_type == output_method.dtype,
f"autocast for torch.{op} produced {output_method.dtype}, should produce torch.{out_type}",
)
self.assertTrue(
(output is not None) or (output_method is not None),
f"{op} not found as an attribute on either Tensor or the requested module {module}",
)
# Accounts for ops that return Tensors, iterables, and other non-Tensors.
# For example, lstm_cell returns a tuple and equal returns bool.
def compare(first, second):
if isinstance(first, torch.Tensor):
return torch.equal(first, second)
elif isinstance(first, collections.abc.Iterable):
return all(compare(f, s) for f, s in zip(first, second, strict=False))
else:
return first == second
# If both torch.* and Tensor.* variants were found, check outputs are identical
if (output is not None) and (output_method is not None):
self.assertTrue(type(output) is type(output_method))
comparison = compare(output, output_method)
self.assertTrue(
comparison, f"torch.{op} result did not match Tensor.{op} result"
)
# Compare numerics to Python-side "autocasting" that (we expect) does the same thing
# as the C++-side autocasting, and should be bitwise accurate.
output_to_compare = output if output is not None else output_method
with torch.amp.autocast(device_type=device, enabled=False):
self.assertFalse(
torch.is_autocast_enabled(device_type=device)
)
if module is not None and hasattr(module, op):
control = getattr(module, op)(
*cast(args, run_as_type), **add_kwargs
)
else:
control = getattr(args[0].to(run_as_type), op)(
*cast(args[1:], run_as_type), **add_kwargs
)
self.assertTrue(type(output_to_compare) is type(control))
comparison = compare(output_to_compare, control)
self.assertTrue(comparison, f"torch.{op} result did not match control")
self.assertTrue(torch.is_autocast_enabled(device_type=device))
self.assertFalse(torch.is_autocast_enabled(device_type=device))
@@ -0,0 +1,635 @@
# mypy: ignore-errors
import torch
from functools import partial
from torch.testing import make_tensor
from torch.testing._internal.opinfo.core import (
OpInfo,
SampleInput,
)
from torch.testing._internal.common_dtype import all_types_and
import numpy as np
# Note: [autograd.Function db]
#
# This is a collection of autograd.Function test cases written as OpInfos
# so they can easily be consumed by OpInfo-based tests to check if a subsystem
# supports autograd.Function.
#
# Axes:
# - saves {output, input, intermediate, non-tensor}
# - {inputs, output} x {single tensor, tensors, arbitrary objects}
# - Uses {mark_dirty, mark_non_differentiable, once_differentiable}
def to_numpy(tensor):
return tensor.cpu().numpy()
class NumpyCube(torch.autograd.Function):
@staticmethod
def forward(input):
input_np = to_numpy(input)
dinput = torch.tensor(3 * input_np ** 2, device=input.device)
return torch.tensor(input_np ** 3, device=input.device), dinput
@staticmethod
def setup_context(ctx, inputs, output):
ctx.save_for_backward(inputs[0], output[1])
ctx.save_for_forward(inputs[0], output[1])
@staticmethod
def backward(ctx, grad_output, grad_saved):
input, dinput = ctx.saved_tensors
return NumpyMul.apply(grad_output, dinput) + 6 * NumpyMul.apply(grad_saved, input)
@staticmethod
def vmap(info, in_dims, input):
result = NumpyCube.apply(input)
return result, (in_dims[0], in_dims[0])
@staticmethod
def jvp(ctx, input_tangent):
input, dinput = ctx.saved_tensors
return NumpyMul.apply(input_tangent, dinput), 6 * NumpyMul.apply(input_tangent, input)
class CubeGenVmap(torch.autograd.Function):
generate_vmap_rule = True
@staticmethod
def forward(x):
return x ** 3, 3 * x ** 2
@staticmethod
def setup_context(ctx, inputs, outputs):
ctx.save_for_backward(inputs[0], outputs[1])
ctx.save_for_forward(inputs[0], outputs[1])
@staticmethod
def backward(ctx, grad_output, grad_saved):
_input, dinput = ctx.saved_tensors
result = grad_output * dinput + 6 * dinput
return result
@staticmethod
def jvp(ctx, input_tangent):
input, dinput = ctx.saved_tensors
return MulGenVmap.apply(input_tangent, dinput), 6 * NumpyMul.apply(input_tangent, input)
def sample_inputs_numpy_cube(opinfo, device, dtype, requires_grad, **kwargs):
make_arg = partial(make_tensor, device=device, dtype=dtype, requires_grad=requires_grad)
yield SampleInput(make_arg(1, low=0.8, high=2), args=())
class NumpyCubeNotComposable(torch.autograd.Function):
@staticmethod
def forward(input):
input_np = to_numpy(input)
return torch.tensor(input_np ** 3, device=input.device), input_np
@staticmethod
def setup_context(ctx, inputs, output):
_, input_np = output
ctx.input_np = input_np
ctx.device = inputs[0].device
@staticmethod
@torch.autograd.function.once_differentiable
def backward(ctx, grad_output, grad_saved):
result_np = 3 * (ctx.input_np ** 2)
return torch.tensor(result_np, device=ctx.device)
class NumpyMul(torch.autograd.Function):
@staticmethod
def forward(x, y):
return torch.tensor(to_numpy(x) * to_numpy(y), device=x.device)
@staticmethod
def setup_context(ctx, inputs, output):
ctx.save_for_backward(*inputs)
ctx.save_for_forward(*inputs)
@staticmethod
def backward(ctx, grad_output):
x, y = ctx.saved_tensors
gx = None
if ctx.needs_input_grad[0]:
gx = NumpyMul.apply(grad_output, y)
gy = None
if ctx.needs_input_grad[1]:
gy = NumpyMul.apply(grad_output, x)
return gx, gy
@staticmethod
def vmap(info, in_dims, x, y):
x_bdim, y_bdim = in_dims
x = x.movedim(x_bdim, -1) if x_bdim is not None else x.unsqueeze(-1)
y = y.movedim(y_bdim, -1) if y_bdim is not None else y.unsqueeze(-1)
result = NumpyMul.apply(x, y)
result = result.movedim(-1, 0)
return result, 0
@staticmethod
def jvp(ctx, x_tangent, y_tangent):
x, y = ctx.saved_tensors
return x_tangent * y + y_tangent * x
def sample_inputs_numpy_mul(opinfo, device, dtype, requires_grad, **kwargs):
make_arg = partial(make_tensor, device=device, dtype=dtype, requires_grad=requires_grad)
# Broadcasting
yield SampleInput(make_arg(4, low=0.9, high=2), args=(make_arg(3, 4, low=0.9, high=2),))
def sample_inputs_numpy_mul_scalar(opinfo, device, dtype, requires_grad, **kwargs):
make_arg = partial(make_tensor, device=device, dtype=dtype, requires_grad=requires_grad)
yield SampleInput(make_arg(4, low=0.9, high=2), args=(), kwargs={"scalar": 3.14})
class MulGenVmap(torch.autograd.Function):
generate_vmap_rule = True
@staticmethod
def forward(x, y):
return x * y
@staticmethod
def setup_context(ctx, inputs, outputs):
ctx.save_for_backward(*inputs)
ctx.save_for_forward(*inputs)
@staticmethod
def backward(ctx, grad_output):
x, y = ctx.saved_tensors
gx = None
if ctx.needs_input_grad[0]:
gx = MulGenVmap.apply(grad_output, y)
gy = None
if ctx.needs_input_grad[1]:
gy = MulGenVmap.apply(grad_output, x)
return gx, gy
@staticmethod
def jvp(ctx, x_tangent, y_tangent):
x, y = ctx.saved_tensors
return x_tangent * y + y_tangent * x
class NumpyExp_(torch.autograd.Function):
@staticmethod
def forward(x):
x_np = to_numpy(x)
np.exp(x_np, x_np)
return x
@staticmethod
def setup_context(ctx, inputs, output):
x, = inputs
ctx.mark_dirty(x)
ctx.save_for_backward(output)
ctx.save_for_forward(output)
@staticmethod
def backward(ctx, grad_output):
output, = ctx.saved_tensors
return NumpyMul.apply(grad_output, output)
@staticmethod
def vmap(info, in_dims, x):
NumpyExp_.apply(x)
return x, in_dims[0]
@staticmethod
def jvp(ctx, x_tangent):
# Doesn't call numpy operations because I didn't want to write NumpyMul_
output, = ctx.saved_tensors
x_tangent.mul_(output)
return x_tangent
class NumpySort(torch.autograd.Function):
@staticmethod
def forward(x, dim):
device = x.device
x = to_numpy(x)
ind = np.argsort(x, axis=dim)
ind_inv = np.argsort(ind, axis=dim)
return (
torch.tensor(x, device=device),
torch.tensor(ind, device=device),
torch.tensor(ind_inv, device=device),
)
@staticmethod
def setup_context(ctx, inputs, output):
_x, dim = inputs
_, ind, ind_inv = output
ctx.mark_non_differentiable(ind, ind_inv)
ctx.save_for_backward(ind, ind_inv)
ctx.save_for_forward(ind, ind_inv)
ctx.dim = dim
@staticmethod
def backward(ctx, grad_output, _0, _1):
ind, ind_inv = ctx.saved_tensors
return NumpyTake.apply(grad_output, ind_inv, ind, ctx.dim), None
@staticmethod
def vmap(info, in_dims, x, dim):
x_bdim, _ = in_dims
x = x.movedim(x_bdim, 0)
# wrap dim
dim = dim if dim >= 0 else dim + x.dim() - 1
return NumpySort.apply(x, dim + 1), (0, 0, 0)
@staticmethod
def jvp(ctx, x_tangent, _):
ind, ind_inv = ctx.saved_tensors
return NumpyTake.apply(x_tangent, ind, ind_inv, ctx.dim), None, None
class SortGenVmap(torch.autograd.Function):
generate_vmap_rule = True
@staticmethod
def forward(x, dim):
ind = torch.argsort(x, dim=dim)
ind_inv = torch.argsort(ind, axis=dim)
result = torch.take_along_dim(x, ind, dim=dim)
return result, ind, ind_inv
@staticmethod
def setup_context(ctx, inputs, outputs):
x, dim = inputs
_, ind, ind_inv = outputs
ctx.mark_non_differentiable(ind, ind_inv)
ctx.save_for_backward(ind, ind_inv)
ctx.save_for_forward(ind, ind_inv)
ctx.dim = dim
@staticmethod
def backward(ctx, grad_output, _0, _1):
ind, ind_inv = ctx.saved_tensors
return TakeGenVmap.apply(grad_output, ind_inv, ind, ctx.dim), None
@staticmethod
def jvp(ctx, x_tangent, _):
ind, ind_inv = ctx.saved_tensors
return TakeGenVmap.apply(x_tangent, ind, ind_inv, ctx.dim), None, None
def sample_inputs_numpy_sort(opinfo, device, dtype, requires_grad, **kwargs):
make_arg = partial(make_tensor, device=device, dtype=dtype, requires_grad=requires_grad)
yield SampleInput(make_arg(3, 5), args=(1,))
def sample_inputs_numpy_take(opinfo, device, dtype, requires_grad, **kwargs):
make_arg = partial(make_tensor, device=device, dtype=dtype, requires_grad=requires_grad)
tensor = make_arg(3, 5)
dim = 1
_, ind, ind_inv = NumpySort.apply(tensor, 1)
yield SampleInput(tensor, args=(ind, ind_inv, dim))
class NumpyTake(torch.autograd.Function):
@staticmethod
def forward(x, ind, ind_inv, dim):
device = x.device
x = to_numpy(x)
ind = to_numpy(ind)
return torch.tensor(np.take_along_axis(x, ind, dim), device=device)
@staticmethod
def setup_context(ctx, inputs, output):
_x, ind, ind_inv, dim = inputs
ctx.save_for_backward(ind, ind_inv)
ctx.save_for_forward(ind, ind_inv)
ctx.dim = dim
@staticmethod
def backward(ctx, grad_output):
ind, ind_inv = ctx.saved_tensors
result = NumpyTake.apply(grad_output, ind_inv, ind, ctx.dim)
return result, None, None, None
@staticmethod
def vmap(info, in_dims, x, ind, ind_inv, dim):
x_bdim, ind_bdim, ind_inv_bdim, _ = in_dims
# wrap dim
logical_dim = x.dim() if x_bdim is None else x_bdim - 1
dim = dim if dim >= 0 else dim + logical_dim
def expand_bdim(x, x_bdim):
if x_bdim is None:
return x.expand(info.batch_size, *x.shape)
return x.movedim(x_bdim, 0)
x = expand_bdim(x, x_bdim)
ind = expand_bdim(ind, ind_bdim)
ind_inv = expand_bdim(ind_inv, ind_inv_bdim)
return NumpyTake.apply(x, ind, ind_inv, dim + 1), 0
@staticmethod
def jvp(ctx, x_tangent, ind_tangent, ind_inv_tangent, _):
if ind_tangent is not None:
raise AssertionError("Expected ind_tangent to be None")
if ind_inv_tangent is not None:
raise AssertionError("Expected ind_inv_tangent to be None")
ind, ind_inv = ctx.saved_tensors
return NumpyTake.apply(x_tangent, ind, ind_inv, ctx.dim)
class TakeGenVmap(torch.autograd.Function):
generate_vmap_rule = True
@staticmethod
def forward(x, ind, ind_inv, dim):
return torch.take_along_dim(x, ind, dim)
@staticmethod
def setup_context(ctx, inputs, outputs):
_x, ind, ind_inv, dim = inputs
ctx.save_for_backward(ind, ind_inv)
ctx.save_for_forward(ind, ind_inv)
ctx.dim = dim
@staticmethod
def backward(ctx, grad_output):
ind, ind_inv = ctx.saved_tensors
result = TakeGenVmap.apply(grad_output, ind_inv, ind, ctx.dim)
return result, None, None, None
@staticmethod
def jvp(ctx, x_tangent, ind_tangent, ind_inv_tangent, _):
ind, ind_inv = ctx.saved_tensors
return TakeGenVmap.apply(x_tangent, ind, ind_inv, ctx.dim)
class Select(torch.autograd.Function):
@staticmethod
def forward(x, idx):
return x[idx]
@staticmethod
def setup_context(ctx, inputs, output):
x, idx = inputs
ctx.x_shape = x.shape
ctx.idx = idx
@staticmethod
def backward(ctx, grad_output):
result = grad_output.new_zeros(ctx.x_shape)
result[ctx.idx] = grad_output
return result, None
@staticmethod
def vmap(info, in_dims, x, idx):
x_bdim, _ = in_dims
x = x.movedim(x_bdim, 1)
return Select.apply(x, idx), 0
@staticmethod
def jvp(ctx, x_tangent, _):
return Select.apply(x_tangent, ctx.idx)
class SelectGenVmap(torch.autograd.Function):
generate_vmap_rule = True
@staticmethod
def forward(x, idx):
return x[idx]
@staticmethod
def setup_context(ctx, inputs, outputs):
x, idx = inputs
ctx.x_shape = x.shape
ctx.idx = idx
@staticmethod
def backward(ctx, grad_output):
result = grad_output.new_zeros(ctx.x_shape)
result[ctx.idx] = grad_output
return result, None
@staticmethod
def jvp(ctx, x_tangent, _):
return SelectGenVmap.apply(x_tangent, ctx.idx)
def sample_inputs_select(opinfo, device, dtype, requires_grad, **kwargs):
make_arg = partial(make_tensor, device=device, dtype=dtype, requires_grad=requires_grad)
yield SampleInput(make_arg(3, 5), args=(2,))
class ScaleGradGenVmap(torch.autograd.Function):
generate_vmap_rule = True
scale = 3.14
@staticmethod
def forward(x):
return x.clone()
@staticmethod
def setup_context(ctx, inputs, outputs):
pass
@staticmethod
def backward(ctx, grad_output):
return grad_output * ScaleGradGenVmap.scale
@staticmethod
def jvp(ctx, x_tangent):
return x_tangent * ScaleGradGenVmap.scale
class ZeroGradientsGenVmap(torch.autograd.Function):
generate_vmap_rule = True
@staticmethod
def forward(x, y):
return x.clone(), y.clone()
@staticmethod
def setup_context(ctx, inputs, outputs):
pass
@staticmethod
def backward(ctx, gx, gy):
# Intentionally returning torch.zeros instead of zeros_like or new_zeros.
# Also intentionally not None.
return (
# Intentionally too-large gradient
torch.zeros(3, 4, *gx.shape, dtype=gx.dtype, device=gx.device),
torch.zeros(gy.shape, dtype=gy.dtype, device=gy.device),
)
@staticmethod
def jvp(ctx, gx, gy):
# Intentionally returning torch.zeros instead of zeros_like or new_zeros.
# Also intentionally not None.
return (
torch.zeros(gx.shape, dtype=gx.dtype, device=gx.device),
torch.zeros(gy.shape, dtype=gy.dtype, device=gy.device),
)
def sample_inputs_forward_default_args(opinfo, device, dtype, requires_grad, **kwargs):
make_arg = partial(make_tensor, device=device, dtype=dtype, requires_grad=requires_grad)
yield SampleInput(make_arg(3, 5))
class ForwardHasDefaultArgs(torch.autograd.Function):
@staticmethod
def forward(x, idx=(2,)):
return x[idx]
@staticmethod
def setup_context(ctx, inputs, output):
x, idx = inputs
ctx.x_shape = x.shape
ctx.idx = idx
@staticmethod
def backward(ctx, grad_output):
result = grad_output.new_zeros(ctx.x_shape)
result[ctx.idx] = grad_output
return result, None
@staticmethod
def vmap(info, in_dims, x, idx):
x_bdim, _ = in_dims
x = x.movedim(x_bdim, 1)
return ForwardHasDefaultArgs.apply(x, idx), 0
@staticmethod
def jvp(ctx, x_tangent, _):
return ForwardHasDefaultArgs.apply(x_tangent, ctx.idx)
autograd_function_db = [
OpInfo(
'NumpyCubeAutogradFunction',
op=NumpyCube.apply,
supports_forward_ad=True,
supports_fwgrad_bwgrad=True,
sample_inputs_func=sample_inputs_numpy_cube,
dtypes=all_types_and(torch.bool, torch.half),
supports_out=False,
),
OpInfo(
'NumpyExpMarkDirtyAutogradFunction',
op=lambda x: NumpyExp_.apply(x.clone()),
inplace_variant=NumpyExp_.apply,
supports_forward_ad=True,
supports_fwgrad_bwgrad=True,
sample_inputs_func=sample_inputs_numpy_cube,
dtypes=all_types_and(torch.bool, torch.half),
supports_out=False,
),
OpInfo(
'NumpyMulAutogradFunction',
op=NumpyMul.apply,
supports_forward_ad=True,
supports_fwgrad_bwgrad=True,
sample_inputs_func=sample_inputs_numpy_mul,
dtypes=all_types_and(torch.bool, torch.half),
supports_out=False,
),
OpInfo(
'NumpyCubeNotComposableAutogradFunction',
op=lambda x: NumpyCubeNotComposable.apply(x)[0],
supports_forward_ad=False,
supports_fwgrad_bwgrad=False,
sample_inputs_func=sample_inputs_numpy_cube,
dtypes=all_types_and(torch.bool, torch.half),
supports_out=False,
),
OpInfo(
'NumpySortAutogradFunction',
op=NumpySort.apply,
supports_forward_ad=False,
supports_fwgrad_bwgrad=False,
sample_inputs_func=sample_inputs_numpy_sort,
dtypes=all_types_and(torch.bool, torch.half),
supports_out=False,
gradcheck_wrapper=lambda y, ind: y,
),
OpInfo(
'NumpyTakeAutogradFunction',
op=NumpyTake.apply,
supports_forward_ad=False,
supports_fwgrad_bwgrad=False,
sample_inputs_func=sample_inputs_numpy_take,
dtypes=all_types_and(torch.bool, torch.half),
supports_out=False,
),
OpInfo(
'SelectAutogradFunction',
op=Select.apply,
supports_forward_ad=True,
supports_fwgrad_bwgrad=True,
sample_inputs_func=sample_inputs_select,
dtypes=all_types_and(torch.bool, torch.half),
supports_out=False,
),
OpInfo(
'CubeGenVmapAutogradFunction',
op=CubeGenVmap.apply,
supports_forward_ad=True,
supports_fwgrad_bwgrad=True,
sample_inputs_func=sample_inputs_numpy_cube,
dtypes=all_types_and(torch.bool, torch.half),
supports_out=False,
),
OpInfo(
'MulGenVmapAutogradFunction',
op=MulGenVmap.apply,
supports_forward_ad=True,
supports_fwgrad_bwgrad=True,
sample_inputs_func=sample_inputs_numpy_mul,
dtypes=all_types_and(torch.bool, torch.half),
supports_out=False,
),
OpInfo(
'SortGenVmapAutogradFunction',
op=SortGenVmap.apply,
supports_forward_ad=True,
supports_fwgrad_bwgrad=True,
sample_inputs_func=sample_inputs_numpy_sort,
dtypes=all_types_and(torch.bool, torch.half),
supports_out=False,
gradcheck_wrapper=lambda y, ind: y,
),
OpInfo(
'SelectGenVmapAutogradFunction',
op=SelectGenVmap.apply,
supports_forward_ad=True,
supports_fwgrad_bwgrad=True,
sample_inputs_func=sample_inputs_select,
dtypes=all_types_and(torch.bool, torch.half),
supports_out=False,
),
OpInfo(
'ScaleGradGenVmapAutogradFunction',
op=ScaleGradGenVmap.apply,
supports_forward_ad=True,
supports_fwgrad_bwgrad=True,
sample_inputs_func=sample_inputs_numpy_cube,
dtypes=all_types_and(torch.bool, torch.half),
supports_out=False,
),
OpInfo(
'ZeroGradientsGenVmapAutogradFunction',
op=ZeroGradientsGenVmap.apply,
supports_forward_ad=True,
supports_fwgrad_bwgrad=True,
sample_inputs_func=sample_inputs_numpy_mul,
dtypes=all_types_and(torch.bool, torch.half),
supports_out=False,
),
OpInfo(
'ForwardHasDefaultArgsAutogradFunction',
op=ForwardHasDefaultArgs.apply,
supports_forward_ad=True,
supports_fwgrad_bwgrad=True,
sample_inputs_func=sample_inputs_forward_default_args,
dtypes=all_types_and(torch.bool, torch.half),
supports_out=False,
),
]
@@ -0,0 +1,164 @@
# mypy: ignore-errors
import os
import re
import sys
__all__ = [
"check_code_for_cuda_kernel_launches",
"check_cuda_kernel_launches",
]
# FILES TO EXCLUDE (match is done with suffix using `endswith`)
# You wouldn't drive without a seatbelt, though, so why would you
# launch a kernel without some safety? Use this as a quick workaround
# for a problem with the checker, fix the checker, then de-exclude
# the files in question.
exclude_files: list[str] = []
# Without using a C++ AST we can't 100% detect kernel launches, so we
# model them as having the pattern "<<<parameters>>>(arguments);"
# We then require that `C10_CUDA_KERNEL_LAUNCH_CHECK` be
# the next statement.
#
# We model the next statement as ending at the next `}` or `;`.
# If we see `}` then a clause ended (bad) if we see a semi-colon then
# we expect the launch check just before it.
#
# Since the kernel launch can include lambda statements, it's important
# to find the correct end-paren of the kernel launch. Doing this with
# pure regex requires recursive regex, which aren't part of the Python
# standard library. To avoid an additional dependency, we build a prefix
# regex that finds the start of a kernel launch, use a paren-matching
# algorithm to find the end of the launch, and then another regex to
# determine if a launch check is present.
# Finds potential starts of kernel launches
kernel_launch_start = re.compile(
r"^.*<<<[^>]+>>>\s*\(", flags=re.MULTILINE
)
# This pattern should start at the character after the final paren of the
# kernel launch. It returns a match if the launch check is not the next statement
has_check = re.compile(
r"\s*;(?![^;}]*C10_CUDA_KERNEL_LAUNCH_CHECK\(\);)", flags=re.MULTILINE
)
def find_matching_paren(s: str, startpos: int) -> int:
"""Given a string "prefix (unknown number of characters) suffix"
and the position of the first `(` returns the index of the character
1 past the `)`, accounting for paren nesting
"""
opening = 0
for i, c in enumerate(s[startpos:]):
if c == '(':
opening += 1
elif c == ')':
opening -= 1
if opening == 0:
return startpos + i + 1
raise IndexError("Closing parens not found!")
def should_exclude_file(filename) -> bool:
for exclude_suffix in exclude_files:
if filename.endswith(exclude_suffix):
return True
return False
def check_code_for_cuda_kernel_launches(code, filename=None):
"""Checks code for CUDA kernel launches without cuda error checks.
Args:
filename - Filename of file containing the code. Used only for display
purposes, so you can put anything here.
code - The code to check
Returns:
The number of unsafe kernel launches in the code
"""
if filename is None:
filename = "##Python Function Call##"
# We break the code apart and put it back together to add
# helpful line numberings for identifying problem areas
code = enumerate(code.split("\n")) # Split by line breaks
code = [f"{lineno}: {linecode}" for lineno, linecode in code] # Number the lines
code = '\n'.join(code) # Put it back together
num_launches_without_checks = 0
for m in kernel_launch_start.finditer(code):
end_paren = find_matching_paren(code, m.end() - 1)
if has_check.match(code, end_paren):
num_launches_without_checks += 1
context = code[m.start():end_paren + 1]
print(f"Missing C10_CUDA_KERNEL_LAUNCH_CHECK in '{filename}'. Context:\n{context}", file=sys.stderr)
return num_launches_without_checks
def check_file(filename):
"""Checks a file for CUDA kernel launches without cuda error checks
Args:
filename - File to check
Returns:
The number of unsafe kernel launches in the file
"""
if not (filename.endswith((".cu", ".cuh"))):
return 0
if should_exclude_file(filename):
return 0
with open(filename) as f:
contents = f.read()
unsafeCount = check_code_for_cuda_kernel_launches(contents, filename)
return unsafeCount
def check_cuda_kernel_launches():
"""Checks all pytorch code for CUDA kernel launches without cuda error checks
Returns:
The number of unsafe kernel launches in the codebase
"""
torch_dir = os.path.dirname(os.path.realpath(__file__))
torch_dir = os.path.dirname(torch_dir) # Go up to parent torch
torch_dir = os.path.dirname(torch_dir) # Go up to parent caffe2
kernels_without_checks = 0
files_without_checks = []
for root, dirnames, filenames in os.walk(torch_dir):
# `$BASE/build` and `$BASE/torch/include` are generated
# so we don't want to flag their contents
if root == os.path.join(torch_dir, "build") or root == os.path.join(torch_dir, "torch/include"):
# Curtail search by modifying dirnames and filenames in place
# Yes, this is the way to do this, see `help(os.walk)`
dirnames[:] = []
continue
for x in filenames:
filename = os.path.join(root, x)
file_result = check_file(filename)
if file_result > 0:
kernels_without_checks += file_result
files_without_checks.append(filename)
if kernels_without_checks > 0:
count_str = f"Found {kernels_without_checks} instances in " \
f"{len(files_without_checks)} files where kernel " \
"launches didn't have checks."
print(count_str, file=sys.stderr)
print("Files without checks:", file=sys.stderr)
for x in files_without_checks:
print(f"\t{x}", file=sys.stderr)
print(count_str, file=sys.stderr)
return kernels_without_checks
if __name__ == "__main__":
unsafe_launches = check_cuda_kernel_launches()
sys.exit(0 if unsafe_launches == 0 else 1)
@@ -0,0 +1 @@
# mypy: ignore-errors
@@ -0,0 +1,497 @@
# mypy: ignore-errors
r"""This file is allowed to initialize CUDA context when imported."""
import functools
import torch
import torch.cuda
from torch.testing._internal.common_utils import LazyVal, TEST_NUMBA, TEST_WITH_ROCM, TEST_CUDA, IS_WINDOWS, IS_MACOS, TEST_XPU
import inspect
import contextlib
import os
import unittest
CUDA_ALREADY_INITIALIZED_ON_IMPORT = torch.cuda.is_initialized()
TEST_MULTIGPU = TEST_CUDA and torch.cuda.device_count() >= 2
CUDA_DEVICE = torch.device("cuda:0") if TEST_CUDA else None
# note: if ROCm is targeted, TEST_CUDNN is code for TEST_MIOPEN
if TEST_WITH_ROCM:
TEST_CUDNN = LazyVal(lambda: TEST_CUDA)
else:
TEST_CUDNN = LazyVal(lambda: TEST_CUDA and torch.backends.cudnn.is_acceptable(torch.tensor(1., device=CUDA_DEVICE)))
TEST_CUDNN_VERSION = LazyVal(lambda: torch.backends.cudnn.version() if TEST_CUDNN else 0)
ROCM_VERSION = LazyVal(lambda : tuple(int(v) for v in torch.version.hip.split('.')[:2]) if torch.version.hip else (0, 0))
SM53OrLater = LazyVal(lambda: torch.cuda.is_available() and torch.cuda.get_device_capability() >= (5, 3))
SM60OrLater = LazyVal(lambda: torch.cuda.is_available() and torch.cuda.get_device_capability() >= (6, 0))
SM70OrLater = LazyVal(lambda: torch.cuda.is_available() and torch.cuda.get_device_capability() >= (7, 0))
SM75OrLater = LazyVal(lambda: torch.cuda.is_available() and torch.cuda.get_device_capability() >= (7, 5))
SM80OrLater = LazyVal(lambda: torch.cuda.is_available() and torch.cuda.get_device_capability() >= (8, 0))
SM89OrLater = LazyVal(lambda: torch.cuda.is_available() and torch.cuda.get_device_capability() >= (8, 9))
SM90OrLater = LazyVal(lambda: torch.cuda.is_available() and torch.cuda.get_device_capability() >= (9, 0))
SM100OrLater = LazyVal(lambda: torch.cuda.is_available() and torch.cuda.get_device_capability() >= (10, 0))
SM120OrLater = LazyVal(lambda: torch.cuda.is_available() and torch.cuda.get_device_capability() >= (12, 0))
IS_THOR = LazyVal(lambda: torch.cuda.is_available() and torch.version.cuda is not None and
((torch.cuda.get_device_capability() == (11, 0) and int(torch.version.cuda[:2]) >= 13) or
(torch.cuda.get_device_capability() == (10, 1) and int(torch.version.cuda[:2]) < 13)))
IS_JETSON = LazyVal(lambda: torch.cuda.is_available() and (torch.cuda.get_device_capability() in [(7, 2), (8, 7)] or IS_THOR))
IS_SM89 = LazyVal(lambda: torch.cuda.is_available() and torch.cuda.get_device_capability() == (8, 9))
IS_SM90 = LazyVal(lambda: torch.cuda.is_available() and torch.cuda.get_device_capability() == (9, 0))
IS_SM100 = LazyVal(lambda: torch.cuda.is_available() and torch.cuda.get_device_capability() == (10, 0))
IS_SM12X = LazyVal(lambda: torch.cuda.is_available() and torch.cuda.get_device_capability()[0] == 12)
@contextlib.contextmanager
def blas_library_context(backend):
prev_backend = torch.backends.cuda.preferred_blas_library()
torch.backends.cuda.preferred_blas_library(backend)
try:
yield
finally:
torch.backends.cuda.preferred_blas_library(prev_backend)
def evaluate_gfx_arch_within(arch_list):
if not torch.cuda.is_available():
return False
gcn_arch_name = torch.cuda.get_device_properties('cuda').gcnArchName
effective_arch = os.environ.get('PYTORCH_DEBUG_FLASH_ATTENTION_GCN_ARCH_OVERRIDE', gcn_arch_name)
# gcnArchName can be complicated strings like gfx90a:sramecc+:xnack-
# Hence the matching should be done reversely
return any(arch in effective_arch for arch in arch_list)
def CDNA3OrLater():
return evaluate_gfx_arch_within(["gfx942", "gfx950"])
def CDNA2OrLater():
return evaluate_gfx_arch_within(["gfx90a", "gfx942"])
def evaluate_platform_supports_flash_attention():
if TEST_WITH_ROCM:
arch_list = ["gfx90a", "gfx942", "gfx1100", "gfx1201", "gfx950"]
if os.environ.get("TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL", "0") != "0":
arch_list += ["gfx1101", "gfx1102", "gfx1150", "gfx1151", "gfx1200"]
return evaluate_gfx_arch_within(arch_list)
if TEST_CUDA:
return not IS_WINDOWS and SM80OrLater
if TEST_XPU:
return True
return False
def evaluate_platform_supports_ck_sdpa():
if TEST_WITH_ROCM:
return torch.backends.cuda.is_ck_sdpa_available()
else:
return False
def evaluate_platform_supports_efficient_attention():
if TEST_WITH_ROCM:
arch_list = ["gfx90a", "gfx942", "gfx1100", "gfx1201", "gfx950"]
if os.environ.get("TORCH_ROCM_AOTRITON_ENABLE_EXPERIMENTAL", "0") != "0":
arch_list += ["gfx1101", "gfx1102", "gfx1150", "gfx1151", "gfx1200"]
return evaluate_gfx_arch_within(arch_list)
if TEST_CUDA:
return True
if TEST_XPU:
return True
return False
def evaluate_platform_supports_cudnn_attention():
return (not TEST_WITH_ROCM) and SM80OrLater and (TEST_CUDNN_VERSION >= 90000)
def evaluate_platform_supports_green_context():
if IS_WINDOWS:
return False
if not _get_torch_cuda_version() >= (12, 8):
return False
driver_version = torch.utils.collect_env.get_nvidia_driver_version(torch.utils.collect_env.run)
if driver_version is None:
return False
return int(driver_version.split('.')[0]) >= 570
PLATFORM_SUPPORTS_FLASH_ATTENTION: bool = LazyVal(lambda: evaluate_platform_supports_flash_attention())
PLATFORM_SUPPORTS_MEM_EFF_ATTENTION: bool = LazyVal(lambda: evaluate_platform_supports_efficient_attention())
PLATFORM_SUPPORTS_CUDNN_ATTENTION: bool = LazyVal(lambda: evaluate_platform_supports_cudnn_attention())
# This condition always evaluates to PLATFORM_SUPPORTS_MEM_EFF_ATTENTION but for logical clarity we keep it separate
PLATFORM_SUPPORTS_FUSED_ATTENTION: bool = LazyVal(lambda: PLATFORM_SUPPORTS_FLASH_ATTENTION or
PLATFORM_SUPPORTS_CUDNN_ATTENTION or
PLATFORM_SUPPORTS_MEM_EFF_ATTENTION)
PLATFORM_SUPPORTS_FUSED_SDPA: bool = TEST_CUDA and not TEST_WITH_ROCM
PLATFORM_SUPPORTS_CK_SDPA: bool = LazyVal(lambda: evaluate_platform_supports_ck_sdpa())
def evaluate_platform_supports_bf16():
if torch.version.cuda:
return SM80OrLater
elif torch.version.hip:
return True
elif TEST_XPU:
return True
return False
def evaluate_platform_supports_bf16_atomics():
if torch.version.cuda:
return SM80OrLater
elif torch.version.hip:
return ROCM_VERSION >= (8, 0)
return False
def evaluate_platform_supports_half_atomics():
if torch.version.hip:
return ROCM_VERSION >= (8, 0)
return True
PLATFORM_SUPPORTS_BF16: bool = LazyVal(lambda: evaluate_platform_supports_bf16())
PLATFORM_SUPPORTS_BF16_ATOMICS: bool = LazyVal(lambda: evaluate_platform_supports_bf16_atomics())
PLATFORM_SUPPORTS_HALF_ATOMICS: bool = LazyVal(lambda: evaluate_platform_supports_half_atomics())
PLATFORM_SUPPORTS_GREEN_CONTEXT: bool = LazyVal(lambda: evaluate_platform_supports_green_context())
def evaluate_platform_supports_workqueue_config():
if IS_WINDOWS:
return False
if not _get_torch_cuda_version() >= (13, 1):
return False
driver_version = torch.utils.collect_env.get_nvidia_driver_version(torch.utils.collect_env.run)
if driver_version is None:
return False
return int(driver_version.split('.')[0]) >= 590
PLATFORM_SUPPORTS_WORKQUEUE_CONFIG: bool = LazyVal(lambda: evaluate_platform_supports_workqueue_config())
def evaluate_platform_supports_fp8():
if torch.cuda.is_available():
if torch.version.hip:
archs = ['gfx94']
if ROCM_VERSION >= (6, 3):
archs.extend(['gfx120'])
if ROCM_VERSION >= (6, 5):
archs.append('gfx95')
for arch in archs:
if arch in torch.cuda.get_device_properties(0).gcnArchName:
return True
return False
else:
return SM90OrLater or torch.cuda.get_device_capability() == (8, 9)
if torch.xpu.is_available():
return True
# As CPU supports FP8 and is always available, return True.
return True
def evaluate_platform_supports_fp8_grouped_gemm():
if torch.cuda.is_available():
if torch.version.hip:
if "USE_MSLK" not in torch.__config__.show():
return False
archs = ['gfx942', 'gfx950']
for arch in archs:
if arch in torch.cuda.get_device_properties(0).gcnArchName:
return True
else:
return SM90OrLater and not SM100OrLater
return False
def evaluate_platform_supports_mx_gemm():
if torch.cuda.is_available():
if torch.version.hip:
if ROCM_VERSION >= (7, 0):
return 'gfx950' in torch.cuda.get_device_properties(0).gcnArchName
else:
return SM100OrLater
return False
def evaluate_platform_supports_mxfp8_grouped_gemm():
if torch.cuda.is_available() and not torch.version.hip:
built_with_mslk = "USE_MSLK" in torch.__config__.show()
return built_with_mslk and IS_SM100
return False
def evaluate_platform_supports_fp8_sparse():
if torch.cuda.is_available():
if torch.version.hip:
return 'gfx950' in torch.cuda.get_device_properties(0).gcnArchName
else:
return (
(SM90OrLater or torch.cuda.get_device_capability() == (8, 9))
and torch.backends.cusparselt.is_available()
and torch.backends.cusparselt.version() >= 602
)
return False
PLATFORM_SUPPORTS_MX_GEMM: bool = LazyVal(lambda: evaluate_platform_supports_mx_gemm())
PLATFORM_SUPPORTS_FP8: bool = LazyVal(lambda: evaluate_platform_supports_fp8())
PLATFORM_SUPPORTS_FP8_SPARSE: bool = LazyVal(lambda: evaluate_platform_supports_fp8_sparse())
PLATFORM_SUPPORTS_FP8_GROUPED_GEMM: bool = LazyVal(lambda: evaluate_platform_supports_fp8_grouped_gemm())
PLATFORM_SUPPORTS_MXFP8_GROUPED_GEMM: bool = LazyVal(lambda: evaluate_platform_supports_mxfp8_grouped_gemm())
if TEST_NUMBA:
try:
import numba.cuda
TEST_NUMBA_CUDA = numba.cuda.is_available()
except (ImportError, RuntimeError, OSError):
TEST_NUMBA_CUDA = False
TEST_NUMBA = False
else:
TEST_NUMBA_CUDA = False
# Used below in `initialize_cuda_context_rng` to ensure that CUDA context and
# RNG have been initialized.
__cuda_ctx_rng_initialized = False
# after this call, CUDA context and RNG must have been initialized on each GPU
def initialize_cuda_context_rng():
global __cuda_ctx_rng_initialized
if not TEST_CUDA:
raise AssertionError('CUDA must be available when calling initialize_cuda_context_rng')
if not __cuda_ctx_rng_initialized:
# initialize cuda context and rng for memory tests
for i in range(torch.cuda.device_count()):
torch.randn(1, device=f"cuda:{i}")
__cuda_ctx_rng_initialized = True
@contextlib.contextmanager
def tf32_off():
old_allow_tf32_matmul = torch.backends.cuda.matmul.allow_tf32
try:
torch.backends.cuda.matmul.allow_tf32 = False
with torch.backends.cudnn.flags(enabled=None, benchmark=None, deterministic=None, allow_tf32=False):
yield
finally:
torch.backends.cuda.matmul.allow_tf32 = old_allow_tf32_matmul
@contextlib.contextmanager
def tf32_on(self, tf32_precision=1e-5):
old_allow_tf32_matmul = torch.backends.cuda.matmul.allow_tf32
old_precision = self.precision
try:
torch.backends.cuda.matmul.allow_tf32 = True
self.precision = tf32_precision
with torch.backends.cudnn.flags(enabled=None, benchmark=None, deterministic=None, allow_tf32=True):
yield
finally:
torch.backends.cuda.matmul.allow_tf32 = old_allow_tf32_matmul
self.precision = old_precision
@contextlib.contextmanager
def tf32_enabled():
"""
Context manager to temporarily enable TF32 for CUDA operations.
Restores the previous TF32 state after exiting the context.
"""
old_allow_tf32_matmul = torch.backends.cuda.matmul.allow_tf32
try:
torch.backends.cuda.matmul.allow_tf32 = True
with torch.backends.cudnn.flags(
enabled=None, benchmark=None, deterministic=None, allow_tf32=True
):
yield
finally:
torch.backends.cuda.matmul.allow_tf32 = old_allow_tf32_matmul
# This is a wrapper that wraps a test to run this test twice, one with
# allow_tf32=True, another with allow_tf32=False. When running with
# allow_tf32=True, it will use reduced precision as specified by the
# argument. For example:
# @dtypes(torch.float32, torch.float64, torch.complex64, torch.complex128)
# @tf32_on_and_off(0.005)
# def test_matmul(self, device, dtype):
# a = ...; b = ...;
# c = torch.matmul(a, b)
# self.assertEqual(c, expected)
# In the above example, when testing torch.float32 and torch.complex64 on CUDA
# on a CUDA >= 11 build on an >=Ampere architecture, the matmul will be running at
# TF32 mode and TF32 mode off, and on TF32 mode, the assertEqual will use reduced
# precision to check values.
#
# This decorator can be used for function with or without device/dtype, such as
# @tf32_on_and_off(0.005)
# def test_my_op(self)
# @tf32_on_and_off(0.005)
# def test_my_op(self, device)
# @tf32_on_and_off(0.005)
# def test_my_op(self, device, dtype)
# @tf32_on_and_off(0.005)
# def test_my_op(self, dtype)
# if neither device nor dtype is specified, it will check if the system has ampere device
# if device is specified, it will check if device is cuda
# if dtype is specified, it will check if dtype is float32 or complex64
# tf32 and fp32 are different only when all the three checks pass
def tf32_on_and_off(tf32_precision=1e-5, *, only_if=True):
def with_tf32_disabled(self, function_call):
with tf32_off():
function_call()
def with_tf32_enabled(self, function_call):
with tf32_on(self, tf32_precision):
function_call()
def wrapper(f):
params = inspect.signature(f).parameters
arg_names = tuple(params.keys())
@functools.wraps(f)
def wrapped(*args, **kwargs):
kwargs.update(zip(arg_names, args, strict=False))
cond = torch.cuda.is_tf32_supported() and only_if
if 'device' in kwargs:
cond = cond and (torch.device(kwargs['device']).type == 'cuda')
if 'dtype' in kwargs:
cond = cond and (kwargs['dtype'] in {torch.float32, torch.complex64})
if cond:
with_tf32_disabled(kwargs['self'], lambda: f(**kwargs))
with_tf32_enabled(kwargs['self'], lambda: f(**kwargs))
else:
f(**kwargs)
return wrapped
return wrapper
# This is a wrapper that wraps a test to run it with TF32 turned off.
# This wrapper is designed to be used when a test uses matmul or convolutions
# but the purpose of that test is not testing matmul or convolutions.
# Disabling TF32 will enforce torch.float tensors to be always computed
# at full precision.
def with_tf32_off(f):
@functools.wraps(f)
def wrapped(*args, **kwargs):
with tf32_off():
return f(*args, **kwargs)
return wrapped
def _get_magma_version():
if 'Magma' not in torch.__config__.show():
return (0, 0)
position = torch.__config__.show().find('Magma ')
version_str = torch.__config__.show()[position + len('Magma '):].split('\n')[0]
return tuple(int(x) for x in version_str.split("."))
def _get_torch_cuda_version():
if torch.version.cuda is None:
return (0, 0)
cuda_version = str(torch.version.cuda)
return tuple(int(x) for x in cuda_version.split("."))
def _get_torch_rocm_version():
if not TEST_WITH_ROCM or torch.version.hip is None:
return (0, 0)
rocm_version = str(torch.version.hip)
rocm_version = rocm_version.split("-", maxsplit=1)[0] # ignore git sha
return tuple(int(x) for x in rocm_version.split("."))
def _get_torch_hipblaslt_version():
if not TEST_WITH_ROCM:
return None
try:
# Access through direct C binding
# versionHipBLASLt returns: MAJOR * 10000 + MINOR * 100 + PATCH
version_int = torch._C._cuda_getHipblasltVersion()
if version_int is None or version_int == 0:
return None
major = version_int // 10000
minor = (version_int % 10000) // 100
patch = version_int % 100
return (major, minor, patch)
except (AttributeError, RuntimeError):
return None
def _check_cusparse_generic_available():
return not TEST_WITH_ROCM
def _check_hipsparse_generic_available():
if not TEST_WITH_ROCM:
return False
if not torch.version.hip:
return False
rocm_version = str(torch.version.hip)
rocm_version = rocm_version.split("-", maxsplit=1)[0] # ignore git sha
rocm_version_tuple = tuple(int(x) for x in rocm_version.split("."))
return not (rocm_version_tuple is None or rocm_version_tuple < (5, 1))
TEST_CUSPARSE_GENERIC = _check_cusparse_generic_available()
TEST_HIPSPARSE_GENERIC = _check_hipsparse_generic_available()
# Shared by test_torch.py and test_multigpu.py
def _create_scaling_models_optimizers(device="cuda", optimizer_ctor=torch.optim.SGD, optimizer_kwargs=None):
# Create a module+optimizer that will use scaling, and a control module+optimizer
# that will not use scaling, against which the scaling-enabled module+optimizer can be compared.
mod_control = torch.nn.Sequential(torch.nn.Linear(8, 8), torch.nn.Linear(8, 8)).to(device=device)
mod_scaling = torch.nn.Sequential(torch.nn.Linear(8, 8), torch.nn.Linear(8, 8)).to(device=device)
with torch.no_grad():
for c, s in zip(mod_control.parameters(), mod_scaling.parameters(), strict=True):
s.copy_(c)
kwargs = {"lr": 1.0}
if optimizer_kwargs is not None:
kwargs.update(optimizer_kwargs)
opt_control = optimizer_ctor(mod_control.parameters(), **kwargs)
opt_scaling = optimizer_ctor(mod_scaling.parameters(), **kwargs)
return mod_control, mod_scaling, opt_control, opt_scaling
# Shared by test_torch.py, test_cuda.py and test_multigpu.py
def _create_scaling_case(device="cuda", dtype=torch.float, optimizer_ctor=torch.optim.SGD, optimizer_kwargs=None):
data = [(torch.randn((8, 8), dtype=dtype, device=device), torch.randn((8, 8), dtype=dtype, device=device)),
(torch.randn((8, 8), dtype=dtype, device=device), torch.randn((8, 8), dtype=dtype, device=device)),
(torch.randn((8, 8), dtype=dtype, device=device), torch.randn((8, 8), dtype=dtype, device=device)),
(torch.randn((8, 8), dtype=dtype, device=device), torch.randn((8, 8), dtype=dtype, device=device))]
loss_fn = torch.nn.MSELoss().to(device)
skip_iter = 2
return _create_scaling_models_optimizers(
device=device, optimizer_ctor=optimizer_ctor, optimizer_kwargs=optimizer_kwargs,
) + (data, loss_fn, skip_iter)
def xfailIfSM89(func):
return func if not IS_SM89 else unittest.expectedFailure(func)
def xfailIfSM90(func):
return func if not IS_SM90 else unittest.expectedFailure(func)
def xfailIfSM89PreCUDA13(func):
"""xfail on SM89 only for CUDA < 13. On CUDA 13+, test should pass on all architectures."""
if IS_SM89 and _get_torch_cuda_version() < (13, 0):
return unittest.expectedFailure(func)
return func
def xfailIfSM100OrLater(func):
return func if not SM100OrLater else unittest.expectedFailure(func)
def xfailIfSM120OrLater(func):
return func if not SM120OrLater else unittest.expectedFailure(func)
def xfailIfSM12X(func):
return func if not IS_SM12X else unittest.expectedFailure(func)
def xfailIfDistributedNotSupported(func):
return func if not (IS_MACOS or IS_JETSON) else unittest.expectedFailure(func)
# When using nvcc from the CUDA toolkit its versuib must be at least the one from ptxas bundled with Triton
TRITON_PTXAS_VERSION = (12, 8)
requires_triton_ptxas_compat = unittest.skipIf(not torch.version.xpu
and torch.version.hip is None
and _get_torch_cuda_version() < TRITON_PTXAS_VERSION,
"Requires CUDA {}.{} to match Tritons ptxas version".format(*TRITON_PTXAS_VERSION))
# Importing this module should NOT eagerly initialize CUDA
if not CUDA_ALREADY_INITIALIZED_ON_IMPORT:
if torch.cuda.is_initialized():
raise AssertionError("CUDA should not be initialized on import")
@@ -0,0 +1,112 @@
# mypy: ignore-errors
# Owner(s): ["oncall: distributed"]
import torch
import torch.nn as nn
class UnitModule(nn.Module):
def __init__(self, device: torch.device):
super().__init__()
self.l1 = nn.Linear(100, 100, device=device)
self.seq = nn.Sequential(
nn.ReLU(),
nn.Linear(100, 100, device=device),
nn.ReLU(),
)
self.l2 = nn.Linear(100, 100, device=device)
def forward(self, x):
return self.l2(self.seq(self.l1(x)))
class CompositeModel(nn.Module):
def __init__(self, device: torch.device):
super().__init__()
self.l1 = nn.Linear(100, 100, device=device)
self.u1 = UnitModule(device)
self.u2 = UnitModule(device)
self.l2 = nn.Linear(100, 100, device=device)
def forward(self, x):
return self.l2(self.u2(self.u1(self.l1(x))))
class UnitParamModule(nn.Module):
def __init__(self, device: torch.device):
super().__init__()
self.l = nn.Linear(100, 100, device=device)
self.seq = nn.Sequential(
nn.ReLU(),
nn.Linear(100, 100, device=device),
nn.ReLU(),
)
self.p = nn.Parameter(torch.randn((100, 100), device=device))
def forward(self, x):
return torch.mm(self.seq(self.l(x)), self.p)
class CompositeParamModel(nn.Module):
def __init__(self, device: torch.device):
super().__init__()
self.l = nn.Linear(100, 100, device=device)
self.u1 = UnitModule(device)
self.u2 = UnitModule(device)
self.p = nn.Parameter(torch.randn((100, 100), device=device))
self.register_buffer(
"buffer", torch.randn((100, 100), device=device), persistent=True
)
def forward(self, x):
a = self.u2(self.u1(self.l(x)))
b = self.p
return torch.mm(a, b)
class FakeSequential(nn.Module):
# Define this class to achieve a desired nested wrapping using the module
# wrap policy with `nn.Sequential`
def __init__(self, *modules: tuple[nn.Module, ...]) -> None:
super().__init__()
self._module_sequence = list(modules)
def forward(self, x: torch.Tensor) -> torch.Tensor:
for module in self._module_sequence:
x = module(x)
return x
class NestedSequentialModel(nn.Module):
def __init__(self, device: torch.device) -> None:
super().__init__()
# This nested structure exercises traversal order to catch differences
# between valid traversals (e.g. BFS and DFS variations).
self.seq1 = nn.Sequential(
nn.Linear(1, 1, device=device),
FakeSequential(
nn.Linear(1, 1, device=device),
nn.ReLU(),
FakeSequential(
nn.Linear(1, 1, device=device),
),
nn.ReLU(),
),
nn.Linear(1, 2, device=device),
)
self.lin = nn.Linear(2, 2, device=device)
self.seq2 = nn.Sequential(
nn.ReLU(),
nn.Linear(2, 3, device=device),
FakeSequential(
nn.Linear(3, 2, bias=False, device=device),
nn.Linear(2, 4, bias=False, device=device),
),
)
# FIXME(rec): forward() is not a method, it's a local function inside __init__
# that is never used. It should probabkly be outdented by four spaces, or removed.
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.seq2(self.lin(self.seq1(x)))
@@ -0,0 +1,243 @@
# mypy: ignore-errors
import torch
# Functions and classes for describing the dtypes a function supports
# NOTE: these helpers should correspond to PyTorch's C++ dispatch macros
# Verifies each given dtype is a torch.dtype
def _validate_dtypes(*dtypes):
for dtype in dtypes:
if not isinstance(dtype, torch.dtype):
raise AssertionError(f"Expected dtype to be torch.dtype, got {type(dtype)}")
return dtypes
# class for tuples corresponding to a PyTorch dispatch macro
class _dispatch_dtypes(tuple):
__slots__ = ()
def __add__(self, other):
if not isinstance(other, tuple):
raise AssertionError(f"Expected other to be a tuple, got {type(other)}")
return _dispatch_dtypes(tuple.__add__(self, other))
_empty_types = _dispatch_dtypes(())
def empty_types():
return _empty_types
_floating_types = _dispatch_dtypes((torch.float32, torch.float64))
def floating_types():
return _floating_types
_floating_types_and_half = _floating_types + (torch.half,)
def floating_types_and_half():
return _floating_types_and_half
def floating_types_and(*dtypes):
return _floating_types + _validate_dtypes(*dtypes)
_floating_and_complex_types = _floating_types + (torch.cfloat, torch.cdouble)
def floating_and_complex_types():
return _floating_and_complex_types
def floating_and_complex_types_and(*dtypes):
return _floating_and_complex_types + _validate_dtypes(*dtypes)
_double_types = _dispatch_dtypes((torch.float64, torch.complex128))
def double_types():
return _double_types
# NB: Does not contain uint16/uint32/uint64 for BC reasons
_integral_types = _dispatch_dtypes(
(torch.uint8, torch.int8, torch.int16, torch.int32, torch.int64)
)
def integral_types():
return _integral_types
def integral_types_and(*dtypes):
return _integral_types + _validate_dtypes(*dtypes)
_all_types = _floating_types + _integral_types
def all_types():
return _all_types
def all_types_and(*dtypes):
return _all_types + _validate_dtypes(*dtypes)
_complex_types = _dispatch_dtypes((torch.cfloat, torch.cdouble))
def complex_types():
return _complex_types
def complex_types_and(*dtypes):
return _complex_types + _validate_dtypes(*dtypes)
_all_types_and_complex = _all_types + _complex_types
def all_types_and_complex():
return _all_types_and_complex
def all_types_and_complex_and(*dtypes):
return _all_types_and_complex + _validate_dtypes(*dtypes)
_all_types_and_half = _all_types + (torch.half,)
def all_types_and_half():
return _all_types_and_half
_all_mps_types = (
_dispatch_dtypes({torch.float, torch.half, torch.bfloat16}) + _integral_types
)
def all_mps_types():
return _all_mps_types
def all_mps_types_and(*dtypes):
return _all_mps_types + _validate_dtypes(*dtypes)
_float8_types = _dispatch_dtypes(
(
torch.float8_e4m3fn,
torch.float8_e4m3fnuz,
torch.float8_e5m2,
torch.float8_e5m2fnuz,
)
)
def float8_types():
return _float8_types
def float8_types_and(*dtypes):
return _float8_types + _validate_dtypes(*dtypes)
def all_types_complex_float8_and(*dtypes):
return _all_types + _complex_types + _float8_types + _validate_dtypes(*dtypes)
def custom_types(*dtypes):
"""Create a list of arbitrary dtypes"""
return _empty_types + _validate_dtypes(*dtypes)
# The functions below are used for convenience in our test suite and thus have no corresponding C++ dispatch macro
# See AT_FORALL_SCALAR_TYPES_WITH_COMPLEX_AND_QINTS.
def get_all_dtypes(
include_half=True,
include_bfloat16=True,
include_bool=True,
include_complex=True,
include_complex32=False,
include_qint=False,
) -> list[torch.dtype]:
dtypes = get_all_int_dtypes() + get_all_fp_dtypes(
include_half=include_half, include_bfloat16=include_bfloat16
)
if include_bool:
dtypes.append(torch.bool)
if include_complex:
dtypes += get_all_complex_dtypes(include_complex32)
if include_qint:
dtypes += get_all_qint_dtypes()
return dtypes
def get_all_math_dtypes(device) -> list[torch.dtype]:
return (
get_all_int_dtypes()
+ get_all_fp_dtypes(
include_half=device.startswith("cuda"), include_bfloat16=False
)
+ get_all_complex_dtypes()
)
def get_all_complex_dtypes(include_complex32=False) -> list[torch.dtype]:
return (
[torch.complex32, torch.complex64, torch.complex128]
if include_complex32
else [torch.complex64, torch.complex128]
)
def get_all_int_dtypes() -> list[torch.dtype]:
return [torch.uint8, torch.int8, torch.int16, torch.int32, torch.int64]
def get_all_fp_dtypes(include_half=True, include_bfloat16=True) -> list[torch.dtype]:
dtypes = [torch.float32, torch.float64]
if include_half:
dtypes.append(torch.float16)
if include_bfloat16:
dtypes.append(torch.bfloat16)
return dtypes
def get_all_qint_dtypes() -> list[torch.dtype]:
return [torch.qint8, torch.quint8, torch.qint32, torch.quint4x2, torch.quint2x4]
def highest_precision_float(device):
if torch.device(device).type == "mps":
return torch.float32
else:
return torch.float64
def highest_precision_complex(device):
if torch.device(device).type == "mps":
return torch.complex64
else:
return torch.complex128
float_to_corresponding_complex_type_map = {
torch.float16: torch.complex32,
torch.float32: torch.complex64,
torch.float64: torch.complex128,
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,322 @@
# mypy: ignore-errors
# Torch
import torch
import torch.cuda
import torch.jit
import torch.jit._logging
import torch.jit.frontend
import torch.jit.quantized
# Testing utils
from torch.testing._internal.common_dtype import floating_and_complex_types_and
from torch.testing._internal.common_utils import TestCase, \
freeze_rng_state, TemporaryFileName, enable_profiling_mode_for_profiling_tests, is_iterable_of_tensors
from torch.testing._internal.common_utils import enable_profiling_mode # noqa: F401
# Standard library
from itertools import chain
from torch._C import TensorType
import io
def check_output_types(self, func, ref_outputs, args, kwargs):
graph = getattr(func, 'last_graph', None)
types = [o.type() for o in graph.outputs()]
self.assertTrue(len(types) == 1)
t = types[0]
torch._C._jit_assert_is_instance(ref_outputs, t)
# Test names in this set are only checked for a single derivative
nn_functional_single_grad = frozenset('test_nn_' + name for name in [
'pdist',
'multilabel_margin_loss',
'max_unpool3d',
'multi_margin_loss',
'binary_cross_entropy',
'binary_cross_entropy_size_average',
'ctc_loss',
'grid_sample',
])
def check_against_reference(self, func, reference_func, output_func, args, kwargs=None,
allow_unused=True, check_types=True, no_grad=False, no_gradgrad=False):
"""Verifies a function performs identically to some reference implementation.
Commonly, this is used to verify that a JIT implementation
(output_func) matches the behavior of the eager implementation
(reference_func).
"""
kwargs = kwargs if kwargs else {}
def allSum(vs):
if isinstance(vs, torch.Tensor):
vs = (vs,)
return sum((i + 1) * v.sum().abs() if v.dtype.is_complex else (i + 1) * v.sum()
for i, v in enumerate(vs)
if v is not None and v.dtype in floating_and_complex_types_and(torch.half, torch.bfloat16))
def clone_tensor(t, preserve_requires_grad):
require_grad = preserve_requires_grad and t.requires_grad
return t.detach().clone().requires_grad_(require_grad)
def clone_inputs(preserve_requires_grad: bool):
inputs: list[torch.Tensor | list[torch.Tensor]] = []
for arg in args:
if isinstance(arg, torch.Tensor):
inputs.append(clone_tensor(arg, preserve_requires_grad))
elif is_iterable_of_tensors(arg):
inputs.append([clone_tensor(t, preserve_requires_grad) for t in arg])
else:
inputs.append(arg)
return inputs
# Returns tensors in args that requires_grad, including tensors in TensorList args
def get_recording_tensors(args):
recording_tensors: list[torch.Tensor] = []
for arg in args:
if isinstance(arg, torch.Tensor) and arg.requires_grad:
recording_tensors.append(arg)
elif is_iterable_of_tensors(arg):
recording_tensors.extend(filter(lambda t: t.requires_grad, arg))
return recording_tensors
# test no gradients case
nograd_inputs = clone_inputs(preserve_requires_grad=False)
outputs = self.runAndSaveRNG(reference_func, nograd_inputs, kwargs)
with enable_profiling_mode_for_profiling_tests():
outputs_test = self.runAndSaveRNG(func, nograd_inputs, kwargs)
self.assertEqual(outputs, outputs_test)
if check_types:
check_output_types(self, func, outputs_test, nograd_inputs, kwargs)
if no_grad:
# skip grad tests
return
with enable_profiling_mode_for_profiling_tests():
# test single grad case
recording_inputs = clone_inputs(preserve_requires_grad=True)
recording_tensors = get_recording_tensors(recording_inputs)
outputs = output_func(self.runAndSaveRNG(reference_func, recording_inputs, kwargs))
grads = torch.autograd.grad(allSum(outputs), recording_tensors,
allow_unused=allow_unused)
outputs_test = output_func(self.runAndSaveRNG(func, recording_inputs, kwargs))
grads_test = torch.autograd.grad(allSum(outputs_test), recording_tensors,
allow_unused=allow_unused)
self.assertEqual(outputs, outputs_test)
self.assertEqual(grads, grads_test)
# test the grad grad case
if self._testMethodName in nn_functional_single_grad or no_gradgrad:
return
outputs = output_func(self.runAndSaveRNG(reference_func, recording_inputs, kwargs))
l1 = allSum(outputs)
grads = torch.autograd.grad(l1, recording_tensors, create_graph=True,
allow_unused=allow_unused)
l2 = (allSum(grads) * l1)
grads2 = torch.autograd.grad(l2, recording_tensors, allow_unused=allow_unused)
recording_inputs = clone_inputs(preserve_requires_grad=True)
recording_tensors = get_recording_tensors(recording_inputs)
outputs_test = output_func(self.runAndSaveRNG(func, recording_inputs, kwargs))
l1_test = allSum(outputs_test)
grads_test = torch.autograd.grad(
l1_test, recording_tensors, create_graph=True, allow_unused=allow_unused)
l2_test = (allSum(grads_test) * l1_test)
grads2_test = torch.autograd.grad(l2_test, recording_tensors, allow_unused=allow_unused)
self.assertEqual(outputs, outputs_test)
self.assertEqual(grads, grads_test)
for g2, g2_test in zip(grads2, grads2_test, strict=True):
if g2 is None and g2_test is None:
continue
self.assertEqual(g2, g2_test, atol=5e-4, rtol=1e-4)
class JitCommonTestCase(TestCase):
def createFunctionFromGraph(self, trace):
graph = trace if isinstance(trace, torch._C.Graph) else trace.graph()
return torch._C._create_function_from_graph("forward", graph)
def assertExportImport(self, trace, inputs):
m = self.createFunctionFromGraph(trace)
self.assertExportImportModule(m, inputs)
def assertExportImportModule(self, m, inputs):
m_import = self.getExportImportCopy(m)
a = self.runAndSaveRNG(m, inputs)
b = self.runAndSaveRNG(m_import, inputs)
self.assertEqual(a, b, "Results of original model and "
"exported/imported version of model differed")
def runAndSaveRNG(self, func, inputs, kwargs=None):
kwargs = kwargs if kwargs else {}
with freeze_rng_state():
results = func(*inputs, **kwargs)
return results
def getExportImportCopy(self, m, also_test_file=True, map_location=None):
buffer = io.BytesIO()
torch.jit.save(m, buffer)
buffer.seek(0)
imported = torch.jit.load(buffer, map_location=map_location)
if not also_test_file:
return imported
with TemporaryFileName() as fname:
torch.jit.save(imported, fname)
return torch.jit.load(fname, map_location=map_location)
def autoDiffErrorMessage(self, should_autodiff_node, nodes_not_in_diff_graph,
fusion_nodes_not_found, non_fusible_nodes_being_fused,
fusion_nodes_found, nodes_in_diff_graph):
err_msg = "\nFailure in testing nodes' autodifferentiation. "
if should_autodiff_node:
err_msg += "One or more nodes were expected to be autodiffed, " \
"but were not found in specified fusible/nonfusible " \
"DifferentiableGraph groups. \nSpecifically:"
# The node is intended to appear in a differentiable graph but doesn't
diff_nodes_missing = []
# The node is intended to appear in a differentiable graph
# outside of a fusion group but instead is in a fusion group
diff_nodes_in_fusion = []
# The node is intended to appear in a fusion group but doesn't
fusion_nodes_missing = []
# The node is intended to appear in a fusion group but instead
# is just in an outer differentiable graph
fusion_nodes_in_diff = []
for node in nodes_not_in_diff_graph:
if node in non_fusible_nodes_being_fused:
diff_nodes_in_fusion.append(node)
else:
diff_nodes_missing.append(node)
for node in fusion_nodes_not_found:
if node in nodes_in_diff_graph:
fusion_nodes_in_diff.append(node)
else:
fusion_nodes_missing.append(node)
if len(diff_nodes_missing) > 0:
err_msg += f"\n {diff_nodes_missing} were not in one of the " \
"DifferentiableGraphs when they were expected to be. " \
"Did you intend for these nodes to be autodiffed? " \
"If not, remove them from the list of nonfusible nodes."
if len(diff_nodes_in_fusion) > 0:
err_msg += f"\n {diff_nodes_in_fusion} were found in one of the FusionGroups " \
"when they were expected to be just in a DifferentiableGraph. If it was " \
"intended for these nodes to be in FusionGroups, reclassify these nodes as " \
"fusible nodes. If these nodes were not intended to be fused, your " \
"autodifferentiation logic might be wrong."
if len(fusion_nodes_missing) > 0:
err_msg += f"\n {fusion_nodes_missing} were not in one of the FusionGroups " \
"of the DifferentiableGraphs when they were expected to be. " \
"They were also not found in an outer DifferentiableGraph. Did you " \
"intend for these nodes to be autodifferentiated? If not, you should " \
"remove these nodes from the test's fusible nodes. Otherwise your " \
"autodifferentiation logic might be wrong."
if len(fusion_nodes_in_diff) > 0:
err_msg += f"\n {fusion_nodes_in_diff} were not in one of the FusionGroups " \
"of the DifferentiableGraphs when they were expected to be, " \
"instead they were found just in an outer DifferentiableGraph. " \
"Did you intend for these nodes to be fused? If not, you should " \
"move these nodes into the test's nonfusible nodes. Otherwise your " \
"autodifferentiation logic might be wrong."
else:
err_msg += "One or more nodes were not expected to be autodiffed " \
"but were found in a DifferentiableGraph or in a FusionGroup " \
"of a DifferentiableGraph. Did you intend for these nodes to be " \
"autodiffed? If so, change this test to expect autodifferentiation. " \
"\nSpecifically:"
if len(fusion_nodes_found) > 0:
err_msg += f"\n {fusion_nodes_found} were not expected to be in " \
"one of the DifferentiableGraphs, but appeared in a FusionGroup " \
"of a DifferentiableGraph. "
if len(nodes_in_diff_graph) > 0:
err_msg += f"\n {nodes_in_diff_graph} were not expected to " \
"be in one of the DifferentiableGraphs but were."
return err_msg
def assertAutodiffNode(self, graph, should_autodiff_node, nonfusible_nodes, fusible_nodes):
diff_nodes = graph.findAllNodes('prim::DifferentiableGraph')
diff_subgraphs = [node.g('Subgraph') for node in diff_nodes]
# Note: currently no tests have fusible_nodes
fusion_nodes = list(chain.from_iterable([g.findAllNodes('prim::FusionGroup') for g in diff_subgraphs]))
fusion_subgraphs = [node.g('Subgraph') for node in fusion_nodes]
# For any non-fusible node, it must show up in one of the DifferentiableGraphs.
nodes_in_diff_graph = []
nodes_not_in_diff_graph = []
non_fusible_nodes_being_fused = []
for node in nonfusible_nodes:
if any(g.findNode(node) is not None for g in diff_subgraphs):
nodes_in_diff_graph.append(node)
else:
nodes_not_in_diff_graph.append(node)
if any(g.findNode(node) is not None for g in fusion_subgraphs):
non_fusible_nodes_being_fused.append(node)
found_all_nonfusible_nodes = len(nodes_in_diff_graph) == len(nonfusible_nodes)
# For any fusible node, it must show up in one of the FusionGroups in one of the DifferentiableGraphs.
fusion_nodes_found = []
fusion_nodes_not_found = []
for node in fusible_nodes:
if any(g.findNode(node) is not None for g in fusion_subgraphs):
fusion_nodes_found.append(node)
else:
fusion_nodes_not_found.append(node)
found_all_fusible_nodes = len(fusion_nodes_found) == len(fusible_nodes)
if should_autodiff_node is not None:
err_msg = self.autoDiffErrorMessage(should_autodiff_node,
nodes_not_in_diff_graph,
fusion_nodes_not_found,
non_fusible_nodes_being_fused,
fusion_nodes_found,
nodes_in_diff_graph)
self.assertEqual(should_autodiff_node,
found_all_nonfusible_nodes and found_all_fusible_nodes, err_msg)
def checkShapeAnalysis(self, out_sizes: list[int] | list[list[int]],
traced_graph, assert_propagation, constant_prop=True):
# repropagte input shapes provided by tracing,
prev_symbolic_shapes_test_enabled = torch._C._jit_symbolic_shapes_test_mode_enabled()
for enable_test_mode in [True, False]:
# here we are testing allowing/disallowing substituting in complete shapes as constants,
# disallowing constants helps stress test partial eval and substitution pipeline
torch._C._jit_set_symbolic_shapes_test_mode(enable_test_mode)
torch._C._jit_erase_non_input_shape_information(traced_graph)
if constant_prop:
torch._C._jit_pass_constant_propagation(traced_graph)
torch._C._jit_pass_propagate_shapes_on_graph(traced_graph)
# Add sizes to default tensor type to avoid checking something out of scope
# and difficulties with tracer leaving in other parts of tensor type
output = next(traced_graph.outputs()).type()
def test_type(type, actual_size):
sizes = type.symbolic_sizes()
out_type = TensorType.get().with_sizes(sizes)
actual_type = TensorType.get().with_sizes(actual_size)
# always check actual shape is a subtype of the output
self.assertTrue(actual_type.isSubtypeOf(out_type))
# and then if assertion flag is provided, check shape analysis
# is successful
if assert_propagation:
self.assertEqual(out_type.sizes(), actual_size)
if output.isSubtypeOf(torch._C.TensorType.get()):
test_type(output, out_sizes)
else:
tuple_elements = output.elements()
for i in range(len(tuple_elements)):
test_type(tuple_elements[i], out_sizes[i])
torch._C._jit_set_symbolic_shapes_test_mode(prev_symbolic_shapes_test_enabled)
@@ -0,0 +1,113 @@
# mypy: ignore-errors
import contextlib
import functools
import inspect
import torch
def bf32_is_not_fp32():
if not torch.backends.mkldnn.is_available():
return False
if not torch.ops.mkldnn._is_mkldnn_bf16_supported():
return False
return True
def tf32_is_not_fp32():
if not torch.backends.mkldnn.is_available():
return False
if not torch.cpu._is_amx_fp16_supported():
return False
return True
@contextlib.contextmanager
def reduced_f32_off():
old_matmul_precision = torch.backends.mkldnn.matmul.fp32_precision
old_conv_precision = torch.backends.mkldnn.conv.fp32_precision
try:
torch.backends.mkldnn.matmul.fp32_precision = "ieee"
torch.backends.mkldnn.conv.fp32_precision = "ieee"
yield
finally:
torch.backends.mkldnn.matmul.fp32_precision = old_matmul_precision
torch.backends.mkldnn.conv.fp32_precision = old_conv_precision
@contextlib.contextmanager
def bf32_on(self, bf32_precision=1e-2):
old_matmul_precision = torch.backends.mkldnn.matmul.fp32_precision
old_conv_precision = torch.backends.mkldnn.conv.fp32_precision
old_precision = self.precision
try:
torch.backends.mkldnn.matmul.fp32_precision = "bf16"
torch.backends.mkldnn.conv.fp32_precision = "bf16"
self.precision = bf32_precision
yield
finally:
torch.backends.mkldnn.matmul.fp32_precision = old_matmul_precision
torch.backends.mkldnn.conv.fp32_precision = old_conv_precision
self.precision = old_precision
@contextlib.contextmanager
def tf32_on(self, tf32_precision=1e-5):
old_matmul_precision = torch.backends.mkldnn.matmul.fp32_precision
old_conv_precision = torch.backends.mkldnn.conv.fp32_precision
old_precision = self.precision
try:
torch.backends.mkldnn.matmul.fp32_precision = "tf32"
torch.backends.mkldnn.conv.fp32_precision = "tf32"
self.precision = tf32_precision
yield
finally:
torch.backends.mkldnn.matmul.fp32_precision = old_matmul_precision
torch.backends.mkldnn.conv.fp32_precision = old_conv_precision
self.precision = old_precision
# This is a wrapper that wraps a test to run this test three times, one with
# reduced_f32 OFF, the others with reduced_f32 ON (including bf32 ON and tf32
# ON). When running with reduced_f32 ON, it will use reduced precision (bf16/
# tf32) as specified by the argument.
def reduced_f32_on_and_off(bf32_precision=1e-2, tf32_precision=1e-5):
def with_reduced_f32_disabled(self, function_call):
with reduced_f32_off():
function_call()
def with_bf32_enabled(self, function_call):
with bf32_on(self, bf32_precision):
function_call()
def with_tf32_enabled(self, function_call):
with tf32_on(self, tf32_precision):
function_call()
def wrapper(f):
params = inspect.signature(f).parameters
arg_names = tuple(params.keys())
@functools.wraps(f)
def wrapped(*args, **kwargs):
kwargs.update(zip(arg_names, args, strict=False))
cond = True
if "device" in kwargs:
cond = cond and (torch.device(kwargs["device"]).type == "cpu")
if "dtype" in kwargs:
cond = cond and (kwargs["dtype"] == torch.float)
bf32_cond = cond and bf32_is_not_fp32()
tf32_cond = cond and tf32_is_not_fp32()
if bf32_cond or tf32_cond:
with_reduced_f32_disabled(kwargs["self"], lambda: f(**kwargs))
if bf32_cond:
with_bf32_enabled(kwargs["self"], lambda: f(**kwargs))
if tf32_cond:
with_tf32_enabled(kwargs["self"], lambda: f(**kwargs))
else:
f(**kwargs)
return wrapped
return wrapper
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,258 @@
# Owner(s): ["oncall: pt2"]
"""
Shared xfail lists for unbacked symint tests.
These lists are used by both test_ops_unbacked.py (base tensor tests)
and test_dtensor_ops.py (DTensor tests with unbacked dimensions).
"""
def xfail(op_name, variant_name="", *, device_type=None, dtypes=None):
return (op_name, variant_name, device_type, dtypes, True)
def skip(op_name, variant_name="", *, device_type=None, dtypes=None):
return (op_name, variant_name, device_type, dtypes, False)
# Ops that have data-dependent errors with unbacked dimensions.
# These fail at the base tensor level (not DTensor-specific).
ops_dde_xfail = {
xfail("_chunk_cat"),
xfail("_unsafe_masked_index_put_accumulate"),
xfail("_upsample_bilinear2d_aa"),
xfail("addmv"),
xfail("allclose"),
xfail("as_strided_scatter"),
xfail("baddbmm"),
xfail("bernoulli"),
xfail("cauchy"),
xfail("cdist"),
xfail("cholesky"),
xfail("chunk"),
xfail("combinations"),
xfail("corrcoef"),
xfail("cov"),
xfail("cross"),
xfail("cummax"),
xfail("cummin"),
xfail("cumulative_trapezoid"),
xfail("diagonal_scatter"),
xfail("diff"),
xfail("dsplit"),
xfail("equal"),
xfail("exponential"),
xfail("fft.fft"),
xfail("fft.fft2"),
xfail("fft.fftn"),
xfail("fft.fftshift"),
xfail("fft.hfft"),
xfail("fft.hfft2"),
xfail("fft.hfftn"),
xfail("fft.ifft"),
xfail("fft.ifft2"),
xfail("fft.ifftn"),
xfail("fft.ifftshift"),
xfail("fft.ihfft"),
xfail("fft.ihfft2"),
xfail("fft.ihfftn"),
xfail("fft.irfft"),
xfail("fft.irfft2"),
xfail("fft.irfftn"),
xfail("fft.rfft"),
xfail("fft.rfft2"),
xfail("fft.rfftn"),
xfail("float"),
xfail("geometric"),
xfail("geqrf"),
xfail("gradient"),
xfail("grid_sampler_2d"),
xfail("hash_tensor"),
xfail("histogram"),
xfail("histogramdd"),
xfail("hsplit"),
xfail("index_fill"),
xfail("inner"),
xfail("kron"),
xfail("linalg.cond"),
xfail("linalg.cross"),
xfail("linalg.householder_product"),
xfail("linalg.ldl_solve"),
xfail("linalg.lstsq"),
xfail("linalg.lstsq", "grad_oriented"),
xfail("linalg.lu_solve"),
xfail("linalg.matrix_norm"),
xfail("linalg.matrix_power"),
xfail("linalg.matrix_rank"),
xfail("linalg.matrix_rank", "hermitian"),
xfail("linalg.multi_dot"),
xfail("linalg.norm"),
xfail("linalg.norm", "subgradients_at_zero"),
xfail("linalg.pinv"),
xfail("linalg.pinv", "hermitian"),
xfail("linalg.pinv", "singular"),
xfail("linalg.qr"),
xfail("linalg.solve"),
xfail("linalg.solve_ex"),
xfail("linalg.solve_triangular"),
xfail("linalg.tensorinv"),
xfail("linalg.tensorsolve"),
xfail("linalg.vander"),
xfail("log_normal"),
xfail("logdet"),
xfail("logsumexp"),
xfail("lu_solve"),
xfail("lu_unpack"),
xfail("masked.amax"),
xfail("masked.amin"),
xfail("masked.argmax"),
xfail("masked.argmin"),
xfail("masked.cumprod"),
xfail("masked.cumsum"),
xfail("masked.log_softmax"),
xfail("masked.logaddexp"),
xfail("masked.logsumexp"),
xfail("masked.mean"),
xfail("masked.median"),
xfail("masked.norm"),
xfail("masked.prod"),
xfail("masked.softmax"),
xfail("masked.softmin"),
xfail("masked.std"),
xfail("masked.sum"),
xfail("masked.var"),
xfail("max_pool2d_with_indices_backward"),
xfail("multinomial"),
xfail("nn.functional.adaptive_avg_pool1d"),
xfail("nn.functional.adaptive_avg_pool2d"),
xfail("nn.functional.adaptive_avg_pool3d"),
xfail("nn.functional.adaptive_max_pool1d"),
xfail("nn.functional.adaptive_max_pool2d"),
xfail("nn.functional.adaptive_max_pool3d"),
xfail("nn.functional.alpha_dropout"),
xfail("nn.functional.avg_pool1d"),
xfail("nn.functional.avg_pool2d"),
xfail("nn.functional.avg_pool3d"),
xfail("nn.functional.batch_norm"),
xfail("nn.functional.bilinear"),
xfail("nn.functional.binary_cross_entropy"),
xfail("nn.functional.binary_cross_entropy_with_logits"),
xfail("nn.functional.channel_shuffle"),
xfail("nn.functional.cosine_similarity"),
xfail("nn.functional.cross_entropy"),
xfail("nn.functional.ctc_loss"),
xfail("nn.functional.dropout"),
xfail("nn.functional.dropout2d"),
xfail("nn.functional.dropout3d"),
xfail("nn.functional.embedding"),
xfail("nn.functional.embedding_bag"),
xfail("nn.functional.feature_alpha_dropout", "with_train"),
xfail("nn.functional.feature_alpha_dropout", "without_train"),
xfail("nn.functional.fractional_max_pool2d"),
xfail("nn.functional.fractional_max_pool3d"),
xfail("nn.functional.gaussian_nll_loss"),
xfail("nn.functional.grid_sample"),
xfail("nn.functional.group_norm"),
xfail("nn.functional.huber_loss"),
xfail("nn.functional.instance_norm"),
xfail("nn.functional.interpolate", "area"),
xfail("nn.functional.interpolate", "bicubic"),
xfail("nn.functional.interpolate", "bilinear"),
xfail("nn.functional.interpolate", "linear"),
xfail("nn.functional.interpolate", "trilinear"),
xfail("nn.functional.l1_loss"),
xfail("nn.functional.local_response_norm"),
xfail("nn.functional.max_pool1d"),
xfail("nn.functional.max_pool2d"),
xfail("nn.functional.max_pool3d"),
xfail("nn.functional.max_unpool1d"),
xfail("nn.functional.max_unpool1d", "grad"),
xfail("nn.functional.max_unpool2d"),
xfail("nn.functional.max_unpool2d", "grad"),
xfail("nn.functional.max_unpool3d"),
xfail("nn.functional.max_unpool3d", "grad"),
xfail("nn.functional.mse_loss"),
xfail("nn.functional.multi_head_attention_forward"),
xfail("nn.functional.multilabel_margin_loss"),
xfail("nn.functional.nll_loss"),
xfail("nn.functional.pad", "circular"),
xfail("nn.functional.pad", "reflect"),
xfail("nn.functional.pad", "replicate"),
xfail("nn.functional.pad", "replicate_negative"),
xfail("nn.functional.pdist"),
xfail("nn.functional.pixel_shuffle"),
xfail("nn.functional.prelu"),
xfail("nn.functional.rrelu"),
xfail("nn.functional.scaled_dot_product_attention"),
xfail("nn.functional.smooth_l1_loss"),
xfail("nn.functional.unfold"),
xfail("nn.functional.upsample_bilinear"),
xfail("normal"),
xfail("normal", "in_place"),
xfail("normal", "number_mean"),
xfail("ormqr"),
xfail("pca_lowrank"),
xfail("pinverse"),
xfail("qr"),
xfail("rand_like"),
xfail("randint_like"),
xfail("randn_like"),
xfail("repeat_interleave"),
xfail("resize_"),
xfail("resize_as_"),
xfail("roll"),
xfail("searchsorted"),
xfail("sparse.mm", "reduce"),
xfail("split"),
xfail("stft"),
xfail("svd_lowrank"),
xfail("sum_to_size"),
xfail("take"),
xfail("take_along_dim"),
xfail("tensordot"),
xfail("tensor_split"),
xfail("to_sparse"),
xfail("trapezoid"),
xfail("trapz"),
xfail("unbind"),
xfail("unbind_copy"),
xfail("uniform"),
xfail("unsafe_chunk"),
xfail("unsafe_split"),
xfail("vsplit"),
}
# Ops that skip for unbacked tests (no valid samples with markable dims)
ops_unbacked_skip = {
skip("arange"),
skip("broadcast_shapes"),
skip("empty"),
skip("empty_permuted"),
skip("empty_strided"),
skip("eye"),
skip("full"),
skip("item"),
skip("linspace"),
skip("linspace", "tensor_overload"),
skip("logspace"),
skip("logspace", "tensor_overload"),
skip("ones"),
skip("randint"),
skip("randn"),
skip("scalar_tensor"),
skip("signal.windows.bartlett"),
skip("signal.windows.blackman"),
skip("signal.windows.cosine"),
skip("signal.windows.exponential"),
skip("signal.windows.gaussian"),
skip("signal.windows.general_cosine"),
skip("signal.windows.general_hamming"),
skip("signal.windows.hamming"),
skip("signal.windows.hann"),
skip("signal.windows.kaiser"),
skip("signal.windows.nuttall"),
skip("zeros"),
# Sparse ops that can't be deepcopied
skip("sparse.sampled_addmm"),
}
@@ -0,0 +1,385 @@
# Owner(s): ["module: unknown"]
from typing import Any
from torch.ao.pruning import BaseSparsifier
import torch
import torch.nn.functional as F
from torch import nn
class ImplementedSparsifier(BaseSparsifier):
def __init__(self, **kwargs: dict[str, Any]) -> None:
super().__init__(defaults=kwargs)
def update_mask(self, module: nn.Module, tensor_name: str, **kwargs: dict[str, Any]) -> None:
module.parametrizations.weight[0].mask[0] = 0 # type: ignore[index, union-attr]
linear_state = self.state['linear1.weight']
linear_state['step_count'] = linear_state.get('step_count', 0) + 1
class MockSparseLinear(nn.Linear):
"""
This class is a MockSparseLinear class to check convert functionality.
It is the same as a normal Linear layer, except with a different type, as
well as an additional from_dense method.
"""
@classmethod
def from_dense(cls, mod: nn.Linear) -> 'MockSparseLinear':
"""
"""
linear = cls(mod.in_features,
mod.out_features)
return linear
def rows_are_subset(subset_tensor: torch.Tensor, superset_tensor: torch.Tensor) -> bool:
"""
Checks to see if all rows in subset tensor are present in the superset tensor
"""
i = 0
for row in subset_tensor:
while i < len(superset_tensor):
if not torch.equal(row, superset_tensor[i]):
i += 1
else:
break
else:
return False
return True
class SimpleLinear(nn.Module):
r"""Model with only Linear layers without biases, some wrapped in a Sequential,
some following the Sequential. Used to test basic pruned Linear-Linear fusion."""
def __init__(self) -> None:
super().__init__()
self.seq = nn.Sequential(
nn.Linear(7, 5, bias=False),
nn.Linear(5, 6, bias=False),
nn.Linear(6, 4, bias=False),
)
self.linear1 = nn.Linear(4, 4, bias=False)
self.linear2 = nn.Linear(4, 10, bias=False)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.seq(x)
x = self.linear1(x)
x = self.linear2(x)
return x
class LinearBias(nn.Module):
r"""Model with only Linear layers, alternating layers with biases,
wrapped in a Sequential. Used to test pruned Linear-Bias-Linear fusion."""
def __init__(self) -> None:
super().__init__()
self.seq = nn.Sequential(
nn.Linear(7, 5, bias=True),
nn.Linear(5, 6, bias=False),
nn.Linear(6, 3, bias=True),
nn.Linear(3, 3, bias=True),
nn.Linear(3, 10, bias=False),
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.seq(x)
return x
class LinearActivation(nn.Module):
r"""Model with only Linear layers, some with bias, some in a Sequential and some following.
Activation functions modules in between each Linear in the Sequential, and each outside layer.
Used to test pruned Linear(Bias)-Activation-Linear fusion."""
def __init__(self) -> None:
super().__init__()
self.seq = nn.Sequential(
nn.Linear(7, 5, bias=True),
nn.ReLU(),
nn.Linear(5, 6, bias=False),
nn.Tanh(),
nn.Linear(6, 4, bias=True),
)
self.linear1 = nn.Linear(4, 3, bias=True)
self.act1 = nn.ReLU()
self.linear2 = nn.Linear(3, 10, bias=False)
self.act2 = nn.Tanh()
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.seq(x)
x = self.linear1(x)
x = self.act1(x)
x = self.linear2(x)
x = self.act2(x)
return x
class LinearActivationFunctional(nn.Module):
r"""Model with only Linear layers, some with bias, some in a Sequential and some following.
Activation functions modules in between each Linear in the Sequential, and functional
activationals are called in between each outside layer.
Used to test pruned Linear(Bias)-Activation-Linear fusion."""
def __init__(self) -> None:
super().__init__()
self.seq = nn.Sequential(
nn.Linear(7, 5, bias=True),
nn.ReLU(),
nn.Linear(5, 6, bias=False),
nn.ReLU(),
nn.Linear(6, 4, bias=True),
)
self.linear1 = nn.Linear(4, 3, bias=True)
self.linear2 = nn.Linear(3, 8, bias=False)
self.linear3 = nn.Linear(8, 10, bias=False)
self.act1 = nn.ReLU()
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.seq(x)
x = self.linear1(x)
x = F.relu(x)
x = self.linear2(x)
x = F.relu(x)
x = self.linear3(x)
x = F.relu(x)
return x
class SimpleConv2d(nn.Module):
r"""Model with only Conv2d layers, all without bias, some in a Sequential and some following.
Used to test pruned Conv2d-Conv2d fusion."""
def __init__(self) -> None:
super().__init__()
self.seq = nn.Sequential(
nn.Conv2d(1, 32, 3, 1, bias=False),
nn.Conv2d(32, 64, 3, 1, bias=False),
)
self.conv2d1 = nn.Conv2d(64, 48, 3, 1, bias=False)
self.conv2d2 = nn.Conv2d(48, 52, 3, 1, bias=False)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.seq(x)
x = self.conv2d1(x)
x = self.conv2d2(x)
return x
class Conv2dBias(nn.Module):
r"""Model with only Conv2d layers, some with bias, some in a Sequential and some outside.
Used to test pruned Conv2d-Bias-Conv2d fusion."""
def __init__(self) -> None:
super().__init__()
self.seq = nn.Sequential(
nn.Conv2d(1, 32, 3, 1, bias=True),
nn.Conv2d(32, 32, 3, 1, bias=True),
nn.Conv2d(32, 64, 3, 1, bias=False),
)
self.conv2d1 = nn.Conv2d(64, 48, 3, 1, bias=True)
self.conv2d2 = nn.Conv2d(48, 52, 3, 1, bias=False)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.seq(x)
x = self.conv2d1(x)
x = self.conv2d2(x)
return x
class Conv2dActivation(nn.Module):
r"""Model with only Conv2d layers, some with bias, some in a Sequential and some following.
Activation function modules in between each Sequential layer, functional activations called
in-between each outside layer.
Used to test pruned Conv2d-Bias-Activation-Conv2d fusion."""
def __init__(self) -> None:
super().__init__()
self.seq = nn.Sequential(
nn.Conv2d(1, 32, 3, 1, bias=True),
nn.ReLU(),
nn.Conv2d(32, 64, 3, 1, bias=True),
nn.Tanh(),
nn.Conv2d(64, 64, 3, 1, bias=False),
nn.ReLU(),
)
self.conv2d1 = nn.Conv2d(64, 48, 3, 1, bias=False)
self.conv2d2 = nn.Conv2d(48, 52, 3, 1, bias=True)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.seq(x)
x = self.conv2d1(x)
x = F.relu(x)
x = self.conv2d2(x)
x = F.hardtanh(x)
return x
class Conv2dPadBias(nn.Module):
r"""Model with only Conv2d layers, all with bias and some with padding > 0,
some in a Sequential and some following. Activation function modules in between each layer.
Used to test that bias is propagated correctly in the special case of
pruned Conv2d-Bias-(Activation)Conv2d fusion, when the second Conv2d layer has padding > 0."""
def __init__(self) -> None:
super().__init__()
self.seq = nn.Sequential(
nn.Conv2d(1, 32, 3, 1, padding=1, bias=True),
nn.ReLU(),
nn.Conv2d(32, 32, 3, 1, bias=False),
nn.ReLU(),
nn.Conv2d(32, 32, 3, 1, padding=1, bias=True),
nn.ReLU(),
nn.Conv2d(32, 32, 3, 1, padding=1, bias=True),
nn.ReLU(),
nn.Conv2d(32, 64, 3, 1, bias=True),
nn.Tanh(),
)
self.conv2d1 = nn.Conv2d(64, 48, 3, 1, padding=1, bias=True)
self.act1 = nn.ReLU()
self.conv2d2 = nn.Conv2d(48, 52, 3, 1, padding=1, bias=True)
self.act2 = nn.Tanh()
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.seq(x)
x = self.conv2d1(x)
x = self.act1(x)
x = self.conv2d2(x)
x = self.act2(x)
return x
class Conv2dPool(nn.Module):
r"""Model with only Conv2d layers, all with bias, some in a Sequential and some following.
Activation function modules in between each layer, Pool2d modules in between each layer.
Used to test pruned Conv2d-Pool2d-Conv2d fusion."""
def __init__(self) -> None:
super().__init__()
self.seq = nn.Sequential(
nn.Conv2d(1, 32, kernel_size=3, padding=1, bias=True),
nn.MaxPool2d(kernel_size=2, stride=2, padding=1),
nn.ReLU(),
nn.Conv2d(32, 64, kernel_size=3, padding=1, bias=True),
nn.Tanh(),
nn.AvgPool2d(kernel_size=2, stride=2, padding=1),
)
self.conv2d1 = nn.Conv2d(64, 48, kernel_size=3, padding=1, bias=True)
self.maxpool = nn.MaxPool2d(kernel_size=2, stride=2, padding=1)
self.af1 = nn.ReLU()
self.conv2d2 = nn.Conv2d(48, 52, kernel_size=3, padding=1, bias=True)
self.conv2d3 = nn.Conv2d(52, 52, kernel_size=3, padding=1, bias=True)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.seq(x)
x = self.conv2d1(x)
x = self.maxpool(x)
x = self.af1(x)
x = self.conv2d2(x)
x = F.avg_pool2d(x, kernel_size=2, stride=2, padding=1)
x = F.relu(x)
x = self.conv2d3(x)
return x
class Conv2dPoolFlattenFunctional(nn.Module):
r"""Model with Conv2d layers, all with bias, some in a Sequential and some following, and then a Pool2d
and a functional Flatten followed by a Linear layer.
Activation functions and Pool2ds in between each layer also.
Used to test pruned Conv2d-Pool2d-Flatten-Linear fusion."""
def __init__(self) -> None:
super().__init__()
self.seq = nn.Sequential(
nn.Conv2d(1, 3, kernel_size=3, padding=1, bias=True),
nn.MaxPool2d(kernel_size=2, stride=2, padding=1),
nn.ReLU(),
nn.Conv2d(3, 5, kernel_size=3, padding=1, bias=True),
nn.Tanh(),
nn.AvgPool2d(kernel_size=2, stride=2, padding=1),
)
self.conv2d1 = nn.Conv2d(5, 7, kernel_size=3, padding=1, bias=True)
self.af1 = nn.ReLU()
self.conv2d2 = nn.Conv2d(7, 11, kernel_size=3, padding=1, bias=True)
self.avg_pool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(11, 13, bias=True)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.seq(x)
x = self.conv2d1(x)
x = F.max_pool2d(x, kernel_size=2, stride=2, padding=1)
x = self.af1(x)
x = self.conv2d2(x)
x = self.avg_pool(x)
x = torch.flatten(x, 1) # test functional flatten
x = self.fc(x)
return x
class Conv2dPoolFlatten(nn.Module):
r"""Model with Conv2d layers, all with bias, some in a Sequential and some following, and then a Pool2d
and a Flatten module followed by a Linear layer.
Activation functions and Pool2ds in between each layer also.
Used to test pruned Conv2d-Pool2d-Flatten-Linear fusion."""
def __init__(self) -> None:
super().__init__()
self.seq = nn.Sequential(
nn.Conv2d(1, 3, kernel_size=3, padding=1, bias=True),
nn.MaxPool2d(kernel_size=2, stride=2, padding=1),
nn.ReLU(),
nn.Conv2d(3, 5, kernel_size=3, padding=1, bias=True),
nn.Tanh(),
nn.AvgPool2d(kernel_size=2, stride=2, padding=1),
)
self.conv2d1 = nn.Conv2d(5, 7, kernel_size=3, padding=1, bias=True)
self.af1 = nn.ReLU()
self.conv2d2 = nn.Conv2d(7, 11, kernel_size=3, padding=1, bias=True)
self.avg_pool = nn.AdaptiveAvgPool2d((2, 2))
self.flatten = nn.Flatten()
self.fc = nn.Linear(44, 13, bias=True)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.seq(x)
x = self.conv2d1(x)
x = F.max_pool2d(x, kernel_size=2, stride=2, padding=1)
x = self.af1(x)
x = self.conv2d2(x)
x = self.avg_pool(x)
x = self.flatten(x)
x = self.fc(x)
return x
class LSTMLinearModel(nn.Module):
"""Container module with an encoder, a recurrent module, and a linear."""
def __init__(
self, input_dim: int, hidden_dim: int, output_dim: int, num_layers: int
) -> None:
super().__init__()
self.lstm = nn.LSTM(input_dim, hidden_dim, num_layers)
self.linear = nn.Linear(hidden_dim, output_dim)
def forward(self, input: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
output, _hidden = self.lstm(input)
decoded = self.linear(output)
return decoded, output
class LSTMLayerNormLinearModel(nn.Module):
"""Container module with an LSTM, a LayerNorm, and a linear."""
def __init__(
self, input_dim: int, hidden_dim: int, output_dim: int, num_layers: int
) -> None:
super().__init__()
self.lstm = nn.LSTM(input_dim, hidden_dim, num_layers)
self.norm = nn.LayerNorm(hidden_dim)
self.linear = nn.Linear(hidden_dim, output_dim)
def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
x, state = self.lstm(x)
x = self.norm(x)
x = self.linear(x)
return x, state
@@ -0,0 +1,693 @@
# mypy: ignore-errors
r"""Importing this file includes common utility methods for checking quantized
tensors and modules.
"""
import numpy as np
import torch
from torch import Tensor
from contextlib import contextmanager
from torch.testing._internal.common_utils import TEST_WITH_TSAN, IS_PPC, IS_MACOS, IS_WINDOWS, IS_ARM64
supported_qengines = list(torch.backends.quantized.supported_engines)
# Note: We currently do not run QNNPACK tests on WINDOWS and MACOS as it is flaky. Issue #29326
# QNNPACK is not supported on PPC
if 'qnnpack' in supported_qengines and any([IS_PPC, TEST_WITH_TSAN, IS_MACOS, IS_WINDOWS]):
supported_qengines.remove('qnnpack')
# FBGEMM and x86 engines require x86 architecture with AVX2/AVX512 support
# They are not supported on ARM64 architectures
if IS_ARM64:
supported_qengines = [qe for qe in supported_qengines if qe not in ('fbgemm', 'x86')]
def _conv_output_shape(input_size, kernel_size, padding, stride, dilation,
output_padding=0):
"""Computes the output shape given convolution parameters."""
return np.floor((input_size + 2 * padding - kernel_size - (kernel_size - 1)
* (dilation - 1)) / stride) + 2 * output_padding + 1
# Quantization references
def _quantize(x, scale, zero_point, qmin=None, qmax=None, dtype=np.uint8):
"""Quantizes a numpy array."""
if qmin is None:
qmin = np.iinfo(dtype).min
if qmax is None:
qmax = np.iinfo(dtype).max
qx = np.round(x / scale + zero_point).astype(np.int64)
qx = np.clip(qx, qmin, qmax)
qx = qx.astype(dtype)
return qx
def _dequantize(qx, scale, zero_point):
"""Dequantizes a numpy array."""
x = (qx.astype(float) - zero_point) * scale
return x
def _requantize(x, multiplier, zero_point, qmin=0, qmax=255, qtype=np.uint8):
"""Requantizes a numpy array, i.e., intermediate int32 or int16 values are
converted back to given type"""
qx = (x * multiplier).round() + zero_point
qx = np.clip(qx, qmin, qmax).astype(qtype)
return qx
def _calculate_dynamic_qparams(X, dtype, reduce_range=False, qscheme=torch.per_tensor_affine):
"""Calculate the dynamic quantization parameters (scale, zero_point)
according to the min and max element of the tensor"""
if qscheme not in (torch.per_tensor_affine, torch.per_tensor_symmetric):
raise AssertionError(
f"Expected qscheme to be per_tensor_affine or per_tensor_symmetric, got {qscheme}"
)
if qscheme == torch.per_tensor_symmetric:
if dtype != torch.qint8:
raise AssertionError(
f"Expected dtype to be torch.qint8 for symmetric qscheme, got {dtype}"
)
if isinstance(X, torch.Tensor):
X = X.numpy()
if dtype == torch.qint8:
if reduce_range:
qmin, qmax = -64, 63
else:
qmin, qmax = -128, 127
else: # dtype == torch.quint8
if reduce_range:
qmin, qmax = 0, 127
else:
qmin, qmax = 0, 255
min_val = X.min()
max_val = X.max()
is_symmetric = (qscheme == torch.per_tensor_symmetric)
if min_val == max_val:
scale = 1.0
zero_point = 0
else:
if is_symmetric:
max_val = max(max_val, -min_val)
min_val = -max_val
scale = (max_val - min_val) / (qmax - qmin)
scale = max(scale, np.finfo(np.float32).eps)
zero_point = 0
else:
max_val = max(max_val, 0.0)
min_val = min(min_val, 0.0)
scale = (max_val - min_val) / (qmax - qmin)
scale = max(scale, np.finfo(np.float32).eps)
zero_point = qmin - round(min_val / scale)
zero_point = max(qmin, zero_point)
zero_point = min(qmax, zero_point)
return [float(scale), int(zero_point)]
def _calculate_dynamic_per_channel_qparams(X, dtype):
"""Calculate the dynamic quantization parameters (scale, zero_point)
according to the min and max element of the tensor"""
if isinstance(X, torch.Tensor):
X = X.numpy()
qmin, qmax = torch.iinfo(dtype).min, torch.iinfo(dtype).max
n_levels = qmax - qmin
scale = np.zeros(X.shape[0], dtype=np.float64)
zero_point = np.zeros(X.shape[0], dtype=np.int64)
for i in range(zero_point.shape[0]):
min_val = X.min()
max_val = X.max()
if min_val == max_val:
scale[i] = 1.0
zero_point[i] = 0
else:
max_val = max(max_val, 0.0)
min_val = min(min_val, 0.0)
scale[i] = (max_val - min_val) / n_levels
scale[i] = max(scale[i], np.finfo(np.float32).eps)
zero_point[i] = qmin - round(min_val / scale[i])
zero_point[i] = max(qmin, zero_point[i])
zero_point[i] = min(qmax, zero_point[i])
return scale, zero_point
def _snr(x, x_hat):
"""Calculates the signal to noise ratio and returns the signal and noise
power, as well as the SNR in dB.
If the input is a list/tuple this function is called recursively on each
element. The result will have the same nested structure as the inputs.
Args:
x, x_hat: Either a tensor or a nested list/tuple of tensors.
Returns:
signal, noise, SNR(in dB): Either floats or a nested list of floats
"""
if isinstance(x, (list, tuple)):
if len(x) != len(x_hat):
raise AssertionError(f"Expected len(x) == len(x_hat), got {len(x)} != {len(x_hat)}")
res = [_snr(x[idx], x_hat[idx]) for idx in range(len(x))]
return res
if x_hat.is_quantized:
x_hat = x_hat.dequantize()
if x.is_quantized:
x = x.dequantize()
noise = (x - x_hat).norm()
if noise == 0:
return 0.0, float('inf'), float('inf')
signal = x.norm()
snr = signal / noise
snr_db = 20 * snr.log10()
return signal, noise, snr_db
@contextmanager
def override_quantized_engine(qengine):
previous = torch.backends.quantized.engine
torch.backends.quantized.engine = qengine
try:
yield
finally:
torch.backends.quantized.engine = previous
@contextmanager
def override_cpu_allocator_for_qnnpack(qengine_is_qnnpack):
try:
if qengine_is_qnnpack:
torch._C._set_default_mobile_cpu_allocator()
yield
finally:
if qengine_is_qnnpack:
torch._C._unset_default_mobile_cpu_allocator()
# TODO: Update all quantization tests to use this decorator.
# Currently for some of the tests it seems to have inconsistent params
# for fbgemm vs qnnpack.
def override_qengines(qfunction):
def test_fn(*args, **kwargs):
for qengine in supported_qengines:
with override_quantized_engine(qengine):
# qfunction should not return anything.
qfunction(*args, **kwargs)
return test_fn
def qengine_is_fbgemm():
return torch.backends.quantized.engine == 'fbgemm'
def qengine_is_qnnpack():
return torch.backends.quantized.engine == 'qnnpack'
def qengine_is_onednn():
return torch.backends.quantized.engine == 'onednn'
def qengine_is_x86():
return torch.backends.quantized.engine == 'x86'
# Helper function used to simulate per-channel fake-quant against any axis
def _permute_to_axis_zero(X, axis):
new_axis_list = list(range(X.dim()))
new_axis_list[axis] = 0
new_axis_list[0] = axis
y = X.permute(tuple(new_axis_list))
return y, new_axis_list
# Reference method for fake quantize
# Note: because scale/zero_point are left as float in the actual kernel, this mimics how fake_quant works for float16/64
def _fake_quantize_per_channel_affine_reference(X, per_channel_scale, per_channel_zero_point, axis, quant_min, quant_max):
dtype = X.dtype
X, permute_axis_list = _permute_to_axis_zero(X.to(torch.float32), axis)
res = torch.zeros_like(X)
for i in range(X.size()[0]):
res[i] = (torch.clamp(torch.round(X[i] * (1.0 / per_channel_scale[i]) +
per_channel_zero_point[i]), quant_min, quant_max) - per_channel_zero_point[i]) * per_channel_scale[i]
out = res.permute(tuple(permute_axis_list))
return out.to(dtype)
# Reference method for the gradient of the fake quantize operator
# Note: because scale/zero_point are left as float in the actual kernel, this mimics how fake_quant works for float16/64
def _fake_quantize_per_channel_affine_grad_reference(dY, X, per_channel_scale, per_channel_zero_point, axis, quant_min, quant_max):
dtype = X.dtype
X, permute_axis_list = _permute_to_axis_zero(X.to(torch.float32), axis)
Xq = torch.zeros_like(X)
for i in range(X.size()[0]):
Xq[i] = torch.round(X[i] * (1.0 / per_channel_scale[i]) + per_channel_zero_point[i])
Xq = Xq.permute(tuple(permute_axis_list))
mask = (Xq >= quant_min) * (Xq <= quant_max)
res = torch.zeros_like(dY)
res[mask] = dY[mask]
return res.to(dtype)
def to_tensor(X, device):
if not isinstance(X, torch.Tensor):
X = torch.tensor(X)
else:
X = X.detach().clone()
return X.to(device=torch.device(device), dtype=torch.float32)
# copy-pasted from
# https://github.com/pytorch/ao/blob/bc4f51da86956275da7db0da6e420c506df97820/torchao/prototype/custom_fp_utils.py#L27C1-L142C29
def _n_ones(n: int) -> int:
return (1 << n) - 1
EBITS_F32, MBITS_F32 = 8, 23
F32_EXP_BIAS = _n_ones(EBITS_F32 - 1)
# copy-pasted from
# https://github.com/pytorch/ao/blob/bc4f51da86956275da7db0da6e420c506df97820/torchao/prototype/custom_fp_utils.py#L27C1-L142C29
def _f32_to_floatx_unpacked(x: Tensor, ebits: int, mbits: int) -> Tensor:
"""Convert FP32 numbers to sub-byte floating point numbers with the given
number of exponent and mantissa bits.
Input: torch.Tensor of dtype torch.float
Output: torch.Tensor of dtype torch.uint8, where the bit encoding is stored
in the least significant bits. e.g.
fp4: bits 0-3 empty and bits 4-7 in fp4_e2m1 encoding
fp6: bits 0-1 empty and bits 2-7 in fp6_e2m3 or fp6_e3m2 encoding
Note: there are no special values (NaN, inf) support in this code. Values
outside the representable range of Floatx after rounding are clamped to the
maximum Floatx magnitude (sign is preserved).
Code below is an adaptation of https://fburl.com/code/ciwofcg4
Background 1: last answer in https://stackoverflow.com/q/8981913
Background 2: Computer Organization and Design, RISC-V edition, Chapter 3.5
"""
if x.dtype != torch.float:
raise AssertionError(f"Expected x.dtype to be torch.float, got {x.dtype}")
if 1 + ebits + mbits > 8:
raise AssertionError(f"Expected 1 + ebits + mbits <= 8, got {1 + ebits + mbits}")
# calculate constants
exp_bias = _n_ones(ebits - 1)
max_int = _n_ones(ebits + mbits)
sign_mask = 1 << (ebits + mbits)
# TODO document this better
magic_adder = _n_ones(MBITS_F32 - mbits - 1)
# all E bits and M bits are 1s
max_normal = 2 ** (_n_ones(ebits) - exp_bias) * (_n_ones(mbits + 1) / (2**mbits))
# E bits = 1, M bits = 0
min_normal = 2 ** (1 - exp_bias)
denorm_exp = (
# exp bias conversion between formats
(F32_EXP_BIAS - exp_bias)
# mantissa length difference between formats
+ (MBITS_F32 - mbits)
# add one to encoded exponent for denormalized numbers
+ 1
)
denorm_mask_int = denorm_exp << MBITS_F32
# reinterpret int32 as float32
denorm_mask_float = torch.tensor(denorm_mask_int, dtype=torch.int32).view(
torch.float32
)
# save the sign
# Note that we have torch.uint32, but some ops like cpu bit shifts
# do not work on it. So, we stay in int32.
x = x.view(torch.int32)
sign = x & 0x80000000
# set everything to positive, will add sign back at the end
x = x ^ sign
# TODO: can the branch floating point comparisons below be done without
# converting to float? probably but need to verify
x = x.view(torch.float)
# rewrite saturate/denorm/norm branches without explicit data dependent
# control flow, to be more compiler friendly
saturate_mask = x >= max_normal
denormal_mask = torch.logical_and(torch.logical_not(saturate_mask), x < min_normal)
normal_mask = torch.logical_not(torch.logical_or(saturate_mask, denormal_mask))
#
# branch 1: saturate to max val - handled later in the code which combines
# the branches
#
#
# branch 2: to conversion to denormal as well as rounding up to normal
#
denormal_x = x + denorm_mask_float
denormal_x = denormal_x.view(torch.int32)
denormal_x -= denorm_mask_int
denormal_x = denormal_x.to(torch.uint8)
#
# branch 3: stay in normal range, adjust the exponent and round
#
normal_x = x.view(torch.int32)
# resulting mantissa is odd
mant_odd = (normal_x >> (MBITS_F32 - mbits)) & 1
# update exponent, rounding bias part 1
val_to_add = ((exp_bias - F32_EXP_BIAS) << MBITS_F32) + magic_adder
normal_x += val_to_add
# rounding bias part 2
normal_x += mant_odd
# take the bits!
normal_x = normal_x >> (MBITS_F32 - mbits)
normal_x = normal_x.to(torch.uint8)
#
# combine the branches
#
x = torch.full_like(x, max_int, dtype=torch.uint8)
x = torch.where(denormal_mask, denormal_x, x)
x = torch.where(normal_mask, normal_x, x)
# add sign back
sign_lp = sign >> (MBITS_F32 + EBITS_F32 - mbits - ebits)
sign_lp = sign_lp.to(torch.uint8)
# Right shift of a negative signed integer can fill the least significant
# bits with either 1s or 0s, depending on the implementation. Since PyTorch
# doesn't have an uint32 dtype, we mask out these bits to get just the
# f4 sign bit
sign_lp = sign_lp & sign_mask
x = x | sign_lp
return x.to(torch.uint8)
# copy-pasted from
# https://github.com/pytorch/ao/blob/29488018d99af7f7339f06353c6b5bbeae8a1493/torchao/prototype/custom_fp_utils.py#L147
def _floatx_unpacked_to_f32(x: Tensor, ebits: int, mbits: int) -> Tensor:
"""Convert sub-byte floating point numbers with the given number of exponent
and mantissa bits to FP32.
Input: torch.Tensor of dtype uint8, where the bit encoding is stored
in the least significant bits. e.g.
fp4: bits 0-3 empty and bits 4-7 in fp4_e2m1 encoding
fp6: bits 0-1 empty and bits 2-7 in fp6_e2m3 or fp6_e3m2 encoding
Output: torch.Tensor of dtype fp32 with the dequantized value
"""
if x.dtype != torch.uint8:
raise AssertionError(f"Expected x.dtype to be torch.uint8, got {x.dtype}")
if 1 + ebits + mbits > 8:
raise AssertionError(f"Expected 1 + ebits + mbits <= 8, got {1 + ebits + mbits}")
sign_mask = 1 << (ebits + mbits)
exp_bias = _n_ones(ebits - 1)
mantissa_mask = _n_ones(mbits)
# save the sign
sign_lp = x & sign_mask
# set everything to positive, will add sign back at the end
x_pos = x ^ sign_lp
#
# 1. Calculate zero mask
#
zero_mask = x_pos == 0
#
# 2. Calculate the denormal path mask
#
denormal_mask = torch.logical_and((x_pos > 0), ((x_pos >> mbits) == 0))
#
# 3. Calculate the normal path
#
# calculate the new exponent and shift it to bits 2:9 of the result
exp_biased_lp = x_pos >> mbits
exp_biased_f32 = exp_biased_lp - exp_bias + F32_EXP_BIAS
exp_biased_f32 = exp_biased_f32.to(torch.int32) << MBITS_F32
# shift the mantissa to bits 10:32 of the result
mantissa_lp_int32 = (x_pos & mantissa_mask).to(torch.int32)
mantissa_f32 = mantissa_lp_int32 << (MBITS_F32 - mbits)
result = exp_biased_f32 | mantissa_f32
#
# 4. Add the zero and denormal casts to the already casted normal path
#
result[zero_mask] = 0
denormal_exp_biased = 1 - exp_bias + F32_EXP_BIAS
# fast path.
# without this, performance for FP4_E2M1 is slower by 2x
if mbits == 1:
result[denormal_mask] = (denormal_exp_biased - mbits) << MBITS_F32
else:
# iterate over all possible values of mantissa
# i=0, j=1
# i=1, j=10,11
# i=2, j=100,101,110,111
# and so on
for i in range(mbits):
for mantissa_cmp in range(1 << i, 1 << (i + 1)):
# left shift mantissa until it overflows (create an implicit 1)
# subtract exponent by the same amount
left_shift = mbits - i
mantissa_f32 = (mantissa_cmp - (1 << i)) << (
left_shift + MBITS_F32 - mbits
)
exp_biased_f32 = (denormal_exp_biased - left_shift) << MBITS_F32
# we can update this in-place since the values won't overlap
# torch.compile() may complain unsupported operand type(s) for |: 'SymInt' and 'int'
# thus we use + instead of | here
mantissa_lp_int32[mantissa_lp_int32 == mantissa_cmp] = (
exp_biased_f32 + mantissa_f32
)
result = torch.where(denormal_mask, mantissa_lp_int32, result)
# add sign back
sign_f32 = sign_lp.to(torch.int32) << (MBITS_F32 - mbits + EBITS_F32 - ebits)
result = result | sign_f32
return result.view(torch.float)
# copied from https://github.com/drisspg/transformer_nuggets/blob/main/transformer_nuggets/mx/to_blocked.py
def ceil_div(a, b):
return (a + b - 1) // b
# NVIDIA Blackwell HW requires scales for MX/NV blocked formats to be in a 128x4 tile layout,
# with a weird 32x4x4 internal layout of that tile. If we want to take swizzled scales and use them
# for non-gemm purposes (like testing), we need to de-swizzle them, then they can be applied much
# more naturally.
def from_blocked(input, input_scales, blocksize) -> torch.Tensor:
# Matrix is in a 128x4 pattern, internally blocked as 32x4x4 nonsense.
# Output should be [input.size(0, input.size(1) // blocksize] scales
output_scales = torch.zeros(
(input.size(0), input.size(1) // blocksize),
device=input.device,
dtype=input_scales.dtype,
)
# Swizzled scales are padded to tiles of 128x4, we need to replicate how that padding
# happened for offset purposes.
# There are K//blocksize scales, padded to groups of 4.
num_col_tiles = ceil_div(ceil_div(input.size(1), blocksize), 4)
# (Very) slow reference implementation using horrifying loops.
for i in range(input.size(0)):
for j in range(input.size(1) // blocksize):
# which 128x4 tile of scaling factors am I in
scale_tile_h = i // 128
scale_tile_w = j // 4
# There are (padded) input_scales.size(1) // 4 tiles along the w dim.
# So offset is 512 * (h_tile * tiles_per_row + tile_in_row)
tile_offset = 512 * (scale_tile_h * num_col_tiles + scale_tile_w)
# indices within the tile - use nomenclature directly from cublas docs
outer = i % 128 # "outer" in cublas docs
inner = j % 4 # "inner" in cublas docs
# Note: "offset" is given in terms of bytes, in cublas docs, but our scales are e8m0,
# anyway, and so 1B == 1 value => use offset directly.
# Formula directly from cublas docs in 3.1.4.3.2
offset = tile_offset + (outer % 32) * 16 + (outer // 32) * 4 + inner
output_scales[i, j] = input_scales[offset]
return output_scales
def from_blocked_format(x_mxfp8, scales_unswizzled, blocksize=32):
# expand scales
scales = torch.repeat_interleave(scales_unswizzled, blocksize, dim=1)
# de-scale and convert
x_f32 = x_mxfp8.to(torch.float) * scales.to(torch.float)
return x_f32.to(torch.bfloat16)
def to_blocked(input_matrix) -> torch.Tensor:
"""
Rearrange a large matrix by breaking it into blocks and applying the rearrangement pattern.
See:
https://docs.nvidia.com/cuda/cublas/index.html#d-block-scaling-factors-layout
Args:
input_matrix: Input tensor of shape (H, W)
Returns:
Rearranged tensor of shape (32*ceil_div(H,128), 16*ceil_div(W,4))
"""
rows, cols = input_matrix.shape
n_row_blocks = ceil_div(rows, 128)
n_col_blocks = ceil_div(cols, 4)
# Calculate the padded shape
padded_rows = n_row_blocks * 128
padded_cols = n_col_blocks * 4
padded = input_matrix
# Ideally we would use torch.nn.pad but it doesn't support float8_e8m0fnu for now
if (rows, cols) != (padded_rows, padded_cols):
padded = torch.zeros((padded_rows, padded_cols), device=input_matrix.device, dtype=input_matrix.dtype)
padded[:rows, :cols] = input_matrix
# Rearrange the blocks
blocks = padded.view(n_row_blocks, 128, n_col_blocks, 4).permute(0, 2, 1, 3)
rearranged = blocks.reshape(-1, 4, 32, 4).transpose(1, 2).reshape(-1, 32, 16)
return rearranged.flatten()
def down_size(size):
if size[-1] % 2 != 0:
raise AssertionError(f"{size} last dim not divisible by two")
return (*size[:-1], size[-1] // 2)
def pack_uint4(uint8_data) -> torch.Tensor:
# converting to uint8 for operations
shape = uint8_data.shape
if shape[-1] % 2 != 0:
raise AssertionError(f"Expected shape[-1] to be divisible by 2, got {shape[-1]}")
uint8_data = uint8_data.contiguous().view(-1)
return (uint8_data[1::2] << 4 | uint8_data[::2]).view(down_size(shape))
# exponent and mantissa bits of `torch.float4_e2m1fn_x2`
FP4_EBITS, FP4_MBITS = 2, 1
def _bfloat16_to_float4_e2m1fn_x2(x):
if x.dtype != torch.bfloat16:
raise AssertionError(f"Expected x.dtype to be torch.bfloat16, got {x.dtype}")
x = _f32_to_floatx_unpacked(x.float(), FP4_EBITS, FP4_MBITS)
x = pack_uint4(x)
x = x.view(torch.float4_e2m1fn_x2)
return x
# This function is extracted from https://github.com/pytorch/ao/blob/v0.12.0/torchao/prototype/mx_formats/mx_tensor.py#L142
def to_mxfp(
data_hp: torch.Tensor,
block_size: int = 32,
format: str = "mxfp8",
):
if data_hp.dtype not in (torch.bfloat16, torch.float):
raise AssertionError(f"{data_hp.dtype} is not supported yet")
if data_hp.shape[-1] % block_size != 0:
raise AssertionError(
f"the last dimension of shape {data_hp.shape} must be divisible by block_size {block_size}"
)
if not data_hp.is_contiguous():
raise AssertionError("unsupported: data_hp must be contiguous")
orig_shape = data_hp.shape
data_hp = data_hp.reshape(
*orig_shape[:-1], orig_shape[-1] // block_size, block_size
)
max_abs = torch.amax(torch.abs(data_hp), -1).unsqueeze(-1)
data_hp = data_hp.to(torch.float32)
max_abs = max_abs.to(torch.float32)
if format == "mxfp8":
F8E4M3_MAX = torch.finfo(torch.float8_e4m3fn).max # 448.0
max_pos = F8E4M3_MAX
elif format == "mxfp4":
F4E2M1_MAX = 6.
max_pos = F4E2M1_MAX
# RCEIL
def _to_mx_rceil(
data_hp: torch.Tensor,
max_abs: torch.Tensor,
max_pos: float,
) -> tuple[torch.Tensor, torch.Tensor]:
E8M0_EXPONENT_BIAS = 127
descale = max_abs / max_pos
exponent = torch.where(
torch.isnan(descale),
0xFF, # Handle biased exponent for nan
# NOTE: descale < (torch.finfo(torch.float32).smallest_normal / 2) is handled through clamping
(
torch.clamp(
torch.ceil(torch.log2(descale)),
min=-E8M0_EXPONENT_BIAS,
max=E8M0_EXPONENT_BIAS,
)
+ E8M0_EXPONENT_BIAS
).to(torch.uint8),
)
descale_fp = torch.where(
exponent == 0,
1.0,
torch.exp2(E8M0_EXPONENT_BIAS - exponent.to(torch.float32)),
)
# scale and saturated cast the data elements to max of target dtype
data_lp = torch.clamp(data_hp * descale_fp, min=-1 * max_pos, max=max_pos)
return exponent, data_lp
scale_e8m0_biased, data_lp = _to_mx_rceil(data_hp, max_abs, max_pos)
# cast to target dtype
if format == "mxfp8":
data_lp = data_lp.to(torch.float8_e4m3fn)
# need to reshape at the end to help inductor fuse things
data_lp = data_lp.reshape(orig_shape)
elif format == "mxfp4":
data_lp = _bfloat16_to_float4_e2m1fn_x2(data_lp.to(torch.bfloat16))
final_shape = list(orig_shape)
final_shape[-1] //= 2
data_lp = data_lp.reshape(final_shape)
scale_e8m0_biased = scale_e8m0_biased.view(torch.float8_e8m0fnu)
scale_e8m0_biased = scale_e8m0_biased.squeeze(-1)
return scale_e8m0_biased, data_lp
# Source: https://github.com/pytorch/ao/blob/568c1932a16ae9f30d48da214a88dc0013e98ed8/torchao/prototype/moe_training/utils.py#L310
def generate_jagged_offs(E, M, multiple_of=16, dtype=torch.int32, device="cuda"):
"""
Utility function for tests and benchmarks.
Generates a tensor of length E, containing random values divisible by `multiple_of`,
from 0 to M, in sorted order, and where the final value in the tensor is always M.
Args:
E (int): The length of the tensor.
M (int): The maximum value in the tensor.
Returns:
torch.Tensor: A tensor of length E with the specified properties.
"""
import random
# Ensure M is divisible by 16
if M % multiple_of != 0:
raise ValueError(f"M must be divisible by {multiple_of}")
# Generate a list of possible values
possible_values = list(range(multiple_of, M + 1, multiple_of))
# If E is larger than the number of possible values, raise an error
if E > len(possible_values):
raise ValueError("E cannot be larger than the number of possible values")
# Randomly select E - 1 values from the possible values (excluding M)
selected_values = torch.tensor(random.sample(possible_values[:-1], E - 1))
# Append M to the selected values
selected_values = torch.cat((selected_values, torch.tensor([M])))
# Sort the selected values
selected_values, _ = torch.sort(selected_values)
return selected_values.to(dtype).to(device)
@@ -0,0 +1,345 @@
# mypy: ignore-errors
import torch
from copy import deepcopy
from torch.utils._pytree import tree_map
import torch.utils._pytree as pytree
# TODO: Move LoggingTensor here.
from torch.testing._internal.logging_tensor import LoggingTensor
# Base class for wrapper-style tensors.
class WrapperTensor(torch.Tensor):
@staticmethod
def __new__(cls, *args, **kwargs):
t, kwargs = cls.get_wrapper_properties(*args, **kwargs)
if "size" not in kwargs:
size = t.size()
else:
size = kwargs["size"]
del kwargs["size"]
if "dtype" not in kwargs:
kwargs["dtype"] = t.dtype
if "layout" not in kwargs:
kwargs["layout"] = t.layout
if "device" not in kwargs:
kwargs["device"] = t.device
if "requires_grad" not in kwargs:
kwargs["requires_grad"] = False
# Ignore memory_format and pin memory for now as I don't know how to
# safely access them on a Tensor (if possible??)
wrapper = torch.Tensor._make_wrapper_subclass(cls, size, **kwargs)
wrapper._validate_methods()
return wrapper
@classmethod
def get_wrapper_properties(cls, *args, **kwargs):
# Should return both an example Tensor and a dictionary of kwargs
# to override any of that example Tensor's properly.
# This is very similar to the `t.new_*(args)` API
raise NotImplementedError("You need to implement get_wrapper_properties")
def _validate_methods(self):
# Skip this if not in debug mode?
# Changing these on the python side is wrong as it would not be properly reflected
# on the c++ side
# This doesn't catch attributes set in the __init__
forbidden_overrides = ["size", "stride", "dtype", "layout", "device", "requires_grad"]
for el in forbidden_overrides:
if getattr(self.__class__, el) is not getattr(torch.Tensor, el):
raise RuntimeError(f"Subclass {self.__class__.__name__} is overwriting the "
f"property {el} but this is not allowed as such change would "
"not be reflected to c++ callers.")
class WrapperTensorWithCustomSizes(WrapperTensor):
@classmethod
def get_wrapper_properties(cls, t, requires_grad=False):
return t, {"requires_grad": requires_grad, "dispatch_sizes_strides_policy": "sizes"}
def __init__(self, t, requires_grad=False):
self.t = t
@classmethod
def __torch_dispatch__(cls, func, types, args=(), kwargs=None):
if not all(issubclass(cls, t) for t in types):
return NotImplemented
if kwargs is None:
kwargs = {}
def unwrap(e):
return e.t if isinstance(e, WrapperTensorWithCustomSizes) else e
def wrap(e):
return WrapperTensorWithCustomSizes(e) if isinstance(e, torch.Tensor) else e
rs = tree_map(wrap, func(*tree_map(unwrap, args), **tree_map(unwrap, kwargs or {})))
return rs
def __repr__(self):
return super().__repr__(tensor_contents=f"t={self.t}")
class WrapperTensorWithCustomStrides(WrapperTensor):
@classmethod
def get_wrapper_properties(cls, t, requires_grad=False):
return t, {"requires_grad": requires_grad, "dispatch_sizes_strides_policy": "strides"}
def __init__(self, t, requires_grad=False):
self.t = t
@classmethod
def __torch_dispatch__(cls, func, types, args=(), kwargs=None):
if not all(issubclass(cls, t) for t in types):
return NotImplemented
if kwargs is None:
kwargs = {}
def unwrap(e):
return e.t if isinstance(e, WrapperTensorWithCustomStrides) else e
def wrap(e):
return WrapperTensorWithCustomStrides(e) if isinstance(e, torch.Tensor) else e
rs = tree_map(wrap, func(*tree_map(unwrap, args), **tree_map(unwrap, kwargs or {})))
return rs
def __repr__(self):
return super().__repr__(tensor_contents=f"t={self.t}")
class DiagTensorBelow(WrapperTensor):
@classmethod
def get_wrapper_properties(cls, diag, requires_grad=False):
if diag.ndim != 1:
raise AssertionError(f"Expected diag.ndim == 1, got {diag.ndim}")
return diag, {"size": diag.size() + diag.size(), "requires_grad": requires_grad}
def __init__(self, diag, requires_grad=False):
self.diag = diag
handled_ops = {}
@classmethod
def __torch_dispatch__(cls, func, types, args=(), kwargs=None):
if not all(issubclass(cls, t) for t in types):
return NotImplemented
# For everything else, call the handler:
fn = cls.handled_ops.get(func.__name__, None)
if fn:
return fn(*args, **(kwargs or {}))
else:
# Note that here, because we don't need to provide the autograd formulas
# we can have a default "fallback" that creates a plain Tensor based
# on the diag elements and calls the func again.
def unwrap(e):
return e.diag.diag() if isinstance(e, DiagTensorBelow) else e
def wrap(e):
if isinstance(e, torch.Tensor) and e.ndim == 1:
return DiagTensorBelow(e)
if isinstance(e, torch.Tensor) and e.ndim == 2 and e.count_nonzero() == e.diag().count_nonzero():
return DiagTensorBelow(e.diag())
return e
rs = tree_map(wrap, func(*tree_map(unwrap, args), **tree_map(unwrap, kwargs or {})))
return rs
def __repr__(self):
return super().__repr__(tensor_contents=f"diag={self.diag}")
class SparseTensor(WrapperTensor):
@classmethod
def get_wrapper_properties(cls, size, values, indices, requires_grad=False):
if values.device != indices.device:
raise AssertionError(f"Expected values.device == indices.device, got {values.device} != {indices.device}")
return values, {"size": size, "requires_grad": requires_grad}
def __init__(self, size, values, indices, requires_grad=False):
self.values = values
self.indices = indices
def __repr__(self):
return super().__repr__(tensor_contents=f"values={self.values}, indices={self.indices}")
def sparse_to_dense(self):
res = torch.zeros(self.size(), dtype=self.values.dtype)
res[self.indices.unbind(1)] = self.values
return res
@staticmethod
def from_dense(t):
indices = t.nonzero()
values = t[indices.unbind(1)]
return SparseTensor(t.size(), values, indices)
@classmethod
def __torch_dispatch__(cls, func, types, args=(), kwargs=None):
func_name = f"{func.__module__}.{func.__name__}"
res = cls._try_call_special_impl(func_name, args, kwargs)
if res is not NotImplemented:
return res
# Otherwise, use a default implementation that construct dense
# tensors and use that to compute values
def unwrap(e):
return e.sparse_to_dense() if isinstance(e, SparseTensor) else e
# Wrap back all Tensors into our custom class
def wrap(e):
# Check for zeros and use that to get indices
return SparseTensor.from_dense(e) if isinstance(e, torch.Tensor) else e
rs = tree_map(wrap, func(*tree_map(unwrap, args), **tree_map(unwrap, kwargs or {})))
return rs
_SPECIAL_IMPLS = {}
@classmethod
def _try_call_special_impl(cls, func, args, kwargs):
if func not in cls._SPECIAL_IMPLS:
return NotImplemented
return cls._SPECIAL_IMPLS[func](args, kwargs)
# Example non-wrapper subclass that stores extra state.
class NonWrapperTensor(torch.Tensor):
def __new__(cls, data):
t = torch.Tensor._make_subclass(cls, data)
t.extra_state = {
'last_func_called': None
}
return t
@classmethod
def __torch_function__(cls, func, types, args=(), kwargs=None):
result = super().__torch_function__(func, types, args, kwargs)
if isinstance(result, cls):
# Do something with the extra state. For the example here, just store the name of the
# last function called (skip for deepcopy so the copy has the same extra state).
if func is torch.Tensor.__deepcopy__:
result.extra_state = deepcopy(args[0].extra_state)
else:
result.extra_state = {
'last_func_called': func.__name__,
}
return result
# new_empty() must be defined for deepcopy to work
def new_empty(self, shape):
return type(self)(torch.empty(shape))
# Class used to store info about subclass tensors used in testing.
class SubclassInfo:
__slots__ = ['name', 'create_fn', 'closed_under_ops']
def __init__(self, name, create_fn, closed_under_ops=True):
self.name = name
self.create_fn = create_fn # create_fn(shape) -> tensor instance
self.closed_under_ops = closed_under_ops
# Helper function to create a subclass of the given class and possibly cache sizes / strides.
def _create_and_access_shape(cls, shape):
sub = cls(torch.randn(shape))
# NB: Wrapper subclasses with custom dispatched sizes / strides cache this info
# on the first call via non-serializable PyCapsules. We purposefully trigger cache
# population here for serialization / deepcopy tests to verify that the presence of this
# cache info doesn't cause problems.
sub.size()
sub.stride()
return sub
subclass_db = {
torch.Tensor: SubclassInfo(
'base_tensor', create_fn=torch.randn
),
NonWrapperTensor: SubclassInfo(
'non_wrapper_tensor',
create_fn=lambda shape: NonWrapperTensor(torch.randn(shape))
),
LoggingTensor: SubclassInfo(
'logging_tensor',
create_fn=lambda shape: LoggingTensor(torch.randn(shape))
),
SparseTensor: SubclassInfo(
'sparse_tensor',
create_fn=lambda shape: SparseTensor.from_dense(torch.randn(shape).relu())
),
DiagTensorBelow: SubclassInfo(
'diag_tensor_below',
create_fn=lambda shape: DiagTensorBelow(torch.randn(shape)),
closed_under_ops=False # sparse semantics
),
WrapperTensorWithCustomSizes: SubclassInfo(
'wrapper_with_custom_sizes',
create_fn=lambda shape: _create_and_access_shape(WrapperTensorWithCustomSizes, shape),
closed_under_ops=False,
),
WrapperTensorWithCustomStrides: SubclassInfo(
'wrapper_with_custom_strides',
create_fn=lambda shape: _create_and_access_shape(WrapperTensorWithCustomStrides, shape),
closed_under_ops=False,
),
}
class SubclassWithTensorFactory(torch.Tensor):
@staticmethod
def __new__(cls, src):
shape = src.shape
kwargs = {}
kwargs["strides"] = src.stride()
kwargs["storage_offset"] = src.storage_offset()
kwargs["device"] = src.device
kwargs["layout"] = src.layout
kwargs["requires_grad"] = src.requires_grad
kwargs["dtype"] = src.dtype
out = torch.Tensor._make_wrapper_subclass(cls, shape, **kwargs)
return out
def __init__(self, src):
self.src = src
def __repr__(self):
return f"{self.__class__.__name__}"
def __tensor_flatten__(self):
return ["src"], None
@classmethod
def __tensor_unflatten__(cls, inner_tensors, meta, outer_size, outer_stride):
src = inner_tensors["src"]
return cls(src)
@classmethod
def __torch_dispatch__(cls, func, types, args, kwargs):
if kwargs is None:
kwargs = {}
def _fn(x):
return x.src * torch.ones(x.src.shape) if x.src.dtype == torch.float32 else x.src
_args = pytree.tree_map_only(cls, _fn, args)
_kwargs = pytree.tree_map_only(cls, _fn, kwargs)
_out = func(*_args, **_kwargs)
_out_flat, _out_spec = pytree.tree_flatten(_out)
out_flat = [cls(o) if isinstance(o, torch.Tensor) else o for o in _out_flat]
return pytree.tree_unflatten(out_flat, _out_spec)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,88 @@
import enum
import functools
import torch
import torch.xpu
from torch.testing._internal.common_utils import IS_WINDOWS, LazyVal, TEST_XPU
XPU_ALREADY_INITIALIZED_ON_IMPORT = torch.xpu.is_initialized()
class XPUCodename(enum.Enum):
PVC = "PVC" # Intel® Data Center GPU Max Series
BMG = "BMG" # Intel® Arc™ Pro Battlemage Graphics
class XPUArch(enum.IntEnum):
Unknown = 0
Xe = 1 # Xe HPC
Xe2 = 2
# device_id -> GPU codename
# From https://github.com/intel/intel-graphics-compiler/blob/master/inc/common/igfxfmid.h
_DEVICE_ID_TO_CODENAME = {
0x0BD0: XPUCodename.PVC,
0x0BD4: XPUCodename.PVC,
0x0BD5: XPUCodename.PVC,
0x0BD6: XPUCodename.PVC,
0x0BD7: XPUCodename.PVC,
0x0BD8: XPUCodename.PVC,
0x0BD9: XPUCodename.PVC,
0x0BDA: XPUCodename.PVC,
0x0BDB: XPUCodename.PVC,
0x0B69: XPUCodename.PVC,
0x0B6E: XPUCodename.PVC,
0xE202: XPUCodename.BMG,
0xE20B: XPUCodename.BMG,
0xE20C: XPUCodename.BMG,
0xE20D: XPUCodename.BMG,
0xE210: XPUCodename.BMG,
0xE212: XPUCodename.BMG,
0xE215: XPUCodename.BMG,
0xE216: XPUCodename.BMG,
0xE220: XPUCodename.BMG,
0xE221: XPUCodename.BMG,
0xE222: XPUCodename.BMG,
0xE223: XPUCodename.BMG,
}
# GPU codename -> architecture
_CODENAME_TO_ARCH = {
XPUCodename.PVC: XPUArch.Xe,
XPUCodename.BMG: XPUArch.Xe2,
}
@functools.lru_cache(1)
def get_xpu_codename() -> XPUCodename | None:
device_id = torch.xpu.get_device_capability()["device_id"]
return _DEVICE_ID_TO_CODENAME.get(device_id)
@functools.lru_cache(1)
def get_xpu_arch() -> XPUArch | None:
codename = get_xpu_codename()
return _CODENAME_TO_ARCH.get(codename, XPUArch.Unknown)
Xe2_Or_Later = LazyVal(
lambda: torch.xpu.is_available() and get_xpu_arch() >= XPUArch.Xe2
)
def evaluate_platform_supports_flash_attention():
if TEST_XPU:
return not IS_WINDOWS and Xe2_Or_Later
return False
PLATFORM_SUPPORTS_FLASH_ATTENTION_XPU: bool = LazyVal(
lambda: evaluate_platform_supports_flash_attention()
)
# Importing this module should NOT eagerly initialize XPU
if not XPU_ALREADY_INITIALIZED_ON_IMPORT:
if torch.xpu.is_initialized():
raise AssertionError("XPU should not be initialized on import")
@@ -0,0 +1,616 @@
# mypy: ignore-errors
import torch
from torch import Tensor
import itertools
from torch.utils._python_dispatch import TorchDispatchMode
from torch.utils._pytree import tree_map, tree_flatten, tree_unflatten
from torch.utils import _pytree as pytree
from functools import partial
from torch.utils._mode_utils import no_dispatch, all_same_mode
import torch.autograd.forward_ad as fwAD
from collections.abc import Callable
import re
def check_attr_consistency(wrapper_tensor, metadata_name, metadata_accessor):
elem = wrapper_tensor.elem
metadata_wrapper_tensor = metadata_accessor(wrapper_tensor)
metadata_elem = metadata_accessor(elem)
if metadata_wrapper_tensor == metadata_elem:
return
raise RuntimeError(
f"This operator is not Composite Compliant: the "
f"{metadata_name} of the tensor was modified directly without "
f"going through the PyTorch dispatcher.")
def check_metadata_consistency(wrapper_tensor, CCT):
# CCT: CompositeCompliantTensor class which is generated using generate_cct
if not isinstance(wrapper_tensor, CCT):
return
things_to_check = {
'shape': Tensor.size,
'dtype': lambda x: x.dtype,
'device': lambda x: x.device,
'numel': Tensor.numel,
'stride': Tensor.stride,
'storage_offset': Tensor.storage_offset,
}
for metadata_name, metadata_accessor in things_to_check.items():
check_attr_consistency(wrapper_tensor, metadata_name, metadata_accessor)
def is_view_fn(func):
return func.overloadpacket.__name__ in {
'as_strided',
'detach',
'diagonal',
'expand',
'expand_as',
'movedim',
'narrow',
'permute',
'select',
'squeeze',
'transpose',
't',
'real',
'imag',
'view_as_real',
'view_as_complex',
'unflatten',
'unfold',
'unsqueeze',
'view',
'view_as',
'unbind',
'split',
'split_with_sizes',
'vsplit',
'hsplit',
'tensor_split',
'chunk',
'swapaxes',
'slice',
'_reshape_alias',
'_unsafe_view',
'_conj',
'alias',
}
# manually populated from native_functions that have inplace_view: True.
# In the future we will probably be able to grab that list directly
def is_inplace_view_fn(func):
return func.overloadpacket.__name__ in {
'as_strided_',
'detach_',
'squeeze_',
'swapaxes_',
'swapdims_',
't_',
'transpose_',
'unsqueeze_',
}
# Introspection please save us
def is_inplace(func):
name = func.overloadpacket.__name__
if re.match('__i.+__', name):
return True
if re.match('__.+__', name):
return False
return name[-1] == '_'
def generate_cct_and_mode(autograd_view_consistency=True):
# This function returns a new class CompositeCompliantTensor
# The two arguments control the behaviour described below.
# autograd_view_consistency:
# If True, alias result using `set_` if func returns a view
# (See Note [Alias Result]).
# Since Forward AD doesn't work with `set_`
# we disable it by setting alias to False.
class CompositeCompliantTensor(torch.Tensor):
elem: torch.Tensor
__slots__ = ['elem']
@staticmethod
def __new__(cls, elem, mode, *args, **kwargs):
if type(elem) is cls:
raise AssertionError(
"Wrapping a CompositeCompliantTensor in a CompositeCompliantTensor is not supported"
)
# The storage of CompositeCompliantTensor should never be used directly
# by a Composite operation; if the Composite
# operator attempts to read from the storage without dispatching then it'll
# raise a RuntimeError due to it being a meta storage.
r = torch.Tensor._make_wrapper_subclass(
cls, elem.size(),
dtype=elem.dtype, layout=elem.layout,
device=elem.device, requires_grad=elem.requires_grad,
strides=elem.stride(), storage_offset=elem.storage_offset())
if elem.requires_grad:
# CompositeCompliantTensor steals the "requires_grad"-ness.
# Why a new copy of `elem`? Because sometimes OpInfo shares inputs between tests...
tmp = torch.empty(
(),
dtype=elem.dtype,
device=elem.device,
layout=elem.layout,
requires_grad=False,
)
# Use set_ rather than empty_strided() + copy_ so that we can preserve
# things like storage_offset.
tmp.set_(
source=elem.untyped_storage().clone(),
storage_offset=elem.storage_offset(),
size=elem.size(),
stride=elem.stride(),
)
r.elem = tmp
else:
r.elem = elem
if r.stride() != r.elem.stride():
raise AssertionError(f"Expected r.stride() == r.elem.stride(), got {r.stride()} != {r.elem.stride()}")
# Propagate conjugate bits to the wrapper tensor
# Ref: https://github.com/albanD/subclass_zoo/issues/24
# Ref: https://github.com/albanD/subclass_zoo/issues/21
torch._C._set_conj(r, r.elem.is_conj())
torch._C._set_neg(r, r.elem.is_neg())
r.mode = mode
return r
def __repr__(self):
return f"CompositeCompliantTensor({self.elem})"
@classmethod
def __torch_dispatch__(cls, func, types, args=(), kwargs=None):
all_args = pytree.arg_tree_leaves(*args, **(kwargs or {}))
modes = tuple(e.mode for e in all_args if isinstance(e, CompositeCompliantTensor))
if not all_same_mode(modes):
raise RuntimeError("Multiple CompositeCompliantTensorModes NYI")
with modes[0]:
return func(*args, **kwargs)
class CompositeCompliantTensorMode(TorchDispatchMode):
def __torch_dispatch__(self, func, types, args=(), kwargs=None):
def unwrap(e):
return e.elem if isinstance(e, CompositeCompliantTensor) else e
def wrap(e):
return CompositeCompliantTensor(e, self) if isinstance(e, torch.Tensor) else e
if func is torch.ops.aten._local_scalar_dense.default:
raise RuntimeError(
".item() is not allowed to be called inside of composite "
"functions in the PyTorch library because not all backends "
"and/or Tensor subclasses (e.g. vmap, ProxyTensor) support them.")
if func.overloadpacket.__name__ in ('set_', 'resize_'):
raise RuntimeError(
f"{func.__name__} is not allowed to be called inside of "
f"Composite operators.")
if is_inplace(func):
# NB: We are making an assumption that if the function is in-place,
# then the first argument is being written to. Introspection please save us!
mutated_argument = args[0]
if not isinstance(mutated_argument, CompositeCompliantTensor) and \
any(isinstance(a, CompositeCompliantTensor) for a in args[1:]):
raise RuntimeError(
'Not composite compliant: performing in-place operation '
f'{func.__name__} where the Tensor being written to is '
'regular Tensor but the other tensors are Tensor Subclasses. '
'Please try to avoid this in-place operation.')
unwrapped_args = tree_map(unwrap, args)
unwrapped_kwargs = tree_map(unwrap, kwargs)
unwrapped_rs = func(*unwrapped_args, **unwrapped_kwargs)
rs = tree_map(wrap, unwrapped_rs)
if is_view_fn(func) and autograd_view_consistency:
# Note [Alias Result]
# Autograd asserts that for B = A.view_fn(...), B and A's storages
# are the same. Here we try to make B alias A to avoid those asserts.
# See https://github.com/pytorch/pytorch/issues/65339 for more information
# about the issue.
with no_dispatch():
# Idea: this is a weird way of getting a storage that aliases the input.
# This is a workaround for #65339.
# 1. under no_dispatch, all of the wrapper tensors look like regular
# tensors with special storage (the storage is nullptr and
# advertises CPU/CUDA device.
# 2. we run func, which ends up running the view operation
# 3. All view operations reuse the input's storage and return
# result Tensor(s) with new sizes/strides/offset that alias
# the input.
# 4. we set the storage (and sizes/strides/offset) of the wrapper
# tensor results to be that of the tensors that alias the input
result = func(*args, **kwargs)
if isinstance(result, (tuple, list)):
for a, b in zip(rs, result, strict=True):
a.set_(b)
else:
rs.set_(result)
# Some operations are allowed to in-place modify the metadata of the
# inputs. The only ones are the "inplace view functions"; when we
# run into these, we manually modify the metadata of the input.
with no_dispatch():
if is_inplace_view_fn(func):
func(*args, **kwargs)
# For each CompositeCompliantTensor t, we check that t and t.elem
# have consistent metadata. If they don't have consistent metadata,
# that means the operator did something fishy.
check = partial(check_metadata_consistency, CCT=CompositeCompliantTensor)
pytree.tree_map_(check, args)
pytree.tree_map_(check, kwargs)
pytree.tree_map_(check, rs)
return rs
return CompositeCompliantTensor, CompositeCompliantTensorMode()
def is_tensorlist(lst):
if not isinstance(lst, list) and not isinstance(lst, tuple):
return False
if len(lst) == 0:
return False
all_tensors = all(isinstance(elt, torch.Tensor) for elt in lst)
if all_tensors:
return True
exists_one_tensor = all(isinstance(elt, torch.Tensor) for elt in lst)
if exists_one_tensor:
raise RuntimeError('This test assumes that PyTorch APIs cannot take '
'mixed lists of Tensor and other things')
return False
def maybe_map(fn, should_map, arg):
return fn(arg) if should_map else arg
def wrap(arg, CCT, cct_mode):
# CCT: CompositeCompliantTensor class which is generated using generate_cct_and_mode
if isinstance(arg, torch.Tensor):
return CCT(arg, cct_mode)
if is_tensorlist(arg):
return [CCT(a, cct_mode) for a in arg]
raise RuntimeError("wrap assumes that the input can be wrapped")
# Given a list of flat arguments, some of which may be Tensors, return all
# possible ways some of the arguments could be CompositeCompliantTensors (CCT).
# For example, given Tensors A, B, C and flat_args = [A, 1, B],
# We would return the following 4 options:
# [CCT(A), 1, CCT(B)]
# [CCT(A), 1, B]
# [A, 1, CCT(B)]
# [A, 1, B]
# NB: Yes, this is exponential. No, we don't care too much because PyTorch ops
# don't accept that many input Tensors.
def generate_subclass_choices(flat_args, CCT, cct_mode):
# CCT: CompositeCompliantTensor class which is generated using generate_cct_and_mode
is_tensor_likes = [isinstance(arg, torch.Tensor) or is_tensorlist(arg) for arg in flat_args]
subclass_options = [[False, True] if is_tensor_like else [False] for is_tensor_like in is_tensor_likes]
for which_args_are_wrapped in itertools.product(*subclass_options):
result = [maybe_map(partial(wrap, CCT=CCT, cct_mode=cct_mode), should_wrap_arg, arg)
for should_wrap_arg, arg in zip(which_args_are_wrapped, flat_args, strict=True)]
yield result, which_args_are_wrapped
# For an operation f(*args, **kwargs), each Tensor argument may either be
# a regular Tensor or a Tensor Subclass. This iterator iterates through
# all of those options.
def generate_subclass_choices_args_kwargs(args, kwargs, CCT, cct_mode):
# CCT: CompositeCompliantTensor class which is generated using generate_cct_and_mode
flat_kwargs, spec = tree_flatten(kwargs)
flat_args_kwargs = list(args) + list(flat_kwargs)
for choice, debug_metadata in generate_subclass_choices(flat_args_kwargs, CCT, cct_mode):
new_args = choice[:len(args)]
new_kwargs = tree_unflatten(choice[len(args):], spec)
which_args_are_wrapped = debug_metadata[:len(args)]
which_kwargs_are_wrapped = tree_unflatten(debug_metadata[len(args):], spec)
yield new_args, new_kwargs, which_args_are_wrapped, which_kwargs_are_wrapped
def raise_composite_compliance_error(err, additional_info=''):
raise RuntimeError(
"Composite compliance check failed with "
"the above error.\n"
f"{additional_info}"
"If you are adding an OpInfo of an "
"existing operator, please feel free to skip this test "
"because the problem was pre-existing and file an issue. "
"Otherwise, if you added a new operator, please read "
"through the Composite Compliance section in "
"aten/src/ATen/native/README.md for how to resolve this. "
) from err
# This test checks ALL possible permutations of calling `op` with arguments
# that are individually either a regular Tensor or a Tensor subclass.
#
# The general strategy is to wrap some Tensor args and kwargs in
# CompositeCompliantTensor wrappers and call the operation.
# If some composite operation does any non-compliant behavior,
# CompositeCompliantTensor will raise an error.
def check_all_permutations(op, args, kwargs, assert_equal_fn):
CCT, cct_mode = generate_cct_and_mode()
expected = op(*args, **kwargs)
for choice in generate_subclass_choices_args_kwargs(args, kwargs, CCT, cct_mode):
new_args, new_kwargs, which_args_are_wrapped, which_kwargs_are_wrapped = choice
try:
actual = op(*new_args, **new_kwargs)
# NOTE: [What errors are Composite Compliance trying to catch?]
#
# There's two things we want to catch:
# - errors that would raise within the torch_dispatch impl
# - data_ptr accesses
# The first is easy to filter for (we could make the error a different
# error class), the second is always going to be a RuntimeError due to
# how it is implemented (if you try to access the data_ptr of the
# wrapper Tensor, it raises you some internal RuntimeError).
#
# So the most general thing to catch here was RuntimeError. If you
# are here and debugging why your test failed, it's plausible that
# the operator itself is broken and that there are other tests failing.
except RuntimeError as err:
raise_composite_compliance_error(
err,
f"- wrapped_args: {which_args_are_wrapped}\n"
f"- wrapped_kwargs: {which_kwargs_are_wrapped}\n"
)
def unwrap(e):
return e.elem if isinstance(e, CCT) else e
assert_equal_fn(tree_map(unwrap, actual), expected)
# Checks via the usage of torch dispatch mode certain anti-patterns that
# are not composite compliant.
#
# In particular, the anti-pattern we are trying to prevent is a user
# creating an empty tensor and then resize_-ing it. Torch Dispatch Mode helps
# here because all factory functions will create tensors that are
# CompositeCompliantTensor.
#
# The general strategy is to wrap all Tensor args and kwargs in
# CompositeCompliantTensor wrappers. If an operator that is
# Composite does any non-compliant behavior,
# CompositeCompliantTensor will raise an error.
def check_with_mode(op, args, kwargs, assert_equal_fn):
CCT, cct_mode = generate_cct_and_mode()
def wrap(e):
return CCT(e, cct_mode) if isinstance(e, torch.Tensor) else e
expected = op(*args, **kwargs)
args = tree_map(wrap, args)
kwargs = tree_map(wrap, kwargs)
try:
with cct_mode:
actual = op(*args, **kwargs)
# see NOTE: [What errors are Composite Compliance trying to catch?]
except RuntimeError as err:
raise_composite_compliance_error(err)
def unwrap(e):
return e.elem if isinstance(e, CCT) else e
assert_equal_fn(tree_map(unwrap, actual), expected)
def gather_leaf_tensors(args, kwargs):
leaf_tensors = []
args, _args_spec = tree_flatten(args)
kwargs, _kwargs_spec = tree_flatten(kwargs)
args = args + kwargs
for arg in args:
if not isinstance(arg, torch.Tensor):
continue
if arg.requires_grad:
leaf_tensors.append(arg)
return leaf_tensors
def compute_expected_grads(op, args, kwargs, output_process_fn_grad=None, gradcheck_wrapper=None):
if gradcheck_wrapper is None:
results = op(*args, **kwargs)
else:
results = gradcheck_wrapper(op, *args, **kwargs)
if output_process_fn_grad is not None:
results = output_process_fn_grad(results)
flat_results = pytree.tree_leaves(results)
flat_results = [r for r in flat_results if isinstance(r, torch.Tensor)]
flat_diff_results = [r for r in flat_results if r.requires_grad]
if len(flat_diff_results) <= 0:
raise AssertionError("Expected len(flat_diff_results) > 0")
grads = [torch.ones(r.shape, device=r.device, dtype=r.dtype) for r in flat_diff_results]
leaf_tensors = gather_leaf_tensors(args, kwargs)
if len(leaf_tensors) <= 0:
raise AssertionError("Expected len(leaf_tensors) > 0")
return torch.autograd.grad(flat_diff_results, leaf_tensors,
grads, allow_unused=True, retain_graph=True)
# Checks if the backward formula is composite compliant by testing
# all possible permutations of {inputs, grad_outputs} being
# CompositeCompliantTensor or regular Tensors.
#
# NB: it is important that op is accepted as a Callable and not an OpInfo,
# this means we can apply check_backward_formula to things that aren't OpInfos
# while debugging.
def check_backward_formula(op: Callable, args, kwargs,
output_process_fn_grad=None,
gradcheck_wrapper=None, assert_equal_fn=None):
CCT, cct_mode = generate_cct_and_mode()
expected = compute_expected_grads(op, args, kwargs, output_process_fn_grad, gradcheck_wrapper)
for choice in generate_subclass_choices_args_kwargs(args, kwargs, CCT, cct_mode):
new_args, new_kwargs, which_args_are_wrapped, which_kwargs_are_wrapped = choice
leaf_tensors = gather_leaf_tensors(new_args, new_kwargs)
if len(leaf_tensors) <= 0:
raise AssertionError("Expected len(leaf_tensors) > 0")
try:
if gradcheck_wrapper is None:
results = op(*new_args, **new_kwargs)
else:
results = gradcheck_wrapper(op, *new_args, **new_kwargs)
if output_process_fn_grad is not None:
results = output_process_fn_grad(results)
# see NOTE: [What errors are Composite Compliance trying to catch?]
except RuntimeError as err:
raise_composite_compliance_error(
err,
f"- wrapped_args: {which_args_are_wrapped}\n"
f"- wrapped_kwargs: {which_kwargs_are_wrapped}\n"
)
flat_results = pytree.tree_leaves(results)
flat_results = [r for r in flat_results if isinstance(r, torch.Tensor)]
flat_diff_results = [r for r in flat_results if r.requires_grad]
if len(flat_diff_results) <= 0:
raise AssertionError("Expected len(flat_diff_results) > 0")
# NB: ones, not ones_like, so we get a regular Tensor here
grads = [torch.ones(r.shape, device=r.device, dtype=r.dtype)
for r in flat_diff_results]
for flat_new_grads, which_grad_is_batched in generate_subclass_choices(grads, CCT, cct_mode):
try:
actual = torch.autograd.grad(flat_diff_results, leaf_tensors, flat_new_grads,
allow_unused=True, retain_graph=True)
# see NOTE: [What errors are Composite Compliance trying to catch?]
except RuntimeError as err:
raise_composite_compliance_error(
err,
f"- wrapped_args: {which_args_are_wrapped}\n"
f"- wrapped_kwargs: {which_kwargs_are_wrapped}\n"
f"- wrapped_grads: {which_grad_is_batched}\n"
)
def unwrap(e):
return e.elem if isinstance(e, CCT) else e
assert_equal_fn(tuple(map(unwrap, actual)), expected, equal_nan=True)
# Checks if the forward AD formula is composite compliant by testing
# all possible permutations of {primals, tangents} being
# CompositeCompliantTensor or regular Tensors.
#
# NB: it is important that op is accepted as a Callable and not an OpInfo,
# this means we can apply check_forward_ad_formula to things that aren't OpInfos
# while debugging.
def check_forward_ad_formula(op: Callable, args, kwargs, gradcheck_wrapper=None, assert_equal_fn=None):
CCT, cct_mode = generate_cct_and_mode(autograd_view_consistency=False)
def maybe_tangent(t):
if type(t) is CCT:
raise AssertionError("Expected type(t) is not CCT")
# Generate `tangent` tensor
# if given object is a Tensor and requires grad is set.
if isinstance(t, torch.Tensor) and t.requires_grad:
return torch.randn_like(t)
elif is_tensorlist(t):
return [torch.randn_like(e) if e.requires_grad else None for e in t]
return None
tangent_args = tuple(maybe_tangent(arg) for arg in args)
flat_kwargs, spec = tree_flatten(kwargs)
flat_tangent_kwargs = tuple(maybe_tangent(arg) for arg in flat_kwargs)
tangent_kwargs = tree_unflatten(flat_tangent_kwargs, spec)
with fwAD.dual_level():
def maybe_make_dual(dual):
# Returns dual tensor if primal is a tensor/tensor subclass
# with requires_grad set.
primal, tangent = dual
if isinstance(primal, torch.Tensor) and primal.requires_grad:
return fwAD.make_dual(primal.detach(), tangent)
elif is_tensorlist(primal):
return tuple(fwAD.make_dual(pri.detach(), tang) if tang is not None else pri
for pri, tang in zip(primal, tangent, strict=True))
return primal
def compute_expected_grad(args, tangent_args, kwargs, tangent_kwargs):
op_args = tuple(map(maybe_make_dual, zip(args, tangent_args, strict=True)))
op_kwargs = {k: maybe_make_dual((v, tangent_kwargs[k])) for k, v in kwargs.items()}
if gradcheck_wrapper is None:
return op(*op_args, **op_kwargs)
return gradcheck_wrapper(op, *op_args, **op_kwargs)
expected = compute_expected_grad(args, tangent_args, kwargs, tangent_kwargs)
expected = tree_map(fwAD.unpack_dual, expected)
expected_primals = tree_map(
lambda x: x.primal,
expected,
is_leaf=lambda x: type(x) is fwAD.UnpackedDualTensor,
)
expected_tangents = tree_map(
lambda x: x.tangent,
expected,
is_leaf=lambda x: type(x) is fwAD.UnpackedDualTensor,
)
# Permutations of arg and kwargs in CCT.
for choice in generate_subclass_choices_args_kwargs(args, kwargs, CCT, cct_mode):
new_args, new_kwargs, which_args_are_wrapped, which_kwargs_are_wrapped = choice
# Permutations tangent arg and tangent kwargs in CCT.
for tang_choice in generate_subclass_choices_args_kwargs(tangent_args, tangent_kwargs, CCT, cct_mode):
new_tang_args, new_tang_kwargs, \
which_tang_args_are_wrapped, which_tang_kwargs_are_wrapped = tang_choice
op_args = tuple(map(maybe_make_dual, zip(new_args, new_tang_args, strict=True)))
op_kwargs = {k: maybe_make_dual((v, new_tang_kwargs[k])) for k, v in new_kwargs.items()}
try:
if gradcheck_wrapper is None:
actual = op(*op_args, **op_kwargs)
else:
actual = gradcheck_wrapper(op, *op_args, **op_kwargs)
# see NOTE: [What errors are Composite Compliance trying to catch?]
except RuntimeError as err:
raise_composite_compliance_error(
err,
f"- wrapped_args: {which_args_are_wrapped}\n"
f"- wrapped_kwargs: {which_kwargs_are_wrapped}\n"
f"- wrapped_tangent_args: {which_tang_args_are_wrapped}\n"
f"- wrapped_tangent_kwargs: {which_tang_kwargs_are_wrapped}\n"
)
def unwrap(e):
return e.elem if isinstance(e, CCT) else e
actual = tree_map(fwAD.unpack_dual, actual)
actual_primals = tree_map(
lambda x: unwrap(x.primal),
actual,
is_leaf=lambda x: type(x) is fwAD.UnpackedDualTensor,
)
actual_tangents = tree_map(
lambda x: unwrap(x.tangent),
actual,
is_leaf=lambda x: type(x) is fwAD.UnpackedDualTensor,
)
assert_equal_fn(actual_primals, expected_primals, equal_nan=True)
assert_equal_fn(actual_tangents, expected_tangents, equal_nan=True)
@@ -0,0 +1,603 @@
# mypy: allow-untyped-defs
import torch
import functools
from torch.testing import make_tensor
from torch.testing._internal.opinfo.core import (
OpInfo,
SampleInput,
)
from torch.testing._internal.common_dtype import all_types_and
import numpy as np
from torch.testing._internal.autograd_function_db import (
sample_inputs_numpy_cube,
sample_inputs_numpy_mul,
sample_inputs_numpy_mul_scalar,
sample_inputs_numpy_sort,
sample_inputs_numpy_take,
)
from torch import Tensor
from torch.types import Number
from typing import * # noqa: F403
# Note: [custom op db]
#
# This is a collection of custom operator test cases written as OpInfos
# so they can easily be consumed by OpInfo-based tests to check if subsystems
# support them correctly.
def to_numpy(tensor):
return tensor.cpu().numpy()
@torch.library.custom_op("_torch_testing::numpy_cube", mutates_args=())
def numpy_cube(x: Tensor) -> tuple[Tensor, Tensor]:
x_np = to_numpy(x)
dx = torch.tensor(3 * x_np ** 2, device=x.device)
return torch.tensor(x_np ** 3, device=x.device), dx
@numpy_cube.register_fake
def _(x):
return x.clone(), x.clone()
def numpy_cube_setup_context(ctx, inputs, output):
x, = inputs
_cube, dx = output
ctx.save_for_backward(x, dx)
def numpy_cube_backward(ctx, grad_out, grad_dx):
x, dx = ctx.saved_tensors
grad_x = numpy_mul(grad_out, dx) + 6 * numpy_mul(grad_dx, x)
return grad_x
numpy_cube.register_autograd(numpy_cube_backward, setup_context=numpy_cube_setup_context)
def numpy_cube_vmap(info, in_dims, x):
result = numpy_cube(x)
return result, (in_dims[0], in_dims[0])
numpy_cube.register_vmap(numpy_cube_vmap)
@torch.library.custom_op("_torch_testing::numpy_mul", mutates_args=())
def numpy_mul(x: Tensor, y: Tensor) -> Tensor:
return torch.tensor(to_numpy(x) * to_numpy(y), device=x.device)
@numpy_mul.register_fake
def _(x, y):
if x.device != y.device:
raise AssertionError(f"x.device={x.device} != y.device={y.device}")
return (x * y).contiguous()
def numpy_mul_setup_context(ctx, inputs, output):
ctx.save_for_backward(*inputs)
def numpy_mul_backward(ctx, grad_out):
x, y = ctx.saved_tensors
grad_x = grad_out * y if ctx.needs_input_grad[0] else None
grad_y = grad_out * x if ctx.needs_input_grad[1] else None
return grad_x, grad_y
numpy_mul.register_autograd(numpy_mul_backward, setup_context=numpy_mul_setup_context)
def numpy_mul_vmap(info, in_dims, x, y):
x_bdim, y_bdim = in_dims
x = x.movedim(x_bdim, -1) if x_bdim is not None else x.unsqueeze(-1)
y = y.movedim(y_bdim, -1) if y_bdim is not None else y.unsqueeze(-1)
result = x * y
result = result.movedim(-1, 0)
return result, 0
numpy_mul.register_vmap(numpy_mul_vmap)
@torch.library.custom_op("_torch_testing::numpy_mul_scalar", mutates_args=())
def numpy_mul_scalar(x: Tensor, *, scalar: float) -> Tensor:
return torch.tensor(to_numpy(x) * scalar, device=x.device)
@numpy_mul_scalar.register_fake
def _(x, *, scalar):
return (x * scalar).contiguous()
def numpy_mul_scalar_setup_context(ctx, inputs, keyword_only_inputs, output):
ctx.scalar = keyword_only_inputs["scalar"]
def numpy_mul_scalar_backward(ctx, grad_out):
grad_x = grad_out * ctx.scalar
return grad_x
numpy_mul_scalar.register_autograd(numpy_mul_scalar_backward, setup_context=numpy_mul_scalar_setup_context)
def numpy_mul_scalar_vmap(info, in_dims, x, *, scalar):
x_bdim, = in_dims
x = x.movedim(x_bdim, -1) if x_bdim is not None else x.unsqueeze(-1)
result = x * scalar
result = result.movedim(-1, 0)
return result, 0
numpy_mul_scalar.register_vmap(numpy_mul_scalar_vmap)
@torch.library.custom_op("_torch_testing::numpy_sort", mutates_args=())
def numpy_sort(x: Tensor, dim: int) -> tuple[Tensor, Tensor, Tensor]:
device = x.device
x = to_numpy(x)
ind = np.argsort(x, axis=dim)
ind_inv = np.argsort(ind, axis=dim)
result = np.take_along_axis(x, ind, axis=dim)
return (
torch.tensor(result, device=device),
torch.tensor(ind, device=device),
torch.tensor(ind_inv, device=device),
)
@numpy_sort.register_fake
def _(x, dim):
return torch.empty_like(x), torch.empty_like(x, dtype=torch.long), torch.empty_like(x, dtype=torch.long)
def numpy_sort_setup_context(ctx, inputs, output):
_out, ind, ind_inv = output
ctx.dim = inputs[1]
ctx.save_for_backward(ind, ind_inv)
ctx.mark_non_differentiable(ind, ind_inv)
def numpy_sort_backward(ctx, grad_out, grad_ind, grad_ind_inv):
ind, ind_inv = ctx.saved_tensors
return numpy_take(grad_out, ind_inv, ind, ctx.dim), None
numpy_sort.register_autograd(numpy_sort_backward, setup_context=numpy_sort_setup_context)
def numpy_sort_vmap(info, in_dims, x, dim):
x_bdim, _ = in_dims
x = x.movedim(x_bdim, 0)
dim = dim if dim >= 0 else dim + x.dim() - 1
result = numpy_sort(x, dim + 1)
return result, (0, 0, 0)
numpy_sort.register_vmap(numpy_sort_vmap)
@torch.library.custom_op("_torch_testing::numpy_take", mutates_args=())
def numpy_take(x: Tensor, ind: Tensor, ind_inv: Tensor, dim: int) -> Tensor:
device = x.device
x = to_numpy(x)
ind = to_numpy(ind)
return torch.tensor(np.take_along_axis(x, ind, dim), device=device)
@numpy_take.register_fake
def _(x, ind, ind_inv, dim):
if x.device != ind.device:
raise AssertionError(f"x.device={x.device} != ind.device={ind.device}")
if x.device != ind_inv.device:
raise AssertionError(f"x.device={x.device} != ind_inv.device={ind_inv.device}")
if ind.dtype != torch.long:
raise AssertionError(f"ind.dtype must be torch.long, got {ind.dtype}")
if ind_inv.dtype != torch.long:
raise AssertionError(f"ind_inv.dtype must be torch.long, got {ind_inv.dtype}")
return torch.empty_like(x)
def numpy_take_setup_context(ctx, inputs, output):
_x, ind, ind_inv, dim = inputs
ctx.dim = dim
ctx.save_for_backward(ind, ind_inv)
def numpy_take_backward(ctx, grad_out):
ind, ind_inv = ctx.saved_tensors
grad_x = numpy_take(grad_out, ind_inv, ind, ctx.dim)
return grad_x, None, None, None
numpy_take.register_autograd(numpy_take_backward, setup_context=numpy_take_setup_context)
def numpy_take_vmap(info, in_dims, x, ind, ind_inv, dim):
x_bdim, ind_bdim, ind_inv_bdim, _ = in_dims
# wrap dim
logical_dim = x.dim() if x_bdim is None else x_bdim - 1
dim = dim if dim >= 0 else dim + logical_dim
def expand_bdim(x, x_bdim):
if x_bdim is None:
return x.expand(info.batch_size, *x.shape)
return x.movedim(x_bdim, 0)
x = expand_bdim(x, x_bdim)
ind = expand_bdim(ind, ind_bdim)
ind_inv = expand_bdim(ind_inv, ind_inv_bdim)
return numpy_take(x, ind, ind_inv, dim + 1), 0
numpy_take.register_vmap(numpy_take_vmap)
@torch.library.custom_op("_torch_testing::numpy_nonzero", mutates_args=())
def numpy_nonzero(x: Tensor) -> Tensor:
x_np = to_numpy(x)
res = np.stack(np.nonzero(x_np), axis=1)
if res.shape[0] <= 1:
raise RuntimeError("not supported")
return torch.tensor(res, device=x.device)
@numpy_nonzero.register_fake
def _(x):
ctx = torch._custom_op.impl.get_ctx()
i0 = ctx.create_unbacked_symint()
shape = [i0, x.dim()]
result = x.new_empty(shape, dtype=torch.long)
return result
def sample_inputs_numpy_nonzero(opinfo, device, dtype, requires_grad, **kwargs):
make_arg = functools.partial(make_tensor, device=device, dtype=dtype, requires_grad=requires_grad)
shape = 10
result = make_arg(shape, low=0.9, high=2)
mask = make_tensor(shape, low=0, high=2, device=device, dtype=torch.long)
with torch.no_grad():
result *= mask
yield SampleInput(result, args=())
def numpy_nonzero_vmap(info, in_dims, x):
raise NotImplementedError("Operator is data-dependent and cannot be vmapped.")
numpy_nonzero.register_vmap(numpy_nonzero_vmap)
@torch.library.custom_op("_torch_testing::numpy_view_copy", mutates_args=())
def numpy_view_copy(x: Tensor, shape: Sequence[int]) -> Tensor:
return torch.tensor(np.copy(to_numpy(x).reshape(shape)), device=x.device)
@numpy_view_copy.register_fake
def _(x, shape) -> Tensor:
return x.clone().view(shape).clone()
def numpy_view_copy_setup_context(ctx, inputs, output) -> None:
ctx.x_shape = inputs[0].shape
def numpy_view_copy_backward(ctx, grad_out):
return torch.ops._torch_testing.numpy_view_copy(grad_out, ctx.x_shape), None
numpy_view_copy.register_autograd(numpy_view_copy_backward, setup_context=numpy_view_copy_setup_context)
def numpy_view_copy_vmap(info, in_dims, x, shape):
x_bdim, _ = in_dims
x = x.movedim(x_bdim, 0)
x_shape = x.shape[0]
batch_shape = (x_shape, *shape)
result = numpy_view_copy(x, batch_shape)
return result, 0
numpy_view_copy.register_vmap(numpy_view_copy_vmap)
def sample_inputs_numpy_view_copy(opinfo, device, dtype, requires_grad, **kwargs):
make_arg = functools.partial(make_tensor, device=device, dtype=dtype, requires_grad=requires_grad)
result = make_arg(2, 3, 4, low=0.9, high=2)
yield SampleInput(result, args=([2, 12],))
@torch.library.custom_op('_torch_testing::numpy_cat', mutates_args=())
def numpy_cat(xs: Sequence[Tensor], dim: int) -> Tensor:
if len(xs) == 0:
raise AssertionError("xs must not be empty")
if not all(x.device == xs[0].device for x in xs):
raise AssertionError("All tensors must be on the same device")
if not all(x.dtype == xs[0].dtype for x in xs):
raise AssertionError("All tensors must have the same dtype")
np_xs = [to_numpy(x) for x in xs]
np_out = np.concatenate(np_xs, axis=dim)
return torch.tensor(np_out, device=xs[0].device)
@numpy_cat.register_fake
def _(xs, dim):
if len(xs) == 0:
raise AssertionError("xs must not be empty")
if not all(x.device == xs[0].device for x in xs):
raise AssertionError("All tensors must be on the same device")
if not all(x.dtype == xs[0].dtype for x in xs):
raise AssertionError("All tensors must have the same dtype")
return torch.cat(xs, dim=dim)
def numpy_cat_setup_context(ctx, inputs, output):
xs, dim = inputs
ctx.dim_sizes = [x.shape[dim] for x in xs]
ctx.dim = dim
def numpy_cat_backward(ctx, grad_out):
dim_sizes = ctx.dim_sizes
dim = ctx.dim
splits = list(np.cumsum(dim_sizes)[:-1])
grad_xs = torch.ops._torch_testing.numpy_split_copy(grad_out, splits, dim)
return grad_xs, None
numpy_cat.register_autograd(numpy_cat_backward, setup_context=numpy_cat_setup_context)
def numpy_cat_vmap(info, in_dims, x, dim):
x_bdim, = in_dims
result = numpy_cat(x, dim)
return result, x_bdim
numpy_cat.register_vmap(numpy_cat_vmap)
def sample_inputs_numpy_cat(opinfo, device, dtype, requires_grad, **kwargs):
make_arg = functools.partial(make_tensor, device=device, dtype=dtype, requires_grad=requires_grad)
r0 = make_arg(2, 3, 4, low=0.9, high=2)
r1 = make_arg(4, 3, 4, low=0.9, high=2)
r2 = make_arg(5, 3, 4, low=0.9, high=2)
yield SampleInput([r0, r1, r2], args=(0,))
@torch.library.custom_op('_torch_testing::numpy_split_copy', mutates_args=())
def numpy_split_copy(x: Tensor, splits: Sequence[int], dim: int) -> List[Tensor]:
x_np = to_numpy(x)
arrs = np.split(x_np, splits, axis=dim)
return [torch.tensor(arr, device=x.device, dtype=x.dtype) for arr in arrs]
@numpy_split_copy.register_fake
def _(x, splits, dim):
return [xi.clone() for xi in torch.tensor_split(x, splits, dim)]
def numpy_split_copy_setup_context(ctx, inputs, output):
_, _, dim = inputs
ctx.dim = dim
def numpy_split_copy_backward(ctx, grad_out):
result = torch.ops._torch_testing.numpy_cat(grad_out, dim=ctx.dim)
return result, None, None
numpy_split_copy.register_autograd(numpy_split_copy_backward, setup_context=numpy_split_copy_setup_context)
def numpy_split_copy_vmap(info, in_dims, x, splits, dim):
x_bdim, _ , _ = in_dims
x = x.movedim(x_bdim, 0)
result = numpy_split_copy(x, splits, dim + 1)
return result, 0
numpy_split_copy.register_vmap(numpy_split_copy_vmap)
def sample_inputs_numpy_split_copy(opinfo, device, dtype, requires_grad, **kwargs):
make_arg = functools.partial(make_tensor, device=device, dtype=dtype, requires_grad=requires_grad)
x = make_arg(2, 9, low=0.9, high=2)
yield SampleInput(x, args=([1, 3, 6], 1))
@torch.library.custom_op('_torch_testing::numpy_split_copy_with_int', mutates_args=())
def numpy_split_copy_with_int(x: Tensor, splits: Sequence[int], dim: int) -> tuple[List[Tensor], int]:
x_np = to_numpy(x)
arrs = np.split(x_np, splits, axis=dim)
return [torch.tensor(arr, device=x.device, dtype=x.dtype) for arr in arrs], len(splits)
@numpy_split_copy_with_int.register_fake
def _(x, splits, dim):
return [xi.clone() for xi in torch.tensor_split(x, splits, dim)], len(splits)
def numpy_split_copy_with_int_setup_context(ctx, inputs, output):
_, _, dim = inputs
ctx.dim = dim
def numpy_split_copy_with_int_backward(ctx, grad_out, _):
return torch.ops._torch_testing.numpy_cat(grad_out, dim=ctx.dim), None, None
numpy_split_copy_with_int.register_autograd(
numpy_split_copy_with_int_backward,
setup_context=numpy_split_copy_with_int_setup_context)
def numpy_split_copy_with_int_vmap(info, in_dims, x, splits, dim):
x_bdim, _ , _ = in_dims
x = x.movedim(x_bdim, 0)
result, len_split = numpy_split_copy_with_int(x, splits, dim + 1)
return (result, len_split), ([0 for _ in range(len(result))], None)
numpy_split_copy_with_int.register_vmap(numpy_split_copy_with_int_vmap)
@torch.library.custom_op("_torch_testing::numpy_nms", mutates_args=())
def numpy_nms(boxes: Tensor, scores: Tensor, iou_threshold: Number) -> Tensor:
# Adapted from Ross Girshick's fast-rcnn implementation at
# https://github.com/rbgirshick/fast-rcnn/blob/master/lib/utils/nms.py
if boxes.device != scores.device:
raise AssertionError(f"boxes.device={boxes.device} != scores.device={scores.device}")
device = boxes.device
boxes = to_numpy(boxes)
scores = to_numpy(scores)
N = boxes.shape[0]
if boxes.shape != (N, 4):
raise AssertionError(f"boxes.shape must be (N, 4), got {boxes.shape}")
if scores.shape != (N,):
raise AssertionError(f"scores.shape must be (N,), got {scores.shape}")
x1 = boxes[:, 0]
y1 = boxes[:, 1]
x2 = boxes[:, 2]
y2 = boxes[:, 3]
areas = (x2 - x1 + 1) * (y2 - y1 + 1)
order = scores.argsort()[::-1]
keep = []
while order.size > 0:
i = order[0]
keep.append(i)
xx1 = np.maximum(x1[i], x1[order[1:]])
yy1 = np.maximum(y1[i], y1[order[1:]])
xx2 = np.minimum(x2[i], x2[order[1:]])
yy2 = np.minimum(y2[i], y2[order[1:]])
w = np.maximum(0.0, xx2 - xx1 + 1)
h = np.maximum(0.0, yy2 - yy1 + 1)
inter = w * h
ovr = inter / (areas[i] + areas[order[1:]] - inter)
inds = np.where(ovr <= iou_threshold)[0]
order = order[inds + 1]
result = torch.tensor(np.stack(keep), device=device)
# Needed for data-dependent condition :(
if result.size(0) < 2:
raise AssertionError(f"result.size(0) must be >= 2, got {result.size(0)}")
return result
@numpy_nms.register_fake
def _(boxes, scores, iou_threshold):
if boxes.device != scores.device:
raise AssertionError(f"boxes.device={boxes.device} != scores.device={scores.device}")
N = boxes.shape[0]
if boxes.shape != (N, 4):
raise AssertionError(f"boxes.shape must be (N, 4), got {boxes.shape}")
if scores.shape != (N,):
raise AssertionError(f"scores.shape must be (N,), got {scores.shape}")
ctx = torch._custom_op.impl.get_ctx()
i0 = ctx.create_unbacked_symint()
result = boxes.new_empty([i0], dtype=torch.int64)
return result
def numpy_nms_vmap(info, in_dims, boxes, scores, iou_threshold):
raise NotImplementedError("Operator is data-dependent and cannot be vmapped.")
numpy_nms.register_vmap(numpy_nms_vmap)
def sample_inputs_numpy_nms(opinfo, device, dtype, requires_grad, **kwargs):
make_arg = functools.partial(make_tensor, device=device, dtype=dtype)
N = 64
xs = make_arg([N], low=0, high=28)
dx = make_arg([N], low=0, high=4)
ys = make_arg([N], low=0, high=28)
dy = make_arg([N], low=0, high=4)
boxes = torch.stack([xs, ys, xs + dx, ys + dy], dim=1).requires_grad_(requires_grad)
scores = make_arg([N], low=0, high=1, requires_grad=requires_grad)
iou_threshold = make_arg([], low=0, high=1).item()
yield SampleInput(boxes, args=(scores, iou_threshold))
custom_op_db = [
OpInfo(
'NumpyCubeCustomOp',
op=numpy_cube._opoverload,
sample_inputs_func=sample_inputs_numpy_cube,
dtypes=all_types_and(torch.bool, torch.half),
supports_out=False,
),
OpInfo(
'NumpyMulCustomOp',
op=numpy_mul._opoverload,
sample_inputs_func=sample_inputs_numpy_mul,
dtypes=all_types_and(torch.bool, torch.half),
supports_out=False,
),
OpInfo(
'NumpyMulScalarCustomOp',
op=numpy_mul_scalar._opoverload,
sample_inputs_func=sample_inputs_numpy_mul_scalar,
dtypes=all_types_and(torch.bool, torch.half),
supports_out=False,
),
OpInfo(
'NumpySortCustomOp',
op=numpy_sort._opoverload,
sample_inputs_func=sample_inputs_numpy_sort,
dtypes=all_types_and(torch.bool, torch.half),
supports_out=False,
),
OpInfo(
'NumpyTakeCustomOp',
op=numpy_take._opoverload,
sample_inputs_func=sample_inputs_numpy_take,
dtypes=all_types_and(torch.bool, torch.half),
supports_out=False,
),
OpInfo(
'NumpyNonzeroCustomOp',
op=numpy_nonzero._opoverload,
sample_inputs_func=sample_inputs_numpy_nonzero,
dtypes=all_types_and(torch.bool, torch.half),
supports_autograd=False,
supports_out=False,
),
OpInfo(
'NumpyNMSCustomOp',
op=torch.ops._torch_testing.numpy_nms,
sample_inputs_func=sample_inputs_numpy_nms,
dtypes=all_types_and(torch.bool, torch.half),
supports_autograd=False,
supports_out=False,
),
OpInfo(
'NumpyViewCopyCustomOp',
op=torch.ops._torch_testing.numpy_view_copy,
sample_inputs_func=sample_inputs_numpy_view_copy,
dtypes=all_types_and(torch.bool, torch.half),
supports_autograd=True,
supports_out=False,
),
OpInfo(
'NumpyCatCustomOp',
op=torch.ops._torch_testing.numpy_cat,
sample_inputs_func=sample_inputs_numpy_cat,
dtypes=all_types_and(torch.bool, torch.half),
supports_autograd=True,
check_batched_grad=False,
check_batched_gradgrad=False,
supports_out=False,
),
OpInfo(
'NumpySplitCopyCustomOp',
op=torch.ops._torch_testing.numpy_split_copy,
sample_inputs_func=sample_inputs_numpy_split_copy,
dtypes=all_types_and(torch.bool, torch.half),
supports_autograd=True,
check_batched_grad=False,
check_batched_gradgrad=False,
supports_out=False,
),
OpInfo(
'NumpySplitCopyWithIntCustomOp',
op=torch.ops._torch_testing.numpy_split_copy_with_int,
sample_inputs_func=sample_inputs_numpy_split_copy,
dtypes=all_types_and(torch.bool, torch.half),
gradcheck_wrapper=lambda op, *args, **kwargs: op(*args, **kwargs)[0],
supports_autograd=True,
check_batched_grad=False,
check_batched_gradgrad=False,
supports_out=False,
),
]
# ==============================================================
# some mechanical test cases
# ==============================================================
lib = torch.library.Library("_torch_testing", "FRAGMENT") # noqa: TOR901
lib.define("source0(Tensor x) -> Tensor")
@torch.library.register_fake("_torch_testing::source0", lib=lib)
def _(x):
return x.clone()
lib.define("source1(Tensor x) -> Tensor")
def source1_fake(x):
return x.clone()
torch.library.register_fake("_torch_testing::source1", source1_fake, lib=lib)
lib.define("source2(Tensor x) -> Tensor")
@torch.library.register_fake("_torch_testing::source2", lib=lib)
def _(x):
return x.clone()
lib.define("source3(Tensor x) -> Tensor")
def source3_fake(x):
return x.clone()
torch.library.register_fake("_torch_testing::source3", source3_fake, lib=lib)
@torch.library.custom_op("_torch_testing::source4", mutates_args=())
def source4(x: Tensor) -> Tensor:
return x.clone()
@source4.register_fake
def _(x):
return x.clone()
@torch.library.custom_op("_torch_testing::source5", mutates_args=())
def source5(x: Tensor) -> Tensor:
return x.clone()
def source5_fake(x):
return x.clone()
source5.register_fake(source5_fake)
@@ -0,0 +1,161 @@
# mypy: ignore-errors
from collections import namedtuple
import torch
import torch.utils._pytree as pytree
from torch.utils._python_dispatch import return_and_correct_aliasing
FancyNamedTuple = namedtuple("FancyNamedTuple", ["foo", "bar"])
# A simple tensor subclass that holds a tensor with custom metadata and custom method
class ConstantExtraMetadataTensor(torch.Tensor):
@staticmethod
def __new__(cls, elem):
shape = elem.shape
kwargs = {}
kwargs["strides"] = elem.stride()
kwargs["storage_offset"] = elem.storage_offset()
kwargs["device"] = elem.device
kwargs["layout"] = elem.layout
kwargs["requires_grad"] = elem.requires_grad
kwargs["dtype"] = elem.dtype
return torch.Tensor._make_wrapper_subclass(cls, shape, **kwargs)
def __init__(self, elem):
self.elem = elem
self.constant_attribute = 4
def __repr__(self):
inner_repr = repr(self.elem)
return f"CustomTensor({inner_repr})"
def get_complicated_metadata(self):
return FancyNamedTuple(self.constant_attribute, self.constant_attribute)
def __tensor_flatten__(self):
return ["elem"], self.constant_attribute
def add_constant(self, a):
self.constant_attribute += a
@staticmethod
def __tensor_unflatten__(inner_tensors, meta, outer_size, outer_stride):
if meta is None:
raise AssertionError("Expected meta to not be None")
elem = inner_tensors["elem"]
out = ConstantExtraMetadataTensor(elem)
out.constant_attribute = meta
return out
@classmethod
def __torch_dispatch__(cls, func, types, args, kwargs):
if kwargs is None:
kwargs = {}
args_inner = pytree.tree_map_only(
ConstantExtraMetadataTensor, lambda x: x.elem, args
)
kwargs_inner = pytree.tree_map_only(
ConstantExtraMetadataTensor, lambda x: x.elem, kwargs
)
out_inner = func(*args_inner, **kwargs_inner)
out_inner_flat, spec = pytree.tree_flatten(out_inner)
# for aten ops that return non-tensors, just assume that
# our cust inner tensors return the same value
out_flat = [
ConstantExtraMetadataTensor(o_inner)
if isinstance(o_inner, torch.Tensor)
else o_inner
for o_inner in out_inner_flat
]
out = pytree.tree_unflatten(out_flat, spec)
return return_and_correct_aliasing(func, args, kwargs, out)
# A simple tensor subclass that always returns plain tensor during __torch_dispatch__
# It is similar to TwoTensor and is used to simulate torchao quantized tensors
class CustomTensorPlainOut(torch.Tensor):
@staticmethod
def __new__(cls, elem1, elem2):
shape = elem1.shape
kwargs = {}
kwargs["strides"] = elem1.stride()
kwargs["storage_offset"] = elem1.storage_offset()
kwargs["device"] = elem1.device
kwargs["layout"] = elem1.layout
kwargs["requires_grad"] = elem1.requires_grad
kwargs["dtype"] = elem1.dtype
return torch.Tensor._make_wrapper_subclass(cls, shape, **kwargs)
def __init__(self, elem1, elem2):
self.elem1 = elem1
self.elem2 = elem2
def get_elem(self):
return self.elem1
def __repr__(self):
inner_repr_1 = repr(self.elem1)
inner_repr_2 = repr(self.elem2)
return f"CustomTensorPlainOut({inner_repr_1}, {inner_repr_2})"
def __tensor_flatten__(self):
return ["elem1", "elem2"], None
@staticmethod
def __tensor_unflatten__(inner_tensors, meta, outer_size, outer_stride):
elem1 = inner_tensors["elem1"]
elem2 = inner_tensors["elem2"]
out = CustomTensorPlainOut(elem1, elem2)
return out
@classmethod
def __torch_dispatch__(cls, func, types, args, kwargs):
# Don't use this tensor with view ops
if kwargs is None:
kwargs = {}
args_inner_1 = pytree.tree_map_only(
CustomTensorPlainOut, lambda x: x.elem1, args
)
kwargs_inner_1 = pytree.tree_map_only(
CustomTensorPlainOut, lambda x: x.elem1, kwargs
)
args_inner_2 = pytree.tree_map_only(
CustomTensorPlainOut, lambda x: x.elem2, args
)
kwargs_inner_2 = pytree.tree_map_only(
CustomTensorPlainOut, lambda x: x.elem2, kwargs
)
out_inner_1 = func(*args_inner_1, **kwargs_inner_1)
out_inner_2 = func(*args_inner_2, **kwargs_inner_2)
out_inner_flat_1, spec = pytree.tree_flatten(out_inner_1)
out_inner_flat_2, spec = pytree.tree_flatten(out_inner_2)
if func.is_view:
new_out = pytree.tree_unflatten(
(
CustomTensorPlainOut(tensor1, tensor2)
for tensor1, tensor2 in zip(
out_inner_flat_1, out_inner_flat_2, strict=True
)
),
spec,
)
return return_and_correct_aliasing(func, args, kwargs, new_out)
out_new = (
out_inner_flat_1[ix] + out_inner_flat_2[ix]
for ix in range(len(out_inner_flat_1))
)
return pytree.tree_unflatten(out_new, spec)
@@ -0,0 +1 @@
# mypy: ignore-errors
@@ -0,0 +1,10 @@
# mypy: ignore-errors
import torch.nn as nn
class Net(nn.Module):
def __init__(self) -> None:
super().__init__()
self.linear = nn.Linear(10, 20)
@@ -0,0 +1,11 @@
# mypy: ignore-errors
import torch.nn as nn
class Net(nn.Module):
def __init__(self) -> None:
super().__init__()
self.linear = nn.Linear(10, 20)
self.relu = nn.ReLU()
@@ -0,0 +1,199 @@
# mypy: ignore-errors
import re
import sys
import time
from functools import partial, wraps
import torch.distributed as dist
import torch.distributed.rpc as rpc
from torch.distributed.rpc import _rref_context_get_debug_info
from torch.testing._internal.common_utils import FILE_SCHEMA, TEST_WITH_TSAN
if not dist.is_available():
print("c10d not available, skipping tests", file=sys.stderr)
sys.exit(0)
INIT_METHOD_TEMPLATE = FILE_SCHEMA + "{file_name}"
def dist_init(
old_test_method=None,
setup_rpc: bool = True,
clean_shutdown: bool = True,
faulty_messages=None,
messages_to_delay=None,
):
"""
We use this decorator for setting up and tearing down state since
MultiProcessTestCase runs each `test*` method in a separate process and
each process just runs the `test*` method without actually calling
'setUp' and 'tearDown' methods of unittest.
Note: pass the string representation of MessageTypes that should be used
with the faulty agent's send function. By default, all retriable messages
("RREF_FORK_REQUEST", "RREF_CHILD_ACCEPT", "RREF_USER_DELETE",
"CLEANUP_AUTOGRAD_CONTEXT_REQ") will use the faulty send (this default is
set from faulty_rpc_agent_test_fixture.py).
"""
# If we use dist_init without arguments (ex: @dist_init), old_test_method is
# appropriately set and we return the wrapper appropriately. On the other
# hand if dist_init has arguments (ex: @dist_init(clean_shutdown=False)),
# old_test_method is None and we return a functools.partial which is the real
# decorator that is used and as a result we recursively call dist_init with
# old_test_method and the rest of the arguments appropriately set.
if old_test_method is None:
return partial(
dist_init,
setup_rpc=setup_rpc,
clean_shutdown=clean_shutdown,
faulty_messages=faulty_messages,
messages_to_delay=messages_to_delay,
)
@wraps(old_test_method)
def new_test_method(self, *arg, **kwargs):
# Setting _ignore_rref_leak to make sure OwnerRRefs are properly deleted
# in tests.
import torch.distributed.rpc.api as api
api._ignore_rref_leak = False
self.worker_id = self.rank
self.setup_fault_injection(faulty_messages, messages_to_delay)
rpc_backend_options = self.rpc_backend_options
if setup_rpc:
if TEST_WITH_TSAN:
# TSAN runs much slower.
rpc_backend_options.rpc_timeout = rpc.constants.DEFAULT_RPC_TIMEOUT_SEC * 5
rpc.constants.DEFAULT_SHUTDOWN_TIMEOUT = 60
rpc.init_rpc(
name=f"worker{self.rank:d}",
backend=self.rpc_backend,
rank=self.rank,
world_size=self.world_size,
rpc_backend_options=rpc_backend_options,
)
return_value = old_test_method(self, *arg, **kwargs)
if setup_rpc:
rpc.shutdown(graceful=clean_shutdown)
return return_value
return new_test_method
def noop() -> None:
pass
def wait_until_node_failure(rank: int, expected_error_regex: str = ".*") -> str:
"""
Loops until an RPC to the given rank fails. This is used to
indicate that the node has failed in unit tests.
Args:
rank (int): Rank of the node expected to fail
expected_error_regex (optional, str): Regex of exception message expected. Useful to ensure a specific failure
occurs, not just any.
"""
while True:
try:
rpc.rpc_sync(f"worker{rank}", noop, args=())
time.sleep(0.1)
except Exception as e:
if re.search(pattern=expected_error_regex, string=str(e)):
return str(e)
def wait_until_pending_futures_and_users_flushed(timeout: int = 20) -> None:
"""
The RRef protocol holds forkIds of rrefs in a map until those forks are
confirmed by the owner. The message confirming the fork may arrive after
our tests check whether this map is empty, which leads to failures and
flaky tests. to_here also does not guarantee that we have finished
processind the owner's confirmation message for the RRef. This function
loops until the map is empty, which means the messages have been received
as processed. Call this function before asserting the map returned by
_get_debug_info is empty.
"""
start = time.time()
while True:
debug_info = _rref_context_get_debug_info()
num_pending_futures = int(debug_info["num_pending_futures"])
num_pending_users = int(debug_info["num_pending_users"])
if num_pending_futures == 0 and num_pending_users == 0:
break
time.sleep(0.1)
if time.time() - start > timeout:
raise ValueError(
f"Timed out waiting to flush pending futures and users, "
f"had {num_pending_futures} pending futures and {num_pending_users} pending users"
)
def get_num_owners_and_forks() -> tuple[str, str]:
"""
Retrieves number of OwnerRRefs and forks on this node from
_rref_context_get_debug_info.
"""
rref_dbg_info = _rref_context_get_debug_info()
num_owners = rref_dbg_info["num_owner_rrefs"]
num_forks = rref_dbg_info["num_forks"]
return num_owners, num_forks
def wait_until_owners_and_forks_on_rank(
num_owners: int, num_forks: int, rank: int, timeout: int = 20
) -> None:
"""
Waits until timeout for num_forks and num_owners to exist on the rank. Used
to ensure proper deletion of RRefs in tests.
"""
start = time.time()
while True:
num_owners_on_rank, num_forks_on_rank = rpc.rpc_sync(
worker_name(rank), get_num_owners_and_forks, args=(), timeout=5
)
num_owners_on_rank = int(num_owners_on_rank)
num_forks_on_rank = int(num_forks_on_rank)
if num_owners_on_rank == num_owners and num_forks_on_rank == num_forks:
return
time.sleep(1)
if time.time() - start > timeout:
raise ValueError(
f"Timed out waiting {timeout} sec for {num_owners} owners and {num_forks} forks on rank,"
f" had {num_owners_on_rank} owners and {num_forks_on_rank} forks"
)
def initialize_pg(init_method, rank: int, world_size: int) -> None:
# This is for tests using `dist.barrier`.
if not dist.is_initialized():
dist.init_process_group(
backend="gloo",
init_method=init_method,
rank=rank,
world_size=world_size,
)
def worker_name(rank: int) -> str:
return f"worker{rank}"
def get_function_event(function_events, partial_event_name):
"""
Returns the first event that matches partial_event_name in the provided
function_events. These function_events should be the output of
torch.autograd.profiler.function_events().
Args:
function_events: function_events returned by the profiler.
event_name (str): partial key that the event was profiled with.
"""
event = [event for event in function_events if partial_event_name in event.name][0] # noqa: RUF015
return event
@@ -0,0 +1 @@
# mypy: allow-untyped-defs
@@ -0,0 +1,103 @@
# mypy: allow-untyped-defs
import sys
from functools import partial, wraps
import torch
import torch.distributed as dist
from torch.distributed import rpc
from torch.testing._internal.common_distributed import (
MultiProcessTestCase,
TEST_SKIPS,
tp_transports,
)
TEST_GPU_NUM = 4
class ShardedTensorTestBase(MultiProcessTestCase):
@property
def world_size(self):
return TEST_GPU_NUM
def init_pg(self, backend="nccl"):
if backend not in ["nccl", "gloo", "mpi", "hccl"]:
raise RuntimeError(f"Backend {backend} not supported!")
dist.init_process_group(
backend=backend,
world_size=self.world_size,
rank=self.rank,
init_method=f"file://{self.file_name}",
)
# set device for nccl pg for collectives
if backend == "nccl":
torch.cuda.set_device(self.rank)
def init_rpc(self):
rpc_backend_options = rpc.TensorPipeRpcBackendOptions(
_transports=tp_transports()
)
rpc_backend_options.init_method = f"file://{self.file_name}"
for rank in range(self.world_size):
rpc_backend_options.set_device_map(
f"worker{rank}", {rank: self.rank, self.rank: rank}
)
rpc.init_rpc(
name=f"worker{self.rank:d}",
rank=self.rank,
world_size=self.world_size,
rpc_backend_options=rpc_backend_options,
)
def init_comms(self, init_rpc=True, backend="nccl"):
if init_rpc:
self.init_rpc()
self.init_pg(backend=backend)
def destroy_comms(self, destroy_rpc=True):
# Wait for all ranks to reach here before starting shutdown.
dist.barrier()
if destroy_rpc:
rpc.shutdown()
dist.destroy_process_group()
def setUp(self) -> None:
super().setUp()
self._spawn_processes()
def assert_sharded_tensor_equal(self, st1, st2):
st1_local_shards = st1.local_shards()
st2_local_shards = st2.local_shards()
self.assertEqual(len(st1_local_shards), len(st2_local_shards))
for i, st1_local_shard in enumerate(st1_local_shards):
self.assertEqual(st1_local_shard.tensor, st2_local_shards[i].tensor)
self.assertEqual(st1_local_shard.metadata, st2_local_shards[i].metadata)
self.assertEqual(st1.metadata(), st2.metadata())
self.assertEqual(st1.sharding_spec(), st2.sharding_spec())
self.assertEqual(len(st1.remote_shards()), len(st2.remote_shards()))
# wrapper to initialize comms (processgroup + rpc)
def with_comms(func=None, init_rpc=True, backend="nccl"):
if func is None:
return partial(
with_comms,
init_rpc=init_rpc,
backend=backend,
)
@wraps(func)
def wrapper(self, *args, **kwargs):
if backend == "nccl" and torch.cuda.device_count() < self.world_size:
sys.exit(TEST_SKIPS[f"multi-gpu-{self.world_size}"].exit_code)
self.init_comms(init_rpc=init_rpc, backend=backend)
func(self, *args, **kwargs)
self.destroy_comms(destroy_rpc=init_rpc)
return wrapper
@@ -0,0 +1,137 @@
# mypy: allow-untyped-defs
import builtins
import torch
from torch.distributed._shard.sharding_spec import (
ChunkShardingSpec,
EnumerableShardingSpec,
ShardMetadata,
)
from torch.distributed._shard.sharding_spec._internals import (
get_chunked_dim_size,
get_split_size,
)
def generate_chunk_sharding_specs_for_test(sharding_dim):
return [
ChunkShardingSpec(
dim=sharding_dim,
placements=[
"rank:0/cuda:0",
"rank:1/cuda:1",
"rank:2/cuda:2",
"rank:3/cuda:3",
],
),
# Test different ordering. (Case 1)
ChunkShardingSpec(
dim=sharding_dim,
placements=[
"rank:2/cuda:2",
"rank:3/cuda:3",
"rank:0/cuda:0",
"rank:1/cuda:1",
],
),
# Test different ordering. (Case 2)
ChunkShardingSpec(
dim=sharding_dim,
placements=[
"rank:3/cuda:3",
"rank:0/cuda:0",
"rank:1/cuda:1",
"rank:2/cuda:2",
],
),
]
def generate_enumerable_sharding_specs_for_test():
return [
EnumerableShardingSpec(
[
ShardMetadata(
shard_offsets=[0, 0],
shard_sizes=[5, 5],
placement="rank:0/cuda:0",
),
ShardMetadata(
shard_offsets=[5, 0],
shard_sizes=[5, 5],
placement="rank:1/cuda:1",
),
ShardMetadata(
shard_offsets=[0, 5],
shard_sizes=[5, 5],
placement="rank:2/cuda:2",
),
ShardMetadata(
shard_offsets=[5, 5],
shard_sizes=[5, 5],
placement="rank:3/cuda:3",
),
]
)
]
def generate_local_weight_sharding_params_for_test(
local_weight, sharded_dim, gpu_num, spec, rank
):
"""
Shard the local weight based the given spec, so we can compare against
the one from sharded tensor.
Args:
local_weight: weight matrix to be sharded.
sharded_dim: The dimension which we shard on.
gpu_num: number of ranks.
spec: sharding spec.
rank: # of cuda process.
Returns:
start_pos: start position of sharded weight on the given rank.
chunk_size: chunk size of sharded weight on the given rank.
"""
sharding_dim_size = local_weight.size(sharded_dim)
split_size = get_split_size(sharding_dim_size, gpu_num)
current_offsets = 0
start_pos = current_offsets
for idx, placement in enumerate(spec.placements):
chunk_size = get_chunked_dim_size(sharding_dim_size, split_size, idx)
if rank == placement.rank():
start_pos = current_offsets
break
current_offsets += chunk_size
return start_pos, chunk_size
def clone_module_parameter(module, param_name):
"""
Clone a parameter from a given existing module.
Args:
module (:class:`torch.nn.Module`): Module whose parameter needs to be cloned.
param_name (str): Name of the parameter of ``module`` that needs to be cloned.
Returns: cloned tensor as :class:`torch.nn.Parameter`.
"""
tensor = getattr(module, param_name)
return torch.nn.Parameter(tensor.detach().clone())
def gen_binary_op_func(python_op, inplace=False):
src_lines = ["def f(lhs, rhs):"]
if "torch" in python_op:
src_lines.append(f" return {python_op}(lhs, rhs)\n")
elif inplace:
src_lines.append(f" lhs {python_op}= rhs\n return lhs\n")
else:
src_lines.append(f" return lhs {python_op} rhs\n")
code_str = "\n".join(src_lines)
g = {"torch": torch}
builtins.exec(code_str, g)
return g["f"]
@@ -0,0 +1,56 @@
# mypy: allow-untyped-defs
import copy
import random
import torch
from torch.distributed._shard import sharded_tensor
from torch.distributed._shard.sharding_spec import ChunkShardingSpec
PLACEMENTS = [
"rank:0/cuda:0",
"rank:1/cuda:1",
"rank:2/cuda:2",
"rank:3/cuda:3",
]
DEFAULT_GPU_NUM = 4
def _chunk_sharding_specs_list_for_test(sharding_dims, seed=0):
spec_list = []
for i in range(len(sharding_dims)):
random.Random(seed + i).shuffle(PLACEMENTS)
spec_list.append(
ChunkShardingSpec(
dim=sharding_dims[i],
placements=copy.deepcopy(PLACEMENTS),
)
)
return spec_list
class MyShardedModel2(torch.nn.Module):
def __init__(self, spec=None, group=None, init_rrefs=True) -> None:
super().__init__()
if spec is not None:
self.sharded_tensor2 = sharded_tensor.rand(
spec, 10, 20, process_group=group, init_rrefs=init_rrefs
)
else:
self.sharded_tensor2 = None
self.random_tensor2 = torch.nn.Parameter(torch.rand(2, 2))
class MyShardedModel1(torch.nn.Module):
def __init__(self, spec=None, group=None, init_rrefs=True) -> None:
super().__init__()
if spec is not None:
self.sharded_tensor1 = sharded_tensor.rand(
spec, 10, 20, process_group=group, init_rrefs=init_rrefs
)
else:
self.sharded_tensor1 = None
self.random_tensor1 = torch.nn.Parameter(torch.rand(2, 2))
self.submodule = MyShardedModel2(spec, group, init_rrefs)
@@ -0,0 +1,41 @@
# mypy: allow-untyped-defs
import torch
import torch.nn as nn
from torch.distributed._shard.sharded_tensor import ShardedTensor
class SimpleMegatronLM(nn.Module):
def __init__(self, linear_size, rank=None, dtype=torch.float32):
super().__init__()
self.fc1 = nn.Linear(*linear_size[0], dtype=dtype)
self.gelu = nn.GELU()
self.fc2 = nn.Linear(*linear_size[1], dtype=dtype)
if rank is not None:
self.fc1.cuda(rank)
self.fc2.cuda(rank)
def forward(self, inp):
return self.fc2(self.gelu(self.fc1(inp)))
def get_weights(self):
if isinstance(self.fc1.weight, ShardedTensor):
weight1 = self.fc1.weight.local_tensor()
else:
weight1 = self.fc1.weight
if isinstance(self.fc2.weight, ShardedTensor):
weight2 = self.fc2.weight.local_tensor()
else:
weight2 = self.fc2.weight
return (weight1, weight2)
def get_biases(self):
return (self.fc1.bias, self.fc2.bias)
def get_weight_grads(self):
return (self.fc1.weight.grad, self.fc2.weight.grad)
def get_bias_grads(self):
return (self.fc1.bias.grad, self.fc2.bias.grad)
@@ -0,0 +1,196 @@
# mypy: allow-untyped-defs
# Copyright (c) Meta Platforms, Inc. and affiliates
import io
import logging
import os
import shutil
import tempfile
from collections.abc import Callable
from functools import wraps
from typing import Any, cast, IO
# introduced as collections.abc.Buffer in Python 3.12
from typing_extensions import Buffer
import torch.distributed as dist
from torch.distributed.checkpoint._extension import (
ExtensionRegistry,
StreamTransformExtension,
)
class Rot13Example(StreamTransformExtension):
"""
This is an example stream transform extension which just does rot13 on each
alphanumeric character of the stream. It is mainly intended as a demonstration
and for testing; there isn't a production use case for this.
"""
def __init__(self, chunk_size: int = io.DEFAULT_BUFFER_SIZE) -> None:
super().__init__()
self._chunk_size = chunk_size
@staticmethod
def from_descriptor(version: str) -> "Rot13Example":
if version.partition(".")[0] != "1":
raise ValueError(f"Unknown extension {version=}")
return Rot13Example()
@staticmethod
def registry_name() -> str:
return "stream.rot13"
def get_descriptor(self) -> str:
return f"{self.registry_name()}/1"
@staticmethod
def _rot13bytes(b: Buffer, count: int) -> None:
b = memoryview(b)
for i in range(count):
ch = b[i]
if ch >= ord("A") and ch <= ord("Z"):
ch += ord("a") - ord("A")
elif ch >= ord("a") and ch <= ord("z"):
ch += ord("A") - ord("a")
b[i] = ch
def transform_to(self, output: IO[bytes]) -> IO[bytes]:
class Writer(io.RawIOBase):
def __init__(self, output: IO[bytes]) -> None:
self.output = output
def writeable(self) -> bool:
return True
def write(self, b: Buffer) -> int | None:
# Don't mutate the input
chunk = bytearray(b)
Rot13Example._rot13bytes(chunk, len(chunk))
return self.output.write(chunk)
def flush(self) -> None:
self.output.flush()
return cast(IO[bytes], Writer(output))
def transform_from(self, input: IO[bytes]) -> IO[bytes]:
class Reader(io.RawIOBase):
def __init__(self, input: IO[bytes]) -> None:
self.input = input
def readable(self) -> bool:
return True
def readinto(self, b: Buffer) -> int | None:
if hasattr(self.input, "readinto"):
count = self.input.readinto(b)
else:
# It's possible self.input is an IO[bytes] with no readinto method.
# In that case, we emulate with a read and copy. In practice,
# all of the current concrete extensions have readinto.
view = memoryview(b)
r = self.input.read(len(view))
if r is None:
count = None
else:
count = len(r)
view[:count] = r
if count == 0 or count is None:
return count
Rot13Example._rot13bytes(b, count)
return count
def seekable(self) -> bool:
return self.input.seekable()
def seek(self, offset: int, whence: int = os.SEEK_SET) -> int:
return self.input.seek(offset, whence)
def tell(self) -> int:
return self.input.tell()
return cast(IO[bytes], Reader(input))
def get_test_extension_registry() -> ExtensionRegistry:
registry = ExtensionRegistry()
registry.register(Rot13Example)
return registry
def with_temp_dir(
func: Callable | None = None,
) -> Callable | None:
"""
Wrapper to initialize temp directory for distributed checkpoint.
"""
if func is None:
raise AssertionError("Expected func to not be None")
@wraps(func)
def wrapper(self, *args: tuple[object], **kwargs: dict[str, Any]) -> None:
if dist.is_initialized():
# Only create temp_dir when rank is 0
if dist.get_rank() == 0:
temp_dir = tempfile.mkdtemp()
print(f"Using temp directory: {temp_dir}")
else:
temp_dir = ""
object_list = [temp_dir]
# Broadcast temp_dir to all the other ranks
os.sync()
dist.broadcast_object_list(object_list)
self.temp_dir = object_list[0]
os.sync()
else:
temp_dir = tempfile.mkdtemp()
print(f"No process group initialized, using temp directory: {temp_dir}")
self.temp_dir = temp_dir
try:
func(self, *args, **kwargs)
finally:
if dist.is_initialized() and dist.get_rank() == 0:
shutil.rmtree(self.temp_dir, ignore_errors=True)
else:
shutil.rmtree(self.temp_dir, ignore_errors=True)
return wrapper
def with_checkpoint_logging(
func: Callable | None = None,
logger_name: str = "torch.distributed.checkpoint",
level: int = logging.INFO,
) -> Callable | None:
"""
Wrapper to configure checkpoint logging for distributed tests.
Args:
func: The test function to wrap
logger_name: Name of the logger to configure (default: 'torch.distributed.checkpoint')
level: Logging level to set (default: logging.INFO)
"""
if func is None:
raise AssertionError("Expected func to not be None")
@wraps(func)
def wrapper(self, *args: tuple[object], **kwargs: dict[str, Any]) -> None:
# Get the logger and store original level
target_logger = logging.getLogger(logger_name)
original_level = target_logger.level
# Set the desired logging level
target_logger.setLevel(level)
try:
func(self, *args, **kwargs)
finally:
# Restore original logging level
target_logger.setLevel(original_level)
return wrapper
@@ -0,0 +1,170 @@
# mypy: allow-untyped-defs
# Owner(s): ["oncall: distributed"]
import copy
from itertools import chain
from typing import Any
import torch
import torch.nn as nn
from torch.distributed._sharded_tensor import ShardedTensor
from torch.distributed._state_dict_utils import _gather_state_dict
from torch.distributed.checkpoint.state_dict import (
_PG,
_STATE,
set_state_dict,
StateDictOptions,
)
from torch.distributed.tensor import DTensor
class VerifyStateDictMixin:
def _compare_tensor(self, orig_tensor, dist_tensor, offload_to_cpu=False):
if isinstance(dist_tensor, (DTensor, ShardedTensor)):
dist_tensor = _gather_state_dict({"mykey": dist_tensor}).pop("mykey")
if offload_to_cpu:
orig_tensor = orig_tensor.cpu()
dist_tensor = dist_tensor.cpu()
self.assertTrue(isinstance(dist_tensor, torch.Tensor))
self.assertTrue(torch.allclose(orig_tensor, dist_tensor))
def _verify_msd(
self,
msd: dict[str, Any],
dist_msd: dict[str, Any],
options: StateDictOptions = StateDictOptions(),
offload_to_cpu=False,
) -> None:
if not options.ignore_frozen_params:
self.assertEqual(len(msd), len(dist_msd))
for fqn, param in msd.items():
dist_param = dist_msd.get(fqn)
if not options.ignore_frozen_params:
self.assertIsNotNone(dist_param, f"{fqn=}")
try:
self._compare_tensor(param, dist_param, offload_to_cpu)
except AssertionError as e:
raise AssertionError(
f"{fqn} has mismatched value {param} {dist_param}"
) from e
elif dist_param is None:
self.assertFalse(param.requires_grad, f"{fqn=}")
def _verify_osd(
self,
model: nn.Module,
optim: torch.optim.Optimizer,
osd: dict[str, Any],
dist_osd: dict[str, Any],
) -> None:
params = list(chain.from_iterable(g["params"] for g in optim.param_groups))
param_pid_mapping = dict(zip(params, range(len(params)), strict=True))
fqn_pid_mapping = {}
for fqn, param in model.named_parameters():
pid = param_pid_mapping[param]
fqn_pid_mapping[fqn] = pid
fqn_pid_mapping[pid] = fqn
# Check optimizer_state_dict state
self.assertEqual(len(osd[_STATE]), len(dist_osd[_STATE]))
for pid, states in osd[_STATE].items():
fqn = fqn_pid_mapping[pid]
dist_states = dist_osd[_STATE].get(fqn, None)
self.assertIsNotNone(dist_states, fqn)
self.assertEqual(len(states), len(dist_states))
for key, state in states.items():
dist_state = states.get(key, None)
self.assertIsNotNone(dist_state)
self._compare_tensor(state, dist_state)
# Check optimizer_state_dict param_group
old_dist_osd_pg = dist_osd[_PG]
if len(osd[_PG]) != len(dist_osd[_PG]):
self.assertTrue(len(dist_osd[_PG]) > len(osd[_PG]))
new_pg = copy.deepcopy(dist_osd[_PG][0])
new_pg["params"] = []
for dist_group in dist_osd[_PG]:
new_pg["params"].extend(dist_group["params"])
dist_osd[_PG] = [new_pg]
self.assertEqual(len(osd[_PG]), len(dist_osd[_PG]))
for group, dist_group in zip(osd[_PG], dist_osd[_PG], strict=True):
self.assertEqual(len(group), len(dist_group))
for key, value in group.items():
# Below doesn't work because param_groups can have None
# values.
# dist_value = dist_group.get(key, None)
# self.assertIsNotNone(dist_value, (dist_group, group))
dist_value = dist_group[key]
if key == "params":
fqns = [fqn_pid_mapping[pid] for pid in value]
self.assertEqual(sorted(fqns), sorted(dist_value))
else:
self.assertEqual(value, dist_value)
dist_osd[_PG] = old_dist_osd_pg
def _verify_osd_by_load(
self,
model: nn.Module,
optim: torch.optim.Optimizer,
new_optim: torch.optim.Optimizer,
dist_osd: dict[str, Any],
) -> None:
new_dist_osd = _gather_state_dict(dist_osd)
set_state_dict(
model,
optimizers=new_optim,
model_state_dict={},
optim_state_dict=new_dist_osd,
)
self.assertEqual(optim.state_dict(), new_optim.state_dict())
class FusionEmbedding(nn.Module):
def __init__(self, vocab_size: int, fusion_vocab_size: int, embed_dim: int) -> None:
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim)
self.fusion_embedding = nn.Embedding(fusion_vocab_size, embed_dim)
class FusionEmbeddingWithHook(nn.Module):
def __init__(self, vocab_size: int, fusion_vocab_size: int, embed_dim: int) -> None:
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim)
self.fusion_embedding = nn.Embedding(fusion_vocab_size, embed_dim)
self._register_state_dict_hook(FusionEmbeddingWithHook._state_dict_hook)
self._register_load_state_dict_pre_hook(
FusionEmbeddingWithHook._load_state_dict_hook, with_module=True
)
def _state_dict_hook(self, destination, prefix, keep_vars):
"""Remove "embedding" from the original embedding in the state_dict
name. This keeps the original state dict name for the embedding
from before fusing with the FusionEmbedding.
"""
key = prefix + "embedding.weight"
new_key = prefix + "weight"
destination[new_key] = destination[key]
del destination[key]
def _load_state_dict_hook(self, state_dict, prefix, *args, **kwargs):
"""Apply extra "embedding" prefix to the state_dict key to
account for the FusionEmbedding wrapping.
"""
if state_dict:
key = prefix + "weight"
new_key = prefix + "embedding.weight"
state_dict[new_key] = state_dict[key]
del state_dict[key]
class FusionEmbeddingWithModifier(FusionEmbeddingWithHook):
# _fqn_modifiers is a private function as a contract between DSD. When users change the state_dict
# keys, they need to provide a mapping from the new key to the original key. This is used to ensure
# consistency between the state_dict keys and fqn.
def _fqn_modifiers(self) -> dict[str, str]:
return {
"weight": "embedding",
}
@@ -0,0 +1,755 @@
# mypy: allow-untyped-defs
import contextlib
import enum
import logging
import os
import threading
from typing import NamedTuple
import torch
import torch.distributed as dist
import torch.distributed.autograd as dist_autograd
import torch.nn as nn
from torch.distributed import rpc
from torch.distributed.nn import RemoteModule
from torch.nn.parallel import DistributedDataParallel
from torch.testing._internal.common_distributed import (
requires_gloo,
requires_nccl,
skip_if_lt_x_gpu,
skip_if_rocm_multiprocess,
)
from torch.testing._internal.dist_utils import dist_init, INIT_METHOD_TEMPLATE
from torch.testing._internal.distributed.rpc.rpc_agent_test_fixture import (
RpcAgentTestFixture,
)
NUM_EM_ROW = 2
D_SPARSE = 3
D_DENSE = 2
D_HID = 3
D_OUT = 1
NUM_TRAINERS = 4
# Trainers + the master + the remote worker
WORLD_SIZE = NUM_TRAINERS + 2
TRAINER_RANKS = list(range(NUM_TRAINERS))
REMOTE_WORKER_RANK = TRAINER_RANKS[-1] + 1
MASTER_RANK = REMOTE_WORKER_RANK + 1
class DdpMode(enum.Enum):
# Don't apply DDP
NONE = enum.auto()
# Apply DDP to the top level nn.Module
OUTSIDE = enum.auto()
# Embed DDP inside the top level nn.Module
INSIDE = enum.auto()
def init_logger():
logger = logging.getLogger(__name__)
level = logging.DEBUG if "debug" in os.environ else logging.INFO
logger.setLevel(level)
console = logging.StreamHandler()
formatter = logging.Formatter(
"%(asctime)s %(filename)s:%(lineno)s %(levelname)s p:%(processName)s t:%(threadName)s: %(message)s"
)
console.setFormatter(formatter)
console.setLevel(level)
# add the handlers to the logger
logger.addHandler(console)
logger.propagate = False
return logger
gLogger = init_logger()
class FeatureSet(NamedTuple):
"""A feature set has 2 types of features"""
dense_features: torch.Tensor
sparse_features: torch.LongTensor
values: torch.Tensor
def _call_method(method, rref, *args, **kwargs):
return method(rref.local_value(), *args, **kwargs)
def _remote_method(method, rref, *args, **kwargs):
args_tup = tuple([method, rref] + list(args))
return rpc.rpc_sync(rref.owner(), _call_method, args=args_tup, kwargs=kwargs)
def _remote_method_async(method, rref, *args, **kwargs):
args_tup = tuple([method, rref] + list(args))
return rpc.rpc_async(rref.owner(), _call_method, args=args_tup, kwargs=kwargs)
class RemoteEM(nn.Module):
def __init__(self, num_embeddings: int, embedding_dim: int):
gLogger.info("Initing RemoteEM with %s %s", num_embeddings, embedding_dim)
super().__init__()
init_em = [0.5] * embedding_dim
self.em = nn.EmbeddingBag(
num_embeddings,
embedding_dim,
_weight=torch.tensor([init_em] * num_embeddings),
)
def forward(self, input: torch.Tensor):
gLogger.debug("Running RemoteEM.forward() on: %s", input)
return self.em(input, offsets=torch.LongTensor(range(input.shape[0])))
# Return a linear module with predefined parameters.
def getLinear(d_in, d_out):
l = nn.Linear(d_in, d_out, bias=False)
w = torch.ones((d_out, d_in))
w[0][0] = -1
w.requires_grad_()
l.weight.data = w
return l
class RemoteNet(nn.Module):
def __init__(self, d_in: int, d_out: int):
gLogger.info("Initing RemoteNet with %s %s", d_in, d_out)
super().__init__()
self.fc = getLinear(d_in, d_out)
self.relu = nn.ReLU()
def forward(self, input: torch.Tensor):
gLogger.debug("Running RemoteNet.forward() on: %s", input)
return self.relu(self.fc(input))
class HybridModel(nn.Module):
def __init__(
self,
remote_em_rref: rpc.RRef,
remote_net_rref: rpc.RRef,
process_group_for_ddp: dist.ProcessGroup = None,
):
super().__init__()
self.remote_em_rref = remote_em_rref
self.remote_net_rref = remote_net_rref
self.fc1 = getLinear(D_DENSE, D_DENSE)
self.fc2 = getLinear(D_HID, D_OUT)
self.non_ddp_params = tuple(self.fc1.parameters()) + tuple(
self.fc2.parameters()
)
self.ddp_params = ()
if process_group_for_ddp is not None:
self.non_ddp_params, self.ddp_params = (
tuple(self.fc1.parameters()),
tuple(self.fc2.parameters()),
)
gLogger.info("Use DDP for the second local net.")
self.fc2 = DistributedDataParallel(
self.fc2, check_reduction=True, process_group=process_group_for_ddp
)
gLogger.info(
"HybridModel has %s groups of parameters.", len(list(self.parameters()))
)
def forward(self, input: FeatureSet):
gLogger.debug("Running HybridModel.forward on %s", input)
sparse = _remote_method(
RemoteEM.forward, self.remote_em_rref, input.sparse_features
)
# The same size of mini batch.
if sparse.shape[0] != input.dense_features.shape[0]:
raise AssertionError(
f"Expected sparse.shape[0] == input.dense_features.shape[0], "
f"got {sparse.shape[0]} != {input.dense_features.shape[0]}"
)
dense = self.fc1(input.dense_features)
x = torch.cat((dense, sparse), 1)
gLogger.debug("Concatenated feature: %s", x)
x = _remote_method(RemoteNet.forward, self.remote_net_rref, x)
return self.fc2(x)
class Trainer:
def __init__(
self,
remote_em_rref: rpc.RRef,
remote_net_rref: rpc.RRef,
ddp_mode: DdpMode,
rank: int,
):
self.rank = rank
self.trainer_group = (
dist.new_group(TRAINER_RANKS)
if ddp_mode in (DdpMode.INSIDE, DdpMode.OUTSIDE)
else None
)
self.remote_em_rref = remote_em_rref
self.remote_net_rref = remote_net_rref
self.hybrid_module = HybridModel(
self.remote_em_rref,
self.remote_net_rref,
self.trainer_group if ddp_mode == DdpMode.INSIDE else None,
)
self.ddp_params, self.non_ddp_params = (
self.hybrid_module.ddp_params,
self.hybrid_module.non_ddp_params,
)
if ddp_mode == DdpMode.OUTSIDE:
gLogger.info("Wrapping the whole hybrid module into DDP.")
self.ddp_params += self.non_ddp_params
self.non_ddp_params = ()
self.hybrid_module = DistributedDataParallel(
self.hybrid_module,
check_reduction=True,
process_group=self.trainer_group,
)
gLogger.info(
"Succeeded in creating a HybridModel instance with "
"%s ddp params and %s other local params.",
len(self.ddp_params),
len(self.non_ddp_params),
)
def destroy_pg(self):
if self.trainer_group:
dist.destroy_process_group(self.trainer_group)
def train_batch(
self,
mini_batch: FeatureSet,
trainer_has_less_inputs: bool,
simulate_uneven_inputs: bool,
):
grads_dict = None
if not simulate_uneven_inputs:
input_batches = [mini_batch]
else:
# Split into microbatches, and trim to simulate uneven inputs.
dense_features = mini_batch.dense_features
sparse_features = mini_batch.sparse_features
values = mini_batch.values
dense_microbatch = torch.split(dense_features, 2)
sparse_microbatch = torch.split(sparse_features, 2)
values_microbatch = torch.split(values, 2)
batches = []
for d, s, v in zip(
dense_microbatch, sparse_microbatch, values_microbatch, strict=True
):
feature_set = FeatureSet(dense_features=d, sparse_features=s, values=v)
batches.append(feature_set)
if trainer_has_less_inputs:
input_batches = batches[: len(batches) // 2]
gLogger.info(
"Trainer reduced input patches from %s "
"to %s to simulate uneven inputs.",
len(batches),
len(input_batches),
)
else:
input_batches = batches
with (
self.hybrid_module.join()
if simulate_uneven_inputs
else contextlib.nullcontext()
):
for b in input_batches:
with dist_autograd.context() as context_id:
output = self.hybrid_module.forward(b)
loss = (output * mini_batch.values).sum()
dist_autograd.backward(context_id, [loss])
grads_dict = dist_autograd.get_gradients(context_id)
gLogger.info(
"Loss is %s for mini batch: %s. Grads dict has %s entries: %s",
loss,
mini_batch,
len(grads_dict),
grads_dict,
)
return (
tuple(grads_dict[param] for param in self.ddp_params),
tuple(grads_dict[param] for param in self.non_ddp_params),
)
def get_training_examples():
n = 16
training_examples = FeatureSet(
dense_features=torch.zeros((n, D_DENSE)),
sparse_features=torch.zeros(n, dtype=torch.long),
values=torch.zeros(n),
)
idx = 0
# Every example has another one that has exactly the same features but an
# opposite value. Therefore, their grads cancel each other in all-reduce.
for value in (-1, 1):
for x in (-1.0 * value, 1.0 * value):
for y in (1.0 * value, -1.0 * value):
for z in (0, 1):
training_examples.dense_features[idx, :] = torch.tensor((x, y))
training_examples.sparse_features[idx] = z
training_examples.values[idx] = value
idx += 1
# Split the examples among NUM_TRAINERS trainers
if n % NUM_TRAINERS != 0:
raise AssertionError(
f"Expected n % NUM_TRAINERS == 0, got {n} % {NUM_TRAINERS} = {n % NUM_TRAINERS}"
)
examples_per_trainer = int(n / NUM_TRAINERS)
return [
FeatureSet(
dense_features=training_examples.dense_features[
start : start + examples_per_trainer, :
],
sparse_features=training_examples.sparse_features[
start : start + examples_per_trainer
],
values=training_examples.values[start : start + examples_per_trainer],
)
for start in range(0, n, examples_per_trainer)
]
shutdown_signal = threading.Condition()
def set_shutdown_signal():
global shutdown_signal
with shutdown_signal:
shutdown_signal.notify()
class DdpUnderDistAutogradTest(RpcAgentTestFixture):
@property
def world_size(self) -> int:
return WORLD_SIZE
def remote_worker_name(self) -> str:
# The name has to be consistent with that in 'dist_init' decorator.
return f"worker{REMOTE_WORKER_RANK}"
def trainer_name(self, rank):
# The name has to be consistent with that in 'dist_init' decorator.
return f"worker{rank}"
def _remote_worker_process(self, ddp_mode):
gLogger.info("The remote worker is running.")
dist.init_process_group(
backend="gloo",
init_method=INIT_METHOD_TEMPLATE.format(file_name=self.file_name),
world_size=self.world_size,
rank=self.rank,
)
if ddp_mode in (DdpMode.INSIDE, DdpMode.OUTSIDE):
# new_group needs to be called on ranks.
dist.new_group(TRAINER_RANKS)
global shutdown_signal
with shutdown_signal:
shutdown_signal.wait()
gLogger.info("Exiting remote worker.")
dist.destroy_process_group()
def _trainer_process(self, rank: int):
gLogger.info("Running the trainer #%s...", rank)
gLogger.info(
"Initing trainer process group by trainer #%s with ranks %s",
rank,
TRAINER_RANKS,
)
dist.init_process_group(
backend="gloo",
init_method=INIT_METHOD_TEMPLATE.format(file_name=self.file_name),
world_size=self.world_size,
rank=self.rank,
)
gLogger.info("Waiting for shutdown signal on trainer #%s...", rank)
global shutdown_signal
with shutdown_signal:
shutdown_signal.wait()
gLogger.info("Exiting the trainer #%s...", rank)
dist.destroy_process_group()
def _master_process(self, ddp_mode: DdpMode, simulate_uneven_inputs: bool):
gLogger.info("Running the master process...")
dist.init_process_group(
backend="gloo",
init_method=INIT_METHOD_TEMPLATE.format(file_name=self.file_name),
world_size=self.world_size,
rank=self.rank,
)
remote_em_rref = rpc.remote(
self.remote_worker_name(), RemoteEM, args=(NUM_EM_ROW, D_SPARSE)
)
remote_net_rref = rpc.remote(
self.remote_worker_name(), RemoteNet, args=(D_DENSE + D_SPARSE, D_HID)
)
gLogger.info("Created remote rrefs on master")
self.do_test_on_master(
ddp_mode, simulate_uneven_inputs, remote_em_rref, remote_net_rref
)
def do_test_on_master(
self,
ddp_mode: DdpMode,
simulate_uneven_inputs: bool,
remote_em_rref: rpc.RRef,
remote_net_rref: rpc.RRef,
):
if simulate_uneven_inputs:
gLogger.info(
"Running DDP + RPC test with simulating uneven inputs across trainers."
)
trainer_rrefs = []
for rank in TRAINER_RANKS:
trainer = self.trainer_name(rank)
trainer_rrefs.append(
rpc.remote(
trainer,
Trainer,
args=(remote_em_rref, remote_net_rref, ddp_mode, rank),
)
)
if ddp_mode in (DdpMode.INSIDE, DdpMode.OUTSIDE):
# new_group needs to be called on ranks.
dist.new_group(TRAINER_RANKS)
training_examples = get_training_examples()
for _ in range(3):
futures = []
num_trainers = len(trainer_rrefs)
for idx, trainer_rref in enumerate(trainer_rrefs):
# Half the trainers will deplete inputs earlier than the rest.
trainer_has_less_inputs = (
simulate_uneven_inputs and idx < num_trainers // 2
)
futures.append(
_remote_method_async(
Trainer.train_batch,
trainer_rref,
training_examples[idx],
trainer_has_less_inputs,
simulate_uneven_inputs,
)
)
for future in futures:
ddp_grads, non_ddp_grads = future.wait()
# When there are uneven inputs, it is not necessary that grads
# cancel each other out, since some trainers contribute 0 grad.
if not simulate_uneven_inputs:
for grad in ddp_grads:
self.assertEqual(
grad,
torch.zeros_like(grad),
msg=f"The grad for any ddp parameter should be zeros, because "
"the training examples' grads cancel each other. Received "
f"gradient {grad}",
)
for grad in non_ddp_grads:
self.assertNotEqual(
grad,
torch.zeros_like(grad),
msg="The grad for any non-ddp parameter shouldn't be zeros",
)
# Destroy process groups
for trainer_rref in trainer_rrefs:
_remote_method_async(Trainer.destroy_pg, trainer_rref).wait()
# Send shutdown signals.
for rank in TRAINER_RANKS:
trainer = self.trainer_name(rank)
rpc.rpc_sync(trainer, set_shutdown_signal, args=())
rpc.rpc_sync(self.remote_worker_name(), set_shutdown_signal, args=())
def _do_test(self, ddp_mode, simulate_uneven_inputs=False):
if self.rank == MASTER_RANK:
self._master_process(ddp_mode, simulate_uneven_inputs)
elif self.rank == REMOTE_WORKER_RANK:
self._remote_worker_process(ddp_mode)
elif self.rank in TRAINER_RANKS:
self._trainer_process(self.rank)
else:
raise RuntimeError(f"Unknown process rank: {self.rank}")
@requires_gloo()
@dist_init
def test_backward_no_ddp(self):
self._do_test(DdpMode.NONE)
@requires_gloo()
@dist_init
def test_backward_ddp_outside(self):
self._do_test(DdpMode.OUTSIDE)
@requires_gloo()
@dist_init
def test_backward_ddp_outside_uneven_inputs(self):
self._do_test(DdpMode.OUTSIDE, simulate_uneven_inputs=True)
@requires_gloo()
@dist_init
def test_backward_ddp_inside(self):
self._do_test(DdpMode.INSIDE)
# Common utils for both CPU and CUDA test suites
class CommonDdpComparisonTest(RpcAgentTestFixture):
@property
def world_size(self) -> int:
return NUM_TRAINERS
def trainer_name(self, rank):
# The name has to be consistent with that in 'dist_init' decorator.
return f"worker{rank}"
@staticmethod
def get_remote_grads(rref, context_id):
return dist_autograd.get_gradients(context_id)[rref.local_value().weight]
class DdpComparisonTest(CommonDdpComparisonTest):
def _run_test_ddp_comparision(self, simulate_uneven_inputs=False):
gLogger.info("Running trainer rank: %s", self.rank)
# Each trainer uses a different random seed. Otherwise, they are going
# to have exactly the same initial model parameters, input, and
# therefore grads. That means the grads will be the same before and
# after DDP's all-reduce.
torch.manual_seed(self.rank)
dist.init_process_group(
backend="gloo",
# Postfix file_name with "pg" since file_name is also used by RPC agent
init_method=INIT_METHOD_TEMPLATE.format(file_name=f"{self.file_name}_pg"),
world_size=self.world_size,
rank=self.rank,
)
net = nn.Linear(2, 3)
ddp_net = DistributedDataParallel(net)
# Odd ranks join early if simulate_uneven_inputs.
num_inputs = 1
if simulate_uneven_inputs:
if self.rank % 2 == 0:
num_inputs += 2
inputs_list = [torch.rand((3, 2)) for _ in range(num_inputs)]
if simulate_uneven_inputs:
gLogger.info(
"Rank %s training with %s inputs.", self.rank, len(inputs_list)
)
# Use distributed autograd. The gradients will be in RPC context map.
grads_dict = {}
with ddp_net.join(simulate_uneven_inputs):
for i, inputs in enumerate(inputs_list):
with dist_autograd.context() as context_id:
loss = ddp_net(inputs).norm()
dist_autograd.backward(context_id, [loss])
grads_dict = dist_autograd.get_gradients(context_id)
gLogger.info("Trainer #%s got grad dict: %s", self.rank, grads_dict)
# Use local autograd. The gradients will be in each variable's '.grad'.
ddp_net.zero_grad()
loss = ddp_net(inputs).norm()
loss.backward()
# The gradients should be the same
for param in net.parameters():
self.assertTrue(
param in grads_dict,
msg=f"Param {param} is not in dist_auto grad dict {grads_dict} for iteration {i}",
)
self.assertEqual(
grads_dict[param],
param.grad,
msg=f"The grads for param {param} are different under local "
f"and dist autograd: {param.grad} \n---\n {grads_dict[param]} for iteration {i}",
)
dist.destroy_process_group()
@requires_gloo()
@dist_init
def test_ddp_comparison(self):
self._run_test_ddp_comparision()
@requires_gloo()
@dist_init
def test_ddp_comparison_uneven_inputs(self):
# test with simulating uneven inputs in DDP
self._run_test_ddp_comparision(simulate_uneven_inputs=True)
@requires_gloo()
@dist_init
def test_ddp_dist_autograd_sparse_grads(self):
# Each trainer uses a different random seed. Otherwise, they are going
# to have exactly the same initial model parameters, input, and
# therefore grads. That means the grads will be the same before and
# after DDP's all-reduce.
torch.manual_seed(self.rank)
dist.init_process_group(
backend="gloo",
init_method=INIT_METHOD_TEMPLATE.format(file_name=self.file_name),
world_size=self.world_size,
rank=self.rank,
)
model = nn.EmbeddingBag(10, 3, sparse=True)
ddp_model = DistributedDataParallel(model)
# Different inputs for each
input = torch.LongTensor(10).random_(0, 10)
offsets = torch.LongTensor([0, 4])
# Run local.
loss = ddp_model(input, offsets).sum()
loss.backward()
with dist_autograd.context() as context_id:
loss = ddp_model(input, offsets).sum()
dist_autograd.backward(context_id, [loss])
grads_dict = dist_autograd.get_gradients(context_id)
self.assertEqual(1, len(grads_dict))
self.assertEqual(model.weight.grad, grads_dict[model.weight])
@requires_gloo()
@dist_init
def test_ddp_dist_autograd_local_vs_remote(self):
# Each trainer uses a different random seed. Otherwise, they are going
# to have exactly the same initial model parameters, input, and
# therefore grads. That means the grads will be the same before and
# after DDP's all-reduce.
torch.manual_seed(self.rank)
dist.init_process_group(
backend="gloo",
init_method=INIT_METHOD_TEMPLATE.format(file_name=self.file_name),
world_size=self.world_size,
rank=self.rank,
)
# Use two different remote device input string, w/ and w/o the default
# device string "cpu", respectively.
for remote_device in ["worker0/cpu", "worker0"]:
remote_layer1 = RemoteModule(
remote_device=remote_device, module_cls=nn.Linear, args=(10, 5, False)
)
layer1 = nn.Linear(10, 5, False)
# Start with the same parameters for remote and local
layer1.weight = remote_layer1.module_rref.to_here().weight
# Run local case.
layer2 = nn.Linear(5, 1)
inputs = torch.rand((10, 10))
ddp_model = DistributedDataParallel(layer2)
loss = ddp_model(layer1(inputs)).sum()
loss.backward()
# Run remote case.
with dist_autograd.context() as context_id:
loss = ddp_model(remote_layer1(inputs)).sum()
dist_autograd.backward(context_id, [loss])
grads_dict = dist_autograd.get_gradients(context_id)
dist.barrier()
self.assertEqual(layer2.weight.grad, grads_dict[layer2.weight])
self.assertEqual(
layer1.weight.grad,
rpc.rpc_sync(
"worker0",
CommonDdpComparisonTest.get_remote_grads,
args=(remote_layer1.module_rref, context_id),
),
)
class CudaDdpComparisonTest(CommonDdpComparisonTest):
@skip_if_lt_x_gpu(NUM_TRAINERS)
@requires_nccl()
@dist_init
@skip_if_rocm_multiprocess
def test_ddp_dist_autograd_local_vs_remote_gpu(self):
# Each trainer uses a different random seed. Otherwise, they are going
# to have exactly the same initial model parameters, input, and
# therefore grads. That means the grads will be the same before and
# after DDP's all-reduce.
torch.manual_seed(self.rank)
dist.init_process_group(
backend="gloo",
init_method=INIT_METHOD_TEMPLATE.format(file_name=self.file_name),
world_size=self.world_size,
rank=self.rank,
)
remote_layer1 = RemoteModule(
remote_device="worker0/cpu", module_cls=nn.Linear, args=(10, 7, False)
)
layer1 = nn.Linear(10, 7, False)
# Start with the same parameters for remote and local
layer1.weight = remote_layer1.module_rref.to_here().weight
layer2 = nn.Linear(7, 5).cuda(self.rank)
ddp_layer2 = DistributedDataParallel(layer2, device_ids=[self.rank])
remote_layer3 = RemoteModule(
remote_device="worker0/cpu", module_cls=nn.Linear, args=(5, 3, False)
)
layer3 = nn.Linear(5, 3, False)
# Start with the same parameters for remote and local
layer3.weight = remote_layer3.module_rref.to_here().weight
layer4 = nn.Linear(3, 1).cuda(self.rank)
ddp_layer4 = DistributedDataParallel(layer4, device_ids=[self.rank])
# Run local case.
inputs = torch.rand((10, 10))
loss = ddp_layer4(
layer3(ddp_layer2(layer1(inputs).cuda(self.rank)).cpu()).cuda(self.rank)
).sum()
loss.backward()
# Run remote case.
with dist_autograd.context() as context_id:
loss = ddp_layer4(
remote_layer3(
ddp_layer2(remote_layer1(inputs).cuda(self.rank)).cpu()
).cuda(self.rank)
).sum()
dist_autograd.backward(context_id, [loss])
grads_dict = dist_autograd.get_gradients(context_id)
dist.barrier()
self.assertEqual(
layer1.weight.grad,
rpc.rpc_sync(
"worker0",
CommonDdpComparisonTest.get_remote_grads,
args=(remote_layer1.module_rref, context_id),
),
)
self.assertEqual(layer2.weight.grad, grads_dict[layer2.weight])
self.assertEqual(
layer3.weight.grad,
rpc.rpc_sync(
"worker0",
CommonDdpComparisonTest.get_remote_grads,
args=(remote_layer3.module_rref, context_id),
),
)
self.assertEqual(layer4.weight.grad, grads_dict[layer4.weight])
@@ -0,0 +1,68 @@
# mypy: allow-untyped-defs
from contextlib import contextmanager
from datetime import timedelta
from functools import partial, wraps
import torch.distributed as dist
import torch.distributed.distributed_c10d as c10d
class MockProcessGroup(dist.ProcessGroup):
def getBackendName(self):
return "mock_process_group"
def create_mock_pg(prefix_store, rank, world_size, timeout):
return MockProcessGroup(rank, world_size)
dist.Backend.register_backend("mock_process_group", create_mock_pg)
def mock_init_dist(rank, world_size):
# !!! WARNING !!!
# Kids don't try this at home, this is a cute pile of hacks that
# depends on a small mountain of c10d internals
if dist.is_initialized():
raise AssertionError("Expected dist to not be initialized")
store = dist.HashStore()
# Trick _store_based_barrier into believing everyone else already checked-in
# Zero is the group index
store.add(f"{c10d.STORE_BASED_BARRIER_PREFIX}:0", world_size - 1)
dist.init_process_group(
backend="mock_process_group",
rank=rank,
world_size=world_size,
store=store,
group_name="fake",
timeout=timedelta(seconds=1),
)
@contextmanager
def with_dist(rank=0, world_size=2):
"""
Context manager that initializer c10d with a fake process group.
"""
mock_init_dist(rank=rank, world_size=world_size)
try:
yield
finally:
dist.destroy_process_group()
def with_fake_comms(func=None, rank=0, world_size=2):
"""
Function wrapper that inits a fake process group designed for testing.
Right now only querying for world size is available
"""
if func is None:
return partial(with_fake_comms, rank=rank, world_size=world_size)
@wraps(func)
def wrapper(self, *args, **kwargs):
with with_dist(rank, world_size):
func(self, *args, **kwargs)
return wrapper
@@ -0,0 +1,35 @@
# mypy: allow-untyped-defs
import torch.distributed as dist
from torch._C._distributed_c10d import FakeProcessGroup
class FakeStore(dist.Store):
"""
A fake store is a fake Key-Value store simply for initialization usage
the of fake process group, one can either use FakeStore or HashStore.
"""
def _create_fake_pg(common_opts, backend_opts):
"""
A fake process group (not related to FakeTensor) is a process group which
doesn't actually do any communication, it just hallucinates some
communication. You can run a single rank with a fake process group
without needing multiple processes (simulates per-rank behavior)
NOTE: This is not a real process group, and it would produce wrong results
for every collective. It should be used as a convenient tool when playing
with distributed but don't care about the actual data.
"""
return FakeProcessGroup._create_internal(
common_opts.group_rank, common_opts.group_size, backend_opts
)
dist.Backend.register_backend(
dist.Backend.FAKE,
_create_fake_pg,
extended_api=True,
devices=["cpu", "cuda", "hpu", "xpu"],
)
@@ -0,0 +1,644 @@
# mypy: allow-untyped-defs
import sys
import threading
import weakref
from dataclasses import dataclass
from functools import partial, reduce
import torch
import torch.distributed as dist
from torch._C._distributed_c10d import (
_create_work_from_future,
AllgatherOptions,
AllreduceOptions,
AllToAllOptions,
BarrierOptions,
BroadcastOptions,
ReduceOp,
ReduceScatterOptions,
ScatterOptions,
Store,
)
from torch.distributed.distributed_c10d import _CollOp, _store_based_barrier, P2POp
from torch.futures import Future
from torch.utils import _pytree as pytree
"""
TODO:
Lots of missing collectives.
Collectives validation.
Make timeout robust by making collectives respect the test deadline.
Make tests robust by making collectives interruptible.
We need some synchronization around cleanup to ensure that timedout ranks don't cause spurious failures.
"""
def flatten_list(lst):
return pytree.tree_leaves(lst)
def ret_work(ret):
fut = Future()
fut.set_result(ret)
return _create_work_from_future(fut)
def binop_reduce(tensors, op):
res = op(torch.stack(tensors), dim=0)
if isinstance(res, torch.Tensor):
return res
# min/max return a namedtuple
return res.values
def bitwise_reduce(tensors, op):
return reduce(op, tensors)
_reduce_ops = {
ReduceOp.SUM: partial(binop_reduce, op=torch.sum),
ReduceOp.AVG: partial(binop_reduce, op=torch.mean),
ReduceOp.PRODUCT: partial(binop_reduce, op=torch.prod),
ReduceOp.MIN: partial(binop_reduce, op=torch.min),
ReduceOp.MAX: partial(binop_reduce, op=torch.max),
ReduceOp.BAND: partial(bitwise_reduce, op=torch.bitwise_and),
ReduceOp.BOR: partial(bitwise_reduce, op=torch.bitwise_or),
ReduceOp.BXOR: partial(bitwise_reduce, op=torch.bitwise_xor),
}
# Note [Hide collectives mutation from autograd]
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Threaded PG is intended to closely simulate the behavior of regular process
# groups. However, our regular PG implementations perform a dispatch through
# c10d, whereas Threaded PG does not for some reason (some superficial
# but not very convincing reasons include that Threaded PG is implemented
# in Python but you can't override Backend in Python, you can only override
# ProcessGroup in Python), thereby bypassing the dispatch step. Now we have
# a problem: c10d's signatures are LIES, they mutate their (output) tensor
# arguments but their annotations don't have mutations on them so we don't
# actually update any view metadata if you do differentiation. This
# ordinarily "doesn't matter" because distributed collectives aren't
# differentiable anyway, but it's possible to tickle this in testing if
# someone tries to touch the grad_fn of a Tensor. There a few ways to
# fix this, but the easiest way was to use the .detach() trick to hide
# the mutations from autograd.
class AllToAll:
@torch.no_grad()
def work(self, data):
world_size = len(data)
for dest_rank in range(world_size):
output_tensor_list, _ = data[dest_rank]
for src_rank in range(world_size):
_, input_tensor_list = data[src_rank]
# See Note [Hide collectives mutation from autograd]
output_tensor_list[src_rank].detach().copy_(
input_tensor_list[dest_rank]
)
class AllToAllBase:
@torch.no_grad()
def work(self, data):
world_size = len(data)
for dest_rank in range(world_size):
output_buffer, _, output_split_sizes, _ = data[dest_rank]
output_indexes = self._size_cumsum(
output_buffer.size(0), output_split_sizes, world_size
)
for src_rank in range(world_size):
_, input_buffer, _, input_split_sizes = data[src_rank]
input_indexes = self._size_cumsum(
input_buffer.size(0), input_split_sizes, world_size
)
# See Note [Hide collectives mutation from autograd]
output_buffer[
output_indexes[src_rank] : output_indexes[src_rank + 1]
].detach().copy_(
input_buffer[
input_indexes[dest_rank] : input_indexes[dest_rank + 1]
]
)
def _size_cumsum(
self,
buf_size: int,
sizes: torch.Tensor | list[int] | None,
world_size: int,
) -> torch.Tensor:
if sizes is None or len(sizes) == 0:
sizes = torch.full((world_size,), buf_size // world_size, dtype=torch.int64)
if not isinstance(sizes, torch.Tensor):
sizes = torch.tensor(sizes, dtype=torch.int64)
if sizes.dtype != torch.int64:
raise AssertionError(
f"Expected sizes.dtype == torch.int64, got {sizes.dtype}"
)
sizes = torch.cumsum(
torch.cat(
(torch.tensor([0], dtype=torch.int64, device=sizes.device), sizes),
dim=0,
),
dim=0,
)
return sizes
class AllReduce:
def __init__(self, op):
if op.op not in _reduce_ops:
raise NotImplementedError(
f"AllReduce op {op.op} not supported on multithreaded pg for now."
)
self.op = op.op
@torch.no_grad()
def work(self, data):
for i in range(len(data[0])):
# use rank0 as the device for sum
rank_0_device = data[0][i].device
# collect all data to the list and make them
# all on rank 0 device
tensors = [
data[src_rank][i].to(rank_0_device) for src_rank in range(len(data))
]
# now mimic reduce across all ranks
res = _reduce_ops[self.op](tensors)
# copy all the reduced value to each rank
for src_rank in range(len(data)):
# See Note [Hide collectives mutation from autograd]
data[src_rank][i].detach().copy_(res.to(data[src_rank][i].device))
class AllGather:
@torch.no_grad()
def work(self, data):
for src_rank in range(len(data)):
in_tensor_list = data[src_rank][1]
# Can't handle all_gather with multiple tensors
if len(in_tensor_list) != 1:
raise AssertionError(
f"Can't handle all_gather with multiple tensors, got {len(in_tensor_list)}"
)
src_tensor = in_tensor_list[0]
for dest in data:
dest_tensor = dest[0][0][src_rank]
# See Note [Hide collectives mutation from autograd]
dest_tensor.detach().copy_(src_tensor)
class Scatter:
def __init__(self, src):
self.src = src
@torch.no_grad()
def work(self, data):
src_in_tensor_list = data[self.src][1]
# Can't handle scatter with multiple input tensor list
if len(src_in_tensor_list) != 1:
raise AssertionError(
f"Can't handle scatter with multiple input tensor list, got {len(src_in_tensor_list)}"
)
src_in_tensors = src_in_tensor_list[0]
for rank, each_rank_data in enumerate(data):
out_tensor_list = each_rank_data[0]
# Can't handle scatter with multiple output tensor
if len(out_tensor_list) != 1:
raise AssertionError(
f"Can't handle scatter with multiple output tensor, got {len(out_tensor_list)}"
)
dest_tensor = out_tensor_list[0]
# See Note [Hide collectives mutation from autograd]
dest_tensor.detach().copy_(src_in_tensors[rank])
class Gather:
def __init__(self, dst):
self.dst = dst
@torch.no_grad()
def work(self, data):
# Can't handle gather with multiple tensor lists
if len(data[self.dst][0]) != 1:
raise AssertionError(
f"Can't handle gather with multiple tensor lists, got {len(data[self.dst][0])}"
)
out_tensor_list = data[self.dst][0][0]
for rank, each_rank_data in enumerate(data):
src_in_tensor_list = each_rank_data[1]
# Can't handle gather with multiple tensor lists
if len(src_in_tensor_list) != 1:
raise AssertionError(
f"Can't handle gather with multiple tensor lists, got {len(src_in_tensor_list)}"
)
dest_tensor = out_tensor_list[rank]
# See Note [Hide collectives mutation from autograd]
dest_tensor.detach().copy_(src_in_tensor_list[0])
class ReduceScatter:
def __init__(self, op):
if op != dist.ReduceOp.SUM and op != dist.ReduceOp.AVG:
raise NotImplementedError(f"ReduceScatter does not support {op}")
self.op = op
@torch.no_grad()
def work(self, data):
start_reduction = [False for _ in range(len(data))]
for each_rank_data in data:
# Can't handle reduce_scatter with multiple scatter list
if len(each_rank_data[1]) != 1:
raise AssertionError(
f"Can't handle reduce_scatter with multiple scatter list, got {len(each_rank_data[1])}"
)
to_scatter = each_rank_data[1][0]
for i in range(len(to_scatter)):
dest_tensor_on_rank_i = data[i][0]
# Can't handle reduce_scatter with multiple output tensor
if len(dest_tensor_on_rank_i) != 1:
raise AssertionError(
f"Can't handle reduce_scatter with multiple output tensor, got {len(dest_tensor_on_rank_i)}"
)
dst_tensor_device = dest_tensor_on_rank_i[0].device
if not start_reduction[i]:
# See Note [Hide collectives mutation from autograd]
dest_tensor_on_rank_i[0].detach().copy_(
to_scatter[i].to(dst_tensor_device)
)
start_reduction[i] = True
else:
# See Note [Hide collectives mutation from autograd]
dest_tensor_on_rank_i[0].detach().add_(
to_scatter[i].to(dst_tensor_device)
)
if self.op == dist.ReduceOp.AVG:
num_ranks = len(data)
for each_rank_data in data:
# See Note [Hide collectives mutation from autograd]
each_rank_data[0][0].detach().div_(num_ranks)
class Broadcast:
def __init__(self, src):
self.src = src
@torch.no_grad()
def work(self, data):
in_tensor_list = flatten_list(data[self.src])
for i in range(len(data)):
if i == self.src:
continue
out_tensor_list = flatten_list(data[i])
for j in range(len(in_tensor_list)):
# See Note [Hide collectives mutation from autograd]
out_tensor_list[j].detach().copy_(in_tensor_list[j])
class Collective:
def __init__(self, world_size, collective, pg):
self._world_size = world_size
self._collective = collective
self._start_cond = threading.Condition()
self._done_cond = threading.Condition()
self._data = [None] * world_size
self._count = 0
self._done = False
self._pg = pg
def join(self, rank, data):
with self._start_cond:
self._data[rank] = data
self._count += 1
# notify rank 0
if self._count == self._world_size:
if rank > 0:
self._start_cond.notify()
if rank == 0:
self._start_cond.wait_for(
lambda: self._count == self._world_size
or self._pg._terminate.is_set()
)
# SystemExit is not a subclass of Exception but BaseException
# and can be distinguished from normal exception raised from program errors
# so that we can hide it from the exception queue
if self._pg._terminate.is_set():
sys.exit("Test termination event occurs.")
with self._done_cond:
# wait for rank 0 to finish
if rank > 0:
self._done_cond.wait_for(
lambda: self._done or self._pg._terminate.is_set()
)
if self._pg._terminate.is_set():
sys.exit("Test termination event occurs.")
else:
# copy data around
self._collective.work(self._data)
self._done = True
self._done_cond.notify_all()
return ret_work(data)
class ProcessLocalGroup(dist.ProcessGroup):
_coll_lock = threading.Lock()
_cur_coll_on_pgs = {}
_terminate = threading.Event()
@classmethod
def _start_coll(cls, collective, pg):
with cls._coll_lock:
# pg_name is unique, we use that to record the mapping between pg and collective
if pg.pg_name not in cls._cur_coll_on_pgs:
cls._cur_coll_on_pgs[pg.pg_name] = Collective(
pg.size(), collective, cls
)
return cls._cur_coll_on_pgs[pg.pg_name]
@classmethod
def _end_coll(cls, collective, pg):
# This is racily called by all ranks, so only one will work
with cls._coll_lock:
if (
pg.pg_name in cls._cur_coll_on_pgs
and cls._cur_coll_on_pgs[pg.pg_name] == collective
):
cls._cur_coll_on_pgs.pop(pg.pg_name)
@classmethod
def exception_handle(cls, exc):
cls._terminate.set()
for coll in cls._cur_coll_on_pgs.values():
with coll._start_cond:
coll._start_cond.notify()
with coll._done_cond:
coll._done_cond.notify_all()
@classmethod
def reset(cls):
with cls._coll_lock:
cls._cur_coll_on_pgs = {}
cls._terminate.clear()
def alltoall_base(
self,
output_buffer: torch.Tensor,
input_buffer: torch.Tensor,
output_split_sizes: list[int] | None,
input_split_sizes: list[int] | None,
opts=AllToAllOptions(),
) -> torch.Tensor:
coll = ProcessLocalGroup._start_coll(AllToAllBase(), self)
res = coll.join(
self._rank,
(output_buffer, input_buffer, output_split_sizes, input_split_sizes),
)
ProcessLocalGroup._end_coll(coll, self)
return res
def alltoall(self, output_tensor_list, input_tensor_list, opts=AllToAllOptions()):
coll = ProcessLocalGroup._start_coll(AllToAll(), self)
res = coll.join(self._rank, (output_tensor_list, input_tensor_list))
ProcessLocalGroup._end_coll(coll, self)
return res
def allreduce(self, tensor_list, opts=AllreduceOptions()):
coll = ProcessLocalGroup._start_coll(AllReduce(opts.reduceOp), self)
res = coll.join(self._rank, tensor_list)
ProcessLocalGroup._end_coll(coll, self)
return res
def allreduce_coalesced(self, tensor_list, opts=AllreduceOptions()):
coll = ProcessLocalGroup._start_coll(AllReduce(opts.reduceOp), self)
res = coll.join(self._rank, tensor_list)
ProcessLocalGroup._end_coll(coll, self)
return res
def barrier(self, opts=BarrierOptions()):
return self.allreduce(tensor_list=[torch.ones(1)])
def allgather(self, output_tensors, input_tensor, opts=AllgatherOptions()):
coll = ProcessLocalGroup._start_coll(AllGather(), self)
res = coll.join(self._rank, (output_tensors, input_tensor))
ProcessLocalGroup._end_coll(coll, self)
return res
def _allgather_base(self, output_tensor, input_tensor, opts=AllgatherOptions()):
tensor_list = list(torch.chunk(output_tensor, self._world_size))
return self.allgather([tensor_list], [input_tensor], opts)
def broadcast(self, tensor_list, opts=BroadcastOptions()):
coll = ProcessLocalGroup._start_coll(Broadcast(opts.rootRank), self)
res = coll.join(self._rank, tensor_list)
ProcessLocalGroup._end_coll(coll, self)
return res
def scatter(self, output_tensors, input_tensors, opts=ScatterOptions()):
coll = ProcessLocalGroup._start_coll(Scatter(opts.rootRank), self)
res = coll.join(self._rank, (output_tensors, input_tensors))
ProcessLocalGroup._end_coll(coll, self)
return res
def gather(self, output_tensors, input_tensors, opts=ScatterOptions()):
coll = ProcessLocalGroup._start_coll(Gather(opts.rootRank), self)
res = coll.join(self._rank, (output_tensors, input_tensors))
ProcessLocalGroup._end_coll(coll, self)
return res
def reduce_scatter(self, output_tensor, scatter_list, opts=ReduceScatterOptions()):
coll = ProcessLocalGroup._start_coll(ReduceScatter(opts.reduceOp), self)
res = coll.join(self._rank, (output_tensor, scatter_list))
ProcessLocalGroup._end_coll(coll, self)
return res
def _reduce_scatter_base(
self, output_tensor, input_tensor, opts=ReduceScatterOptions()
):
tensor_list = list(torch.chunk(input_tensor, self._world_size))
return self.reduce_scatter([output_tensor], [tensor_list], opts)
def reduce_scatter_tensor_coalesced(
self, output_tensors, input_tensors, opts=ReduceScatterOptions()
):
works = [
self._reduce_scatter_base(output_tensor, input_tensor, opts)
for output_tensor, input_tensor in zip(
output_tensors, input_tensors, strict=True
)
]
for work in works[:-1]:
work.wait()
return works[-1]
def allgather_into_tensor_coalesced(
self, output_tensor_list, input_tensor_list, opts=AllgatherOptions()
):
res = None
for o_t, i_t in zip(output_tensor_list, input_tensor_list, strict=True):
res = self._allgather_base(o_t, i_t)
return res
def __init__(self, rank, world_size):
super().__init__(rank, world_size)
self._rank = rank
self._world_size = world_size
world = dist.distributed_c10d._world
if isinstance(world, ThreadLocalWorld):
world = world._get_world()
self._world = weakref.ref(world)
self._ctx = torch.autograd.set_multithreading_enabled(False)
def size(self):
return self._world_size
@property
def pg_name(self):
"""
return the global registered name of the current pg in the world
"""
return self._world().pg_names[self]
@property
def group_name(self):
return self.pg_name
def getBackendName(self):
return "threaded"
def __repr__(self):
return f"ThreadedPG world_size:{self._world_size} rank:{self._rank}"
def _create_threaded_pg(prefix_store, rank, world_size, timeout):
pg = ProcessLocalGroup(rank, world_size)
# https://github.com/pytorch/pytorch/pull/103033 changed store based barrier to optional
# When device mesh involves sub groups while store based barrier is not enabled in c10d,
# even though threaded pg actual collectives are assumed to be single threaded,
# different threads may be initializing different groups,
# leading to race conditions.
# For example, if we have a mesh of [[0, 1], [2, 3]], the sub groups
# (dim 0 and 1) would be initialized in different threads independently.
# In this case we can no longer rely on class or global variables
# but have to rely on store based barrier to make sure each group
# is ready separately before we can invoke collectives in any of the groups.
# the prefix store is already per group so we pass an empty name here
_store_based_barrier(rank, prefix_store, "", world_size, timeout)
return pg
dist.Backend.register_backend("threaded", _create_threaded_pg, devices=["cpu", "cuda"])
@dataclass
class WorldData:
default_pg: dist.ProcessGroup
pg_map: dict[dist.ProcessGroup, tuple[str, Store | None]]
pg_names: dict[dist.ProcessGroup, str]
pg_group_ranks: dict[dist.ProcessGroup, dict[int, int]]
pg_backend_config: dict[dist.ProcessGroup, str]
group_count: int
tags_to_pg: dict[str, list[dist.ProcessGroup]]
pg_to_tag: dict[dist.ProcessGroup, str]
pg_coalesce_state: dict[dist.ProcessGroup, list[_CollOp | P2POp]]
comms: list
class ThreadLocalWorld:
_world = threading.local()
def _get_world(self) -> WorldData:
if not hasattr(ThreadLocalWorld._world, "world"):
ThreadLocalWorld._world.world = WorldData(
None, {}, {}, {}, {}, 0, {}, {}, {}, []
)
return ThreadLocalWorld._world.world
@property
def default_pg(self):
return self._get_world().default_pg
@default_pg.setter
def default_pg(self, value):
self._get_world().default_pg = value
@property
def pg_map(self):
return self._get_world().pg_map
@property
def pg_names(self):
return self._get_world().pg_names
@property
def pg_group_ranks(self):
return self._get_world().pg_group_ranks
@property
def pg_backend_config(self):
return self._get_world().pg_backend_config
@property
def group_count(self) -> int:
return self._get_world().group_count
@group_count.setter
def group_count(self, value):
self._get_world().group_count = value
@property
def tags_to_pg(self):
return self._get_world().tags_to_pg
@property
def pg_to_tag(self):
return self._get_world().pg_to_tag
@property
def pg_coalesce_state(self) -> dict[dist.ProcessGroup, list[_CollOp | P2POp]]:
return self._get_world().pg_coalesce_state
@property
def comms(self):
return self._get_world().comms
_old_pg_world = None
_ctx_manager = None
def _install_threaded_pg():
global _old_pg_world
global _ctx_manager
_old_pg_world = dist.distributed_c10d._world
dist.distributed_c10d._world = ThreadLocalWorld()
_ctx_manager = torch.autograd.set_multithreading_enabled(False)
return dist.distributed_c10d._world
def _uninstall_threaded_pg():
global _ctx_manager
dist.distributed_c10d._world = _old_pg_world
# Restore autograd multithreading state that was disabled in _install_threaded_pg
if _ctx_manager is not None:
_ctx_manager.__exit__(None, None, None)
_ctx_manager = None
@@ -0,0 +1,754 @@
# mypy: allow-untyped-defs
import enum
import torch
import torch.distributed.rpc as rpc
import torch.testing._internal.dist_utils as dist_utils
from torch import nn, Tensor
from torch._jit_internal import Future
from torch.distributed.nn import RemoteModule
from torch.distributed.nn.api.remote_module import (
_REMOTE_MODULE_PICKLED_ATTRIBUTES,
_RemoteModule,
)
from torch.testing._internal.common_distributed import skip_if_lt_x_gpu
from torch.testing._internal.common_utils import TemporaryFileName, TEST_WITH_ROCM
from torch.testing._internal.distributed.rpc.rpc_agent_test_fixture import (
RpcAgentTestFixture,
)
_PARAM_VAL = torch.nn.Parameter(torch.ones(1))
# RPC handler for querying the device on the destination worker.
def remote_device(module_rref):
for param in module_rref.local_value().parameters():
return param.device
# RPC handler for querying __dict__ on the destination worker.
def remote_module_attributes(remote_module):
return remote_module.__dict__
# RPC handler for running forward on the destination worker.
def remote_forward(remote_module, args):
return remote_module.forward(*args)
# RPC handler for running forward_async on the destination worker.
def remote_forward_async(remote_module, args):
# Since future cannot be pickled and sent over the RPC layer,
# have to wait and behave just like ``forward_sync``.
return remote_module.forward_async(*args).wait()
# RPC handler for getting training mode on the destination worker.
def get_remote_training_arg(module_rref):
return module_rref.local_value().training
class ModuleCreationMode(enum.Enum):
MODULE_CTOR_WITH_INTERFACE = "module_ctor_with_interface"
MODULE_CTOR = "module_ctor"
@torch.jit.interface
class MyModuleInterface:
def forward(
self, tensor: Tensor, number: int, word: str = "default"
) -> tuple[str, int, Tensor]:
# pyre-ignore[7]: Pyre and torch.jit.interface don't mix well
pass
@torch.jit.interface
class RemoteMyModuleInterface:
def forward(
self, tensor: Tensor, number: int, word: str = "default"
) -> tuple[str, int, Tensor]:
# pyre-ignore[7]: Pyre and torch.jit.interface don't mix well
pass
def forward_async(
self, tensor: Tensor, number: int, word: str = "default"
) -> Future[tuple[str, int, Tensor]]:
pass
class MyModule(nn.Module):
def __init__(self, first_arg, first_kwarg=-1):
super().__init__()
self.param1 = _PARAM_VAL
def forward(
self, tensor: Tensor, number: int, word: str = "default"
) -> tuple[str, int, Tensor]:
return word, number, tensor
class BadModule:
def __init__(self, first_arg, first_kwarg=-1):
pass
def create_scripted_module(first_arg, first_kwarg=-1):
module = MyModule(first_arg, first_kwarg=first_kwarg)
scripted_module = torch.jit.script(module)
return scripted_module
# Common utils for both CPU and CUDA test suites
class CommonRemoteModuleTest(RpcAgentTestFixture):
@property
def world_size(self): # Override setting in RpcAgentTestFixture
return 2
@staticmethod
def _create_remote_module_iter(remote_device, modes=None):
if modes is None:
modes = ModuleCreationMode.__members__.values()
args = (1,)
kwargs = dict(first_kwarg=2)
if ModuleCreationMode.MODULE_CTOR in modes:
remote_module = RemoteModule(remote_device, MyModule, args, kwargs)
yield remote_module
if ModuleCreationMode.MODULE_CTOR_WITH_INTERFACE in modes:
remote_module = _RemoteModule(
remote_device,
create_scripted_module,
args,
kwargs,
_module_interface_cls=MyModuleInterface,
)
scripted_remote_module = torch.jit.script(remote_module)
yield scripted_remote_module
class RemoteModuleTest(CommonRemoteModuleTest):
@dist_utils.dist_init
def test_bad_module(self):
if self.rank != 0:
return
dst_worker_name = dist_utils.worker_name((self.rank + 1) % self.world_size)
remote_device = f"{dst_worker_name}/cpu"
args = (1,)
kwargs = dict(first_kwarg=2)
with self.assertRaisesRegex(
ValueError,
r"Expect `module_cls\(\*args, \*\*kwargs\)` returns an instance of <class nn.Module>,",
):
RemoteModule(remote_device, BadModule, args, kwargs).forward()
with self.assertRaisesRegex(
ValueError,
r"Expect `module_cls\(\*args, \*\*kwargs\)` returns an instance of <class nn.Module>,",
):
RemoteModule(remote_device, BadModule, args, kwargs).forward()
@dist_utils.dist_init
def test_forward_async(self):
if self.rank != 0:
return
dst_worker_name = dist_utils.worker_name((self.rank + 1) % self.world_size)
args = (torch.ones(1), 2, "3")
for remote_module in self._create_remote_module_iter(dst_worker_name):
ret_fut = remote_module.forward_async(*args)
ret = ret_fut.wait()
self.assertEqual(ret, tuple(reversed(args)))
@dist_utils.dist_init
def test_forward_async_script(self):
if self.rank != 0:
return
dst_worker_name = dist_utils.worker_name((self.rank + 1) % self.world_size)
scripted_remote_module = next(
self._create_remote_module_iter(
dst_worker_name, modes=[ModuleCreationMode.MODULE_CTOR_WITH_INTERFACE]
)
)
@torch.jit.script
def run_forward_async(scripted_remote_module: RemoteMyModuleInterface):
ret_fut = scripted_remote_module.forward_async(torch.ones(1), 2, "3")
ret = ret_fut.wait()
return ret
ret = run_forward_async(scripted_remote_module)
self.assertEqual(ret, ("3", 2, torch.ones(1)))
@dist_utils.dist_init
def test_forward_sync(self):
if self.rank != 0:
return
dst_worker_name = dist_utils.worker_name((self.rank + 1) % self.world_size)
args = (torch.ones(1), 2, "3")
for remote_module in self._create_remote_module_iter(dst_worker_name):
ret = remote_module.forward(*args)
self.assertEqual(ret, tuple(reversed(args)))
@dist_utils.dist_init
def test_forward_sync_script(self):
if self.rank != 0:
return
dst_worker_name = dist_utils.worker_name((self.rank + 1) % self.world_size)
scripted_remote_module = next(
self._create_remote_module_iter(
dst_worker_name, modes=[ModuleCreationMode.MODULE_CTOR_WITH_INTERFACE]
)
)
@torch.jit.script
def run_forward(scripted_remote_module: MyModuleInterface):
ret = scripted_remote_module.forward(torch.ones(1), 2, "3")
return ret
ret = run_forward(scripted_remote_module)
self.assertEqual(ret, ("3", 2, torch.ones(1)))
@dist_utils.dist_init
def test_forward_with_kwargs(self):
if self.rank != 0:
return
dst_worker_name = dist_utils.worker_name((self.rank + 1) % self.world_size)
args = (torch.ones(1), 2)
kwargs = dict(word="3")
# Only test Python nn.Module, because script module methods don't support taking kwargs.
for remote_module in self._create_remote_module_iter(
dst_worker_name, modes=[ModuleCreationMode.MODULE_CTOR]
):
ret_fut = remote_module.forward_async(*args, **kwargs)
ret = ret_fut.wait()
self.assertEqual(ret, tuple(reversed(args + ("3",))))
ret = remote_module.forward(*args, **kwargs)
self.assertEqual(ret, tuple(reversed(args + ("3",))))
@dist_utils.dist_init
def test_remote_parameters(self):
if self.rank != 0:
return
dst_worker_name = dist_utils.worker_name((self.rank + 1) % self.world_size)
# Only test Python nn.Module, because script module methods don't support ``remote_parameters``.
for remote_module in self._create_remote_module_iter(
dst_worker_name, modes=[ModuleCreationMode.MODULE_CTOR]
):
param_rrefs = remote_module.remote_parameters()
self.assertEqual(len(param_rrefs), 1)
self.assertTrue(torch.equal(param_rrefs[0].to_here(), _PARAM_VAL))
@dist_utils.dist_init
def test_get_module_rref(self):
if self.rank != 0:
return
dst_worker_name = dist_utils.worker_name((self.rank + 1) % self.world_size)
# Only test Python nn.Module, because script module methods don't support ``get_module_rref``.
for remote_module in self._create_remote_module_iter(
dst_worker_name, modes=[ModuleCreationMode.MODULE_CTOR]
):
rref = remote_module.get_module_rref()
self.assertEqual(rref, remote_module.module_rref)
for param in rref.to_here().parameters():
self.assertTrue(torch.equal(param, _PARAM_VAL))
@dist_utils.dist_init
def test_train_eval(self):
if self.rank != 0:
return
dst_worker_name = dist_utils.worker_name((self.rank + 1) % self.world_size)
for remote_module in self._create_remote_module_iter(
dst_worker_name, modes=[ModuleCreationMode.MODULE_CTOR]
):
remote_module.train()
ret1 = rpc.rpc_sync(
dst_worker_name,
get_remote_training_arg,
args=(remote_module.get_module_rref(),),
)
self.assertEqual(ret1, True)
remote_module.eval()
ret2 = rpc.rpc_sync(
dst_worker_name,
get_remote_training_arg,
args=(remote_module.get_module_rref(),),
)
self.assertEqual(ret2, False)
@dist_utils.dist_init
def test_unsupported_methods(self):
if self.rank != 0:
return
dst_worker_name = dist_utils.worker_name((self.rank + 1) % self.world_size)
for remote_module in self._create_remote_module_iter(
dst_worker_name, modes=[ModuleCreationMode.MODULE_CTOR]
):
with self.assertRaisesRegex(
ValueError, r"Method ``register_buffer`` not supported for RemoteModule"
):
remote_module.register_buffer("buffer", torch.ones(5))
with self.assertRaisesRegex(
ValueError,
r"Method ``register_parameter`` not supported for RemoteModule",
):
remote_module.register_parameter(
"param", torch.nn.Parameter(torch.ones(1))
)
with self.assertRaisesRegex(
ValueError, r"Method ``add_module`` not supported for RemoteModule"
):
remote_module.add_module("empty", None)
with self.assertRaisesRegex(
ValueError, r"Method ``apply`` not supported for RemoteModule"
):
fn = torch.rand((3, 3), requires_grad=False)
remote_module.apply(fn)
with self.assertRaisesRegex(
ValueError, r"Method ``cuda`` not supported for RemoteModule"
):
remote_module.cuda()
with self.assertRaisesRegex(
ValueError, r"Method ``cpu`` not supported for RemoteModule"
):
remote_module.cpu()
with self.assertRaisesRegex(
ValueError, r"Method ``type`` not supported for RemoteModule"
):
remote_module.type(torch.FloatTensor)
with self.assertRaisesRegex(
ValueError, r"Method ``float`` not supported for RemoteModule"
):
remote_module.float()
with self.assertRaisesRegex(
ValueError, r"Method ``double`` not supported for RemoteModule"
):
remote_module.double()
with self.assertRaisesRegex(
ValueError, r"Method ``bfloat16`` not supported for RemoteModule"
):
remote_module.bfloat16()
with self.assertRaisesRegex(
ValueError, r"Method ``to`` not supported for RemoteModule"
):
remote_module.to("cpu", dtype=torch.int32)
def hook(module, grad_input, grad_output):
pass
with self.assertRaisesRegex(
ValueError,
r"Method ``register_backward_hook`` not supported for RemoteModule",
):
remote_module.register_backward_hook(hook)
with self.assertRaisesRegex(
ValueError,
r"Method ``register_forward_pre_hook`` not supported for RemoteModule",
):
remote_module.register_forward_pre_hook(hook)
with self.assertRaisesRegex(
ValueError,
r"Method ``register_forward_hook`` not supported for RemoteModule",
):
remote_module.register_forward_hook(hook)
with self.assertRaisesRegex(
ValueError, r"Method ``state_dict`` not supported for RemoteModule"
):
remote_module.state_dict()
with self.assertRaisesRegex(
ValueError, r"Method ``load_state_dict`` not supported for RemoteModule"
):
remote_module.load_state_dict({})
with self.assertRaisesRegex(
ValueError,
r"Method ``parameters`` not supported for RemoteModule. Please use ``remote_parameters`` instead.",
):
remote_module.parameters()
with self.assertRaisesRegex(
ValueError,
r"Method ``named_parameters`` not supported for RemoteModule",
):
remote_module.named_parameters()
with self.assertRaisesRegex(
ValueError, r"Method ``buffers`` not supported for RemoteModule"
):
remote_module.buffers()
with self.assertRaisesRegex(
ValueError, r"Method ``named_buffers`` not supported for RemoteModule"
):
remote_module.named_buffers()
with self.assertRaisesRegex(
ValueError, r"Method ``children`` not supported for RemoteModule"
):
remote_module.children()
with self.assertRaisesRegex(
ValueError, r"Method ``named_children`` not supported for RemoteModule"
):
remote_module.named_children()
with self.assertRaisesRegex(
ValueError, r"Method ``modules`` not supported for RemoteModule"
):
remote_module.modules()
with self.assertRaisesRegex(
ValueError, r"Method ``named_modules`` not supported for RemoteModule"
):
remote_module.named_modules()
with self.assertRaisesRegex(
ValueError, r"Method ``requires_grad_`` not supported for RemoteModule"
):
remote_module.requires_grad_()
with self.assertRaisesRegex(
ValueError, r"Method ``zero_grad`` not supported for RemoteModule"
):
remote_module.zero_grad()
with self.assertRaisesRegex(
ValueError, r"Method ``share_memory`` not supported for RemoteModule"
):
remote_module.share_memory()
with self.assertRaisesRegex(
ValueError, r"Method ``extra_repr`` not supported for RemoteModule"
):
remote_module.extra_repr()
@dist_utils.dist_init
def test_send_remote_module_with_a_new_attribute_not_pickled_over_the_wire(self):
if self.rank != 0:
return
dst_worker_name = dist_utils.worker_name((self.rank + 1) % self.world_size)
# If a new attribute is added to this RemoteModule after the initialization,
# and it will be sent over the wire by RPC,
# this new field will not be pickled, because it's not specified in _REMOTE_MODULE_PICKLED_ATTRIBUTES.
# Note that adding a new attribute out of constructor should rarely happen.
# If a new attribute is added to RemoteModule constructor,
# there is a sanity check to enforce developers to add this attribute to either
# _REMOTE_MODULE_PICKLED_ATTRIBUTES or _REMOTE_MODULE_ATTRIBUTES_IGNORE_FOR_PICKLING.
for remote_module in self._create_remote_module_iter(
dst_worker_name, modes=[ModuleCreationMode.MODULE_CTOR]
):
new_attr_name = "new_attr"
setattr(remote_module, new_attr_name, 1)
attrs = rpc.rpc_sync(
dst_worker_name, remote_module_attributes, (remote_module,)
)
self.assertNotIn(new_attr_name, attrs)
@dist_utils.dist_init
def test_remote_module_py_pickle_not_supported(self):
if self.rank != 0:
return
dst_worker_name = dist_utils.worker_name((self.rank + 1) % self.world_size)
for remote_module in self._create_remote_module_iter(
dst_worker_name, modes=[ModuleCreationMode.MODULE_CTOR]
):
with TemporaryFileName() as fname:
with self.assertRaisesRegex(
RuntimeError,
"Cannot pickle RemoteModule in python pickler. RemoteModule can only be pickled when using RPC",
):
torch.save(remote_module, fname)
@dist_utils.dist_init
def test_remote_module_py_pickle_not_supported_script(self):
if self.rank != 0:
return
dst_worker_name = dist_utils.worker_name((self.rank + 1) % self.world_size)
for remote_module in self._create_remote_module_iter(
dst_worker_name, modes=[ModuleCreationMode.MODULE_CTOR_WITH_INTERFACE]
):
with (
TemporaryFileName() as fname,
self.assertRaisesRegex(
torch.jit.Error, "can only be pickled when using RPC"
),
):
torch.save(remote_module, fname)
class ThreeWorkersRemoteModuleTest(CommonRemoteModuleTest):
@property
def world_size(self): # Override setting in CommonRemoteModuleTest
return 3
@dist_utils.dist_init
def test_send_remote_module_over_the_wire(self):
if self.rank != 0:
return
dst_worker1_name = dist_utils.worker_name((self.rank + 1) % self.world_size)
dst_worker2_name = dist_utils.worker_name((self.rank + 2) % self.world_size)
# Unpickled attributes include both the inherent attributes of RemoteModule
# (not inherited from the superclass) and two installed methods.
expected_unpickled_attrs = list(_REMOTE_MODULE_PICKLED_ATTRIBUTES)
expected_unpickled_attrs.append("forward_async")
expected_unpickled_attrs.append("forward")
# Create a remote module on worker1 and then pass it to worker2 over the RPC layer.
for remote_module in self._create_remote_module_iter(
dst_worker1_name, modes=[ModuleCreationMode.MODULE_CTOR]
):
# Test querying some simple attributes from worker2.
attrs = rpc.rpc_sync(
dst_worker2_name, remote_module_attributes, (remote_module,)
)
self.assertListEqual(list(attrs.keys()), expected_unpickled_attrs)
self.assertEqual(attrs["on"], "worker1")
self.assertEqual(attrs["device"], "cpu")
self.assertFalse(attrs["is_device_map_set"])
self.assertFalse(attrs["is_scriptable"])
# Test the installed methods on worker1's can be initiated by worker2 over RPC layer.
# NOTE: In practice a remote module should be directly stored on the worker that runs ``forward``` or ``forward_async``,
# not have another worker to initiate forward over the RPC layer.
args = (torch.ones(1), 2, "3")
ret1 = rpc.rpc_sync(dst_worker2_name, remote_forward, (remote_module, args))
self.assertEqual(ret1, tuple(reversed(args)))
ret2 = rpc.rpc_sync(
dst_worker2_name, remote_forward_async, (remote_module, args)
)
self.assertEqual(ret2, tuple(reversed(args)))
@dist_utils.dist_init
def test_send_remote_module_over_the_wire_script_not_supported(self):
if self.rank != 0:
return
dst_worker1_name = dist_utils.worker_name((self.rank + 1) % self.world_size)
dst_worker2_name = dist_utils.worker_name((self.rank + 2) % self.world_size)
# Unpickled attributes include both the inherent attributes of RemoteModule
# (not inherited from the superclass) and two installed methods.
expected_unpickled_attrs = list(_REMOTE_MODULE_PICKLED_ATTRIBUTES)
expected_unpickled_attrs.append("forward_async")
expected_unpickled_attrs.append("forward")
with self.assertRaisesRegex(
RuntimeError, "Passing a script RemoteModule over RPC is not supported."
):
# Create a remote module on worker1 and then pass it to worker2 over the RPC layer.
for remote_module in self._create_remote_module_iter(
dst_worker1_name, modes=[ModuleCreationMode.MODULE_CTOR_WITH_INTERFACE]
):
# Test querying some simple attributes from worker2.
rpc.rpc_sync(
dst_worker2_name, remote_module_attributes, (remote_module,)
)
@dist_utils.dist_init
def test_create_remote_module_from_module_rref(self):
if self.rank != 0:
return
dst_worker1_name = dist_utils.worker_name((self.rank + 1) % self.world_size)
dst_worker2_name = dist_utils.worker_name((self.rank + 2) % self.world_size)
# Create a remote module on worker1 and then pass its `module_rref` to worker2 over the RPC layer.
for remote_module in self._create_remote_module_iter(
dst_worker1_name, modes=[ModuleCreationMode.MODULE_CTOR]
):
remote_module2 = rpc.rpc_sync(
dst_worker2_name,
RemoteModule.init_from_module_rref,
(dst_worker2_name, remote_module.get_module_rref()),
)
args = (torch.ones(1), 2, "3")
ret1 = rpc.rpc_sync(dst_worker1_name, remote_forward, (remote_module, args))
ret2 = rpc.rpc_sync(
dst_worker2_name, remote_forward, (remote_module2, args)
)
self.assertEqual(ret1, ret2)
class CudaRemoteModuleTest(CommonRemoteModuleTest):
@skip_if_lt_x_gpu(1)
@dist_utils.dist_init
def test_valid_device(self):
if self.rank != 0:
return
dst_rank = (self.rank + 1) % self.world_size
dst_worker_name = dist_utils.worker_name(dst_rank)
for remote_module in self._create_remote_module_iter(
f"{dst_worker_name}/cuda:0", modes=[ModuleCreationMode.MODULE_CTOR]
):
device = rpc.rpc_sync(
dst_worker_name, remote_device, (remote_module.module_rref,)
)
self.assertEqual(device.type, "cuda")
self.assertEqual(device.index, 0)
# Test rank works as well.
for remote_module in self._create_remote_module_iter(
f"rank:{dst_rank}/cuda:0", modes=[ModuleCreationMode.MODULE_CTOR]
):
device = rpc.rpc_sync(
dst_worker_name, remote_device, (remote_module.module_rref,)
)
self.assertEqual(device.type, "cuda")
self.assertEqual(device.index, 0)
@skip_if_lt_x_gpu(1)
@dist_utils.dist_init
def test_invalid_devices(self):
if self.rank != 0:
return
dst_worker_name = dist_utils.worker_name((self.rank + 1) % self.world_size)
with self.assertRaisesRegex(
RuntimeError,
r"Expected one of .+ device type at start of device string",
):
[
m.forward()
for m in self._create_remote_module_iter(
f"{dst_worker_name}/foo",
modes=[ModuleCreationMode.MODULE_CTOR],
)
]
if TEST_WITH_ROCM:
errorString = (
r"HIP error: invalid device ordinal\n"
r"HIP kernel errors might be asynchronously reported at some other API call, "
r"so the stacktrace below might be incorrect.\n"
r"For debugging consider passing AMD_SERIALIZE_KERNEL=3"
)
else:
errorString = r"CUDA error: invalid device ordinal"
with self.assertRaisesRegex(RuntimeError, errorString):
[
m.forward()
for m in self._create_remote_module_iter(
f"{dst_worker_name}/cuda:100",
modes=[ModuleCreationMode.MODULE_CTOR],
)
]
with self.assertRaisesRegex(RuntimeError, r"Invalid device string: 'cpu2'"):
[
m.forward()
for m in self._create_remote_module_iter(
f"{dst_worker_name}/cpu2",
modes=[ModuleCreationMode.MODULE_CTOR],
)
]
with self.assertRaisesRegex(RuntimeError, r"Device string must not be empty"):
[
m.forward()
for m in self._create_remote_module_iter(
f"{dst_worker_name}/",
modes=[ModuleCreationMode.MODULE_CTOR],
)
]
with self.assertRaisesRegex(
ValueError,
r"Could not parse remote_device: worker1/cuda:0/cuda:1. The valid format is '<workername>/<device>'",
):
[
m.forward()
for m in self._create_remote_module_iter(
f"{dst_worker_name}/cuda:0/cuda:1",
modes=[ModuleCreationMode.MODULE_CTOR],
)
]
with self.assertRaisesRegex(
ValueError,
r"Could not parse remote_device: /. The valid format is '<workername>/<device>'",
):
[
m.forward()
for m in self._create_remote_module_iter(
"/",
modes=[ModuleCreationMode.MODULE_CTOR],
)
]
with self.assertRaisesRegex(
ValueError,
r"Could not parse remote_device: /cuda:0. The valid format is '<workername>/<device>'",
):
[
m.forward()
for m in self._create_remote_module_iter(
"/cuda:0",
modes=[ModuleCreationMode.MODULE_CTOR],
)
]
@skip_if_lt_x_gpu(1)
@dist_utils.dist_init
def test_input_moved_to_cuda_device(self):
if self.rank != 0:
return
dst_worker_name = dist_utils.worker_name((self.rank + 1) % self.world_size)
# These two CPU tensors (in args and kwargs) should be implicitly moved to an appropriate cuda device.
t1 = torch.ones(1)
args = (t1, 2)
t2 = t1 * 2
kwargs = dict(word=t2)
# Only test Python nn.Module, because script module methods don't support taking kwargs.
for remote_module in self._create_remote_module_iter(
f"{dst_worker_name}/cuda:0", modes=[ModuleCreationMode.MODULE_CTOR]
):
ret_fut = remote_module.forward_async(*args, **kwargs)
ret = ret_fut.wait()
self.assertEqual(ret, tuple(reversed(args + (t2,))))
# TODO: Once the RPC backend can support directly sending GPU tensors, the expected device type should be "cuda:0".
self.assertEqual(ret[0].device.type, "cpu")
self.assertEqual(ret[2].device.type, "cpu")
ret = remote_module.forward(*args, **kwargs)
self.assertEqual(ret, tuple(reversed(args + (t2,))))
# TODO: Once the RPC backend can support directly sending GPU tensors, the expected device type should be "cuda:0".
self.assertEqual(ret[0].device.type, "cpu")
self.assertEqual(ret[2].device.type, "cpu")
@skip_if_lt_x_gpu(1)
@dist_utils.dist_init
def test_input_moved_to_cuda_device_script(self):
if self.rank != 0:
return
dst_worker_name = dist_utils.worker_name((self.rank + 1) % self.world_size)
scripted_remote_module = next(
self._create_remote_module_iter(
f"{dst_worker_name}/cuda:0",
modes=[ModuleCreationMode.MODULE_CTOR_WITH_INTERFACE],
)
)
@torch.jit.script
def run_forward(scripted_remote_module: MyModuleInterface):
ret = scripted_remote_module.forward(torch.ones(1), 2, "3")
return ret
ret = run_forward(scripted_remote_module)
self.assertEqual(ret, ("3", 2, torch.ones(1)))
# TODO: Once the RPC backend can support directly sending GPU tensors, the expected device type should be "cuda:0".
self.assertEqual(ret[2].device.type, "cpu")
@@ -0,0 +1,281 @@
# mypy: allow-untyped-defs
import threading
import torch
import torch.distributed.autograd as dist_autograd
import torch.distributed.rpc as rpc
from torch import optim
from torch.distributed.optim import DistributedOptimizer
from torch.testing._internal.dist_utils import dist_init
from torch.testing._internal.distributed.rpc.rpc_agent_test_fixture import (
RpcAgentTestFixture,
)
class MyModule:
lock = threading.Lock()
def __init__(self, requires_grad=True):
# cannot directly use torch.manual_seed(0) as all threads share the same
# default generator. The race from multiple RPC threads could mess up
# the draw order from the default RNG instance, leading to
# non-deterministic behavior. Hence, create a dedicated RNG here.
g_cpu = torch.Generator()
g_cpu.manual_seed(0)
self.w = torch.rand((3, 3), requires_grad=requires_grad, generator=g_cpu)
def forward(self, t1):
return torch.mm(self.w, t1)
def get_w(self):
return self.w
class FailingOptimizer(optim.Optimizer):
def __init__(self, params):
super().__init__(params, {})
def step(self, closure=None):
raise ValueError("Error running optimizer.")
class OptimizerFailingOnConstructor(optim.Optimizer):
def __init__(self, params):
super().__init__(params, {})
raise ValueError("Error creating optimizer.")
def step(self, closure=None):
raise NotImplementedError
def _call_method(method, obj_rref, *args, **kwargs):
return method(obj_rref.local_value(), *args, **kwargs)
def remote_method(method, obj_rref, *args, **kwargs):
"""
Call rpc.remote on a method in a remote object.
Args:
method: the method (for example, Class.method)
obj_rref (RRef): remote reference to the object
args: positional arguments to pass to the method
kwargs: keyword arguments to pass to the method
Returns a RRef to the remote method call result.
"""
return rpc.remote(
obj_rref.owner(),
_call_method,
args=[method, obj_rref] + list(args),
kwargs=kwargs,
)
def rpc_async_method(method, obj_rref, *args, **kwargs):
"""
Call rpc.rpc_async on a method in a remote object.
Args:
method: the method (for example, Class.method)
obj_rref (RRef): remote reference to the object
args: positional arguments to pass to the method
kwargs: keyword arguments to pass to the method
Returns a Future to the method call result.
"""
return rpc.rpc_async(
obj_rref.owner(),
_call_method,
args=[method, obj_rref] + list(args),
kwargs=kwargs,
)
class DistOptimizerTest(RpcAgentTestFixture):
@dist_init()
def test_dist_optim_exception(self):
# distributed version
owner1 = f"worker{(self.rank + 1) % self.world_size:d}"
owner2 = f"worker{(self.rank + 2) % self.world_size:d}"
remote_module1 = rpc.remote(owner1, MyModule)
remote_module2 = rpc.remote(owner2, MyModule)
remote_param1 = remote_method(MyModule.get_w, remote_module1)
remote_param2 = remote_method(MyModule.get_w, remote_module2)
dist_optim = DistributedOptimizer(
FailingOptimizer, [remote_param1, remote_param2]
)
with dist_autograd.context() as context_id:
g_cpu = torch.Generator()
g_cpu.manual_seed(0)
t1 = torch.rand((3, 3), requires_grad=True, generator=g_cpu)
t2 = torch.rand((3, 3), requires_grad=True, generator=g_cpu)
output1 = rpc_async_method(MyModule.forward, remote_module1, t2)
output2 = rpc_async_method(MyModule.forward, remote_module2, output1.wait())
loss = torch.add(output2.wait(), t1).sum()
dist_autograd.backward(context_id, [loss])
with self.assertRaisesRegex(Exception, "Error running optimizer"):
dist_optim.step(context_id)
@dist_init()
def test_dist_optim_exception_on_constructor(self):
# distributed version
owner1 = f"worker{(self.rank + 1) % self.world_size:d}"
owner2 = f"worker{(self.rank + 2) % self.world_size:d}"
remote_module1 = rpc.remote(owner1, MyModule)
remote_module2 = rpc.remote(owner2, MyModule)
remote_param1 = remote_method(MyModule.get_w, remote_module1)
remote_param2 = remote_method(MyModule.get_w, remote_module2)
with self.assertRaisesRegex(Exception, "Error creating optimizer."):
DistributedOptimizer(
OptimizerFailingOnConstructor, [remote_param1, remote_param2]
)
def _test_dist_optim_base(self, optim_cls, *args, **kwargs):
# local version
module1 = MyModule()
module2 = MyModule()
params = [module1.get_w(), module2.get_w()]
local_optim = optim_cls(params, *args, **kwargs)
old_w1 = module1.w.detach().clone()
old_w2 = module2.w.detach().clone()
g_cpu = torch.Generator()
g_cpu.manual_seed(0)
t1 = torch.rand((3, 3), requires_grad=True, generator=g_cpu)
t2 = torch.rand((3, 3), requires_grad=True, generator=g_cpu)
output1 = module1.forward(t2)
output2 = module2.forward(output1)
loss = torch.add(output2, t1).sum()
loss.backward()
local_optim.step()
# distributed version
owner1 = f"worker{(self.rank + 1) % self.world_size:d}"
owner2 = f"worker{(self.rank + 2) % self.world_size:d}"
remote_module1 = rpc.remote(owner1, MyModule)
remote_module2 = rpc.remote(owner2, MyModule)
remote_param1 = remote_method(MyModule.get_w, remote_module1)
remote_param2 = remote_method(MyModule.get_w, remote_module2)
# sanity check: local and remote initial weights should match
self.assertEqual(old_w1, remote_param1.to_here())
self.assertEqual(old_w2, remote_param2.to_here())
dist_optim = DistributedOptimizer(
optim_cls, [remote_param1, remote_param2], *args, **kwargs
)
with dist_autograd.context() as context_id:
g_cpu.manual_seed(0)
t1 = torch.rand((3, 3), requires_grad=True, generator=g_cpu)
t2 = torch.rand((3, 3), requires_grad=True, generator=g_cpu)
output1 = rpc_async_method(MyModule.forward, remote_module1, t2)
output2 = rpc_async_method(MyModule.forward, remote_module2, output1.wait())
loss = torch.add(output2.wait(), t1)
dist_autograd.backward(context_id, [loss.sum()])
dist_optim.step(context_id)
new_w1 = rpc_async_method(MyModule.get_w, remote_module1).wait()
new_w2 = rpc_async_method(MyModule.get_w, remote_module2).wait()
# ensure optimizer changed weights
self.assertNotEqual(old_w1, new_w1)
self.assertNotEqual(old_w2, new_w2)
# ensure local equals remote
self.assertEqual(new_w1, module1.get_w())
self.assertEqual(new_w2, module2.get_w())
@dist_init()
def test_dist_optim(self):
self._test_dist_optim_base(optim.Adagrad, lr=0.05)
self._test_dist_optim_base(optim.Adam, lr=1e-2, amsgrad=True)
self._test_dist_optim_base(optim.AdamW, lr=0.05, amsgrad=True)
self._test_dist_optim_base(optim.SGD, lr=0.05)
self._test_dist_optim_base(
optim.SGD, lr=1e-3, momentum=1, weight_decay=1, nesterov=True
)
self._test_dist_optim_base(optim.Adadelta, rho=0.95)
self._test_dist_optim_base(optim.RMSprop, lr=0.05)
self._test_dist_optim_base(optim.Adamax, lr=0.05)
self._test_dist_optim_base(optim.Rprop, lr=0.05)
def _test_dist_optim_none_grads(self, optim_cls, *args, **kwargs):
# local version
module1 = MyModule()
module2 = MyModule(requires_grad=False)
params = [module1.get_w(), module2.get_w()]
local_optim = optim_cls(params, *args, **kwargs)
old_w1 = module1.w.detach().clone()
old_w2 = module2.w.detach().clone()
g_cpu = torch.Generator()
g_cpu.manual_seed(0)
t1 = torch.rand((3, 3), requires_grad=True, generator=g_cpu)
t2 = torch.rand((3, 3), requires_grad=True, generator=g_cpu)
output1 = module1.forward(t2)
output2 = module2.forward(output1)
loss = torch.add(output2, t1).sum()
loss.backward()
local_optim.step()
# distributed version
owner1 = f"worker{(self.rank + 1) % self.world_size:d}"
owner2 = f"worker{(self.rank + 2) % self.world_size:d}"
remote_module1 = rpc.remote(owner1, MyModule)
remote_module2 = rpc.remote(owner2, MyModule, args=(False,))
remote_param1 = remote_module1.remote().get_w()
remote_param2 = remote_module2.remote().get_w()
# sanity check: local and remote initial weights should match
self.assertEqual(old_w1, remote_param1.to_here())
self.assertEqual(old_w2, remote_param2.to_here())
dist_optim = DistributedOptimizer(
optim_cls, [remote_param1, remote_param2], *args, **kwargs
)
with dist_autograd.context() as context_id:
g_cpu.manual_seed(0)
t1 = torch.rand((3, 3), requires_grad=True, generator=g_cpu)
t2 = torch.rand((3, 3), requires_grad=True, generator=g_cpu)
output1 = remote_module1.rpc_async().forward(t2)
output2 = remote_module2.rpc_async().forward(output1.wait())
loss = torch.add(output2.wait(), t1)
dist_autograd.backward(context_id, [loss.sum()])
dist_optim.step(context_id)
new_w1 = remote_module1.rpc_async().get_w().wait()
new_w2 = remote_module2.rpc_async().get_w().wait()
# ensure optimizer changed weights for w1
self.assertNotEqual(old_w1, new_w1)
# ensure optimizer not changed weights for w2
self.assertEqual(old_w2, new_w2)
# ensure local equals remote
self.assertEqual(new_w1, module1.get_w())
self.assertEqual(new_w2, module2.get_w())
@dist_init()
def test_dist_optim_none_grads(self):
self._test_dist_optim_none_grads(optim.SGD, lr=0.05)
self._test_dist_optim_none_grads(optim.RMSprop, lr=0.05)
self._test_dist_optim_none_grads(optim.Rprop, lr=0.05)
self._test_dist_optim_none_grads(optim.Adadelta, rho=0.95)
@@ -0,0 +1,140 @@
# mypy: allow-untyped-defs
# If you need to modify this file to make this test pass, please also apply same edits accordingly to
# https://github.com/pytorch/examples/blob/master/distributed/rpc/batch/parameter_server.py
# and https://pytorch.org/tutorials/intermediate/rpc_async_execution.html#batch-updating-parameter-server
import threading
from datetime import datetime
from time import perf_counter
import torch
import torch.distributed.rpc as rpc
import torch.nn as nn
from torch import optim
from torch.testing._internal.dist_utils import dist_init, worker_name
from torch.testing._internal.distributed.rpc.rpc_agent_test_fixture import (
RpcAgentTestFixture,
)
batch_size = 20
in_features = 100
out_features = 30
num_batches = 4
def timed_log(text):
print(f"{datetime.now().strftime('%H:%M:%S')} {text}")
class BatchUpdateParameterServer:
def __init__(self, batch_update_size):
self.model = nn.Linear(in_features, out_features)
self.lock = threading.Lock()
self.future_model = torch.futures.Future()
self.batch_update_size = batch_update_size
self.curr_update_size = 0
self.optimizer = optim.SGD(self.model.parameters(), lr=0.001, momentum=0.9)
for p in self.model.parameters():
p.grad = torch.zeros_like(p)
def get_model(self):
return self.model
@staticmethod
@rpc.functions.async_execution
def update_and_fetch_model(ps_rref, grads):
self = ps_rref.local_value()
for p, g in zip(self.model.parameters(), grads, strict=True):
if p.grad is None:
p.grad = g
else:
p.grad += g
with self.lock:
timed_log(
f"PS got {self.curr_update_size}/{self.batch_update_size} updates"
)
self.curr_update_size += 1
fut = self.future_model
if self.curr_update_size >= self.batch_update_size:
for p in self.model.parameters():
p.grad /= self.batch_update_size
self.curr_update_size = 0
self.optimizer.step()
self.optimizer.zero_grad()
fut.set_result(self.model)
timed_log("PS updated model")
self.future_model = torch.futures.Future()
return fut
class Trainer:
def __init__(self, ps_rref):
self.ps_rref = ps_rref
self.loss_fn = nn.L1Loss()
def get_next_batch(self):
for _ in range(num_batches):
inputs = torch.randn(batch_size, in_features)
labels = torch.zeros(batch_size, out_features)
yield inputs, labels
def train(self):
name = rpc.get_worker_info().name
m = self.ps_rref.rpc_sync().get_model()
for inputs, labels in self.get_next_batch():
timed_log(f"{name} processing one batch")
self.loss_fn(m(inputs), labels).backward()
timed_log(f"{name} reporting grads")
m = rpc.rpc_sync(
self.ps_rref.owner(),
BatchUpdateParameterServer.update_and_fetch_model,
args=(self.ps_rref, [p.grad for p in m.cpu().parameters()]),
)
timed_log(f"{name} got updated model")
def run_trainer(ps_rref):
trainer = Trainer(ps_rref)
trainer.train()
def run_ps(trainers):
timed_log("Start training")
start = perf_counter()
ps_rref = rpc.RRef(BatchUpdateParameterServer(len(trainers)))
futs = [
rpc.rpc_async(trainer, run_trainer, args=(ps_rref,)) for trainer in trainers
]
torch.futures.wait_all(futs)
stop = perf_counter()
timed_log("Finish training")
timed_log(f"Time spent training: {stop - start}s")
class ParameterServerTest(RpcAgentTestFixture):
@dist_init(setup_rpc=False)
def test_batch_updating_parameter_server(self):
if self.rank != 0:
rpc.init_rpc(
name=worker_name(self.rank),
backend=self.rpc_backend,
rank=self.rank,
world_size=self.world_size,
rpc_backend_options=self.rpc_backend_options,
)
else:
rpc.init_rpc(
name=worker_name(self.rank),
backend=self.rpc_backend,
rank=self.rank,
world_size=self.world_size,
rpc_backend_options=self.rpc_backend_options,
)
run_ps([f"{worker_name(r)}" for r in range(1, self.world_size)])
rpc.shutdown()
@@ -0,0 +1,265 @@
# mypy: allow-untyped-defs
# If you need to modify this file to make this test pass, please also apply same edits accordingly to
# https://github.com/pytorch/examples/blob/master/distributed/rpc/rl/main.py
# and https://pytorch.org/tutorials/intermediate/rpc_tutorial.html
import numpy as np
import torch
import torch.distributed.rpc as rpc
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.distributed.rpc import remote, rpc_async, rpc_sync, RRef
from torch.distributions import Categorical
from torch.testing._internal.dist_utils import dist_init, worker_name
from torch.testing._internal.distributed.rpc.rpc_agent_test_fixture import (
RpcAgentTestFixture,
)
TOTAL_EPISODE_STEP = 5000
GAMMA = 0.1
SEED = 543
def _call_method(method, rref, *args, **kwargs):
r"""
a helper function to call a method on the given RRef
"""
return method(rref.local_value(), *args, **kwargs)
def _remote_method(method, rref, *args, **kwargs):
r"""
a helper function to run method on the owner of rref and fetch back the
result using RPC
"""
args = [method, rref] + list(args)
return rpc_sync(rref.owner(), _call_method, args=args, kwargs=kwargs)
class Policy(nn.Module):
r"""
Borrowing the ``Policy`` class from the Reinforcement Learning example.
Copying the code to make these two examples independent.
See https://github.com/pytorch/examples/tree/master/reinforcement_learning
"""
def __init__(self) -> None:
super().__init__()
self.affine1 = nn.Linear(4, 128)
self.dropout = nn.Dropout(p=0.6)
self.affine2 = nn.Linear(128, 2)
self.saved_log_probs = []
self.rewards = []
def forward(self, x):
x = self.affine1(x)
x = self.dropout(x)
x = F.relu(x)
action_scores = self.affine2(x)
return F.softmax(action_scores, dim=1)
class DummyEnv:
r"""
A dummy environment that implements the required subset of the OpenAI gym
interface. It exists only to avoid a dependency on gym for running the
tests in this file. It is designed to run for a set max number of iterations,
returning random states and rewards at each step.
"""
def __init__(self, state_dim=4, num_iters=10, reward_threshold=475.0):
self.state_dim = state_dim
self.num_iters = num_iters
self.iter = 0
self.reward_threshold = reward_threshold
def seed(self, manual_seed):
torch.manual_seed(manual_seed)
def reset(self):
self.iter = 0
return torch.randn(self.state_dim)
def step(self, action):
self.iter += 1
state = torch.randn(self.state_dim)
reward = torch.rand(1).item() * self.reward_threshold
done = self.iter >= self.num_iters
info = {}
return state, reward, done, info
class Observer:
r"""
An observer has exclusive access to its own environment. Each observer
captures the state from its environment, and send the state to the agent to
select an action. Then, the observer applies the action to its environment
and reports the reward to the agent.
"""
def __init__(self) -> None:
self.id = rpc.get_worker_info().id
self.env = DummyEnv()
self.env.seed(SEED)
def run_episode(self, agent_rref, n_steps):
r"""
Run one episode of n_steps.
Arguments:
agent_rref (RRef): an RRef referencing the agent object.
n_steps (int): number of steps in this episode
"""
state, _ep_reward = self.env.reset(), 0
for _ in range(n_steps):
# send the state to the agent to get an action
action = _remote_method(Agent.select_action, agent_rref, self.id, state)
# apply the action to the environment, and get the reward
state, reward, done, _ = self.env.step(action)
# report the reward to the agent for training purpose
_remote_method(Agent.report_reward, agent_rref, self.id, reward)
if done:
break
class Agent:
def __init__(self, world_size):
self.ob_rrefs = []
self.agent_rref = RRef(self)
self.rewards = {}
self.saved_log_probs = {}
self.policy = Policy()
self.optimizer = optim.Adam(self.policy.parameters(), lr=1e-2)
self.eps = np.finfo(np.float32).eps.item()
self.running_reward = 0
self.reward_threshold = DummyEnv().reward_threshold
for ob_rank in range(1, world_size):
ob_info = rpc.get_worker_info(worker_name(ob_rank))
self.ob_rrefs.append(remote(ob_info, Observer))
self.rewards[ob_info.id] = []
self.saved_log_probs[ob_info.id] = []
def select_action(self, ob_id, state):
r"""
This function is mostly borrowed from the Reinforcement Learning example.
See https://github.com/pytorch/examples/tree/master/reinforcement_learning
The main difference is that instead of keeping all probs in one list,
the agent keeps probs in a dictionary, one key per observer.
NB: no need to enforce thread-safety here as GIL will serialize
executions.
"""
probs = self.policy(state.unsqueeze(0))
m = Categorical(probs)
action = m.sample()
self.saved_log_probs[ob_id].append(m.log_prob(action))
return action.item()
def report_reward(self, ob_id, reward):
r"""
Observers call this function to report rewards.
"""
self.rewards[ob_id].append(reward)
def run_episode(self, n_steps=0):
r"""
Run one episode. The agent will tell each observer to run n_steps.
"""
# make async RPC to kick off an episode on all observers
futs = [
rpc_async(
ob_rref.owner(),
_call_method,
args=(Observer.run_episode, ob_rref, self.agent_rref, n_steps),
)
for ob_rref in self.ob_rrefs
]
# wait until all observers have finished this episode
for fut in futs:
fut.wait()
def finish_episode(self):
r"""
This function is mostly borrowed from the Reinforcement Learning example.
See https://github.com/pytorch/examples/tree/master/reinforcement_learning
The main difference is that it joins all probs and rewards from
different observers into one list, and uses the minimum observer rewards
as the reward of the current episode.
"""
# joins probs and rewards from different observers into lists
R, probs, rewards = 0, [], []
for ob_id in self.rewards:
probs.extend(self.saved_log_probs[ob_id])
rewards.extend(self.rewards[ob_id])
# use the minimum observer reward to calculate the running reward
min_reward = min(sum(self.rewards[ob_id]) for ob_id in self.rewards)
self.running_reward = 0.05 * min_reward + (1 - 0.05) * self.running_reward
# clear saved probs and rewards
for ob_id in self.rewards:
self.rewards[ob_id] = []
self.saved_log_probs[ob_id] = []
policy_loss, returns = [], []
for r in rewards[::-1]:
R = r + GAMMA * R
returns.insert(0, R)
returns = torch.tensor(returns)
returns = (returns - returns.mean()) / (returns.std() + self.eps)
for log_prob, R in zip(probs, returns, strict=True):
policy_loss.append(-log_prob * R)
self.optimizer.zero_grad()
policy_loss = torch.cat(policy_loss).sum()
policy_loss.backward()
self.optimizer.step()
return min_reward
def run_agent(agent, n_steps):
while True:
agent.run_episode(n_steps=n_steps)
agent.finish_episode()
if agent.running_reward > agent.reward_threshold:
print(f"Solved! Running reward is now {agent.running_reward}!")
break
class ReinforcementLearningRpcTest(RpcAgentTestFixture):
@dist_init(setup_rpc=False)
def test_rl_rpc(self):
if self.rank == 0:
# Rank 0 is the agent.
rpc.init_rpc(
name=worker_name(self.rank),
backend=self.rpc_backend,
rank=self.rank,
world_size=self.world_size,
rpc_backend_options=self.rpc_backend_options,
)
agent = Agent(self.world_size)
run_agent(agent, n_steps=int(TOTAL_EPISODE_STEP / (self.world_size - 1)))
# Ensure training was run. We don't really care about whether the task was learned,
# since the purpose of the test is to check the API calls.
self.assertGreater(agent.running_reward, 0.0)
else:
# Other ranks are observers that passively wait for instructions from the agent.
rpc.init_rpc(
name=worker_name(self.rank),
backend=self.rpc_backend,
rank=self.rank,
world_size=self.world_size,
rpc_backend_options=self.rpc_backend_options,
)
rpc.shutdown()
@@ -0,0 +1,337 @@
# mypy: allow-untyped-defs
import time
import torch
import torch.distributed.rpc as rpc
from torch.distributed.rpc.api import _delete_all_user_and_unforked_owner_rrefs
from torch.testing._internal.dist_utils import (
dist_init,
wait_until_owners_and_forks_on_rank,
wait_until_pending_futures_and_users_flushed,
worker_name,
)
from torch.testing._internal.distributed.rpc.rpc_agent_test_fixture import (
RpcAgentTestFixture,
)
def my_sleep_func(seconds=1):
time.sleep(seconds)
return torch.mul(torch.tensor(1), torch.tensor(1))
@torch.jit.script
def my_script_func(tensor):
return torch.add(tensor, tensor)
def add_rref_to_value(rref, value):
return rref.to_here() + value
class FaultyAgentRpcTest(RpcAgentTestFixture):
# no faulty_messages defined so this fails all retryable messages - see
# faulty_rpc_agent_test_fixture.py for the list of retryable messages.
@dist_init(messages_to_delay={})
def test_check_failed_messages(self):
if self.rank == 0:
dst_worker_b = worker_name((self.rank + 1) % self.world_size)
dst_worker_c = worker_name((self.rank + 2) % self.world_size)
# Worker0 sends RPC to Worker1 and creates an RRef there
rref = rpc.remote(
dst_worker_b, torch.add, args=(torch.ones(2, 2), torch.ones(2, 2))
)
# Worker0 sends an RPC to Worker2 with the RRef as an arg
rpc.remote(dst_worker_c, add_rref_to_value, args=(rref, torch.ones(2, 2)))
# check if the output is as expected
self.assertEqual(
rref.to_here(), torch.add(torch.ones(2, 2), torch.ones(2, 2))
)
# explicitly delete all User RRefs
_delete_all_user_and_unforked_owner_rrefs()
@dist_init
def test_verify_backend_options(self):
self.assertEqual(
self.rpc_backend, rpc.backend_registry.BackendType.FAULTY_TENSORPIPE
)
self.assertEqual(self.rpc_backend_options.num_worker_threads, 8)
self.assertEqual(self.rpc_backend_options.num_fail_sends, 3)
self.assertEqual(len(self.rpc_backend_options.messages_to_fail), 4)
self.assertEqual(len(self.rpc_backend_options.messages_to_delay), 2)
self.assertEqual(
self.rpc_backend_options.rpc_timeout, rpc.constants.DEFAULT_RPC_TIMEOUT_SEC
)
@dist_init(faulty_messages=["RREF_FORK_REQUEST", "RREF_CHILD_ACCEPT"])
def test_custom_faulty_messages(self):
self.assertEqual(
{"RREF_FORK_REQUEST", "RREF_CHILD_ACCEPT"},
set(self.rpc_backend_options.messages_to_fail),
)
@dist_init(faulty_messages=[])
def test_no_faulty_messages(self):
self.assertEqual(len(self.rpc_backend_options.messages_to_fail), 0)
@dist_init(messages_to_delay={"SCRIPT_CALL": 1.5})
def test_custom_messages_to_delay(self):
self.assertEqual(
self.rpc_backend_options.messages_to_delay, {"SCRIPT_CALL": 1.5}
)
def _test_remote_message_dropped_pickle(self, dst=None):
if self.rank != 0:
return
dst_rank = dst if dst is not None else (self.rank + 1) % self.world_size
dst_worker = f"worker{dst_rank}"
# Since we fail python_remote_call messages synchronously, the future
# corresponding to this remote call will be marked with an error when
# this function returns.
rref = rpc.remote(dst_worker, my_sleep_func, args=(1,))
# Call to ensure pending callbacks are run.
wait_until_pending_futures_and_users_flushed()
# Attempt to fork the RRef should raise an error indicating the rpc.remote timeout.
with self.assertRaisesRegex(RuntimeError, "RRef creation"):
rref._serialize()
# Test that using RRef as arg over RPC (which forks) results in the same
# error
with self.assertRaisesRegex(RuntimeError, "RRef creation"):
rpc.rpc_async(dst_worker, add_rref_to_value, args=(rref, 1))
@dist_init(faulty_messages=["PYTHON_REMOTE_CALL"])
def test_remote_message_dropped_pickle(self):
self._test_remote_message_dropped_pickle()
@dist_init(faulty_messages=["PYTHON_REMOTE_CALL"])
def test_remote_message_dropped_pickle_to_self(self):
self._test_remote_message_dropped_pickle(self.rank)
def _test_remote_message_dropped_timeout(self, func, args, dst=None):
if self.rank != 0:
return
# test the case where rpc.remote() message creation is completely dropped.
dst_rank = dst if dst is not None else (self.rank + 1) % self.world_size
dst_worker = f"worker{dst_rank}"
# Since we fail python_remote_call messages synchronously, the future
# corresponding to this remote call will be marked with an error when
# this function returns.
rref = rpc.remote(dst_worker, func, args=args)
# Call to ensure pending callbacks are run.
wait_until_pending_futures_and_users_flushed()
with self.assertRaisesRegex(RuntimeError, "RRef creation"):
rref.to_here()
# Note: during shutdown, logs will indicate "Could not find OwnerRRef..."
# on the owning nodes, this is expected because the OwnerRRef was never
# successfully created. Therefore, delAllUsers will work as expected.
@dist_init(faulty_messages=["SCRIPT_REMOTE_CALL"])
def test_builtin_remote_message_dropped_timeout(self):
func = torch.add
args = (torch.tensor(1), torch.tensor(1))
self._test_remote_message_dropped_timeout(func, args)
@dist_init(faulty_messages=["SCRIPT_REMOTE_CALL"])
def test_builtin_remote_message_dropped_timeout_to_self(self):
func = torch.add
args = (torch.tensor(1), torch.tensor(1))
self._test_remote_message_dropped_timeout(func, args, dst=0)
@dist_init(faulty_messages=["PYTHON_REMOTE_CALL"])
def test_udf_remote_message_dropped_timeout(self):
func = my_sleep_func
args = (2,)
self._test_remote_message_dropped_timeout(func, args)
@dist_init(faulty_messages=["PYTHON_REMOTE_CALL"])
def test_udf_remote_message_dropped_timeout_to_self(self):
func = my_sleep_func
args = (2,)
self._test_remote_message_dropped_timeout(func, args, dst=0)
def _test_remote_message_delay_timeout(self, func, args, dst=None):
if self.rank != 0:
return
# Test the case where remote message is eventually processed on the owner,
# but the future on the creator times out before the response comes back.
dst_rank = dst if dst is not None else (self.rank + 1) % self.world_size
dst_worker = f"worker{dst_rank}"
# 10 ms timeout
rref = rpc.remote(dst_worker, func, args=args, timeout=0.001)
# Future corresponding to the remote creation should time out.
expected_error = self.get_timeout_error_regex()
with self.assertRaisesRegex(RuntimeError, expected_error):
rref._get_future().wait()
# Call to ensure pending callbacks are run.
wait_until_pending_futures_and_users_flushed()
# to_here() should now pick up that rpc.remote() creation has failed.
with self.assertRaisesRegex(RuntimeError, "RRef creation"):
rref.to_here()
# Test the case where rpc.remote() times out, but to_here() has already
# started blocking before.
# NOTE: we only test this when not sending to self, as to_here() calls
# calls localValue(), which does not send an RPC and thus does not have
# a timeout. This can be supported by allowing future.wait() to
# take in an optional timeout (https://github.com/pytorch/pytorch/issues/39280)
if dst_rank != self.rank:
slow_rref = rpc.remote(dst_worker, func, args=args, timeout=2)
with self.assertRaisesRegex(RuntimeError, expected_error):
# to_here() should raise timeout error, since it does not know about the
# status of rpc.remote().
slow_rref.to_here(0.001)
# Note: If we proceed with shutdown, UserRRef will send out a RRefUserDelete
# but this can be a noop since it may not exist on the owner yet. Later,
# the owner can process the RRef creation and wait for the delete message,
# thus leading to a timeout.
# Therefore, we wait until we get notification that pending owners have
# been confirmed before sending out RRefUserDeletes.
if dst_rank != self.rank:
wait_until_owners_and_forks_on_rank(2, 2, rank=dst_rank)
@dist_init(faulty_messages=[], messages_to_delay={"PYTHON_REMOTE_CALL": 2})
def test_udf_remote_message_delay_timeout(self):
func = my_sleep_func
args = (2,)
self._test_remote_message_delay_timeout(func, args)
@dist_init(faulty_messages=[], messages_to_delay={"PYTHON_REMOTE_CALL": 2})
def test_udf_remote_message_delay_timeout_to_self(self):
func = my_sleep_func
args = (1,)
self._test_remote_message_delay_timeout(func, args, dst=0)
@dist_init(
faulty_messages=[],
messages_to_delay={"SCRIPT_REMOTE_CALL": 2, "SCRIPT_RREF_FETCH_CALL": 1},
)
def test_remote_message_builtin_delay_timeout(self):
func = torch.add
args = (torch.tensor(1), torch.tensor(1))
self._test_remote_message_delay_timeout(func, args)
@dist_init(
faulty_messages=[],
messages_to_delay={"SCRIPT_REMOTE_CALL": 2, "SCRIPT_RREF_FETCH_CALL": 1},
)
def test_remote_message_builtin_delay_timeout_to_self(self):
func = torch.add
args = (torch.tensor(1), torch.tensor(1))
self._test_remote_message_delay_timeout(func, args, dst=0)
@dist_init(
faulty_messages=[],
messages_to_delay={"SCRIPT_REMOTE_CALL": 2, "SCRIPT_RREF_FETCH_CALL": 1},
)
def test_remote_message_script_delay_timeout(self):
func = my_script_func
args = (torch.tensor(1),)
self._test_remote_message_delay_timeout(func, args)
@dist_init(
faulty_messages=[],
messages_to_delay={"SCRIPT_REMOTE_CALL": 2, "SCRIPT_RREF_FETCH_CALL": 1},
)
def test_remote_message_script_delay_timeout_to_self(self):
func = my_script_func
args = (torch.tensor(1),)
self._test_remote_message_delay_timeout(func, args, dst=0)
@dist_init(faulty_messages=[], messages_to_delay={"SCRIPT_RREF_FETCH_CALL": 1})
def test_rref_to_here_timeout(self):
if self.rank != 0:
return
dst_rank = (self.rank + 1) % self.world_size
dst_worker = f"worker{dst_rank}"
rref = rpc.remote(
dst_worker, torch.add, args=(torch.tensor(1), torch.tensor(1))
)
expected_error = self.get_timeout_error_regex()
with self.assertRaisesRegex(RuntimeError, expected_error):
rref.to_here(0.01)
rref.to_here()
@dist_init(faulty_messages=[])
def test_rpc_builtin_timeout(self):
next_rank = (self.rank + 1) % self.world_size
dst_worker = worker_name(next_rank)
expected_error = self.get_timeout_error_regex()
# PYTHON_CALL message types which correspond to Python UDF over RPC
# by default get a delay (see faulty_rpc_agent_test_fixture)
with self.assertRaisesRegex(RuntimeError, expected_error):
rpc.rpc_sync(
dst_worker,
torch.add,
args=(torch.tensor(1), torch.tensor(1)),
timeout=1,
)
fut = rpc.rpc_async(
dst_worker, torch.add, args=(torch.tensor(1), torch.tensor(1)), timeout=1
)
with self.assertRaisesRegex(RuntimeError, expected_error):
fut.wait()
# Ensure that the currently set default timeout is large enough such
# that RPCs with delays still complete.
fut = rpc.rpc_async(
dst_worker, torch.add, args=(torch.tensor(1), torch.tensor(1))
)
fut.wait()
# Ensure timeout if we set a new default and don't override
rpc._set_rpc_timeout(0.001)
fut = rpc.rpc_async(
dst_worker, torch.add, args=(torch.tensor(1), torch.tensor(1))
)
with self.assertRaisesRegex(RuntimeError, expected_error):
fut.wait()
# Ensure run to completion if we specify timeout of 0
fut = rpc.rpc_async(
dst_worker, torch.add, args=(torch.tensor(1), torch.tensor(1)), timeout=0
)
fut.wait()
# Reset for clean shutdown
rpc._set_rpc_timeout(rpc.constants.DEFAULT_RPC_TIMEOUT_SEC)
@dist_init(faulty_messages=[], messages_to_delay={"SCRIPT_CALL": 1.5})
def test_rpc_script_timeout(self):
next_rank = (self.rank + 1) % self.world_size
dst_worker = worker_name(next_rank)
expected_error = self.get_timeout_error_regex()
with self.assertRaisesRegex(RuntimeError, expected_error):
rpc.rpc_sync(dst_worker, my_script_func, args=(torch.tensor(1),), timeout=1)
fut = rpc.rpc_async(
dst_worker, my_script_func, args=(torch.tensor(1),), timeout=1
)
with self.assertRaisesRegex(RuntimeError, expected_error):
fut.wait()
# Ensure that the currently set default timeout is large enough such
# that RPCs with delays still complete.
fut = rpc.rpc_async(dst_worker, my_script_func, args=(torch.tensor(1),))
fut.wait()
# Ensure timeout if we set a new default and don't override
rpc._set_rpc_timeout(0.001)
fut = rpc.rpc_async(dst_worker, my_script_func, args=(torch.tensor(1),))
with self.assertRaisesRegex(RuntimeError, expected_error):
fut.wait()
# Ensure run to completion if we specify timeout of 0
rpc._set_rpc_timeout(0.001)
fut = rpc.rpc_async(
dst_worker, my_script_func, args=(torch.tensor(1),), timeout=0
)
fut.wait()
# Reset for clean shutdown
rpc._set_rpc_timeout(rpc.constants.DEFAULT_RPC_TIMEOUT_SEC)
@@ -0,0 +1,64 @@
# mypy: allow-untyped-defs
import torch.distributed.rpc as rpc
import torch.distributed.rpc._testing # noqa: F401
from torch.testing._internal.distributed.rpc.rpc_agent_test_fixture import (
RpcAgentTestFixture,
)
# The following message types are currently retried in the RREF protocol and
# distributed autograd. Thus only these messages should be tested with the
# Faulty RPC Agent.
retryable_message_types = [
"RREF_FORK_REQUEST",
"RREF_CHILD_ACCEPT",
"RREF_USER_DELETE",
"CLEANUP_AUTOGRAD_CONTEXT_REQ",
]
# The following messages incur the corresponding delay in seconds while being
# processed in FaultyTensorPipeAgent's enqueueSend() function.
default_messages_to_delay = {
"PYTHON_CALL": 1.5, # Python UDF
"SCRIPT_CALL": 1.5, # Script/Builtin
}
class FaultyRpcAgentTestFixture(RpcAgentTestFixture):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.messages_to_fail = retryable_message_types
self.messages_to_delay = default_messages_to_delay
@property
def rpc_backend(self):
return rpc.backend_registry.BackendType["FAULTY_TENSORPIPE"]
@property
def rpc_backend_options(self):
return rpc.backend_registry.construct_rpc_backend_options(
self.rpc_backend,
init_method=self.init_method,
num_worker_threads=8,
num_fail_sends=3,
messages_to_fail=self.messages_to_fail,
messages_to_delay=self.messages_to_delay,
)
def setup_fault_injection(self, faulty_messages, messages_to_delay):
if faulty_messages is not None:
self.messages_to_fail = faulty_messages
if messages_to_delay is not None:
self.messages_to_delay = messages_to_delay
def get_shutdown_error_regex(self):
error_regexes = [
"Exception in thread pool task",
"Connection reset by peer",
"Connection closed by peer",
]
return "|".join([f"({error_str})" for error_str in error_regexes])
def get_timeout_error_regex(self):
return "RPC ran for more than"
@@ -0,0 +1,113 @@
# mypy: allow-untyped-defs
import torch
import torch.distributed.autograd as dist_autograd
import torch.distributed.rpc as rpc
from torch import Tensor
from torch.distributed.rpc import rpc_async
from torch.testing import FileCheck
from torch.testing._internal.dist_utils import dist_init, worker_name
from torch.testing._internal.distributed.rpc.rpc_agent_test_fixture import (
RpcAgentTestFixture,
)
@torch.jit.script
def local_add(t1, t2):
return torch.add(t1, t2)
@torch.jit.script
def remote_add(t1, t2, dst: str): # noqa: E999
return rpc_async(dst, local_add, (t1, t2)).wait()
@torch.jit.script
def fork_add(t1, t2, dst: str):
fut = torch.jit._fork(remote_add, t1, t2, dst)
return torch.jit._wait(fut)
class JitDistAutogradTest(RpcAgentTestFixture):
@dist_init
def test_get_gradients(self):
@torch.jit.script
def dist_get_gradients(context_id: int) -> dict[Tensor, Tensor]:
return dist_autograd.get_gradients(context_id)
FileCheck().check("get_gradients").run(str(dist_get_gradients.graph))
with dist_autograd.context() as context_id:
t1 = torch.rand((3, 3), requires_grad=True)
t2 = torch.rand((3, 3), requires_grad=True)
t3 = torch.add(t1, t2)
dist_autograd.backward(context_id, [t3.sum()])
grads = dist_get_gradients(context_id)
self.assertEqual(2, len(grads))
self.assertIn(t1, grads)
self.assertIn(t2, grads)
self.assertEqual(torch.ones(3, 3), grads[t1])
self.assertEqual(torch.ones(3, 3), grads[t2])
@dist_init
def test_dist_backward(self):
if self.rank != 0:
return
@torch.jit.script
def dist_backward_script(context_id: int, loss: torch.Tensor):
dist_autograd.backward(context_id, [loss])
FileCheck().check("dist_backward").run(str(dist_backward_script.graph))
with dist_autograd.context() as context_id:
t1 = torch.rand(3, 3, requires_grad=True)
t2 = torch.rand(3, 3, requires_grad=True)
dst_worker_name = worker_name((self.rank + 1) % self.world_size)
loss = rpc.rpc_sync(dst_worker_name, torch.add, args=(t1, t2)).sum()
dist_backward_script(context_id, loss)
@dist_init
def test_jit_fork_within_context(self):
with dist_autograd.context() as context_id:
t1 = torch.rand((3, 3), requires_grad=True)
t2 = torch.rand((3, 3), requires_grad=True)
dst_worker_name = worker_name((self.rank + 1) % self.world_size)
res = fork_add(t1, t2, dst_worker_name)
loss = res.sum()
dist_autograd.backward(context_id, [loss])
grads = dist_autograd.get_gradients(context_id)
self.assertEqual(2, len(grads))
self.assertIn(t1, grads)
self.assertIn(t2, grads)
@dist_init
def test_restore_context_after_swtich_to_jit_thread(self):
if self.rank != 0:
return
@torch.jit.script
def forward_script(
context_id: int, dst_worker_name: str, t1: Tensor, t2: Tensor
) -> tuple[Tensor, Tensor]:
res1_fut = rpc.rpc_async(dst_worker_name, local_add, (t1, t1))
res1 = res1_fut.wait() # After this, the script runs in a new JIT thread.
loss1 = res1.sum()
# SendRpcBackward is not attached, since DistAutogradContext is lost here.
res2_fut = rpc.rpc_async(dst_worker_name, local_add, (t2, t2))
res2 = res2_fut.wait()
loss2 = res2.sum()
return loss1, loss2
with dist_autograd.context() as context_id:
t1 = torch.ones((2, 3), requires_grad=True)
t2 = torch.ones((2, 3), requires_grad=True)
dst_worker_name = worker_name((self.rank + 1) % self.world_size)
loss0, loss1 = forward_script(context_id, dst_worker_name, t1, t2)
dist_autograd.backward(context_id, [loss0, loss1])
grad0, grad1 = dist_autograd.get_gradients(context_id)
self.assertEqual(grad0, grad1)
@@ -0,0 +1,219 @@
# mypy: allow-untyped-defs
import torch
import torch.distributed.rpc as rpc
from torch import Tensor
from torch.distributed.rpc import RRef
from torch.testing._internal.dist_utils import (
dist_init,
wait_until_pending_futures_and_users_flushed,
worker_name,
)
from torch.testing._internal.distributed.rpc.rpc_agent_test_fixture import (
RpcAgentTestFixture,
)
@torch.jit.script
def two_args_two_kwargs(
first_arg,
second_arg,
first_kwarg=torch.tensor([3, 3]),
second_kwarg=torch.tensor([4, 4]),
):
return first_arg + second_arg + first_kwarg + second_kwarg
@torch.jit.script
def script_rpc_async_call(
dst_worker_name: str, args: tuple[Tensor, Tensor], kwargs: dict[str, Tensor]
):
fut = rpc.rpc_async(dst_worker_name, two_args_two_kwargs, args, kwargs)
ret = fut.wait()
return ret
@torch.jit.script
def rpc_async_call_with_timeout(
dst_worker_name: str,
args: tuple[Tensor, Tensor],
kwargs: dict[str, Tensor],
timeout: float,
):
fut = rpc.rpc_async(dst_worker_name, two_args_two_kwargs, args, kwargs, timeout)
ret = fut.wait()
return ret
@torch.jit.script
def rpc_async_call_with_timeout_future_ret(
dst_worker_name: str,
args: tuple[Tensor, Tensor],
kwargs: dict[str, Tensor],
timeout: float,
):
fut = rpc.rpc_async(dst_worker_name, two_args_two_kwargs, args, kwargs, timeout)
return fut
@torch.jit.script
def rpc_async_call_future_ret(
dst_worker_name: str, args: tuple[Tensor, Tensor], kwargs: dict[str, Tensor]
):
fut = rpc.rpc_async(dst_worker_name, two_args_two_kwargs, args, kwargs)
return fut
@torch.jit.script
def rref_to_here(rref_var: RRef[Tensor]) -> Tensor:
return rref_var.to_here()
@torch.jit.script
def rref_to_here_with_timeout(rref_var: RRef[Tensor], timeout: float) -> Tensor:
return rref_var.to_here(timeout)
@torch.jit.script
def rpc_async_with_rref_arg(dst_worker_name: str, args: tuple[RRef[Tensor]]) -> Tensor:
fut = rpc.rpc_async(dst_worker_name, rref_to_here, args)
ret = fut.wait()
return ret
class JitFaultyAgentRpcTest(RpcAgentTestFixture):
"""
Run tests for rpc_async in JIT under the faulty agent test fixture to test
arbitrary timeouts.
"""
@dist_init(faulty_messages=[], messages_to_delay={"SCRIPT_CALL": 1.5})
def test_timeout_in_torchscript_function(self):
# Call rpc_async + fut.wait() in torchscript function and ensure that
# timeout is raised.
if self.rank != 0:
return
dst_worker_name = worker_name((self.rank + 1) % self.world_size)
args = (torch.tensor([1, 1]), torch.tensor([2, 2]))
kwargs = {
"first_kwarg": torch.tensor([2, 2]),
"second_kwarg": torch.tensor([3, 3]),
}
expected_error = self.get_timeout_error_regex()
# Ensure that we get a timeout if we override the default timeout and
# the RPC takes longer to execute.
with self.assertRaisesRegex(RuntimeError, expected_error):
rpc_async_call_with_timeout(dst_worker_name, args, kwargs, 0.5)
# Ensure that we timeout if we don't specify a timeout but the default
# is less than the RPC takes to execute.
rpc._set_rpc_timeout(0.001)
with self.assertRaisesRegex(RuntimeError, expected_error):
script_rpc_async_call(dst_worker_name, args, kwargs)
# Ensure that we run to completion if zero timeout is specified.
ret = rpc_async_call_with_timeout(dst_worker_name, args, kwargs, 0)
self.assertEqual(ret, torch.tensor([8, 8]))
# reset for clean shutdown
rpc._set_rpc_timeout(rpc.constants.DEFAULT_RPC_TIMEOUT_SEC)
@dist_init(faulty_messages=[], messages_to_delay={"SCRIPT_CALL": 1.5})
def test_timeout_in_python(self):
# Ensures timeouts are raised if we call rpc_async from within a
# torchscript function, but wait on the future in python.
if self.rank != 0:
return
dst_worker_name = worker_name((self.rank + 1) % self.world_size)
args = (torch.tensor([1, 1]), torch.tensor([2, 2]))
kwargs = {
"first_kwarg": torch.tensor([2, 2]),
"second_kwarg": torch.tensor([3, 3]),
}
expected_error = self.get_timeout_error_regex()
fut = rpc_async_call_with_timeout_future_ret(dst_worker_name, args, kwargs, 0.5)
with self.assertRaisesRegex(RuntimeError, expected_error):
fut.wait()
# Ensure timeout if we don't specify but the default is less than the
# RPC takes to execute.
rpc._set_rpc_timeout(0.001)
fut = rpc_async_call_future_ret(dst_worker_name, args, kwargs)
with self.assertRaisesRegex(RuntimeError, expected_error):
fut.wait()
# Ensure run to completion if zero timeout is specified
fut = rpc_async_call_with_timeout_future_ret(dst_worker_name, args, kwargs, 0)
result = fut.wait()
self.assertEqual(result, torch.tensor([8, 8]))
# reset for clean shutdown
rpc._set_rpc_timeout(rpc.constants.DEFAULT_RPC_TIMEOUT_SEC)
@dist_init(faulty_messages=["SCRIPT_REMOTE_CALL"])
def test_remote_timeout_to_here_in_jit(self):
# Test that calling to_here() in JIT will raise timeout error if
# rpc.remote failed.
if self.rank != 0:
return
dst_rank = (self.rank + 1) % self.world_size
dst_worker = f"worker{dst_rank}"
rref = rpc.remote(
dst_worker, torch.add, args=(torch.tensor(1), torch.tensor(1))
)
# Will ensure error handling callbacks are run.
wait_until_pending_futures_and_users_flushed()
# Call to_here() within a ScriptFunction and ensure it raises
with self.assertRaisesRegex(RuntimeError, "RRef creation"):
rref_to_here(rref)
@dist_init(faulty_messages=[], messages_to_delay={"SCRIPT_RREF_FETCH_CALL": 1})
def test_rref_to_here_timeout_in_jit(self):
if self.rank != 0:
return
dst_rank = (self.rank + 1) % self.world_size
dst_worker = f"worker{dst_rank}"
rref = rpc.remote(
dst_worker, torch.add, args=(torch.tensor(1), torch.tensor(1))
)
expected_error = self.get_timeout_error_regex()
with self.assertRaisesRegex(RuntimeError, expected_error):
rref_to_here_with_timeout(rref, 0.01)
rref_to_here_with_timeout(rref, 100)
@dist_init(faulty_messages=["SCRIPT_REMOTE_CALL"])
def test_rref_timeout_pickle_in_jit(self):
if self.rank != 0:
return
dst_rank = (self.rank + 1) % self.world_size
dst_worker = f"worker{dst_rank}"
rref = rpc.remote(
dst_worker, torch.add, args=(torch.tensor(1), torch.tensor(1))
)
# Will ensure error handling callbacks are run.
wait_until_pending_futures_and_users_flushed()
# Call RPC with RRef arg in JIT, which will go through JIT pickling and
# ensure error is raised.
with self.assertRaisesRegex(RuntimeError, "RRef creation"):
rpc_async_with_rref_arg(dst_worker, (rref,))
@dist_init(faulty_messages=["SCRIPT_REMOTE_CALL"])
def test_rref_timeout_pickle_script_func(self):
# Similar to above test, but calls python rpc with script function.
if self.rank != 0:
return
dst_rank = (self.rank + 1) % self.world_size
dst_worker = f"worker{dst_rank}"
rref = rpc.remote(
dst_worker, torch.add, args=(torch.tensor(1), torch.tensor(1))
)
# Will ensure error handling callbacks are run.
wait_until_pending_futures_and_users_flushed()
# Call RPC with script function that takes RRef, ensure timeout during pickling
with self.assertRaisesRegex(RuntimeError, "RRef creation"):
rpc.rpc_sync(dst_worker, rref_to_here, args=(rref,))
@@ -0,0 +1,63 @@
# mypy: allow-untyped-defs
import os
from abc import ABC, abstractmethod
import torch.testing._internal.dist_utils
class RpcAgentTestFixture(ABC):
@property
def world_size(self) -> int:
return 4
@property
def init_method(self):
use_tcp_init = os.environ.get("RPC_INIT_WITH_TCP", None)
if use_tcp_init == "1":
master_addr = os.environ["MASTER_ADDR"]
master_port = os.environ["MASTER_PORT"]
return f"tcp://{master_addr}:{master_port}"
else:
return self.file_init_method
@property
def file_init_method(self):
return torch.testing._internal.dist_utils.INIT_METHOD_TEMPLATE.format(
file_name=self.file_name
)
@property
@abstractmethod
def rpc_backend(self):
pass
@property
@abstractmethod
def rpc_backend_options(self):
pass
def setup_fault_injection(self, faulty_messages, messages_to_delay): # noqa: B027
"""Method used by dist_init to prepare the faulty agent.
Does nothing for other agents.
"""
# Shutdown sequence is not well defined, so we may see any of the following
# errors when running tests that simulate errors via a shutdown on the
# remote end.
@abstractmethod
def get_shutdown_error_regex(self):
"""
Return various error message we may see from RPC agents while running
tests that check for failures. This function is used to match against
possible errors to ensure failures were raised properly.
"""
@abstractmethod
def get_timeout_error_regex(self):
"""
Returns a partial string indicating the error we should receive when an
RPC has timed out. Useful for use with assertRaisesRegex() to ensure we
have the right errors during timeout.
"""
@@ -0,0 +1,28 @@
# mypy: allow-untyped-defs
import torch.distributed.rpc as rpc
from torch.testing._internal.common_distributed import tp_transports
from torch.testing._internal.distributed.rpc.rpc_agent_test_fixture import (
RpcAgentTestFixture,
)
class TensorPipeRpcAgentTestFixture(RpcAgentTestFixture):
@property
def rpc_backend(self):
return rpc.backend_registry.BackendType["TENSORPIPE"]
@property
def rpc_backend_options(self):
return rpc.backend_registry.construct_rpc_backend_options(
self.rpc_backend, init_method=self.init_method, _transports=tp_transports()
)
def get_shutdown_error_regex(self):
# FIXME Once we consolidate the error messages returned by the
# TensorPipe agent put some more specific regex here.
error_regexes = [".*"]
return "|".join([f"({error_str})" for error_str in error_regexes])
def get_timeout_error_regex(self):
return "RPC ran for more than"
@@ -0,0 +1,188 @@
# mypy: allow-untyped-defs
import os
import sys
import unittest
from torch.testing._internal.common_distributed import MultiProcessTestCase
from torch.testing._internal.common_utils import (
find_free_port,
IS_SANDCASTLE,
TEST_WITH_DEV_DBG_ASAN,
)
from torch.testing._internal.distributed.ddp_under_dist_autograd_test import (
CudaDdpComparisonTest,
DdpComparisonTest,
DdpUnderDistAutogradTest,
)
from torch.testing._internal.distributed.nn.api.remote_module_test import (
CudaRemoteModuleTest,
RemoteModuleTest,
ThreeWorkersRemoteModuleTest,
)
from torch.testing._internal.distributed.rpc.dist_autograd_test import (
CudaDistAutogradTest,
DistAutogradTest,
FaultyAgentDistAutogradTest,
TensorPipeAgentDistAutogradTest,
TensorPipeCudaDistAutogradTest,
)
from torch.testing._internal.distributed.rpc.dist_optimizer_test import (
DistOptimizerTest,
)
from torch.testing._internal.distributed.rpc.examples.parameter_server_test import (
ParameterServerTest,
)
from torch.testing._internal.distributed.rpc.examples.reinforcement_learning_rpc_test import (
ReinforcementLearningRpcTest,
)
from torch.testing._internal.distributed.rpc.faulty_agent_rpc_test import (
FaultyAgentRpcTest,
)
from torch.testing._internal.distributed.rpc.jit.dist_autograd_test import (
JitDistAutogradTest,
)
from torch.testing._internal.distributed.rpc.jit.rpc_test import JitRpcTest
from torch.testing._internal.distributed.rpc.jit.rpc_test_faulty import (
JitFaultyAgentRpcTest,
)
from torch.testing._internal.distributed.rpc.rpc_agent_test_fixture import (
RpcAgentTestFixture,
)
from torch.testing._internal.distributed.rpc.rpc_test import (
CudaRpcTest,
RpcTest,
TensorPipeAgentCudaRpcTest,
TensorPipeAgentRpcTest,
)
def _check_and_set_tcp_init():
# if we are running with TCP init, set main address and port
# before spawning subprocesses, since different processes could find
# different ports.
use_tcp_init = os.environ.get("RPC_INIT_WITH_TCP", None)
if use_tcp_init == "1":
os.environ["MASTER_ADDR"] = "127.0.0.1"
os.environ["MASTER_PORT"] = str(find_free_port())
def _check_and_unset_tcp_init():
use_tcp_init = os.environ.get("RPC_INIT_WITH_TCP", None)
if use_tcp_init == "1":
del os.environ["MASTER_ADDR"]
del os.environ["MASTER_PORT"]
# The tests for the RPC module need to cover multiple possible combinations:
# - different aspects of the API, each one having its own suite of tests;
# - different agents (ProcessGroup, TensorPipe, ...);
# To avoid a combinatorial explosion in code size, and to prevent forgetting to
# add a combination, these are generated automatically by the code in this file.
# Here, we collect all the test suites that we need to cover.
# We then have one separate file for each agent, from which
# we call the generate_tests function of this file, passing to it a fixture for
# the agent, which then gets mixed-in with each test suite.
@unittest.skipIf(
TEST_WITH_DEV_DBG_ASAN,
"Skip ASAN as torch + multiprocessing spawn have known issues",
)
class SpawnHelper(MultiProcessTestCase):
def setUp(self):
super().setUp()
_check_and_set_tcp_init()
self._spawn_processes()
def tearDown(self):
_check_and_unset_tcp_init()
super().tearDown()
# This list contains test suites that are agent-agnostic and that only verify
# compliance with the generic RPC interface specification. These tests should
# *not* make use of implementation details of a specific agent (options,
# attributes, ...). These test suites will be instantiated multiple times, once
# for each agent (except the faulty agent, which is special).
GENERIC_TESTS = [
RpcTest,
ParameterServerTest,
DistAutogradTest,
DistOptimizerTest,
JitRpcTest,
JitDistAutogradTest,
RemoteModuleTest,
ThreeWorkersRemoteModuleTest,
DdpUnderDistAutogradTest,
DdpComparisonTest,
ReinforcementLearningRpcTest,
]
GENERIC_CUDA_TESTS = [
CudaRpcTest,
CudaDistAutogradTest,
CudaRemoteModuleTest,
CudaDdpComparisonTest,
]
# This list contains test suites that will only be run on the TensorPipeAgent.
# These suites should be standalone, and separate from the ones in the generic
# list (not subclasses of those!).
TENSORPIPE_TESTS = [
TensorPipeAgentRpcTest,
TensorPipeAgentDistAutogradTest,
]
TENSORPIPE_CUDA_TESTS = [
TensorPipeAgentCudaRpcTest,
TensorPipeCudaDistAutogradTest,
]
# This list contains test suites that will only be run on the faulty RPC agent.
# That agent is special as it's only used to perform fault injection in order to
# verify the error handling behavior. Thus the faulty agent will only run the
# suites in this list, which were designed to test such behaviors, and not the
# ones in the generic list.
FAULTY_AGENT_TESTS = [
FaultyAgentRpcTest,
FaultyAgentDistAutogradTest,
JitFaultyAgentRpcTest,
]
def generate_tests(
prefix: str,
mixin: type[RpcAgentTestFixture],
tests: list[type[RpcAgentTestFixture]],
module_name: str,
) -> dict[str, type[RpcAgentTestFixture]]:
"""Mix in the classes needed to autogenerate the tests based on the params.
Takes a series of test suites, each written against a "generic" agent (i.e.,
derived from the abstract RpcAgentTestFixture class), as the `tests` args.
Takes a concrete subclass of RpcAgentTestFixture, which specializes it for a
certain agent, as the `mixin` arg. Produces all combinations of them.
Returns a dictionary of class names to class type
objects which can be inserted into the global namespace of the calling
module. The name of each test will be a concatenation of the `prefix` arg
and the original name of the test suite.
The `module_name` should be the name of the calling module so
that the classes can be fixed to make it look like they belong to it, which
is necessary for pickling to work on them.
"""
ret: dict[str, type[RpcAgentTestFixture]] = {}
for test_class in tests:
if IS_SANDCASTLE and TEST_WITH_DEV_DBG_ASAN:
print(
f"Skipping test {test_class} on sandcastle for the following reason: "
"Skip dev-asan as torch + multiprocessing spawn have known issues",
file=sys.stderr,
)
continue
name = f"{prefix}{test_class.__name__}"
class_ = type(name, (test_class, mixin, SpawnHelper), {})
class_.__module__ = module_name
ret[name] = class_
return ret
@@ -0,0 +1,28 @@
import torch
import torch._dynamo.test_case
import torch.utils._pytree as pytree
class PytreeRegisteringTestCase(torch._dynamo.test_case.TestCase):
"""TestCase that prunes all temporary pytree registrations and resets Dynamo."""
def setUp(self) -> None:
super().setUp()
self._registered_pytree_nodes: list[type] = []
self._registered_constant_nodes: list[type] = []
def tearDown(self) -> None:
for cls in reversed(self._registered_pytree_nodes):
pytree._deregister_pytree_node(cls)
for cls in reversed(self._registered_constant_nodes):
pytree._deregister_pytree_node(cls)
torch._dynamo.reset()
super().tearDown()
def register_pytree_node(self, cls, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
pytree.register_pytree_node(cls, *args, **kwargs)
self._registered_pytree_nodes.append(cls)
def register_constant(self, cls: type) -> None:
pytree.register_constant(cls)
self._registered_constant_nodes.append(cls)
@@ -0,0 +1,144 @@
"""
This file contains the list of tests that are known to fail under Dynamo
We generate xFailIfTorchDynamo* for all tests in `dynamo_expected_failures`
We generate skipIfTorchDynamo* for all tests in `dynamo_skips`
We generate runWithoutCompiledAutograd for all tests in `compiled_autograd_skips`
For an easier-than-manual way of generating and updating these lists,
see scripts/compile_tests/update_failures.py
If you're adding a new test, and it's failing PYTORCH_TEST_WITH_DYNAMO=1,
either add the appropriate decorators to your test or add skips for them
via test/dynamo_skips and test/dynamo_expected_failures.
*These are not exactly unittest.expectedFailure and unittest.skip. We'll
always execute the test and then suppress the signal, if necessary.
If your tests crashes, or is slow, please use @skipIfTorchDynamo instead.
The expected failure and skip files are located in test/dynamo_skips and
test/dynamo_expected_failures. They're individual files rather than a list so
git will merge changes easier.
"""
import logging
import os
import sys
def find_test_dir() -> str | None:
# Find the path to the dynamo expected failure and skip files.
from os.path import abspath, basename, dirname, exists, join, normpath
if sys.platform == "win32":
return None
# Check relative to this file (local build):
test_dir = normpath(join(dirname(abspath(__file__)), "../../../test"))
if exists(join(test_dir, "dynamo_expected_failures")):
return test_dir
# Check relative to __main__ (installed builds relative to test file):
main = sys.modules["__main__"]
file = getattr(main, "__file__", None)
if file is None:
# Generated files do not have a module.__file__
return None
test_dir = dirname(abspath(file))
while dirname(test_dir) != test_dir:
if basename(test_dir) == "test" and exists(
join(test_dir, "dynamo_expected_failures")
):
return test_dir
test_dir = dirname(test_dir)
# Not found
return None
test_dir = find_test_dir()
if not test_dir:
logger = logging.getLogger(__name__)
logger.warning(
"test/dynamo_expected_failures directory not found - known dynamo errors won't be skipped."
)
# Tests that run without strict mode in PYTORCH_TEST_WITH_INDUCTOR=1.
# Please don't add anything to this list.
FIXME_inductor_non_strict = {
"test_modules",
"test_ops",
"test_ops_gradients",
"test_torch",
}
# We generate unittest.expectedFailure for all of the following tests
# when run under PYTORCH_TEST_WITH_DYNAMO=1.
# see NOTE [dynamo_test_failures.py] for more details
#
# This lists exists so we can more easily add large numbers of failing tests,
if test_dir is None:
dynamo_expected_failures = set()
dynamo_skips = set()
inductor_expected_failures = set()
inductor_skips = set()
compiled_autograd_skips = set()
else:
dynamo_failures_directory = os.path.join(test_dir, "dynamo_expected_failures")
dynamo_skips_directory = os.path.join(test_dir, "dynamo_skips")
dynamo_expected_failures = set(os.listdir(dynamo_failures_directory))
dynamo_skips = set(os.listdir(dynamo_skips_directory))
inductor_failures_directory = os.path.join(test_dir, "inductor_expected_failures")
inductor_skips_directory = os.path.join(test_dir, "inductor_skips")
inductor_expected_failures = set(os.listdir(inductor_failures_directory))
inductor_skips = set(os.listdir(inductor_skips_directory))
compiled_autograd_skips_directory = os.path.join(
test_dir, "compiled_autograd_skips"
)
compiled_autograd_skips = set(os.listdir(compiled_autograd_skips_directory))
# TODO: due to case sensitivity problems, for now list these files by hand
extra_dynamo_skips = {
"TestProxyTensorOpInfoCPU.test_make_fx_exhaustive_T_cpu_float32",
"TestProxyTensorOpInfoCPU.test_make_fx_exhaustive_t_cpu_float32",
"TestProxyTensorOpInfoCPU.test_make_fx_fake_exhaustive_T_cpu_float32",
"TestProxyTensorOpInfoCPU.test_make_fx_fake_exhaustive_t_cpu_float32",
"TestProxyTensorOpInfoCPU.test_make_fx_symbolic_exhaustive_T_cpu_float32",
"TestProxyTensorOpInfoCPU.test_make_fx_symbolic_exhaustive_t_cpu_float32",
"TestProxyTensorOpInfoCPU.test_make_fx_symbolic_exhaustive_inplace_T_cpu_float32",
"TestProxyTensorOpInfoCPU.test_make_fx_symbolic_exhaustive_inplace_t_cpu_float32",
"TestProxyTensorOpInfoCPU.test_make_fx_symbolic_exhaustive_out_T_cpu_float32",
"TestProxyTensorOpInfoCPU.test_make_fx_symbolic_exhaustive_out_t_cpu_float32",
}
dynamo_skips = dynamo_skips.union(extra_dynamo_skips)
# verify some invariants
for test in (
dynamo_expected_failures
| dynamo_skips
| inductor_expected_failures
| inductor_skips
):
if len(test.split(".")) != 2:
raise AssertionError(f'Invalid test name: "{test}"')
dynamo_intersection = dynamo_expected_failures.intersection(dynamo_skips)
if len(dynamo_intersection) > 0:
raise AssertionError(
"there should be no overlap between dynamo_expected_failures "
"and dynamo_skips, got " + str(dynamo_intersection)
)
inductor_intersection = inductor_expected_failures.intersection(inductor_skips)
if len(inductor_intersection) > 0:
raise AssertionError(
"there should be no overlap between inductor_expected_failures "
"and inductor_skips, got " + str(inductor_intersection)
)
@@ -0,0 +1,52 @@
import sys
from torch.utils._config_module import Config, install_config_module
e_bool = True
e_int = 1
e_float = 1.0
e_string = "string"
e_list = [1]
e_set = {1}
e_tuple = (1,)
e_dict = {1: 2}
e_none: bool | None = None
e_optional: bool | None = True
e_ignored = True
_e_ignored = True
magic_cache_config_ignored = True
# [@compile_ignored: debug]
e_compile_ignored = True
e_config: bool = Config(default=True)
e_jk: bool = Config(justknob="does_not_exist", default=True)
e_jk_false: bool = Config(justknob="does_not_exist", default=False)
e_env_default: bool = Config(env_name_default="ENV_TRUE", default=False)
e_env_default_FALSE: bool = Config(env_name_default="ENV_FALSE", default=True)
e_env_default_str: bool = Config(env_name_default="ENV_STR", default="default")
e_env_default_str_empty: bool = Config(
env_name_default="ENV_STR_EMPTY", default="default"
)
e_env_force: bool = Config(env_name_force="ENV_TRUE", default=False)
e_aliased_bool: bool = Config(
alias="torch.testing._internal.fake_config_module2.e_aliasing_bool"
)
e_deprecated: bool = Config(
default=True, deprecated=True, deprecation_message="is no longer needed"
)
e_not_deprecated: bool = Config(default=False)
e_deprecated_alias: bool = Config(
alias="torch.testing._internal.fake_config_module.e_not_deprecated",
deprecated=True,
deprecation_message="use something else instead",
)
class nested:
e_bool = True
_cache_config_ignore_prefix = ["magic_cache_config"]
_save_config_ignore = ["e_ignored"]
install_config_module(sys.modules[__name__])
@@ -0,0 +1,13 @@
import sys
from torch.utils._config_module import Config, install_config_module
e_aliasing_bool = False
e_env_default_multi: bool = Config(
env_name_default=["ENV_TRUE", "ENV_FALSE"], default=False
)
e_env_force_multi: bool = Config(env_name_force=["ENV_FAKE", "ENV_TRUE"], default=False)
install_config_module(sys.modules[__name__])
@@ -0,0 +1,11 @@
import sys
from typing import Callable # noqa: UP035
from torch.utils._config_module import install_config_module
e_list = [1]
e_set = {1}
e_func: Callable | None = None
install_config_module(sys.modules[__name__])
@@ -0,0 +1,684 @@
# mypy: ignore-errors
import functools
import unittest
import torch
from functorch.experimental.control_flow import map
from torch._higher_order_ops.flex_attention import (
flex_attention as flex_attention_hop,
)
from torch.nn.attention.flex_attention import (
_create_empty_block_mask,
create_block_mask,
flex_attention,
)
from torch.testing import make_tensor
from torch._higher_order_ops.inline_asm_elementwise import inline_asm_elementwise
from torch.testing._internal.common_device_type import onlyCUDA
from torch.testing._internal.common_dtype import all_types_and, custom_types
from torch.testing._internal.opinfo.core import DecorateInfo, OpInfo, SampleInput
from torch._higher_order_ops.invoke_subgraph import mark_compile_region
from torch._higher_order_ops import InvokeQuant, invoke_quant_packed
def sample_inputs_map(opinfo, device, dtype, requires_grad, **kwargs):
make_arg = functools.partial(
make_tensor, device=device, dtype=dtype, requires_grad=requires_grad
)
yield SampleInput(
[make_arg(2, 2, 2, low=0.1, high=2), make_arg(2, 2, 2, low=0.1, high=2)],
args=(make_arg(1, low=0.1, high=2), make_arg(1, low=0.1, high=2)),
)
def inner_f(x, y0, y1):
return [x[0].cos().add_(1.0) * y0, (x[1] + y1.sin()).cos_().view(x[1].size())]
def simple_map(xs, y0, y1):
def f(x, y0, y1):
return inner_f(x, y0, y1)
return map(f, xs, y0, y1)
def nested_map(xs, y0, y1):
def f1(xx, y0, y1):
def f2(x, y0, y1):
return inner_f(x, y0, y1)
return map(f2, xx, y0, y1)
return map(f1, xs, y0, y1)
def triple_nested_map(xs, y0, y1):
def f0(xs, y0, y1):
def f1(xx, y0, y1):
def f2(x, y0, y1):
return inner_f(x, y0, y1)
return map(f2, xx, y0, y1)
return map(f1, xs, y0, y1)
return map(f0, xs, y0, y1)
# PLEASE DON'T ADD ANYTHING NEW TO THIS LIST,
# and do add an OpInfo for your HOP.
# The OpInfo lets us do automated testing for the HOP to check that
# your HOP will work correctly with PyTorch!
#
# Your new HOP may fail some automated testing. That's OK. If you don't
# care about certain features (like torch.export), it's fine to xfail those
# failing tests. It is less fine to xfail a more critical check (like checking
# if torch.compile works with your HOP, or if your HOP has a docstring).
# If you don't know if a test is fine to xfail, please ask.
#
# There are legitimate reasons why something cannot be added to this list
# (e.g. it uses executorch which is not in PyTorch). If that's the case then
# please leave a comment.
FIXME_hop_that_doesnt_have_opinfo_test_allowlist = [
"custom_function_call",
"autograd_function_apply",
"run_and_save_rng_state",
"run_with_rng_state",
"run_dtensor_rng_op",
"graphsafe_run_with_rng_state",
"out_dtype",
"trace_wrapped",
'tag_activation_checkpoint',
'executorch_call_delegate',
'wrap',
'wrap_with_set_grad_enabled',
'auto_functionalized_v2',
'associative_scan',
'flat_apply', # is WIP, doesn't pass any of the tests yet
'wrap_with_autocast',
'wrap_activation_checkpoint',
'run_const_graph',
'auto_functionalized',
"map", # T183144629
"map_impl",
"with_effects",
"strict_mode",
"_export_tracepoint",
"call_torchbind",
"triton_kernel_wrapper_mutation",
"triton_kernel_wrapper_functional",
"hints_wrapper",
"dynamo_bypassing_wrapper", # TODO(soulitzer)
"foreach_map",
"aoti_call_delegate",
"print",
"inductor_compiled_code", # Tested separately in test_inductor_wrap_inductor_compile_regions
"invoke_leaf_function", # Needs torch.compile, tested separately in test_leaf_function*
]
torch.library.define(
"testlib::mutating_custom_op",
"(Tensor(a!) x, Tensor(b!) z) -> (Tensor, Tensor, Tensor)",
tags=torch.Tag.pt2_compliant_tag,
)
@torch.library.impl("testlib::mutating_custom_op", "cpu")
def foo_impl_cpu(x, z):
x.add_(5)
z.add_(5)
return x.clone(), z.clone(), x + z
@torch.library.impl("testlib::mutating_custom_op", "cuda")
def foo_impl_cuda(x, z):
x.add_(5)
z.add_(5)
return x.clone(), z.clone(), x + z
@torch.library.impl("testlib::mutating_custom_op", "xpu")
def foo_impl_xpu(x, z):
x.add_(5)
z.add_(5)
return x.clone(), z.clone(), x + z
@torch.library.register_fake("testlib::mutating_custom_op")
def foo_impl_abstract(x, z):
return x.clone(), z.clone(), x + z
def sample_inputs_cond(opinfo, device, dtype, requires_grad, **kwargs):
make_arg = functools.partial(
make_tensor, device=device, dtype=dtype, requires_grad=requires_grad
)
yield SampleInput(make_arg(2, 2, 2, low=0.1, high=2))
def simple_cond(x):
return torch.cond(x.sum() > 2, lambda x: (x.cos(),), lambda x: (x.sin(),), [x])
def sample_inputs_invoke_subgraph(opinfo, device, dtype, requires_grad, **kwargs):
make_arg = functools.partial(
make_tensor, device=device, dtype=dtype, requires_grad=requires_grad
)
yield SampleInput(make_arg(2, 2, 2, low=0.1, high=2))
@mark_compile_region
def fn_for_invoke_subgraph(x):
return torch.sin(x)
def simple_invoke_subgraph(x):
return fn_for_invoke_subgraph(x)
def sample_inputs_auto_functionalize(opinfo, device, dtype, requires_grad, **kwargs):
make_arg = functools.partial(
make_tensor, device=device, dtype=dtype, requires_grad=False
)
yield SampleInput(
make_arg(2, 2, 2, low=0.1, high=2), make_arg(2, 2, 2, low=0.1, high=2)
)
def simple_auto_functionalize(x, z):
return torch.ops.testlib.mutating_custom_op(x, z)
def sample_inputs_flex_attention(opinfo, device, dtype, requires_grad, **kwargs):
make_arg = functools.partial(
make_tensor, device=device, dtype=dtype, requires_grad=requires_grad
)
def score_mod(score, b, h, m, n):
return score + h
q, k, v = (make_arg(2, 2, 128, 8, low=0.1, high=2) for _ in range(3))
block_mask = _create_empty_block_mask(q, k)
yield SampleInput(q, k, v, score_mod, block_mask)
def sample_inputs_flex_attention_backward(
opinfo, device, dtype, requires_grad, **kwargs
):
make_arg = functools.partial(
make_tensor, device=device, dtype=dtype, requires_grad=False
)
def score_mod(score, b, h, m, n):
return score
def mask_mod(b, h, m, n):
return m >= n
q, k, v = (make_arg(2, 2, 128, 16, low=0.1, high=2) for _ in range(3))
block_mask = create_block_mask(mask_mod, B=2, H=2, Q_LEN=128, KV_LEN=128, device=device)
scale = 1.0 / q.size(-1) ** 0.5
out, logsumexp, _ = flex_attention_hop(
q, k, v, score_mod, block_mask.as_tuple(), scale, {},
)
yield SampleInput(
q,
args=(
k, v, out.detach(), logsumexp.detach(), torch.rand_like(out), None,
score_mod, None, block_mask.as_tuple(),
scale, {}, (), (),
),
)
def sample_inputs_flex_attention_backward_explicit_buffers(
opinfo, device, dtype, requires_grad, **kwargs
):
make_arg = functools.partial(
make_tensor, device=device, dtype=dtype, requires_grad=False
)
mask_offset = torch.full((), 128, device=device, dtype=torch.int32)
def score_mod(score, b, h, m, n):
return score
def mask_mod(b, h, m, n):
return m + mask_offset >= n
q, k, v = (make_arg(2, 2, 128, 16, low=0.1, high=2) for _ in range(3))
block_mask = create_block_mask(mask_mod, B=2, H=2, Q_LEN=128, KV_LEN=128, device=device)
scale = 1.0 / q.size(-1) ** 0.5
out, logsumexp, _ = flex_attention_hop(
q, k, v, score_mod, block_mask.as_tuple(), scale, {},
)
yield SampleInput(
q,
args=(
k, v, out.detach(), logsumexp.detach(), torch.rand_like(out), None,
score_mod, None, block_mask.as_tuple(),
scale, {}, (), (),
),
)
def simple_flex_attention_backward(
query,
key,
value,
out,
logsumexp,
grad_out,
grad_logsumexp,
fw_graph,
joint_graph,
block_mask,
scale,
kernel_options,
score_mod_other_buffers,
mask_mod_other_buffers,
):
return torch.ops.higher_order.flex_attention_backward(
query,
key,
value,
out,
logsumexp,
grad_out,
grad_logsumexp,
fw_graph,
joint_graph,
block_mask,
scale,
kernel_options,
score_mod_other_buffers,
mask_mod_other_buffers,
)
def sample_inputs_while_loop(opinfo, device, dtype, requires_grad, **kwargs):
make_arg = functools.partial(
make_tensor, device=device, dtype=dtype, requires_grad=False
)
yield SampleInput(
torch.tensor(3),
make_arg(2, 3, 4, low=0.1, high=2),
)
def simple_while_loop(iter_t, x):
def cond_fn(iter_t, x):
return iter_t > 0
def body_fn(iter_t, x):
return iter_t - 1, x.cos()
return torch._higher_order_ops.while_loop(cond_fn, body_fn, (iter_t, x))
def simple_while_loop_stack_output(iter_t, x):
def cond_fn(iter_t, x):
return iter_t > 0
def body_fn(iter_t, x):
return iter_t - 1, x.cos()
return torch._higher_order_ops.while_loop_stack_output(
cond_fn, body_fn, (iter_t, x), tuple()
)
def sample_inputs_local_map_hop(opinfo, device, dtype, requires_grad, **kwargs):
# TODO: once HOPs support DTensor inputs, we should also test DTensors
make_arg = functools.partial(
make_tensor, device=device, dtype=dtype, requires_grad=False
)
yield SampleInput(
make_arg(2, 3, 4, low=0.1, high=2),
make_arg(2, 3, 4, low=0.1, high=2),
)
def simple_local_map_hop(inp1, inp2):
def body_gm(inp1, inp2):
return inp1.cos() + inp2.sin()
gm = torch.fx.symbolic_trace(body_gm)
if not torch.distributed.is_available():
raise AssertionError("Expected torch.distributed to be available")
from torch.distributed.tensor.placement_types import Replicate
gm.meta["local_map_kwargs"] = {
"in_placements": (Replicate(), Replicate(), Replicate()),
"out_placements": ((Replicate(), Replicate(), Replicate()),),
}
# TODO: Dynamo would rewrite this op differently
return torch._higher_order_ops.local_map_hop(gm, inp1, inp2)
def sample_inputs_scan(opinfo, device, dtype, requires_grad, **kwargs):
make_arg = functools.partial(
make_tensor, device=device, dtype=dtype, requires_grad=requires_grad
)
yield SampleInput(
make_arg(2, 2, low=0.1, high=2),
make_arg(2, 2, 2, low=0.1, high=2),
)
def simple_scan(init, xs):
def combine_fn(carry, x):
result = carry @ x + x
return result, carry.clone()
return torch._higher_order_ops.scan(combine_fn, init, xs)
quant_tracer = InvokeQuant()
def simple_invoke_quant(x):
def fn(x, y):
return (torch.sin(x) * y,)
return quant_tracer(fn, x, x)[0] * 2.0
def simple_invoke_quant_packed(x):
def fn(x):
return (torch.sin(x),)
return invoke_quant_packed(fn, x)[0] * 2.0
def sample_inputs_inline_asm(opinfo, device, dtype, requires_grad, **kwargs):
make_arg = functools.partial(
make_tensor, device=device, dtype=dtype, requires_grad=requires_grad
)
yield SampleInput(make_arg(2, 2, 2, low=0.1, high=2))
def simple_inline_asm(x):
if torch.version.hip:
return inline_asm_elementwise(
x,
asm_str="v_mov_b32_e32 $0, $1",
constraints="=v, v",
dtype=torch.float32,
)
return inline_asm_elementwise(
x, asm_str="mov.f32 $0, $1;", constraints="=f,f", dtype=torch.float32
)
hop_db = [
OpInfo(
name="scan",
variant_test_name="simple",
op=simple_scan,
sample_inputs_func=sample_inputs_scan,
dtypes=all_types_and(torch.bool, torch.half),
supports_out=False,
check_batched_grad=False,
check_batched_gradgrad=False,
check_batched_forward_grad=False,
check_inplace_batched_forward_grad=False,
supports_autograd=False,
# "torch.compile with aot_autograd does not currently support double backward."
supports_gradgrad=False,
),
OpInfo(
name="invoke_subgraph",
variant_test_name="simple",
op=simple_invoke_subgraph,
sample_inputs_func=sample_inputs_invoke_subgraph,
dtypes=all_types_and(torch.bool, torch.half),
supports_out=False,
check_batched_grad=False,
check_batched_gradgrad=False,
check_batched_forward_grad=False,
check_inplace_batched_forward_grad=False,
supports_autograd=True,
# "torch.compile with aot_autograd does not currently support double backward."
supports_gradgrad=False,
),
OpInfo(
name="map",
variant_test_name="simple",
op=simple_map,
sample_inputs_func=sample_inputs_map,
dtypes=all_types_and(torch.bool, torch.half),
supports_out=False,
check_batched_grad=False,
check_batched_gradgrad=False,
check_batched_forward_grad=False,
check_inplace_batched_forward_grad=False,
),
OpInfo(
name="map",
variant_test_name="nested",
op=nested_map,
sample_inputs_func=sample_inputs_map,
dtypes=all_types_and(torch.bool, torch.half),
supports_out=False,
check_batched_grad=False,
check_batched_gradgrad=False,
check_batched_forward_grad=False,
check_inplace_batched_forward_grad=False,
),
OpInfo(
name="map",
variant_test_name="triple_nested",
op=triple_nested_map,
sample_inputs_func=sample_inputs_map,
dtypes=all_types_and(torch.bool, torch.half),
supports_out=False,
check_batched_grad=False,
check_batched_gradgrad=False,
check_batched_forward_grad=False,
check_inplace_batched_forward_grad=False,
),
OpInfo(
name="cond",
variant_test_name="simple",
op=simple_cond,
sample_inputs_func=sample_inputs_cond,
dtypes=all_types_and(torch.bool, torch.half),
supports_out=False,
check_batched_grad=False,
check_batched_gradgrad=False,
check_batched_forward_grad=False,
check_inplace_batched_forward_grad=False,
supports_autograd=True,
# "torch.compile with aot_autograd does not currently support double backward."
supports_gradgrad=False,
),
OpInfo(
name="invoke_quant",
variant_test_name="simple",
op=simple_invoke_quant,
sample_inputs_func=sample_inputs_invoke_subgraph,
dtypes=all_types_and(torch.bool, torch.half),
supports_out=False,
check_batched_grad=False,
check_batched_gradgrad=False,
check_batched_forward_grad=False,
check_inplace_batched_forward_grad=False,
supports_autograd=True,
# "torch.compile with aot_autograd does not currently support double backward."
skips=(
DecorateInfo(unittest.expectedFailure, "TestHOP", "test_aot_export"),
DecorateInfo(
unittest.expectedFailure, "TestHOP", "test_pre_dispatch_export"
),
DecorateInfo(unittest.expectedFailure, "TestHOP", "test_serialize_export"),
DecorateInfo(unittest.expectedFailure, "TestHOP", "test_retrace_export"),
),
# "torch.compile with aot_autograd does not currently support double backward."
supports_gradgrad=False,
),
OpInfo(
name="invoke_quant_packed",
variant_test_name="simple",
op=simple_invoke_quant_packed,
sample_inputs_func=sample_inputs_invoke_subgraph,
dtypes=all_types_and(torch.bool, torch.half),
supports_out=False,
check_batched_grad=False,
check_batched_gradgrad=False,
check_batched_forward_grad=False,
check_inplace_batched_forward_grad=False,
supports_autograd=True,
# "torch.compile with aot_autograd does not currently support double backward."
supports_gradgrad=False,
),
OpInfo(
name="while_loop",
variant_test_name="simple",
op=simple_while_loop,
sample_inputs_func=sample_inputs_while_loop,
dtypes=all_types_and(torch.bool, torch.half),
supports_out=False,
check_batched_grad=False,
check_batched_gradgrad=False,
check_batched_forward_grad=False,
check_inplace_batched_forward_grad=False,
supports_autograd=False,
),
OpInfo(
name="while_loop_stack_output",
variant_test_name="simple",
op=simple_while_loop_stack_output,
sample_inputs_func=sample_inputs_while_loop,
dtypes=all_types_and(torch.bool, torch.half),
supports_out=False,
check_batched_grad=False,
check_batched_gradgrad=False,
check_batched_forward_grad=False,
check_inplace_batched_forward_grad=False,
supports_autograd=False,
),
OpInfo(
name="auto_functionalize",
variant_test_name="simple",
op=simple_auto_functionalize,
sample_inputs_func=sample_inputs_auto_functionalize,
dtypes=all_types_and(torch.bool, torch.half),
supports_out=False,
check_batched_grad=False,
check_batched_gradgrad=False,
check_batched_forward_grad=False,
check_inplace_batched_forward_grad=False,
supports_autograd=False,
),
OpInfo(
name="flex_attention",
variant_test_name="simple",
op=flex_attention,
sample_inputs_func=sample_inputs_flex_attention,
dtypes=custom_types(torch.float16, torch.float32),
supports_out=False,
check_batched_grad=False,
check_batched_gradgrad=False,
check_batched_forward_grad=False,
check_inplace_batched_forward_grad=False,
skips=(
DecorateInfo(unittest.expectedFailure, "TestHOP", "test_aot_export"),
DecorateInfo(
unittest.expectedFailure, "TestHOP", "test_pre_dispatch_export"
),
DecorateInfo(unittest.expectedFailure, "TestHOP", "test_serialize_export"),
DecorateInfo(unittest.expectedFailure, "TestHOP", "test_retrace_export"),
),
decorators=[onlyCUDA],
),
OpInfo(
name="flex_attention_backward",
variant_test_name="simple",
op=simple_flex_attention_backward,
sample_inputs_func=sample_inputs_flex_attention_backward,
dtypes=custom_types(torch.float16, torch.float32),
supports_out=False,
check_batched_grad=False,
check_batched_gradgrad=False,
check_batched_forward_grad=False,
check_inplace_batched_forward_grad=False,
supports_autograd=False,
supports_gradgrad=False,
skips=(
DecorateInfo(unittest.expectedFailure, "TestHOP", "test_aot_export"),
DecorateInfo(
unittest.expectedFailure, "TestHOP", "test_pre_dispatch_export"
),
DecorateInfo(unittest.expectedFailure, "TestHOP", "test_serialize_export"),
DecorateInfo(unittest.expectedFailure, "TestHOP", "test_retrace_export"),
),
decorators=[onlyCUDA],
),
OpInfo(
name="flex_attention_backward",
variant_test_name="explicit_buffers",
op=simple_flex_attention_backward,
sample_inputs_func=sample_inputs_flex_attention_backward_explicit_buffers,
dtypes=custom_types(torch.float16, torch.float32),
supports_out=False,
check_batched_grad=False,
check_batched_gradgrad=False,
check_batched_forward_grad=False,
check_inplace_batched_forward_grad=False,
supports_autograd=False,
supports_gradgrad=False,
skips=(
DecorateInfo(unittest.expectedFailure, "TestHOP", "test_aot_export"),
DecorateInfo(
unittest.expectedFailure, "TestHOP", "test_pre_dispatch_export"
),
DecorateInfo(unittest.expectedFailure, "TestHOP", "test_serialize_export"),
DecorateInfo(unittest.expectedFailure, "TestHOP", "test_retrace_export"),
),
decorators=[onlyCUDA],
),
OpInfo(
name="local_map_hop",
variant_test_name="simple",
op=simple_local_map_hop,
sample_inputs_func=sample_inputs_local_map_hop,
dtypes=custom_types(torch.float16, torch.float32),
supports_out=False,
check_batched_grad=False,
check_batched_gradgrad=False,
check_batched_forward_grad=False,
check_inplace_batched_forward_grad=False,
skips=(
DecorateInfo(unittest.expectedFailure, "TestHOP", "test_aot_export"),
DecorateInfo(
unittest.expectedFailure, "TestHOP", "test_pre_dispatch_export"
),
DecorateInfo(unittest.expectedFailure, "TestHOP", "test_serialize_export"),
DecorateInfo(unittest.expectedFailure, "TestHOP", "test_retrace_export"),
),
decorators=[
onlyCUDA,
unittest.skipIf(
not torch.distributed.is_available(), "requires distributed build"
),
],
),
OpInfo(
name="inline_asm_elementwise",
variant_test_name="simple",
op=simple_inline_asm,
sample_inputs_func=sample_inputs_inline_asm,
dtypes=custom_types(torch.float32),
supports_out=False,
check_batched_grad=False,
check_batched_gradgrad=False,
check_batched_forward_grad=False,
check_inplace_batched_forward_grad=False,
supports_autograd=False,
decorators=[onlyCUDA],
),
]
@@ -0,0 +1,383 @@
# mypy: ignore-errors
from collections import defaultdict
from collections.abc import Iterable
import numpy as np
import torch
import hypothesis
from functools import reduce
from importlib.metadata import version
from hypothesis import assume
from hypothesis import settings
from hypothesis import strategies as st
from hypothesis.extra import numpy as stnp
from hypothesis.strategies import SearchStrategy
from torch.testing._internal.common_quantized import _calculate_dynamic_qparams, _calculate_dynamic_per_channel_qparams
# Setup for the hypothesis tests.
# The tuples are (torch_quantized_dtype, zero_point_enforce), where the last
# element is enforced zero_point. If None, any zero_point point within the
# range of the data type is OK.
# Tuple with all quantized data types.
_ALL_QINT_TYPES = (
torch.quint8,
torch.qint8,
torch.qint32,
)
# Enforced zero point for every quantized data type.
# If None, any zero_point point within the range of the data type is OK.
_ENFORCED_ZERO_POINT = defaultdict(lambda: None, {
torch.quint8: None,
torch.qint8: None,
torch.qint32: 0
})
def _get_valid_min_max(qparams):
scale, zero_point, _quantized_type = qparams
adjustment = 1 + torch.finfo(torch.float).eps
_long_type_info = torch.iinfo(torch.long)
long_min, long_max = _long_type_info.min / adjustment, _long_type_info.max / adjustment
# make sure intermediate results are within the range of long
min_value = max((long_min - zero_point) * scale, (long_min / scale + zero_point))
max_value = min((long_max - zero_point) * scale, (long_max / scale + zero_point))
return np.float32(min_value), np.float32(max_value)
# This wrapper wraps around `st.floats` and checks the version of `hypothesis`, if
# it is too old, removes the `width` parameter (which was introduced)
# in 3.67.0
def _floats_wrapper(*args, **kwargs):
if 'width' in kwargs and hypothesis.version.__version_info__ < (3, 67, 0):
# As long as nan, inf, min, max are not specified, reimplement the width
# parameter for older versions of hypothesis.
no_nan_and_inf = (
(('allow_nan' in kwargs and not kwargs['allow_nan']) or
'allow_nan' not in kwargs) and
(('allow_infinity' in kwargs and not kwargs['allow_infinity']) or
'allow_infinity' not in kwargs))
min_and_max_not_specified = (
len(args) == 0 and
'min_value' not in kwargs and
'max_value' not in kwargs
)
if no_nan_and_inf and min_and_max_not_specified:
if kwargs['width'] == 16:
kwargs['min_value'] = torch.finfo(torch.float16).min
kwargs['max_value'] = torch.finfo(torch.float16).max
elif kwargs['width'] == 32:
kwargs['min_value'] = torch.finfo(torch.float32).min
kwargs['max_value'] = torch.finfo(torch.float32).max
elif kwargs['width'] == 64:
kwargs['min_value'] = torch.finfo(torch.float64).min
kwargs['max_value'] = torch.finfo(torch.float64).max
kwargs.pop('width')
return st.floats(*args, **kwargs)
def floats(*args, **kwargs):
if 'width' not in kwargs:
kwargs['width'] = 32
return _floats_wrapper(*args, **kwargs)
"""Hypothesis filter to avoid overflows with quantized tensors.
Args:
tensor: Tensor of floats to filter
qparams: Quantization parameters as returned by the `qparams`.
Returns:
True
Raises:
hypothesis.UnsatisfiedAssumption
Note: This filter is slow. Use it only when filtering of the test cases is
absolutely necessary!
"""
def assume_not_overflowing(tensor, qparams):
min_value, max_value = _get_valid_min_max(qparams)
assume(tensor.min() >= min_value)
assume(tensor.max() <= max_value)
return True
"""Strategy for generating the quantization parameters.
Args:
dtypes: quantized data types to sample from.
scale_min / scale_max: Min and max scales. If None, set to 1e-3 / 1e3.
zero_point_min / zero_point_max: Min and max for the zero point. If None,
set to the minimum and maximum of the quantized data type.
Note: The min and max are only valid if the zero_point is not enforced
by the data type itself.
Generates:
scale: Sampled scale.
zero_point: Sampled zero point.
quantized_type: Sampled quantized type.
"""
@st.composite
def qparams(draw, dtypes=None, scale_min=None, scale_max=None,
zero_point_min=None, zero_point_max=None):
if dtypes is None:
dtypes = _ALL_QINT_TYPES
if not isinstance(dtypes, (list, tuple)):
dtypes = (dtypes,)
quantized_type = draw(st.sampled_from(dtypes))
_type_info = torch.iinfo(quantized_type)
qmin, qmax = _type_info.min, _type_info.max
# TODO: Maybe embed the enforced zero_point in the `torch.iinfo`.
_zp_enforced = _ENFORCED_ZERO_POINT[quantized_type]
if _zp_enforced is not None:
zero_point = _zp_enforced
else:
_zp_min = qmin if zero_point_min is None else zero_point_min
_zp_max = qmax if zero_point_max is None else zero_point_max
zero_point = draw(st.integers(min_value=_zp_min, max_value=_zp_max))
if scale_min is None:
scale_min = torch.finfo(torch.float).eps
if scale_max is None:
scale_max = torch.finfo(torch.float).max
scale = draw(floats(min_value=scale_min, max_value=scale_max, width=32))
return scale, zero_point, quantized_type
"""Strategy to create different shapes.
Args:
min_dims / max_dims: minimum and maximum rank.
min_side / max_side: minimum and maximum dimensions per rank.
Generates:
Possible shapes for a tensor, constrained to the rank and dimensionality.
Example:
# Generates 3D and 4D tensors.
@given(Q = qtensor(shapes=array_shapes(min_dims=3, max_dims=4))
some_test(self, Q):...
"""
@st.composite
def array_shapes(draw, min_dims=1, max_dims=None, min_side=1, max_side=None, max_numel=None):
"""Return a strategy for array shapes (tuples of int >= 1)."""
if min_dims >= 32:
raise AssertionError(f"Expected min_dims < 32, got {min_dims}")
if max_dims is None:
max_dims = min(min_dims + 2, 32)
if max_dims >= 32:
raise AssertionError(f"Expected max_dims < 32, got {max_dims}")
if max_side is None:
max_side = min_side + 5
candidate = st.lists(st.integers(min_side, max_side), min_size=min_dims, max_size=max_dims)
if max_numel is not None:
candidate = candidate.filter(lambda x: reduce(int.__mul__, x, 1) <= max_numel)
return draw(candidate.map(tuple))
"""Strategy for generating test cases for tensors.
The resulting tensor is in float32 format.
Args:
shapes: Shapes under test for the tensor. Could be either a hypothesis
strategy, or an iterable of different shapes to sample from.
elements: Elements to generate from for the returned data type.
If None, the strategy resolves to float within range [-1e6, 1e6].
qparams: Instance of the qparams strategy. This is used to filter the tensor
such that the overflow would not happen.
Generates:
X: Tensor of type float32. Note that NaN and +/-inf is not included.
qparams: (If `qparams` arg is set) Quantization parameters for X.
The returned parameters are `(scale, zero_point, quantization_type)`.
(If `qparams` arg is None), returns None.
"""
@st.composite
def tensor(draw, shapes=None, elements=None, qparams=None, dtype=np.float32):
if isinstance(shapes, SearchStrategy):
_shape = draw(shapes)
else:
_shape = draw(st.sampled_from(shapes))
if qparams is None:
if elements is None:
elements = floats(-1e6, 1e6, allow_nan=False, width=32)
X = draw(stnp.arrays(dtype=dtype, elements=elements, shape=_shape))
assume(not (np.isnan(X).any() or np.isinf(X).any()))
return X, None
qparams = draw(qparams)
if elements is None:
min_value, max_value = _get_valid_min_max(qparams)
elements = floats(min_value, max_value, allow_infinity=False,
allow_nan=False, width=32)
X = draw(stnp.arrays(dtype=dtype, elements=elements, shape=_shape))
# Recompute the scale and zero_points according to the X statistics.
scale, zp = _calculate_dynamic_qparams(X, qparams[2])
enforced_zp = _ENFORCED_ZERO_POINT.get(qparams[2], None)
if enforced_zp is not None:
zp = enforced_zp
return X, (scale, zp, qparams[2])
@st.composite
def per_channel_tensor(draw, shapes=None, elements=None, qparams=None):
if isinstance(shapes, SearchStrategy):
_shape = draw(shapes)
else:
_shape = draw(st.sampled_from(shapes))
if qparams is None:
if elements is None:
elements = floats(-1e6, 1e6, allow_nan=False, width=32)
X = draw(stnp.arrays(dtype=np.float32, elements=elements, shape=_shape))
assume(not (np.isnan(X).any() or np.isinf(X).any()))
return X, None
qparams = draw(qparams)
if elements is None:
min_value, max_value = _get_valid_min_max(qparams)
elements = floats(min_value, max_value, allow_infinity=False,
allow_nan=False, width=32)
X = draw(stnp.arrays(dtype=np.float32, elements=elements, shape=_shape))
# Recompute the scale and zero_points according to the X statistics.
scale, zp = _calculate_dynamic_per_channel_qparams(X, qparams[2])
enforced_zp = _ENFORCED_ZERO_POINT.get(qparams[2], None)
if enforced_zp is not None:
zp = enforced_zp
# Permute to model quantization along an axis
axis = int(np.random.randint(0, X.ndim, 1))
permute_axes = np.arange(X.ndim)
permute_axes[0] = axis
permute_axes[axis] = 0
X = np.transpose(X, permute_axes)
return X, (scale, zp, axis, qparams[2])
"""Strategy for generating test cases for tensors used in Conv.
The resulting tensors is in float32 format.
Args:
spatial_dim: Spatial Dim for feature maps. If given as an iterable, randomly
picks one from the pool to make it the spatial dimension
batch_size_range: Range to generate `batch_size`.
Must be tuple of `(min, max)`.
input_channels_per_group_range:
Range to generate `input_channels_per_group`.
Must be tuple of `(min, max)`.
output_channels_per_group_range:
Range to generate `output_channels_per_group`.
Must be tuple of `(min, max)`.
feature_map_range: Range to generate feature map size for each spatial_dim.
Must be tuple of `(min, max)`.
kernel_range: Range to generate kernel size for each spatial_dim. Must be
tuple of `(min, max)`.
max_groups: Maximum number of groups to generate.
elements: Elements to generate from for the returned data type.
If None, the strategy resolves to float within range [-1e6, 1e6].
qparams: Strategy for quantization parameters. for X, w, and b.
Could be either a single strategy (used for all) or a list of
three strategies for X, w, b.
Generates:
(X, W, b, g): Tensors of type `float32` of the following drawen shapes:
X: (`batch_size, input_channels, H, W`)
W: (`output_channels, input_channels_per_group) + kernel_shape
b: `(output_channels,)`
groups: Number of groups the input is divided into
Note: X, W, b are tuples of (Tensor, qparams), where qparams could be either
None or (scale, zero_point, quantized_type)
Example:
@given(tensor_conv(
spatial_dim=2,
batch_size_range=(1, 3),
input_channels_per_group_range=(1, 7),
output_channels_per_group_range=(1, 7),
feature_map_range=(6, 12),
kernel_range=(3, 5),
max_groups=4,
elements=st.floats(-1.0, 1.0),
qparams=qparams()
))
"""
@st.composite
def tensor_conv(
draw, spatial_dim=2, batch_size_range=(1, 4),
input_channels_per_group_range=(3, 7),
output_channels_per_group_range=(3, 7), feature_map_range=(6, 12),
kernel_range=(3, 7), max_groups=1, can_be_transposed=False,
elements=None, qparams=None
):
# Resolve the minibatch, in_channels, out_channels, iH/iW, iK/iW
batch_size = draw(st.integers(*batch_size_range))
input_channels_per_group = draw(
st.integers(*input_channels_per_group_range))
output_channels_per_group = draw(
st.integers(*output_channels_per_group_range))
groups = draw(st.integers(1, max_groups))
input_channels = input_channels_per_group * groups
output_channels = output_channels_per_group * groups
if isinstance(spatial_dim, Iterable):
spatial_dim = draw(st.sampled_from(spatial_dim))
feature_map_shape = [draw(st.integers(*feature_map_range)) for _ in range(spatial_dim)]
kernels = [draw(st.integers(*kernel_range)) for _ in range(spatial_dim)]
tr = False
weight_shape = (output_channels, input_channels_per_group) + tuple(kernels)
bias_shape = output_channels
if can_be_transposed:
tr = draw(st.booleans())
if tr:
weight_shape = (input_channels, output_channels_per_group) + tuple(kernels)
bias_shape = output_channels
# Resolve the tensors
if qparams is not None:
if isinstance(qparams, (list, tuple)):
if len(qparams) != 3:
raise AssertionError("Need 3 qparams for X, w, b")
else:
qparams = [qparams] * 3
X = draw(tensor(shapes=(
(batch_size, input_channels) + tuple(feature_map_shape),),
elements=elements, qparams=qparams[0]))
W = draw(tensor(shapes=(weight_shape,), elements=elements,
qparams=qparams[1]))
b = draw(tensor(shapes=(bias_shape,), elements=elements,
qparams=qparams[2]))
return X, W, b, groups, tr
# We set the deadline in the currently loaded profile.
# Creating (and loading) a separate profile overrides any settings the user
# already specified.
hypothesis_version = tuple(map(int, version("hypothesis").split(".")[:3]))
if (3, 16, 0) <= hypothesis_version < (3, 27, 0):
# Hypothesis 3.16 → 3.26: use `timeout` instead of `deadline`
settings.register_profile("no_deadline", timeout=hypothesis.unlimited)
else:
# Hypothesis >=3.27: use `deadline=None`
settings.register_profile("no_deadline", deadline=None)
# Activate the profile
settings.load_profile("no_deadline")
def assert_deadline_disabled():
"""Check that deadlines are effectively disabled across Hypothesis versions."""
if hypothesis_version < (3, 27, 0):
import warnings
warning_message = (
"Your version of hypothesis is outdated. "
"To avoid `DeadlineExceeded` errors, please update. "
f"Current hypothesis version: {hypothesis.__version__}"
)
warnings.warn(warning_message, stacklevel=2)
else:
if settings().deadline is not None:
raise AssertionError("Expected settings().deadline to be None")
@@ -0,0 +1,470 @@
# mypy: ignore-errors
import contextlib
import functools
import logging
import os
import re
import sys
import unittest
from subprocess import CalledProcessError
import torch
import torch._inductor.async_compile # noqa: F401 required to warm up AsyncCompile pools
import torch._inductor.config as config
from torch._inductor.codecache import CppCodeCache
from torch._inductor.codegen.common import (
get_custom_backend_config_for_device,
get_custom_backend_pass_for_device,
get_scheduling_for_device,
get_wrapper_codegen_for_device,
init_backend_registration,
register_backend_for_device,
)
from torch._inductor.codegen.wrapper import PythonWrapperCodegen
from torch._inductor.compile_fx import shape_env_from_inputs
from torch._inductor.custom_graph_pass import CustomGraphModulePass, CustomGraphPass
from torch._inductor.graph import GraphLowering
from torch._inductor.utils import (
get_gpu_shared_memory,
get_gpu_type,
GPU_TYPES,
is_big_gpu,
is_gpu,
OrderedSet,
)
from torch.fx.experimental.proxy_tensor import make_fx
from torch.utils._helion import has_helion
from torch.utils._pallas import has_pallas_package, has_tpu_pallas
from torch.utils._triton import has_triton
from torch.utils._config_module import ConfigModule
from torch.testing._internal.common_device_type import (
get_desired_device_type_test_bases,
)
from torch.testing._internal.common_utils import (
IS_CI,
IS_WINDOWS,
LazyVal,
TestCase,
)
from collections.abc import Callable
log: logging.Logger = logging.getLogger(__name__)
def test_cpu():
try:
CppCodeCache.load("")
return True
except (
CalledProcessError,
OSError,
torch._inductor.exc.InvalidCxxCompiler,
torch._inductor.exc.CppCompileError,
):
return False
HAS_CPU = LazyVal(test_cpu)
HAS_TRITON = has_triton()
HAS_PALLAS = has_pallas_package()
HAS_HELION = has_helion()
if HAS_TRITON:
import triton
TRITON_HAS_CPU = "cpu" in triton.backends.backends
else:
TRITON_HAS_CPU = False
HAS_CUDA_AND_TRITON = torch.cuda.is_available() and HAS_TRITON
HAS_XPU_AND_TRITON = torch.xpu.is_available() and HAS_TRITON
HAS_MPS = torch.mps.is_available()
HAS_GPU = HAS_CUDA_AND_TRITON or HAS_XPU_AND_TRITON
HAS_GPU_AND_TRITON = HAS_GPU
GPU_TYPE = get_gpu_type()
HAS_MULTIGPU = any(
getattr(torch, gpu).is_available() and getattr(torch, gpu).device_count() >= 2
for gpu in GPU_TYPES
)
_desired_test_bases = get_desired_device_type_test_bases(allow_xpu=True)
RUN_GPU = HAS_GPU and any(
is_gpu(getattr(x, "device_type", "")) for x in _desired_test_bases
)
RUN_CPU = HAS_CPU and any(
getattr(x, "device_type", "") == "cpu" for x in _desired_test_bases
)
HAS_TPU = has_tpu_pallas()
# TPU is a privateuse1 backend that isn't in _desired_test_bases (it requires
# runtime initialization before the test base is registered). Check the env var
# directly, matching the same semantics as RUN_CPU/RUN_GPU: when the env var is
# unset, run if the hardware is available; when set, only run if "tpu" is listed.
_only_for = os.environ.get("PYTORCH_TESTING_DEVICE_ONLY_FOR", "")
RUN_TPU = HAS_TPU and ("tpu" in _only_for.split(",") if _only_for else True)
def _check_has_dynamic_shape(
self: TestCase,
code,
):
for_loop_found = False
has_dynamic = False
lines = code.split("\n")
for line in lines:
if "for(" in line:
for_loop_found = True
if re.search(r";.*ks.*;", line) is not None:
has_dynamic = True
break
self.assertTrue(
has_dynamic, msg=f"Failed to find dynamic for loop variable\n{code}"
)
self.assertTrue(for_loop_found, f"Failed to find for loop\n{code}")
def skipDeviceIf(cond, msg, *, device):
if cond:
def decorate_fn(fn):
@functools.wraps(fn)
def inner(self, *args, **kwargs):
if not hasattr(self, "device"):
warn_msg = (
"Expect the test class to have attribute device but not found. "
)
if hasattr(self, "device_type"):
warn_msg += "Consider using the skip device decorators in common_device_type.py"
log.warning(warn_msg)
if self.device == device:
raise unittest.SkipTest(msg)
return fn(self, *args, **kwargs)
return inner
else:
def decorate_fn(fn):
return fn
return decorate_fn
def skip_windows_ci(name: str, file: str) -> None:
if IS_WINDOWS and IS_CI:
module = os.path.basename(file).strip(".py")
sys.stderr.write(
f"Windows CI does not have necessary dependencies for {module} tests yet\n"
)
if name == "__main__":
sys.exit(0)
raise unittest.SkipTest("requires sympy/functorch/filelock")
# TODO: Remove HAS_MPS condition when `HAS_GPU` includes HAS_MPS
requires_gpu = functools.partial(
unittest.skipIf, not (HAS_GPU or HAS_MPS), "requires gpu"
)
requires_triton = functools.partial(unittest.skipIf, not HAS_TRITON, "requires triton")
requires_helion = functools.partial(unittest.skipIf, not HAS_HELION, "requires helion")
def requires_gpu_with_enough_memory(min_mem_required):
def inner(fn):
total_memory = sys.maxsize
if torch.xpu.is_available():
total_memory = torch.xpu.get_device_properties().total_memory
elif torch.cuda.is_available():
total_memory = torch.cuda.get_device_properties().total_memory
if (
not (torch.cuda.is_available() or torch.xpu.is_available())
or total_memory < min_mem_required
):
return unittest.skip(
f"Only if the GPU device has at least {min_mem_required / 1e9:.3f}GB memory to be safe"
)(fn)
else:
return fn
return inner
skipCUDAIf = functools.partial(skipDeviceIf, device="cuda")
skipXPUIf = functools.partial(skipDeviceIf, device="xpu")
skipCPUIf = functools.partial(skipDeviceIf, device="cpu")
IS_A100 = LazyVal(lambda: HAS_CUDA_AND_TRITON and get_gpu_shared_memory() == 166912)
IS_H100 = LazyVal(lambda: HAS_CUDA_AND_TRITON and get_gpu_shared_memory() == 232448)
IS_BIG_GPU = LazyVal(lambda: HAS_GPU_AND_TRITON and is_big_gpu())
def dummy_graph() -> GraphLowering:
"""
Create a graph. This is useful for unit testing code which accesses
V.graph.sizevars.
"""
example_inputs = [torch.randn(10) for _ in range(2)]
gm = make_fx(torch.add, tracing_mode="fake")(*example_inputs)
shape_env = shape_env_from_inputs(example_inputs)
graph = GraphLowering(
gm,
shape_env=shape_env,
)
return graph
def maybe_skip_size_asserts(op):
"""
For certain ops, there meta and eager implementation returns different
strides. This cause size/strides assert fail. Skip adding those
asserts for now.
"""
if (
op.aten_name
in (
"fft_hfftn",
"fft_hfft",
"fft_hfft2",
"fft_ihfftn",
"fft_fft",
"fft_fft2",
"fft_fftn",
"fft_ifft",
"fft_ifft2",
"fft_ifftn",
"fft_irfft",
"fft_irfft2",
"fft_irfftn",
"fft_ihfft",
"fft_ihfft2",
"fft_rfft",
"fft_rfft2",
"fft_rfftn",
"linalg_eig",
"linalg_eigvals",
)
and "TORCHINDUCTOR_SIZE_ASSERTS" not in os.environ
):
return torch._inductor.config.patch(size_asserts=False)
else:
return contextlib.nullcontext()
def get_func_call() -> str:
return (
"void inductor_entry_impl("
if torch._inductor.config.cpp_wrapper
else "def call("
)
def get_kernel_launch() -> str:
return "call_triton_" if torch._inductor.config.cpp_wrapper else ".run("
def clone_preserve_strides_offset(x, device=None):
if not isinstance(x, torch.Tensor):
return x
buffer = torch.as_strided(
x, (x.untyped_storage().size() // x.element_size(),), (1,), 0
)
if not device:
buffer = buffer.clone()
else:
buffer = buffer.to(device, copy=True)
out = torch.as_strided(buffer, x.size(), x.stride(), x.storage_offset())
return out
# define the e4m3/e5m2 constants
E4M3_MAX_POS = torch.finfo(torch.float8_e4m3fn).max
E5M2_MAX_POS = torch.finfo(torch.float8_e5m2).max
E4M3FNUZ_MAX_POS = torch.finfo(torch.float8_e4m3fnuz).max
E5M2FNUZ_MAX_POS = torch.finfo(torch.float8_e5m2fnuz).max
FP16_MAX_POS: float = torch.finfo(torch.float16).max
EPS: float = 1e-12
Tensor = torch.Tensor
def _to_fp8_saturated(x: Tensor, float8_dtype: torch.dtype) -> Tensor:
# The default behavior in PyTorch for casting to `float8_e4m3fn`
# and `e5m2` is to not saturate. In this context, we should saturate.
# A common case where we want to saturate is when the history of a
# tensor has a maximum value of `amax1`, and the current amax value
# is `amax2`, where `amax1 < amax2`. This is common when using delayed
# scaling.
if float8_dtype == torch.float8_e4m3fn:
x = x.clamp(min=-1 * E4M3_MAX_POS, max=E4M3_MAX_POS)
elif float8_dtype == torch.float8_e5m2:
x = x.clamp(min=-1 * E5M2_MAX_POS, max=E5M2_MAX_POS)
elif float8_dtype == torch.float8_e4m3fnuz:
x = x.clamp(min=-1 * E4M3FNUZ_MAX_POS, max=E4M3FNUZ_MAX_POS)
elif float8_dtype == torch.float8_e5m2fnuz:
x = x.clamp(min=-1 * E5M2FNUZ_MAX_POS, max=E5M2FNUZ_MAX_POS)
else:
raise TypeError(f"Unsupported float8_dtype: {float8_dtype}")
return x.to(float8_dtype)
@torch.no_grad()
def _amax_to_scale(
amax: torch.Tensor, float8_dtype: torch.dtype, orig_dtype: torch.dtype
) -> torch.Tensor:
# To make scale dtype to be fp32 for accuracy
amax = amax.float()
if float8_dtype == torch.float8_e4m3fn:
res = E4M3_MAX_POS / torch.clamp(amax, min=EPS)
else: # e5m2
res = E5M2_MAX_POS / torch.clamp(amax, min=EPS)
# Ensure that the scale is representable in float16,
# this helps when amax is small. We are assuming that we don't need
# to care about this for float32/bfloat16.
if orig_dtype is torch.float16:
res = torch.clamp(res, max=FP16_MAX_POS)
return res
def _quantize_tensorwise(x: Tensor, float8_dtype: torch.dtype):
amax = torch.max(torch.abs(x))
scale = _amax_to_scale(amax, float8_dtype, x.dtype)
x_fp8 = _to_fp8_saturated(x * scale, float8_dtype)
inverse_scale = scale.reciprocal()
return x_fp8, inverse_scale
def _quantize_rowwise(x: Tensor, float8_dtype: torch.dtype):
amax = torch.max(torch.abs(x), dim=1, keepdim=True).values
scale = _amax_to_scale(amax, float8_dtype, x.dtype)
x_fp8 = _to_fp8_saturated(x * scale, float8_dtype)
inverse_scale = scale.reciprocal()
return x_fp8, inverse_scale
def _quantize_blockwise(
x: Tensor, float8_dtype: torch.dtype, block_outer: int, block_inner: int
):
min_outer = min(block_outer, x.shape[0])
min_inner = min(block_inner, x.shape[1])
x = x.unflatten(1, (-1, min_inner)).unflatten(0, (-1, min_outer))
amax = x.abs().amax(dim=[1, 3], keepdim=True).float()
scale = _amax_to_scale(amax, float8_dtype, x.dtype)
x = x.flatten(2, 3).flatten(0, 1)
scale = scale.flatten(2, 3).flatten(0, 1)
scale_expanded = scale.repeat_interleave(min_outer, dim=0).repeat_interleave(
min_inner, dim=1
)
x_fp8 = _to_fp8_saturated(
x / scale_expanded, # Ensures that scaling doesn't cause inf/nan values
float8_dtype,
)
inverse_scale = scale.reciprocal()
return x_fp8, inverse_scale
class MockGraphHandler(GraphLowering):
"""Minimal mock graph handler for testing virtualized context."""
def __init__(self, name_to_buffer=None):
import torch._inductor.sizevars
self.sizevars = torch._inductor.sizevars.SizeVarAllocator()
self.name_to_buffer = name_to_buffer or {}
self.graph_inputs = {}
self.mutated_buffers = OrderedSet()
self.removed_buffers = OrderedSet()
self.constants = {}
self.scheduler = None
def get_dtype(self, buffer_name: str) -> torch.dtype: # noqa: ARG002
"""Return default dtype for any buffer (for testing)."""
return torch.float32
@contextlib.contextmanager
def patch_inductor_backend(
device: str,
python_wrapper_codegen: PythonWrapperCodegen = None,
custom_pass: CustomGraphModulePass = None,
custom_backend_config: ConfigModule = None,
):
"""
Patch the inductor backend for a specific device.
"""
# Make sure the backend is already registered
init_backend_registration()
# Get the original registration parameters
original_scheduling = get_scheduling_for_device(device)
original_python_wrapper = get_wrapper_codegen_for_device(device, False)
original_cpp_wrapper = get_wrapper_codegen_for_device(device, True)
original_fx_wrapper = get_wrapper_codegen_for_device(device, fx_wrapper=True)
original_custom_pass = get_custom_backend_pass_for_device(device)
original_custom_backend_config = get_custom_backend_config_for_device(device)
try:
# Register modified backend for the device
register_backend_for_device(
device,
original_scheduling,
(
python_wrapper_codegen
if python_wrapper_codegen is not None
else original_python_wrapper
),
original_cpp_wrapper,
original_fx_wrapper,
custom_pass if custom_pass is not None else original_custom_pass,
(
custom_backend_config
if custom_backend_config is not None
else original_custom_backend_config
),
)
yield
finally:
# Restore the original backend
register_backend_for_device(
device,
original_scheduling,
original_python_wrapper,
original_cpp_wrapper,
original_fx_wrapper,
original_custom_pass,
original_custom_backend_config,
)
def patch_custom_fallback_pass(predicate: Callable[[torch.fx.Node], bool]) -> contextlib.ContextDecorator:
"""
Create a custom pass which falls back based on the provided predicate. For example,
we could provide a predicate which returns True for all aten.add.default nodes.
Returns a context activating the pass.
"""
class Pass(CustomGraphPass):
def __call__(self, graph: torch.fx.Graph):
for node in graph.nodes:
if predicate(node):
node.meta["should_fallback"] = True
def uuid(self):
return None
return config.patch(post_grad_custom_pre_pass=Pass())
@@ -0,0 +1,726 @@
# mypy: ignore-errors
# Torch
from torch.jit.annotations import BroadcastingList2, BroadcastingList3 # noqa: F401
import torch.nn.functional as F
import torch
import torch.cuda
import torch.jit
import torch.jit._logging
import torch.jit.frontend
from torch.testing._internal.common_nn import module_tests, get_new_module_tests
from torch.testing._internal.common_utils import is_iterable_of_tensors, noncontiguous_like
import collections
from copy import deepcopy
from typing import Any
import math # noqa: F401
# Testing utils
from torch import inf
if torch.get_default_dtype() != torch.float32:
raise AssertionError(f"Expected torch.get_default_dtype() == torch.float32, got {torch.get_default_dtype()}")
L = 20
M = 10
S = 5
def unpack_variables(args):
if isinstance(args, tuple):
return tuple(unpack_variables(elem) for elem in args)
else:
return args
class dont_convert(tuple):
__slots__ = ()
non_differentiable = collections.namedtuple('non_differentiable', ['tensor'])
def create_input(call_args, requires_grad=True, non_contiguous=False, call_kwargs=None, dtype=torch.float, device=None):
if not isinstance(call_args, tuple):
call_args = (call_args,)
def map_arg(arg):
def maybe_non_contig(tensor):
if not non_contiguous or tensor.numel() < 2:
return tensor.clone()
return noncontiguous_like(tensor)
def conjugate(tensor):
return tensor.conj()
if isinstance(arg, (torch.Size, dont_convert)):
return arg
elif isinstance(arg, tuple) and len(arg) == 0:
var = conjugate(torch.randn((), dtype=dtype, device=device))
var.requires_grad = requires_grad
return var
elif isinstance(arg, tuple) and not isinstance(arg[0], torch.Tensor):
return conjugate(maybe_non_contig(torch.randn(*arg, dtype=dtype, device=device))).requires_grad_(requires_grad)
# double check casting
elif isinstance(arg, non_differentiable):
if isinstance(arg.tensor, torch.Tensor):
return conjugate(maybe_non_contig(arg.tensor.to(device=device)))
return conjugate(maybe_non_contig(arg.tensor.to(device=device)))
elif isinstance(arg, torch.Tensor):
if arg.is_complex() != dtype.is_complex:
raise RuntimeError("User provided tensor is real for a test that runs with complex dtype, ",
"which is not supported for now")
# NOTE: We do clone() after detach() here because we need to be able to change size/storage of v afterwards
v = conjugate(maybe_non_contig(arg)).detach().to(device=device).clone()
v.requires_grad = requires_grad and (v.is_floating_point() or v.is_complex())
return v
elif callable(arg):
return map_arg(arg(dtype=dtype, device=device))
else:
return arg
args_out = tuple(map_arg(arg) for arg in call_args)
kwargs_out = {k: map_arg(v) for k, v in call_kwargs.items()} if call_kwargs else {}
return args_out, kwargs_out
# NB: JIT script tests for all nn functional interfaces, script mode does
# not support in_place operations yet, so no inplace operation tests added.
# removed all the deprecated functions
#
# (
# method name,
# input size/constructing fn,
# args (tuple represents shape of a tensor arg),
# test variant name(will be used at test name suffix,
# 'inplace' skips grad tests), // optional
# (True, nonfusible_nodes, fusible_nodes) for autodiff // optional
# fn to determine if test should be skipped, // optional
# fn mapping output to part that should be gradcheck'ed, // optional
# kwargs for function, // optional
# )
def get_nn_functional_tests():
nn_functional_tests = [
('conv1d', (S, S, S), ((S, S, S),)),
('conv2d', (S, S, S, S), ((S, S, S, S),)),
('conv3d', (S, S, S, S, S), ((S, S, S, S, S),)),
('conv_transpose1d', (S, S, S), ((S, S, S),)),
('conv_transpose2d', (S, S, S, S), ((S, S, S, S),)),
('conv_transpose3d', (S, S, S, S, S), ((S, S, S, S, S),)),
('conv_tbc', (S, S, S), ((S, S, S), (S,), 2)),
('avg_pool1d', (S, S, S), (3,)),
('avg_pool2d', (S, S, S, S), (3,), '', (True,)),
('avg_pool3d', (S, S, S, S, S), (3,)),
('fractional_max_pool2d', (S, S, S, S), (3, [2, 3],)),
('max_pool1d', (S, S, S), (2, 1)),
('max_pool1d', (S, S, S), (2, 1, 1, 1, False, True), 'with_indices'),
('max_pool2d', (S, S, S, S), (2, 1), '', (True, 'aten::max_pool2d_with_indices')),
('max_pool2d', (S, S, S, S), (2, 1, 1, 1, False, True), 'with_indices', (True, 'aten::max_pool2d_with_indices')),
('max_pool3d', (S, S, S, S, S), (2, 1)),
('max_unpool1d', torch.tensor([[[2., 4]]]), (torch.tensor([[[1, 3]]]), 2, 2, 0)),
('max_unpool2d', torch.tensor([[[[2., 4]]]]), (torch.tensor([[[[1, 3]]]]), 2, 2, 0)),
('max_unpool3d', torch.tensor([[[[[2., 4]]]]]), (torch.tensor([[[[[1, 3]]]]]), 2, 2, 0)),
('lp_pool1d', (S, S, S), (2., 3, 2,)),
('lp_pool2d', (S, S, S, S), (2., 3, 2,)),
('lp_pool3d', (S, S, S, S, S), (2., 3, 2,)),
('adaptive_max_pool1d', (S, S, S), (5,)),
('adaptive_max_pool2d', (S, S, S, S), ([5, 7],)),
('adaptive_max_pool3d', (S, S, S, S, S), ([3, 2, 2],)),
('adaptive_avg_pool1d', (S, S, S), (5,), '', (True,)),
('adaptive_avg_pool2d', (S, S, S, S), ([5, 7],), '', (True,)),
('adaptive_avg_pool3d', (S, S, S, S, S), ([3, 2, 2],), '', (True,)),
('dropout', (S, S, S), (0.5,), '', (True, 'aten::native_dropout')),
('alpha_dropout', (S, S, S), (0.5,)),
('dropout2d', (S, S, S), (0.5,)),
('dropout2d', (S, S, S, S), (0.5,), 'batched'),
('dropout3d', (S, S, S, S), (0.5,)),
('dropout3d', (S, S, S, S, S), (0.5,), 'batched'),
('feature_alpha_dropout', (S, S, S), (0.5,)),
('threshold', (S, S, S), (0.1, 2.), '', (True,)),
('threshold', (S, S, S), (0.1, 2., True), 'inplace'),
('relu', (S, S, S), (), '', (True,)),
('relu', (S, S, S), (), 'inplace'),
('glu', (S - 1, S - 1, S - 1), (),),
('hardtanh', (S, S, S), (-0.5, 0.5), '', (True,)),
('hardtanh', (S, S, S), (-0.5, 0.5, True), 'inplace'),
('relu6', (S, S, S), (), '', (True,)),
('relu6', (S, S, S), (True), 'inplace'),
('elu', (S, S, S), (0.9,),),
('elu', (S, S, S), (0.9, True), 'inplace'),
('selu', (S, S, S), (),),
('selu', (S, S, S), (True), 'inplace'),
('celu', (S, S, S), (0.9,),),
('celu', (S, S, S), (0.9, True), 'inplace'),
('leaky_relu', (S, S, S), (0.02,), '', (True,)),
('leaky_relu', (S, S, S), (0.02,), 'inplace'),
('rrelu', (S, S), (0.1, 0.3, False),),
('rrelu', (S, S), (0.1, 0.3, False, True), 'inplace'),
('hardshrink', (S, S, S), (0.4,), '', (True,)),
('tanhshrink', (S, S, S), (),),
('softsign', (S, S, S), (),),
('softplus', (S, S, S), (), '', (True,)),
('softmin', (S, S, S), (0,),),
('softmax', (S, S, S), (0,), '', (True,)),
('softmax', (S, S, S), (0, 3, torch.double), 'with_all_args', (True,)),
('tanh', (S, S, S), (), '', (True,)),
('sigmoid', (S, S, S), (), '', (True,)),
('silu', (S, S, S), (), '', (True,)),
('log_softmax', (S, S, S), (0,), '', (True,)),
('linear', (S, S), ((M, S),), '', (True, ['aten::linear'])),
('linear', (S, S), ((M, S), (M,)), 'addmm', (True, ['aten::linear'])),
('bilinear', (S, S, S), ((S, S, M), torch.zeros(M, S, M),),),
('embedding', torch.tensor([[1, 2, 4, 5], [4, 3, 2, 5]]), (torch.rand(6, 3), ), '', (True,)),
('embedding_bag', torch.tensor([1, 2, 4, 2]), (torch.rand(5, 3), torch.tensor([0, 4]),),),
('batch_norm', (S, S),
(non_differentiable(torch.randn(S)), non_differentiable(torch.ones(S)), None, None, True, ),
'training', (True, 'aten::_batch_norm_impl_index')),
('batch_norm', (0, S, S, S),
(non_differentiable(torch.randn(S)), non_differentiable(torch.ones(S)),
non_differentiable(torch.randn(S)), non_differentiable(torch.ones(S)), True, ),
'size_zero', (True, 'aten::_batch_norm_impl_index')),
('batch_norm', (0, S, S, S),
(non_differentiable(torch.randn(S)), non_differentiable(torch.ones(S)),
non_differentiable(torch.randn(S)), non_differentiable(torch.ones(S)), True, ),
'size_zero_inference', (True, 'aten::_batch_norm_impl_index')),
('batch_norm', (S, S),
(non_differentiable(torch.randn(S)), non_differentiable(torch.ones(S)),
non_differentiable(torch.randn(S)), non_differentiable(torch.ones(S)), True, ),
'with_weight_and_bias_training', (True, 'aten::_batch_norm_impl_index')),
('batch_norm', (S, S), (non_differentiable(torch.randn(S)), non_differentiable(torch.ones(S)),
None, non_differentiable(torch.ones(S)), True, ),
'with_only_bias_training', (True, 'aten::_batch_norm_impl_index')),
('batch_norm', (S, S), (non_differentiable(torch.randn(S)), non_differentiable(torch.ones(S)),
non_differentiable(torch.randn(S)), None, True, ),
'with_only_weight_training', (True, 'aten::_batch_norm_impl_index')),
('batch_norm', (S, S), (non_differentiable(torch.randn(S)), non_differentiable(torch.ones(S)),
None, None, False, ),
'inference', (True, 'aten::_batch_norm_impl_index')),
('batch_norm', (S, S), (non_differentiable(torch.randn(S)), non_differentiable(torch.ones(S)),
non_differentiable(torch.randn(S)), non_differentiable(torch.ones(S)), False, ),
'with_weight_and_bias_inference', (True, 'aten::_batch_norm_impl_index')),
('batch_norm', (S, S), (non_differentiable(torch.randn(S)), non_differentiable(torch.ones(S)),
None, non_differentiable(torch.ones(S)), False, ),
'with_only_bias_inference', (True, 'aten::_batch_norm_impl_index')),
('batch_norm', (S, S), (non_differentiable(torch.randn(S)), non_differentiable(torch.ones(S)),
non_differentiable(torch.randn(S)), None, False, ),
'with_only_weight_inference', (True, 'aten::_batch_norm_impl_index')),
('instance_norm', (S, S, S), (non_differentiable(torch.zeros(S)), non_differentiable(torch.ones(S))),),
('layer_norm', (S, S, S, S), ([5],), '',
(False, ['aten::contiguous', 'aten::_batch_norm_impl_index'])),
('layer_norm', (S, S, S, S), ([5], non_differentiable(torch.rand(S)),), 'with_only_weight',
(False, ['aten::contiguous', 'aten::_batch_norm_impl_index'])),
('layer_norm', (S, S, S, S), ([5], None, non_differentiable(torch.rand(S)),), 'with_only_bias',
(False, ['aten::contiguous', 'aten::_batch_norm_impl_index'])),
('layer_norm', (S, S, S, S), ([5], non_differentiable(torch.rand(S)),
non_differentiable(torch.rand(S))), 'with_weight_and_bias',
(False, ['aten::contiguous', 'aten::_batch_norm_impl_index', 'aten::addcmul'])),
('group_norm', (S, S, S), (1, torch.rand(5),),),
('local_response_norm', (S, S, S), (2, ),),
('nll_loss', F.log_softmax(torch.randn(3, 5), dim=0), (torch.tensor([1, 0, 4]),), '',),
('poisson_nll_loss', torch.rand(S, 2), (torch.rand(S, 2),),),
('poisson_nll_loss', torch.rand(S, 2), (torch.rand(S, 2), True, True), 'full'),
('kl_div', F.log_softmax(torch.randn(S, 10), 1), (F.softmax(torch.randn(S, 10), 1),),),
('cross_entropy', (3, S), (torch.randint(S, (3,), dtype=torch.int64),),),
('binary_cross_entropy_with_logits', (3,), (torch.empty(3).random_(2), ),),
('smooth_l1_loss', (3, S), (non_differentiable(torch.rand(3, S)),),),
('huber_loss', (3, S), (non_differentiable(torch.rand(3, S)),),),
('l1_loss', (3, S), (non_differentiable(torch.rand(3, S)),),),
('mse_loss', (3, S), (non_differentiable(torch.rand(3, S)),),),
('smooth_l1_loss', (3, S), ((torch.rand(3, S)),), 'with_grad'),
('huber_loss', (3, S), ((torch.rand(3, S)),), 'with_grad'),
('l1_loss', (3, S), ((torch.rand(3, S)),), 'with_grad'),
('mse_loss', (3, S), ((torch.rand(3, S)),), 'with_grad'),
('margin_ranking_loss', (S,), ((S,), (S,)),),
('hinge_embedding_loss', (3, S), (non_differentiable(torch.rand(3, S)),),),
('soft_margin_loss', (3, S), (non_differentiable(torch.rand(3, S)),),),
('multilabel_soft_margin_loss', (3, S), (non_differentiable(torch.rand(3, S)),),),
('cosine_embedding_loss', (S, S), ((S, S), non_differentiable(torch.rand(S,))),),
('pixel_shuffle', (1, 9, 4, 4), (3,),),
('pixel_unshuffle', (1, 1, 12, 12), (3,),),
('affine_grid', (S, 2, 3), (torch.Size([S, 1, 7, 7]),),),
('pad', (3, 3, 4, 2), ([1, 1],),),
('pairwise_distance', (S, S), ((S, S),),),
('pdist', (S, S), (),),
('cosine_similarity', (S, S), ((S, S),),),
('triplet_margin_loss', (S, S), ((S, S), (S, S)),),
('normalize', (S, S, S), (),),
('unfold', (S, S, S, S), ([2, 3]),),
('fold', (1, 3 * 2 * 2, 12), ([4, 5], [2, 2]),),
('grid_sample', (S, S, S, S), (non_differentiable(torch.rand(S, S, S, 2)),),),
('gumbel_softmax', (S, S), (2.,), '', (True, ['aten::softmax', 'aten::add', 'aten::div'], ['aten::neg'])),
('gumbel_softmax', (S, S), (2., True,), 'hard', (True, ['aten::softmax', 'aten::add', 'aten::div'], ['aten::neg'])),
('multilabel_margin_loss', torch.tensor([[0.2, -0.2, 0.07]]), (torch.tensor([[0, 0, 1]]),),),
('multi_margin_loss', (S, S), (non_differentiable(torch.randint(S, (S, ), dtype=torch.int64)),
1, 1., non_differentiable(torch.randn(S))),),
('binary_cross_entropy', torch.randn(3, 2).sigmoid(), (non_differentiable(torch.rand(3, 2)),
non_differentiable(torch.randn(3, 2))),),
('binary_cross_entropy', torch.randn(3, 2).sigmoid(),
(non_differentiable(torch.rand(3, 2)),
non_differentiable(torch.randn(3, 2)), None, None, 'mean'), 'size_average'),
('ctc_loss', torch.rand(S, S, S).log_softmax(2).detach().requires_grad_(),
(torch.randint(1, S, (S, S), dtype=torch.long), torch.full((S,), S, dtype=torch.long),
torch.randint(1, S, (S,), dtype=torch.long))),
('upsample', torch.randn(S, S, M, M), (None, 2.), 'with_scale'),
('upsample', torch.randn(S, S, M, M), (4,), 'with_size'),
('interpolate', torch.zeros(3, 3).view(1, 1, 3, 3), (2,), 'nearest_4d'),
('interpolate', torch.randn(S, S, M, M), (None, 2.), 'nearest_4d_with_scale'),
('interpolate', torch.randn(S, S, M, M), (4,), 'nearest_4d_with_size'),
('interpolate', torch.zeros(3, 3).view(1, 1, 3, 3), (2,), 'area_4d'),
('interpolate', torch.randn(S, S, M, M), (None, 2.), 'area_4d_with_scale'),
('interpolate', torch.randn(S, S, M, M), (4,), 'area_4d_with_size'),
('interpolate', torch.zeros(3, 3).view(1, 1, 3, 3), (2,), 'bilinear_4d'),
('interpolate', torch.randn(S, S, M, M), (None, 2.), 'bilinear_4d_with_scale'),
('interpolate', torch.randn(S, S, M, M), (4,), 'bilinear_4d_with_size'),
('interpolate', torch.zeros(3, 3).view(1, 1, 3, 3), (2,), 'bicubic_4d'),
('interpolate', torch.randn(S, S, M, M), (None, 2.), 'bicubic_4d_with_scale'),
('interpolate', torch.randn(S, S, M, M), (4,), 'bicubic_4d_with_size'),
('interpolate', torch.zeros(3, 3).view(1, 3, 3), (2,), 'nearest_3d'),
('interpolate', torch.randn(S, M, M), (None, 2.), 'nearest_3d_with_scale'),
('interpolate', torch.randn(S, M, M), (4,), 'nearest_3d_with_size'),
('interpolate', torch.zeros(3, 3).view(1, 3, 3), (2,), 'area_3d'),
('interpolate', torch.randn(S, M, M), (None, 2.), 'area_3d_with_scale'),
('interpolate', torch.randn(S, M, M), (4,), 'area_3d_with_size'),
('interpolate', torch.zeros(3, 3).view(1, 3, 3), (2,), 'linear_3d'),
('interpolate', torch.randn(S, M, M), (None, 2.), 'linear_3d_with_scale'),
('interpolate', torch.randn(S, M, M), (4,), 'linear_3d_with_size'),
('interpolate', torch.randn(S, M, M, M, M), (None, 2.), 'nearest_5d_with_scale'),
('interpolate', torch.randn(S, M, M, M, M), (4,), 'nearest_5d_with_size'),
('interpolate', torch.zeros(3, 3, 3).view(1, 1, 3, 3, 3), (2,), 'area_5d'),
('interpolate', torch.randn(S, M, M, M, M), (None, 2.), 'area_5d_with_scale'),
('interpolate', torch.randn(S, M, M, M, M), (4,), 'area_5d_with_size'),
('interpolate', torch.zeros(3, 3, 3).view(1, 1, 3, 3, 3), (2,), 'trilinear_5d'),
('interpolate', torch.randn(S, M, M, M, M), (None, 2.), 'trilinear_5d_with_scale'),
('interpolate', torch.randn(S, M, M, M, M), (4,), 'trilinear_5d_with_size'),
('interpolate', torch.zeros(3, 3).view(1, 1, 3, 3), (2, None, 'nearest', None, False),
'nearest_4d_not_recompute_scale_factor'),
('interpolate', torch.randn(S, S, M, M), (4, None, 'nearest', None, False),
'nearest_4d_with_size_not_recompute_scale_factor'),
('interpolate', torch.randn(S, S, M, M), (None, 2., 'bilinear', None, False),
'bilinear_4d_with_scale_not_recompute_scale_factor'),
('interpolate', torch.randn(S, S, M, M), (4, None, 'bilinear', None, False),
'bilinear_4d_with_size_not_recompute_scale_factor'),
('interpolate', torch.randn(S, S, M, M), (None, 2., 'bicubic', None, False),
'bicubic_4d_with_scale_not_recompute_scale_factor'),
('interpolate', torch.randn(S, S, M, M), (4, None, 'bicubic', None, False),
'bicubic_4d_with_size_not_recompute_scale_factor'),
('interpolate', torch.randn(S, M, M), (None, 2., 'nearest', None, False),
'nearest_3d_with_scale_not_recompute_scale_factor'),
('interpolate', torch.randn(S, M, M), (4, None, 'nearest', None, False),
'nearest_3d_with_size_not_recompute_scale_factor'),
('interpolate', torch.randn(S, M, M), (None, 2., 'linear', None, False),
'linear_3d_with_scale_not_recompute_scale_factor'),
('interpolate', torch.randn(S, M, M), (4, None, 'linear', None, False),
'linear_3d_with_size_not_recompute_scale_factor'),
('interpolate', torch.randn(S, M, M, M, M), (None, 2., 'nearest', None, False),
'nearest_5d_with_scale_not_recompute_scale_factor'),
('interpolate', torch.randn(S, M, M, M, M), (4, None, 'nearest', None, False),
'nearest_5d_with_size_not_recompute_scale_factor'),
('interpolate', torch.randn(S, M, M, M, M), (None, 2., 'trilinear', None, False),
'trilinear_5d_with_scale_not_recompute_scale_factor'),
('interpolate', torch.randn(S, M, M, M, M), (4, None, 'trilinear', None, False),
'trilinear_5d_with_size_not_recompute_scale_factor'),
]
return nn_functional_tests
script_template = '''
def the_method({}):
return {}
'''
def value_to_literal(value):
if isinstance(value, str):
# Quotes string and escapes special characters
return ascii(value)
if isinstance(value, torch.Tensor):
return 'torch.' + str(value)
else:
return str(value)
def get_call(method_name, func_type, args, kwargs):
kwargs_str = ', '.join([k + '=' + value_to_literal(v) for k, v in kwargs.items()])
self_arg = args[0]
if func_type == 'method':
args = args[1:]
argument_str = ', '.join(args)
argument_str += ', ' if len(args) and len(kwargs) else ''
argument_str += kwargs_str
if func_type == 'functional' or func_type == 'function':
call = f'torch.{method_name}({argument_str})'
elif func_type == 'method':
call = f'{self_arg}.{method_name}({argument_str})'
elif func_type == 'nn_functional':
call = f'torch.nn.functional.{method_name}({argument_str})'
else:
raise TypeError('Unsupported function type')
return call
def get_constant(x):
if x == inf:
return 'math.inf'
if x == -inf:
return '-math.inf'
return x
def get_script_args(args):
formals: list[str] = []
tensors: list[torch.Tensor | list[torch.Tensor]] = []
actuals: list[str] = []
for arg in args:
if isinstance(arg, torch.Tensor):
name = f'i{len(formals)}'
formals.append(name)
actuals.append(name)
tensors.append(arg)
elif is_iterable_of_tensors(arg):
name = f'i{len(formals)}'
formals.append(name + ': List[torch.Tensor]')
actuals.append(name)
tensors.append(list(arg))
elif isinstance(arg, str):
actuals.append(f"'{arg}'")
else:
actuals.append(str(get_constant(arg)))
return (formals, tensors, actuals)
# create a script function from (name, func_type, output_process_fn),
# and returns the compiled function and example inputs
def gen_script_fn_and_args(method_name, func_type, *args, **kwargs):
formals, tensors, actuals = get_script_args(args)
call = get_call(method_name, func_type, actuals, kwargs)
script = script_template.format(', '.join(formals), call)
CU = torch.jit.CompilationUnit(script)
return CU.the_method, tensors
# create a script function from (name, func_type),
# returns a function takes in (args, kwargs) and runs the compiled function
def create_script_fn(self, method_name, func_type):
# function returns tuple containing original output and
# filtered output to be used in checking gradients
def script_fn(*args, **kwargs):
fn, tensors = gen_script_fn_and_args(method_name, func_type, *args, **kwargs)
self.assertExportImport(fn.graph, tensors)
output = fn(*tensors)
# skip type annotate function attributes for now, see: https://github.com/python/mypy/issues/2087
script_fn.last_graph = fn.graph_for(*tensors) # type: ignore[attr-defined]
return output
return script_fn
class SplitInputs:
all_tensors: list[Any]
tensor_args: list[Any]
nontensor_args: list[Any]
arg_types: list[str]
tensor_kwargs: dict[str, Any]
kwarg_order: list[str]
nontensor_kwargs: dict[str, Any]
kwarg_types: dict[str, Any]
@staticmethod
def _is_tensor_input(arg):
return isinstance(arg, torch.Tensor) or is_iterable_of_tensors(arg)
def __init__(self, args, kwargs):
self.arg_types = ['t' if self._is_tensor_input(arg) else 's' for arg in args]
self.kwarg_types = {k: 't' if self._is_tensor_input(v) else 's' for k, v in kwargs.items()}
self.tensor_args = [arg for arg in args if self._is_tensor_input(arg)]
self.nontensor_args = [arg for arg in args if not self._is_tensor_input(arg)]
self.tensor_kwargs = {k: v for k, v in kwargs.items() if self._is_tensor_input(v)}
self.nontensor_kwargs = {k: v for k, v in kwargs.items() if not self._is_tensor_input(v)}
self.all_tensors = [*self.tensor_args, *[v for k, v in self.tensor_kwargs.items()]]
self.kwarg_order = [k for k, v in kwargs.items()]
def nontensors_match(self, other: 'SplitInputs'):
if self.arg_types != other.arg_types:
return False
if self.kwarg_types != other.kwarg_types:
return False
if self.kwarg_order != other.kwarg_order:
return False
if self.nontensor_args != other.nontensor_args:
return False
if self.nontensor_kwargs != other.nontensor_kwargs:
return False
return True
# make a new function where all non-tensor arguments in 'args' have been partially
# applied, and all tensor arguments remain.
# used to trace functions when some arguments are not tensors
def partial_apply_nontensors(fn, args, kwargs):
inputs = SplitInputs(args, kwargs)
def new_fn(*tensors_):
tensors = iter(tensors_)
full_args = [args[i] if s == 's' else next(tensors) for i, s in enumerate(inputs.arg_types)]
full_kwargs = {k: kwargs[k] if s == 's' else next(tensors) for k, s in inputs.kwarg_types.items()}
return fn(*full_args, **full_kwargs)
return new_fn, inputs
# create a trace function from input fn
def create_traced_fn(self, fn, cache_traced_fn=False):
def traced_fn(*inputs, **kwargs):
# `check_trace` is set to False because check_trace is run with @no_grad
# Also, `check_against_reference` already does all the checks
# against python function
fn_tensors, split_inputs = partial_apply_nontensors(fn, inputs, kwargs)
if not cache_traced_fn or not hasattr(traced_fn, 'traced'):
traced = torch.jit.trace(fn_tensors, split_inputs.all_tensors, check_trace=False)
self.assertExportImport(traced.graph, split_inputs.all_tensors)
output = traced(*split_inputs.all_tensors)
if cache_traced_fn:
traced_fn.traced = traced
traced_fn.split_inputs = split_inputs
else:
# Guard to check that nontensor inputs are the same as during tracing
self.assertTrue(traced_fn.split_inputs.nontensors_match(split_inputs))
output = traced_fn.traced(*split_inputs.all_tensors)
traced = traced_fn.traced
# skip type annotate function attributes for now, see: https://github.com/python/mypy/issues/2087
traced_fn.last_graph = traced.graph_for(*split_inputs.all_tensors) # type: ignore[attr-defined]
traced_fn.graph = traced.graph # type: ignore[attr-defined]
return output
return traced_fn
# known to be failing in script
EXCLUDE_SCRIPT = {
'test_norm_fro_default',
'test_norm_fro_cpu',
'test_norm_nuc',
'test_norm_fro',
'test_norm_nuc_batched',
# aten op has additional cudnn argument
'test_nn_unfold',
# flaky test - TODO fix
'test_nn_ctc_loss',
# unknown builtin op
'test_nn_fold',
# jit doesn't support sparse tensors.
'test_to_sparse',
'test_to_sparse_dim',
}
# generates a script function and set of example inputs
# from a specified test in the format of nn_functional_tests
def get_nn_functional_compiled_fn_and_inputs(name, self_size, args, variant_name='', *extra_args):
test_name = 'test_nn_' + name
if variant_name != '':
test_name = test_name + '_' + variant_name
self_variable = create_input((self_size,))[0][0]
# need to record this because methods can change the size (e.g. unsqueeze)
args_variable, _kwargs_variable = create_input(args)
self_tensor = deepcopy(self_variable.data)
args_tensor = deepcopy(unpack_variables(args_variable))
f_args_variable = (self_variable,) + args_variable
f_args_tensor = (self_tensor,) + args_tensor # noqa: F841
with torch._jit_internal._disable_emit_hooks():
script_fn, inputs = gen_script_fn_and_args(name, "nn_functional", *f_args_variable)
return script_fn, inputs
EXCLUDE_SCRIPT_MODULES = {
'test_nn_AdaptiveAvgPool2d_tuple_none',
'test_nn_AdaptiveAvgPool3d_tuple_none',
'test_nn_AdaptiveMaxPool2d_tuple_none',
'test_nn_AdaptiveMaxPool3d_tuple_none',
# Doesn't use future division, so this is not supported
'test_nn_CrossMapLRN2d',
# Derivative for aten::_scaled_dot_product_flash_attention_backward is not implemented
'test_nn_TransformerDecoderLayer_gelu_activation',
'test_nn_TransformerDecoderLayer_relu_activation',
'test_nn_TransformerEncoderLayer_gelu_activation',
'test_nn_TransformerEncoderLayer_relu_activation',
'test_nn_Transformer_multilayer_coder',
}
script_method_template = '''
def forward({}):
return {}
'''
def create_script_module(self, nn_module, constructor_args, *args, **kwargs):
def script_module(*args, **kwargs):
_formals, tensors, actuals = get_script_args(args)
method_args = ', '.join(['self'] + actuals)
call_args_str = ', '.join(actuals)
call = f"self.submodule({call_args_str})"
script = script_method_template.format(method_args, call)
submodule_constants = []
if kwargs.get('is_constant'):
submodule_constants = ['submodule']
# Create module to use the script method
class TheModule(torch.jit.ScriptModule):
__constants__ = submodule_constants
def __init__(self) -> None:
super().__init__()
self.submodule = nn_module(*constructor_args)
def make_module(script):
module = TheModule()
# check __repr__
str(module)
module.define(script)
return module
module = make_module(script)
if self:
self.assertExportImportModule(module, tensors)
module(*args)
# skip type annotate function attributes for now, see: https://github.com/python/mypy/issues/2087
create_script_module.last_graph = module.graph # type: ignore[attr-defined]
return module
return script_module
def check_alias_annotation(method_name, args, kwargs, *, aten_name, func_type='method'):
formals, tensors, actuals = get_script_args(args)
call = get_call(method_name, func_type, actuals, kwargs)
script = script_template.format(', '.join(formals), call)
CU = torch.jit.CompilationUnit(script)
# to clean up IR
torch._C._jit_pass_inline(CU.the_method.graph)
torch._C._jit_pass_constant_propagation(CU.the_method.graph)
torch._C._jit_check_alias_annotation(CU.the_method.graph, tuple(tensors), aten_name)
def get_nn_module_name_from_kwargs(**kwargs):
if 'module_name' in kwargs:
return kwargs['module_name']
elif 'fullname' in kwargs:
return kwargs['fullname']
elif 'constructor' in kwargs:
return kwargs['constructor'].__name__
def get_nn_mod_test_name(**kwargs):
if 'fullname' in kwargs:
test_name = kwargs['fullname']
else:
test_name = get_nn_module_name_from_kwargs(**kwargs)
if 'desc' in kwargs:
test_name = f"{test_name}_{kwargs['desc']}"
return f'test_nn_{test_name}'
def get_nn_module_class_from_kwargs(**kwargs):
name = get_nn_module_name_from_kwargs(**kwargs)
index = name.find("_")
if index == -1:
return name
else:
return name[0:name.find("_")]
def try_get_nn_module_compiled_mod_and_inputs(*args, **kwargs):
name = get_nn_module_name_from_kwargs(**kwargs)
if 'desc' in kwargs and 'eval' in kwargs['desc']:
# eval() is not supported, so skip these tests
return
test_name = name
if 'desc' in kwargs:
test_name = f"{test_name}_{kwargs['desc']}"
test_name = get_nn_mod_test_name(**kwargs)
if test_name in EXCLUDE_SCRIPT_MODULES:
return
if 'constructor' in kwargs:
nn_module = kwargs['constructor']
else:
nn_module = getattr(torch.nn, name)
if "FunctionalModule" in str(nn_module):
return
if 'constructor_args_fn' in kwargs:
constructor_args = kwargs['constructor_args_fn']()
else:
constructor_args = kwargs.get('constructor_args', ())
# Set up inputs from tuple of sizes or constructor fn
input_dtype = torch.double
if 'input_fn' in kwargs:
input = kwargs['input_fn']()
if isinstance(input, torch.Tensor):
input = (input,)
if all(tensor.is_complex() for tensor in input):
input_dtype = torch.cdouble
else:
input = (kwargs['input_size'],)
# Extra parameters to forward()
if 'extra_args' in kwargs:
input = input + kwargs['extra_args']
if 'target_size' in kwargs:
input = input + (kwargs['target_size'],)
elif 'target_fn' in kwargs:
if torch.is_tensor(input):
input = (input,)
input = input + (kwargs['target_fn'](),)
args_variable, _kwargs_variable = create_input(input, dtype=input_dtype)
f_args_variable = deepcopy(unpack_variables(args_variable))
out_var = deepcopy(f_args_variable)
_args, mod = f_args_variable, create_script_module(
None, nn_module, constructor_args, *f_args_variable
)(*f_args_variable)
return mod, out_var
def get_all_nn_module_tests():
# additional modules test
# TODO: delete this list once we make all nn_tests work
additional_module_tests = [
{
'module_name': 'Bilinear',
'constructor_args': (S, S, M),
'input_size': (S, S),
'extra_args': ((S, S),)
},
{
'module_name': 'RNNCell',
'constructor_args': (S, S),
'input_size': (S, S),
},
{
'module_name': 'LSTMCell',
'constructor_args': (S, S),
'input_size': (S, S),
},
{
'module_name': 'GRUCell',
'constructor_args': (S, S),
'input_size': (S, S),
},
{
'module_name': 'MultiheadAttention',
'constructor_args': (128, 8),
'input_size': (10, 8, 128),
'extra_args': (torch.randn(10, 8, 128), torch.randn(10, 8, 128)),
'slowTest': True
},
{
'module_name': 'Transformer',
'constructor_args': (1, 1, 1, 1, 2),
'input_size': (3, 1, 1),
'extra_args': (torch.randn(1, 1, 1),),
'slowTest': True
}
]
return module_tests + get_new_module_tests() + additional_module_tests
@@ -0,0 +1,897 @@
# mypy: ignore-errors
# Torch
from torch.autograd import Variable
from torch.autograd.function import _nested_map
from torch.jit.annotations import BroadcastingList2, BroadcastingList3 # noqa: F401
from torch.onnx import OperatorExportTypes
import torch
import torch.cuda
import torch.jit
import torch.jit._logging
import torch.jit.frontend
import torch.jit.quantized
import zipfile
import functools
# Testing utils
from torch.testing import FileCheck
from torch.testing._internal.common_utils import IS_WINDOWS, \
freeze_rng_state, enable_profiling_mode_for_profiling_tests, ProfilingMode, TEST_BAILOUTS, \
is_iterable_of_tensors
from torch.testing._internal.common_jit import JitCommonTestCase
from torch.testing._internal.common_utils import enable_profiling_mode # noqa: F401
# Standard library
from contextlib import contextmanager
from functools import reduce
from io import StringIO
from collections import defaultdict
import importlib.util
import inspect
import io
import math
import os
import pickle
import sys
import tempfile
import textwrap
from importlib.abc import Loader
from typing import Any
RUN_CUDA = torch.cuda.is_available()
RUN_CUDA_MULTI_GPU = RUN_CUDA and torch.cuda.device_count() > 1
RUN_CUDA_HALF = RUN_CUDA
# HIP supports half, no version check necessary
if torch.cuda.is_available() and not torch.version.hip:
CUDA_VERSION = torch._C._cuda_getCompiledVersion()
for d in range(torch.cuda.device_count()):
major = torch.cuda.get_device_capability(d)[0]
if (major < 6):
RUN_CUDA_HALF = False
def execWrapper(code, glob, loc):
exec(code, glob, loc)
def do_input_map(fn, input):
return _nested_map(lambda t: isinstance(t, torch.Tensor), fn)(input)
def clear_class_registry():
torch._C._jit_clear_class_registry()
torch.jit._recursive.concrete_type_store = torch.jit._recursive.ConcreteTypeStore()
torch.jit._state._clear_class_state()
def get_execution_plan(graph_executor_state):
execution_plans = list(graph_executor_state.execution_plans.values())
num_plans = len(execution_plans)
if num_plans != 1:
raise RuntimeError('This test assumes this GraphExecutor should '
f'only have one execution plan, got: {num_plans}')
return execution_plans[0]
class _AssertRaisesRegexWithHighlightContext:
"""
A context manager that is useful for checking that error messages highlight
the correct part of the source code.
"""
def __init__(self, test_case, exception, regex, highlight):
self.test_case = test_case
self.exception_type = exception
self.regex = regex
self.highlight = highlight
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
with self.test_case.assertRaisesRegex(self.exception_type, self.regex):
if type:
raise value
if self.highlight:
FileCheck().check_source_highlighted(self.highlight).run(str(value))
return True
FUSION_GROUP = "prim::TensorExprGroup"
class JitTestCase(JitCommonTestCase):
_do_cuda_memory_leak_check = True
_restored_warnings = False
class capture_stdout(list):
"""
Replace sys.stdout with a temporary StringIO
"""
def __enter__(self):
self.sys_stdout = sys.stdout
self.stringio = StringIO()
sys.stdout = self.stringio
return self
def __exit__(self, *args):
self.append(str(self.stringio.getvalue()))
del self.stringio
sys.stdout = self.sys_stdout
class capture_stderr(list):
"""
Replace sys.stderr with a temporary StringIO
"""
def __enter__(self):
self.sys_stderr = sys.stderr
self.stringio = StringIO()
sys.stderr = self.stringio
return self
def __exit__(self, *args):
self.append(str(self.stringio.getvalue()))
del self.stringio
sys.stderr = self.sys_stderr
def setHooks(self):
torch._C._jit_set_emit_hooks(self.emitModuleHook, self.emitFunctionHook)
def clearHooks(self):
torch._C._jit_set_emit_hooks(None, None)
def setUp(self):
super().setUp()
# unittest overrides all warning filters and forces all of them to show up
# after we install our own to silence those coming from inside PyTorch.
# This will ensure that our filter still takes precedence.
if not JitTestCase._restored_warnings:
torch.jit.TracerWarning.ignore_lib_warnings()
JitTestCase._restored_warnings = True
self.setHooks()
def tearDown(self):
super().tearDown()
# needs to be cleared because python might be unloaded before
# the callback gets destructed
self.clearHooks()
clear_class_registry()
def assertAllFused(self, graph, except_for=()):
# note this helper collects nodes on 'fast path' only
# i.e. the true blocks of specialized checks
def get_nodes_and_parents_recursively(block, kind, acc):
for node in block.nodes():
if node.kind() == kind:
acc[block].append(node)
elif node.kind() == 'prim::DifferentiableGraph':
get_nodes_and_parents_recursively(node.g('Subgraph'), kind, acc)
elif node.kind() == 'prim::If' and (node.inputs().__next__().node().kind() == 'aten::all' or
node.inputs().__next__().node().kind() == 'prim::TypeCheck' or
node.inputs().__next__().node().kind() == 'prim::RequiresGradCheck'):
get_nodes_and_parents_recursively(node.blocks().__next__(), kind, acc)
else:
for inner_block in node.blocks():
get_nodes_and_parents_recursively(inner_block, kind, acc)
allowed_nodes = {'prim::Constant', FUSION_GROUP, 'prim::BailoutTemplate',
'prim::TupleConstruct', 'prim::If', 'prim::TypeCheck', 'prim::RequiresGradCheck'} | set(except_for)
fusion_groups : dict[torch._C.Block, list[torch._C.Node]] = defaultdict(list)
get_nodes_and_parents_recursively(graph, FUSION_GROUP, fusion_groups)
self.assertTrue(len(fusion_groups) == 1, f'got {graph}')
(graph, fusion_nodes) = next(iter(fusion_groups.items()))
# the block contains one FUSION_GROUP and the rest of nodes are `allowed_nodes`
self.assertTrue(len(fusion_nodes) == 1, f'got {graph}')
self.assertTrue(all(node.kind() in allowed_nodes for node in graph.nodes()),
f'got {graph}')
def _isHookExceptionOk(self, e):
se = str(e)
allowed = ("Could not export Python function",
"closures are not exportable")
for a in allowed:
if a in se:
return True
return False
def _compared_saved_loaded(self, m):
def extract_files(buffer):
# crack open the zip format to get at the main module code
with zipfile.ZipFile(buffer) as archive:
# check that we have no duplicate names
self.assertEqual(len(set(archive.namelist())), len(archive.namelist()))
files = list(filter(lambda x: x.startswith('archive/code/'), archive.namelist()))
# unwrap all the code files into strings
code_files_str = filter(lambda x: x.endswith('.py'), files)
code_files = []
for f in code_files_str:
with archive.open(f) as stream:
code_files.append("".join([line.decode() for line in stream]))
# unpickled all the debug files
debug_files_str = filter(lambda f: f.endswith('.debug_pkl'), files)
debug_files = []
for f in debug_files_str:
with archive.open(f) as stream:
debug_files.append(pickle.load(stream))
return code_files, debug_files
# disable the hook while we parse code, otherwise we will re-enter the hook
with torch._jit_internal._disable_emit_hooks():
try:
# short-circuit if this is an empty function or module
if len(m.code) == 0:
return
if isinstance(m, torch._C.ScriptModule):
if len(m._method_names()) == 0:
return
# save the module to a buffer
buffer = io.BytesIO()
torch.jit.save(m, buffer)
# copy the data in the buffer so we can restore it later. This
# is because py2 and py3 have different semantics with zipfile
# and it's easier to just work with a fresh copy each time.
buffer_copy = buffer.getvalue()
code_files, _debug_files = extract_files(buffer)
except RuntimeError as e:
if not self._isHookExceptionOk(e):
raise
else:
return
# import the model again (from a the copy we made of the original)
buffer2 = io.BytesIO(buffer_copy)
imported = torch.jit.load(buffer2)
# save it again
saved_module_buffer_2 = io.BytesIO()
torch.jit.save(imported, saved_module_buffer_2)
saved_module_buffer_2.seek(0)
code_files_2, _debug_files_2 = extract_files(saved_module_buffer_2)
for a, b in zip(code_files, code_files_2, strict=True):
self.assertMultiLineEqual(a, b)
if isinstance(m, torch._C.ScriptModule):
self.assertTrue(torch._C._ivalue_tags_match(m, imported._c))
def emitFunctionHook(self, func):
# func has invalid names for export, skip the jitter check
if func.name == "<lambda>" or "aten::" in func.name:
return
self._compared_saved_loaded(func)
def emitModuleHook(self, module):
self._compared_saved_loaded(module)
def getExportImportCopyWithPacking(self, m, also_test_file=True, map_location=None):
buffer = io.BytesIO()
m.apply(lambda s: s._pack() if s._c._has_method('_pack') else None)
torch.jit.save(m, buffer)
m.apply(lambda s: s._unpack() if s._c._has_method('_unpack') else None)
buffer.seek(0)
imported = torch.jit.load(buffer, map_location=map_location)
imported.apply(lambda s: s._unpack() if s._c._has_method('_unpack') else None)
if not also_test_file:
return imported
# Ideally we would like to not have to manually delete the file, but NamedTemporaryFile
# opens the file, and it cannot be opened multiple times in Windows. To support Windows,
# close the file after creation and try to remove it manually
with tempfile.NamedTemporaryFile(delete=False) as f:
try:
f.close()
imported.save(f.name)
result = torch.jit.load(f.name, map_location=map_location)
finally:
os.unlink(f.name)
result.apply(lambda s: s._unpack() if s._c._has_method('_unpack') else None)
return result
def assertGraphContains(self, graph, kind, consider_subgraphs=False):
if consider_subgraphs:
strgraph = str(graph)
count = strgraph.count(kind) - strgraph.count(f'with {kind}')
self.assertTrue(count > 0)
return
def nodes(block):
out = []
for node in block.nodes():
if node.kind() == kind:
out.append(node)
for block in node.blocks():
out += nodes(block)
return out
out_nodes = nodes(graph)
self.assertTrue(len(out_nodes) > 0)
def assertGraphContainsExactly(self, graph, kind, num_kind_nodes, consider_subgraphs=False):
def perform_assert(graph, kind, actual, expected, consider_subgraphs):
if actual == expected:
return
subgraph = 'including' if consider_subgraphs else 'excluding'
raise AssertionError(
f'{graph}\nError: graph contains {actual} {kind} nodes ({subgraph} subgraphs) but expected {expected}')
if consider_subgraphs:
strgraph = str(graph)
count = strgraph.count(kind) - strgraph.count(f'with {kind}')
perform_assert(graph, kind, count, num_kind_nodes,
consider_subgraphs)
return
def nodes(block):
out = []
for node in block.nodes():
if node.kind() == kind:
out.append(node)
for block in node.blocks():
out += nodes(block)
return out
out_nodes = nodes(graph)
perform_assert(graph, kind, len(out_nodes), num_kind_nodes,
consider_subgraphs)
def assertExpectedONNXGraph(self, g, *args, **kwargs):
g = torch.onnx._optimize_trace(g, operator_export_type=OperatorExportTypes.ONNX)
self.assertExpectedGraph(g, *args, **kwargs)
def assertExpectedGraph(self, trace, *args, **kwargs):
if isinstance(trace, torch._C.Graph):
graph = trace
else:
graph = trace.graph()
torch._C._jit_pass_lint(graph)
torch._C._jit_pass_dce(graph)
torch._C._jit_pass_lint(graph)
graph = torch._C._jit_pass_canonicalize(graph)
torch._C._jit_pass_lint(graph)
self.assertExpected(str(graph), *args, **kwargs)
def run_pass(self, name, trace):
if isinstance(trace, torch._C.Graph):
graph = trace
set_graph = False
else:
set_graph = True
graph = trace.graph()
torch._C._jit_pass_lint(graph)
result = getattr(torch._C, '_jit_pass_' + name)(graph)
if result is not None and not isinstance(result, bool):
graph = result
torch._C._jit_pass_lint(graph)
if set_graph:
trace.set_graph(graph)
return graph
def get_frame_vars(self, frames_up):
frame = inspect.currentframe()
if not frame:
raise RuntimeError("failed to inspect frame")
i = 0
while i < frames_up + 1:
frame = frame.f_back
if not frame:
raise RuntimeError("failed to get frame")
i += 1
defined_vars: dict[str, Any] = {}
defined_vars.update(frame.f_locals)
defined_vars.update(frame.f_globals)
return defined_vars
def assertRaisesRegexWithHighlight(self, exception, regex, highlight):
return _AssertRaisesRegexWithHighlightContext(self, exception, regex, highlight)
def checkScriptRaisesRegex(self, script, inputs, exception, regex,
name=None, outputs=None, capture_output=False,
frames_up=1, profiling=ProfilingMode.PROFILING):
"""
Checks that a given function will throw the correct exception,
when executed with normal python, the string frontend, and the
AST frontend. Logic taken from `checkScript` (see comments there
for details)
"""
with enable_profiling_mode_for_profiling_tests():
# Normal Python
with self.assertRaisesRegex(exception, regex):
if isinstance(script, str):
frame = self.get_frame_vars(frames_up)
the_locals: dict[str, Any] = {}
execWrapper(script, glob=frame, loc=the_locals)
frame.update(the_locals)
python_fn = frame[name]
else:
python_fn = script
python_fn(*inputs)
# String frontend
with self.assertRaisesRegex(exception, regex):
if isinstance(script, str):
cu = torch.jit.CompilationUnit(script, _frames_up=frames_up)
string_frontend = getattr(cu, name)
else:
source = textwrap.dedent(inspect.getsource(script))
cu = torch.jit.CompilationUnit(source, _frames_up=frames_up)
string_frontend = getattr(cu, script.__name__)
string_frontend(*inputs)
# Python AST frontend
if not isinstance(script, str):
with self.assertRaisesRegex(exception, regex):
ge = torch.jit.script(python_fn)
ge(*inputs)
def checkBailouts(self, model, inputs, expected):
state = model.get_debug_state()
plan = get_execution_plan(state)
num_bailouts = plan.code.num_bailouts()
for i in range(num_bailouts):
plan.code.request_bailout(i)
bailout_outputs = model(*inputs)
self.assertEqual(bailout_outputs, expected)
def checkScript(self,
script,
inputs,
name='func',
optimize=True,
inputs_requires_grad=False,
capture_output=False,
frames_up=1,
profiling=ProfilingMode.PROFILING,
atol=None,
rtol=None):
"""
Checks that a given script generates the same output as the Python
version using the given inputs.
"""
with torch.jit.optimized_execution(optimize), enable_profiling_mode_for_profiling_tests():
extra_profile_runs = any(isinstance(x, torch.Tensor) and x.requires_grad for x in inputs)
if isinstance(script, str):
# Compile the string to a Script function
# with enable_profiling_mode():
cu = torch.jit.CompilationUnit(script, _frames_up=frames_up)
# Execute the Python function so we can run it later and get its
# outputs
frame = self.get_frame_vars(frames_up)
the_locals: dict[str, Any] = {}
execWrapper(script, glob=frame, loc=the_locals)
frame.update(the_locals)
python_fn = frame[name]
scripted_fn = getattr(cu, name)
else:
# Check the string frontend first
source = textwrap.dedent(inspect.getsource(script))
self.checkScript(
source,
inputs,
script.__name__,
optimize=optimize,
inputs_requires_grad=inputs_requires_grad,
capture_output=capture_output,
profiling=profiling,
frames_up=2)
# Continue checking the Python frontend
scripted_fn = torch.jit.script(script, _frames_up=1)
python_fn = script
if inputs_requires_grad:
recording_inputs = do_input_map(lambda t: t.detach().requires_grad_(), inputs)
else:
recording_inputs = inputs
if capture_output:
with self.capture_stdout() as script_stdout:
script_outputs = scripted_fn(*recording_inputs)
with self.capture_stdout():
opt_script_outputs = scripted_fn(*recording_inputs)
with self.capture_stdout():
python_outputs = python_fn(*inputs)
if not IS_WINDOWS:
self.assertExpected(script_stdout[0], subname='stdout')
self.assertEqual(python_outputs, opt_script_outputs, atol=atol, rtol=rtol)
else:
# profiling run
script_outputs = scripted_fn(*recording_inputs)
if inputs_requires_grad or extra_profile_runs:
opt_script_outputs = scripted_fn(*recording_inputs)
# optimized run
opt_script_outputs = scripted_fn(*recording_inputs)
if TEST_BAILOUTS:
self.checkBailouts(scripted_fn, inputs, opt_script_outputs)
python_outputs = python_fn(*inputs)
self.assertEqual(python_outputs, script_outputs, atol=atol, rtol=rtol)
self.assertEqual(script_outputs, opt_script_outputs, atol=atol, rtol=rtol)
return scripted_fn
def checkTrace(self, func, reference_tensors, input_tensors=None,
drop=None, allow_unused=False, verbose=False,
inputs_require_grads=True, check_tolerance=1e-5, export_import=True,
_force_outplace=False, grad_atol=None, grad_rtol=None):
# TODO: check gradients for parameters, not just inputs
def allSum(vs):
# drop allows us to remove some values from ever being used
# to test unused outputs
if drop is not None:
vs = vs[:-drop]
# we don't want all the grad for all the outputs to be the same
# so we multiply each by a constant
return sum(math.log(i + 2) * v.sum() for i, v in enumerate(vs) if v is not None)
if input_tensors is None:
input_tensors = reference_tensors
def flatten_inputs(inputs):
def input_reduce(input, fn, acc):
if isinstance(input, torch.Tensor):
fn(input, acc)
elif isinstance(input, dict):
reduce(lambda acc, key: input_reduce(input[key], fn, acc), input, acc)
else:
reduce(lambda acc, val: input_reduce(val, fn, acc), input, acc)
return acc
return tuple(input_reduce(recording_inputs, lambda t, acc: acc.append(t), []))
nograd_inputs = reference_tensors
if inputs_require_grads:
recording_inputs = do_input_map(lambda t: t.clone().requires_grad_(), reference_tensors)
flattened_recording_inputs = flatten_inputs(recording_inputs)
else:
recording_inputs = reference_tensors
# `check_trace` is set to False because check_trace is run with @no_grad
# Also, `checkTrace` already does all the checks
# against python function
ge = torch.jit.trace(func, input_tensors, check_tolerance=check_tolerance,
_force_outplace=_force_outplace, check_trace=False)
if export_import:
ge = self.getExportImportCopy(ge)
if verbose:
print(ge.graph)
# test no gradients case
outputs = func(*nograd_inputs)
outputs_ge = ge(*nograd_inputs)
self.assertEqual(outputs, outputs_ge)
# test gradients case
outputs = func(*recording_inputs)
if inputs_require_grads:
grads = torch.autograd.grad(allSum(outputs), flattened_recording_inputs,
allow_unused=allow_unused)
outputs_ge = ge(*recording_inputs)
if inputs_require_grads:
grads_ge = torch.autograd.grad(allSum(outputs_ge), flattened_recording_inputs,
allow_unused=allow_unused)
self.assertEqual(outputs, outputs_ge)
if inputs_require_grads:
self.assertEqual(grads, grads_ge, atol=grad_atol, rtol=grad_rtol)
# test the grad grad case
outputs = func(*recording_inputs)
l1 = allSum(outputs)
if inputs_require_grads:
grads = torch.autograd.grad(l1, flattened_recording_inputs, create_graph=True,
allow_unused=allow_unused)
if inputs_require_grads:
l2 = (allSum(grads) * l1)
grads2 = torch.autograd.grad(l2, flattened_recording_inputs, allow_unused=allow_unused)
if inputs_require_grads:
recording_inputs = do_input_map(lambda t: Variable(t, requires_grad=True), reference_tensors)
flattened_recording_inputs = flatten_inputs(recording_inputs)
outputs_ge = ge(*recording_inputs)
l1_ge = allSum(outputs_ge)
if inputs_require_grads:
grads_ge = torch.autograd.grad(
l1_ge, flattened_recording_inputs, create_graph=True, allow_unused=allow_unused)
if inputs_require_grads:
l2_ge = (allSum(grads_ge) * l1_ge)
grads2_ge = torch.autograd.grad(l2_ge, flattened_recording_inputs, allow_unused=allow_unused)
self.assertEqual(outputs, outputs_ge)
if inputs_require_grads:
self.assertEqual(grads, grads_ge, atol=grad_atol, rtol=grad_rtol)
for g2, g2_ge in zip(grads2, grads2_ge, strict=True):
if g2 is None and g2_ge is None:
continue
self.assertEqual(g2, g2_ge, atol=8e-4, rtol=8e-4)
return ge
def checkModule(self, nn_module, args):
"""
Check that a nn.Module's results in Script mode match eager and that it
can be exported
"""
sm = torch.jit.script(nn_module)
with freeze_rng_state():
eager_out = nn_module(*args)
with freeze_rng_state():
script_out = sm(*args)
self.assertEqual(eager_out, script_out)
self.assertExportImportModule(sm, args)
return sm
class NoTracerWarnContextManager:
def __enter__(self):
self.prev = torch._C._jit_get_tracer_state_warn()
torch._C._jit_set_tracer_state_warn(False)
def __exit__(self, *args):
torch._C._jit_set_tracer_state_warn(self.prev)
@contextmanager
def inline_everything_mode(should_inline):
old = torch._C._jit_get_inline_everything_mode()
torch._C._jit_set_inline_everything_mode(should_inline)
try:
yield
finally:
torch._C._jit_set_inline_everything_mode(old)
@contextmanager
def set_fusion_group_inlining(inlining):
old = torch._C._debug_get_fusion_group_inlining()
torch._C._debug_set_fusion_group_inlining(inlining)
try:
yield
finally:
torch._C._debug_set_fusion_group_inlining(old)
# note: not re-entrant, use unnested only
@contextmanager
def disable_autodiff_subgraph_inlining(enabled=True):
torch._C._debug_set_autodiff_subgraph_inlining(not enabled)
try:
yield
finally:
torch._C._debug_set_autodiff_subgraph_inlining(True)
def _inline_everything(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
with inline_everything_mode(True):
fn(*args, **kwargs)
return wrapper
# this exists for forward compatibility reasons temporarily.
# TODO(suo) remove
def _tmp_donotuse_dont_inline_everything(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
with inline_everything_mode(False):
fn(*args, **kwargs)
return wrapper
# make it easy to quickly define/trace a function for these tests
def _trace(*args, **kwargs):
def wrapper(func):
return torch.jit.trace(func, args, **kwargs)
return wrapper
def enable_cpu_fuser(fn):
def wrapper(*args, **kwargs):
torch._C._jit_override_can_fuse_on_cpu_legacy(True)
torch._C._jit_override_can_fuse_on_cpu(True)
torch._C._jit_set_te_must_use_llvm_cpu(False)
try:
fn(*args, **kwargs)
finally:
torch._C._jit_override_can_fuse_on_cpu_legacy(False)
torch._C._jit_override_can_fuse_on_cpu(False)
torch._C._jit_set_te_must_use_llvm_cpu(True)
return wrapper
def enable_cpu_fuser_if(cond):
if cond:
return enable_cpu_fuser
else:
def noop_fuser(fn):
def wrapper(*args, **kwargs):
return fn(*args, **kwargs)
return wrapper
return noop_fuser
def get_forward(c):
return c._get_method('forward')
def get_forward_graph(c):
return c._get_method('forward').graph
def get_module_method(m, module, method):
return m._c.getattr(module)._get_method(method)
def attrs_with_prefix(module, prefix):
return [x for x, _ in module._modules._c.items()
if x.startswith(prefix)]
def warmup_backward(f, *args):
profiling_count = 3
results = []
for _ in range(profiling_count):
if len(args) > 0:
r = torch.autograd.grad(f, *args)
results.append(r)
else:
f.backward(retain_graph=True)
return results
# TODO: Remove me once https://bugs.python.org/issue42666 is resolved
def make_global(*args):
for arg in args:
setattr(sys.modules[arg.__module__], arg.__name__, arg)
# Helper function to eval Python3 code without causing a syntax error for
# this file under py2
def _get_py3_code(code, fn_name):
with tempfile.TemporaryDirectory() as tmp_dir:
script_path = os.path.join(tmp_dir, 'script.py')
with open(script_path, 'w') as f:
f.write(code)
spec = importlib.util.spec_from_file_location(fn_name, script_path)
module = importlib.util.module_from_spec(spec)
loader = spec.loader
if not isinstance(loader, Loader): # Assert type to meet MyPy requirement
raise AssertionError(f"Expected loader to be Loader, got {type(loader)}")
loader.exec_module(module)
fn = getattr(module, fn_name)
return fn
class TensorExprTestOptions:
def __init__(self) -> None:
self.old_profiling_executor = torch._C._jit_set_profiling_executor(True)
self.old_profiling_mode = torch._C._get_graph_executor_optimize(True)
self.old_cpu_fuser_state = torch._C._jit_can_fuse_on_cpu()
self.old_gpu_fuser_state = torch._C._jit_can_fuse_on_gpu()
torch._C._jit_override_can_fuse_on_cpu(True)
torch._C._jit_override_can_fuse_on_gpu(True)
self.texpr_fuser_state = torch._C._jit_texpr_fuser_enabled()
torch._C._jit_set_texpr_fuser_enabled(True)
self.old_fusion_inlining = torch._C._debug_get_fusion_group_inlining()
torch._C._debug_set_fusion_group_inlining(False)
self.old_te_must_use_llvm_cpu = torch._C._jit_get_te_must_use_llvm_cpu()
torch._C._jit_set_te_must_use_llvm_cpu(False)
def restore(self):
torch._C._jit_set_profiling_executor(self.old_profiling_executor)
torch._C._get_graph_executor_optimize(self.old_profiling_mode)
torch._C._jit_set_texpr_fuser_enabled(self.texpr_fuser_state)
torch._C._jit_override_can_fuse_on_gpu(self.old_gpu_fuser_state)
torch._C._jit_override_can_fuse_on_cpu(self.old_cpu_fuser_state)
torch._C._debug_set_fusion_group_inlining(self.old_fusion_inlining)
torch._C._jit_set_te_must_use_llvm_cpu(self.old_te_must_use_llvm_cpu)
def clone_inputs(args):
inputs: list[torch.Tensor | list[torch.Tensor]] = []
for arg in args:
if isinstance(arg, torch.Tensor):
inputs.append(arg.detach().clone())
elif is_iterable_of_tensors(arg):
inputs.append([t.detach().clone() for t in arg])
else:
inputs.append(arg)
return inputs
def get_traced_sample_variant_pairs(device, dtype, op):
# tuples of (variant, sample)
outputs: list[tuple[Any, Any]] = []
samples = op.sample_inputs(device, dtype)
# Acquires variants to test
func = op.get_op()
method = op.get_method()
variants = {
# TODO: inplace tests currently fail, fix and add inplace variant
'function': func, 'method': method,
}
# TODO: find better way to standardize on op registration itself..
has_fake_function = op.name in ["resize_", 'resize_as_']
if has_fake_function:
variants = {'method': getattr(torch.Tensor, op.name)}
# In eager mode, these ops can take (Tensor, bool) args; but in
# JIT they can only take (Tensor, Scalar), and bool is not a
# scalar in the JIT type system. So to test these in JIT, the bool
# is converted to an int for the test.
ops_with_unsupported_bool_args = [
{
"name": "div_floor_rounding",
"arg_idx": [0],
},
{
"name": "div_no_rounding_mode",
"arg_idx": [0],
},
{
"name": "div_trunc_rounding",
"arg_idx": [0],
},
{
"name": "index_fill",
"arg_idx": [2],
},
{
"name": "full_like",
"arg_idx": [0],
},
{
"name": "mul",
"arg_idx": [0],
},
{
"name": "new_full",
"arg_idx": [1],
},
]
# doesn't support tracing
if has_fake_function:
return outputs
for sample in samples:
for variant in variants.values():
if variant is None:
continue
if is_lambda(variant):
continue
matching_ops = filter(lambda x: op.formatted_name == x["name"], ops_with_unsupported_bool_args)
for op_data in matching_ops:
for idx in op_data["arg_idx"]:
args = list(sample.args)
if len(sample.args) > idx and isinstance(sample.args[idx], bool):
args[idx] = int(args[idx])
sample.args = tuple(args)
outputs.append((variant, sample))
return outputs
# types.LambdaType gave false positives
def is_lambda(lamb):
LAMBDA = lambda: 0 # noqa: E731
return isinstance(lamb, type(LAMBDA)) and lamb.__name__ == LAMBDA.__name__
@@ -0,0 +1,167 @@
# mypy: ignore-errors
import torch
from torch.utils._pytree import tree_map
from collections.abc import Iterator
import logging
import contextlib
import itertools
from torch.utils._dtype_abbrs import dtype_abbrs as _dtype_abbrs
from torch.utils._python_dispatch import TorchDispatchMode
from torch.utils.weak import WeakTensorKeyDictionary
import functools
from torch._C._profiler import gather_traceback, symbolize_tracebacks
logger = logging.getLogger("LoggingTensor")
# How the chain of calls works for LoggingTensor:
# 1. Call torch.sin
# 2. Attempt __torch_function__. In LoggingTensor torch function is disabled so we bypass it entirely
# 3. Enter dispatcher, wind your way through Autograd
# 4. Hit Python dispatch key, call __torch_dispatch__
# This Tensor can work with autograd in two ways:
# - The wrapped Tensor does not require gradients. In that case, the LoggingTensor
# can require gradients if the user asks for it as a constructor kwarg.
# - The wrapped Tensor can require gradients. In that case autograd will be tracked
# for the wrapped Tensor and the LoggingTensor itself cannot require gradients.
# WARNING: We allow these two possibilities for testing purposes. You should NEVER use both in a single
# test or you might get surprising behavior.
# TODO: TensorBase should work
class LoggingTensor(torch.Tensor):
elem: torch.Tensor
__slots__ = ['elem']
context = contextlib.nullcontext
@staticmethod
def __new__(cls, elem, *args, **kwargs):
# The wrapping tensor (LoggingTensor) shouldn't hold any
# memory for the class in question, but it should still
# advertise the same device as before
r = torch.Tensor._make_wrapper_subclass(
cls, elem.size(),
strides=elem.stride(), storage_offset=elem.storage_offset(),
# TODO: clone storage aliasing
dtype=elem.dtype, layout=elem.layout,
device=elem.device, requires_grad=kwargs.get("requires_grad", False)
)
# ...the real tensor is held as an element on the tensor.
r.elem = elem.detach() if r.requires_grad else elem
return r
def __repr__(self):
return super().__repr__(tensor_contents=f"{self.elem}")
@classmethod
def __torch_dispatch__(cls, func, types, args=(), kwargs=None):
def unwrap(e):
return e.elem if isinstance(e, cls) else e
def wrap(e):
return cls(e) if isinstance(e, torch.Tensor) else e
with cls.context():
rs = tree_map(wrap, func(*tree_map(unwrap, args), **tree_map(unwrap, kwargs)))
logging.getLogger("LoggingTensor").info(f"{func.__module__}.{func.__name__}", args, kwargs, rs) # noqa: G004
return rs
class LoggingTensorMode(TorchDispatchMode):
def __torch_dispatch__(self, func, types, args=(), kwargs=None):
if kwargs is None:
kwargs = {}
rs = func(*args, **kwargs)
logging.getLogger("LoggingTensor").info(f"{func.__module__}.{func.__name__}", args, kwargs, rs) # noqa: G004
return rs
class LoggingTensorReentrant(LoggingTensor):
context = torch.overrides.enable_reentrant_dispatch
# https://stackoverflow.com/questions/36408496/python-logging-handler-to-append-to-list
class LoggingTensorHandler(logging.Handler):
def __init__(
self, log_list: list[str], use_shortid_for_all_tensors: bool,
with_type: bool, tracebacks_list: list | None) -> None:
logging.Handler.__init__(self)
self.log_list = log_list
self.use_shortid_for_all_tensors = use_shortid_for_all_tensors
self.tracebacks_list = tracebacks_list
self.memo = WeakTensorKeyDictionary()
self.next_id = 0
self.with_type = with_type
def _shortid(self, t: torch.Tensor) -> int:
if t not in self.memo:
self.memo[t] = self.next_id
self.next_id += 1
return self.memo[t]
def _fmt(self, a: object, with_type: bool = False) -> str:
cond_cls = torch.Tensor if self.use_shortid_for_all_tensors else LoggingTensor
if isinstance(a, cond_cls):
maybe_type = ""
if with_type and self.with_type:
maybe_type = f": {_dtype_abbrs[a.dtype]}[{', '.join(map(str, a.shape))}]"
x = f"${self._shortid(a)}{maybe_type}"
return x
else:
return repr(a)
def emit(self, record):
fmt_args = ", ".join(
itertools.chain(
(str(tree_map(self._fmt, a)) for a in record.args[0]),
(f"{k}={str(tree_map(self._fmt, v))}" for k, v in record.args[1].items()),
)
)
fmt_rets = tree_map(functools.partial(self._fmt, with_type=True), record.args[2])
self.log_list.append(f'{fmt_rets} = {record.msg}({fmt_args})')
if self.tracebacks_list is not None:
self.tracebacks_list.append(record.traceback)
def log_input(name: str, var: object) -> None:
logger.info("input", (name,), {}, var) # noqa: PLE1205
class GatherTraceback(logging.Filter):
def __init__(self, python=True, script=True, cpp=False):
self.python = python
self.script = script
self.cpp = cpp
def filter(self, record):
record.traceback = gather_traceback(python=self.python, script=self.script, cpp=self.cpp)
return True
@contextlib.contextmanager
def capture_logs(is_mode=False, python_tb=False, script_tb=False, cpp_tb=False) -> Iterator[list[str]]:
collect_traceback = python_tb or script_tb or cpp_tb
log_list: list[str] = []
tracebacks_list: list[str] = []
handler = LoggingTensorHandler(
log_list,
with_type=True,
use_shortid_for_all_tensors=is_mode,
tracebacks_list=tracebacks_list if collect_traceback else None
)
logger.addHandler(handler)
logger.setLevel(logging.INFO)
logger.propagate = False
if collect_traceback:
logger.addFilter(GatherTraceback(python=python_tb, script=script_tb, cpp=cpp_tb))
try:
if collect_traceback:
yield log_list, tracebacks_list
else:
yield log_list
finally:
symbolized_tracebacks = symbolize_tracebacks(tracebacks_list)
tracebacks_list.clear()
tracebacks_list.extend(symbolized_tracebacks)
logger.removeHandler(handler)
@contextlib.contextmanager
def capture_logs_with_logging_tensor_mode(python_tb=False, script_tb=False, cpp_tb=False):
with LoggingTensorMode(), capture_logs(True, python_tb, script_tb, cpp_tb) as logs:
yield logs
@@ -0,0 +1,243 @@
# mypy: ignore-errors
import torch._dynamo.test_case
import unittest.mock
import os
import contextlib
import torch._logging
import torch._logging._internal
from contextlib import AbstractContextManager
from collections.abc import Callable
from torch._dynamo.utils import LazyString
from torch._inductor import config as inductor_config
import logging
import io
@contextlib.contextmanager
def preserve_log_state():
prev_state = torch._logging._internal._get_log_state()
torch._logging._internal._set_log_state(torch._logging._internal.LogState())
try:
yield
finally:
torch._logging._internal._set_log_state(prev_state)
torch._logging._internal._init_logs()
def log_settings(settings):
exit_stack = contextlib.ExitStack()
settings_patch = unittest.mock.patch.dict(os.environ, {"TORCH_LOGS": settings})
exit_stack.enter_context(preserve_log_state())
exit_stack.enter_context(settings_patch)
torch._logging._internal._init_logs()
return exit_stack
def log_api(**kwargs):
exit_stack = contextlib.ExitStack()
exit_stack.enter_context(preserve_log_state())
torch._logging.set_logs(**kwargs)
return exit_stack
def kwargs_to_settings(**kwargs):
INT_TO_VERBOSITY = {10: "+", 20: "", 40: "-"}
settings = []
def append_setting(name, level):
if isinstance(name, str) and isinstance(level, int) and level in INT_TO_VERBOSITY:
settings.append(INT_TO_VERBOSITY[level] + name)
return
else:
raise ValueError("Invalid value for setting")
for name, val in kwargs.items():
if isinstance(val, bool):
settings.append(name)
elif isinstance(val, int):
append_setting(name, val)
elif isinstance(val, dict) and name == "modules":
for module_qname, level in val.items():
append_setting(module_qname, level)
else:
raise ValueError("Invalid value for setting")
return ",".join(settings)
# Note on testing strategy:
# This class does two things:
# 1. Runs two versions of a test:
# 1a. patches the env var log settings to some specific value
# 1b. calls torch._logging.set_logs(..)
# 2. patches the emit method of each setup handler to gather records
# that are emitted to each console stream
# 3. passes a ref to the gathered records to each test case for checking
#
# The goal of this testing in general is to ensure that given some settings env var
# that the logs are setup correctly and capturing the correct records.
def make_logging_test(**kwargs):
def wrapper(fn):
@inductor_config.patch({"fx_graph_cache": False})
def test_fn(self):
torch._dynamo.reset()
records = []
# run with env var
if len(kwargs) == 0:
with self._handler_watcher(records):
fn(self, records)
else:
with log_settings(kwargs_to_settings(**kwargs)), self._handler_watcher(records):
fn(self, records)
# run with API
torch._dynamo.reset()
records.clear()
with log_api(**kwargs), self._handler_watcher(records):
fn(self, records)
return test_fn
return wrapper
def make_settings_test(settings):
def wrapper(fn):
def test_fn(self):
torch._dynamo.reset()
records = []
# run with env var
with log_settings(settings), self._handler_watcher(records):
fn(self, records)
return test_fn
return wrapper
class LoggingTestCase(torch._dynamo.test_case.TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls._exit_stack.enter_context(
unittest.mock.patch.dict(os.environ, {"___LOG_TESTING": ""})
)
cls._exit_stack.enter_context(
unittest.mock.patch("torch._dynamo.config.suppress_errors", True)
)
cls._exit_stack.enter_context(
unittest.mock.patch("torch._dynamo.config.verbose", False)
)
@classmethod
def tearDownClass(cls):
cls._exit_stack.close()
torch._logging._internal.log_state.clear()
torch._logging._init_logs()
def hasRecord(self, records, m):
return any(m in r.getMessage() for r in records)
def getRecord(self, records, m):
record = None
for r in records:
# NB: not r.msg because it looks like 3.11 changed how they
# structure log records
if m in r.getMessage():
self.assertIsNone(
record,
msg=LazyString(
lambda: f"multiple matching records: {record} and {r} among {records}"
),
)
record = r
if record is None:
self.fail(f"did not find record with {m} among {records}")
return record
# This patches the emit method of each handler to gather records
# as they are emitted
def _handler_watcher(self, record_list):
exit_stack = contextlib.ExitStack()
def emit_post_hook(record):
nonlocal record_list
record_list.append(record)
# registered logs are the only ones with handlers, so patch those
for log_qname in torch._logging._internal.log_registry.get_log_qnames():
logger = logging.getLogger(log_qname)
num_handlers = len(logger.handlers)
self.assertLessEqual(
num_handlers,
2,
"All pt2 loggers should only have at most two handlers (debug artifacts and messages above debug level).",
)
self.assertGreater(num_handlers, 0, "All pt2 loggers should have more than zero handlers")
for handler in logger.handlers:
old_emit = handler.emit
def new_emit(record):
old_emit(record)
emit_post_hook(record)
exit_stack.enter_context(
unittest.mock.patch.object(handler, "emit", new_emit)
)
return exit_stack
def logs_to_string(module, log_option):
"""Example:
logs_to_string("torch._inductor.compile_fx", "post_grad_graphs")
returns the output of TORCH_LOGS="post_grad_graphs" from the
torch._inductor.compile_fx module.
"""
log_stream = io.StringIO()
handler = logging.StreamHandler(stream=log_stream)
@contextlib.contextmanager
def tmp_redirect_logs():
try:
logger = torch._logging.getArtifactLogger(module, log_option)
logger.addHandler(handler)
yield
finally:
logger.removeHandler(handler)
def ctx_manager():
exit_stack = log_settings(log_option)
exit_stack.enter_context(tmp_redirect_logs())
return exit_stack
return log_stream, ctx_manager
def multiple_logs_to_string(module: str, *log_options: str) -> tuple[list[io.StringIO], Callable[[], AbstractContextManager[None]]]:
"""Example:
multiple_logs_to_string("torch._inductor.compile_fx", "pre_grad_graphs", "post_grad_graphs")
returns the output of TORCH_LOGS="pre_graph_graphs, post_grad_graphs" from the
torch._inductor.compile_fx module.
"""
log_streams = [io.StringIO() for _ in range(len(log_options))]
handlers = [logging.StreamHandler(stream=log_stream) for log_stream in log_streams]
@contextlib.contextmanager
def tmp_redirect_logs():
loggers = [torch._logging.getArtifactLogger(module, option) for option in log_options]
try:
for logger, handler in zip(loggers, handlers, strict=True):
logger.addHandler(handler)
yield
finally:
for logger, handler in zip(loggers, handlers, strict=True):
logger.removeHandler(handler)
def ctx_manager() -> AbstractContextManager[None]:
exit_stack = log_settings(", ".join(log_options))
exit_stack.enter_context(tmp_redirect_logs())
return exit_stack # type: ignore[return-value]
return log_streams, ctx_manager
@@ -0,0 +1,4 @@
# mypy: ignore-errors
import torch.testing._internal.opinfo.core
import torch.testing._internal.opinfo.definitions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,26 @@
# mypy: ignore-errors
from torch.testing._internal.opinfo.core import OpInfo
from torch.testing._internal.opinfo.definitions import (
_masked,
fft,
linalg,
signal,
special,
)
# Operator database
op_db: list[OpInfo] = [
*fft.op_db,
*linalg.op_db,
*signal.op_db,
*special.op_db,
*_masked.op_db,
]
python_ref_db: list[OpInfo] = [
*fft.python_ref_db,
*linalg.python_ref_db,
*special.python_ref_db,
]
@@ -0,0 +1,939 @@
# mypy: ignore-errors
import unittest
from functools import partial
import numpy as np
import torch
from torch.testing import make_tensor
from torch.testing._internal.common_device_type import precisionOverride
from torch.testing._internal.common_dtype import (
all_types_and,
all_types_and_complex_and,
)
from torch.testing._internal.common_utils import TEST_SCIPY, TEST_WITH_ROCM
from torch.testing._internal.opinfo.core import (
DecorateInfo,
ErrorInput,
OpInfo,
sample_inputs_spectral_ops,
SampleInput,
SpectralFuncInfo,
SpectralFuncType,
)
from torch.testing._internal.opinfo.refs import (
_find_referenced_opinfo,
_inherit_constructor_args,
PythonRefInfo,
)
has_scipy_fft = False
if TEST_SCIPY:
try:
import scipy.fft
has_scipy_fft = True
except ModuleNotFoundError:
pass
class SpectralFuncPythonRefInfo(SpectralFuncInfo):
"""
An OpInfo for a Python reference of an elementwise unary operation.
"""
def __init__(
self,
name, # the stringname of the callable Python reference
*,
op=None, # the function variant of the operation, populated as torch.<name> if None
torch_opinfo_name, # the string name of the corresponding torch opinfo
torch_opinfo_variant="",
**kwargs,
): # additional kwargs override kwargs inherited from the torch opinfo
self.torch_opinfo_name = torch_opinfo_name
self.torch_opinfo = _find_referenced_opinfo(
torch_opinfo_name, torch_opinfo_variant, op_db=op_db
)
if not isinstance(self.torch_opinfo, SpectralFuncInfo):
raise AssertionError(
f"Expected torch_opinfo to be SpectralFuncInfo, got {type(self.torch_opinfo)}"
)
inherited = self.torch_opinfo._original_spectral_func_args
ukwargs = _inherit_constructor_args(name, op, inherited, kwargs)
super().__init__(**ukwargs)
def error_inputs_fft(op_info, device, **kwargs):
make_arg = partial(make_tensor, device=device, dtype=torch.float32)
# Zero-dimensional tensor has no dimension to take FFT of
yield ErrorInput(
SampleInput(make_arg()),
error_type=IndexError,
error_regex="Dimension specified as -1 but tensor has no dimensions",
)
def error_inputs_fftn(op_info, device, **kwargs):
make_arg = partial(make_tensor, device=device, dtype=torch.float32)
# Specifying a dimension on a zero-dimensional tensor
yield ErrorInput(
SampleInput(make_arg(), dim=(0,)),
error_type=IndexError,
error_regex="Dimension specified as 0 but tensor has no dimensions",
)
def sample_inputs_fft_with_min(
op_info, device, dtype, requires_grad=False, *, min_size, **kwargs
):
yield from sample_inputs_spectral_ops(
op_info, device, dtype, requires_grad, **kwargs
)
if TEST_WITH_ROCM:
# FIXME: Causes floating point exception on ROCm
return
# Check the "Invalid number of data points" error isn't too strict
# https://github.com/pytorch/pytorch/pull/109083
a = make_tensor(min_size, dtype=dtype, device=device, requires_grad=requires_grad)
yield SampleInput(a)
def sample_inputs_fftshift(op_info, device, dtype, requires_grad, **kwargs):
def mt(shape, **kwargs):
return make_tensor(
shape, device=device, dtype=dtype, requires_grad=requires_grad, **kwargs
)
yield SampleInput(mt((9, 10)))
yield SampleInput(mt((50,)), kwargs=dict(dim=0))
yield SampleInput(mt((5, 11)), kwargs=dict(dim=(1,)))
yield SampleInput(mt((5, 6)), kwargs=dict(dim=(0, 1)))
yield SampleInput(mt((5, 6, 2)), kwargs=dict(dim=(0, 2)))
# Operator database
op_db: list[OpInfo] = [
SpectralFuncInfo(
"fft.fft",
aten_name="fft_fft",
decomp_aten_name="_fft_c2c",
ref=np.fft.fft,
ndimensional=SpectralFuncType.OneD,
dtypes=all_types_and_complex_and(torch.bool),
# CUDA supports Half/ComplexHalf Precision FFT only on SM53 or later archs
dtypesIfCUDA=all_types_and_complex_and(torch.bool, torch.half, torch.complex32),
sample_inputs_func=partial(sample_inputs_fft_with_min, min_size=1),
error_inputs_func=error_inputs_fft,
# https://github.com/pytorch/pytorch/issues/80411
gradcheck_fast_mode=True,
supports_forward_ad=True,
supports_fwgrad_bwgrad=True,
# See https://github.com/pytorch/pytorch/pull/78358
check_batched_forward_grad=False,
),
SpectralFuncInfo(
"fft.fft2",
aten_name="fft_fft2",
ref=np.fft.fft2,
decomp_aten_name="_fft_c2c",
ndimensional=SpectralFuncType.TwoD,
dtypes=all_types_and_complex_and(torch.bool),
# CUDA supports Half/ComplexHalf Precision FFT only on SM53 or later archs
dtypesIfCUDA=all_types_and_complex_and(torch.bool, torch.half, torch.complex32),
sample_inputs_func=partial(sample_inputs_fft_with_min, min_size=(1, 1)),
error_inputs_func=error_inputs_fftn,
# https://github.com/pytorch/pytorch/issues/80411
gradcheck_fast_mode=True,
supports_forward_ad=True,
supports_fwgrad_bwgrad=True,
# See https://github.com/pytorch/pytorch/pull/78358
check_batched_forward_grad=False,
decorators=[precisionOverride({torch.float: 1e-4, torch.cfloat: 1e-4})],
skips=(
DecorateInfo(
unittest.skip("Skipped!"),
"TestCommon",
"test_complex_half_reference_testing",
device_type="cuda",
dtypes=[torch.complex32],
active_if=TEST_WITH_ROCM,
),
# RuntimeError: [srcBuf length] > 0 INTERNAL ASSERT FAILED
DecorateInfo(
unittest.expectedFailure,
"TestCommon",
"test_out",
device_type="mps",
),
# AssertionError: The values for attribute 'shape' do not match: torch.Size([5, 3, 10]) != torch.Size([5, 3, 11]).
DecorateInfo(
unittest.expectedFailure,
"TestCommon",
"test_out_warning",
device_type="mps",
),
),
),
SpectralFuncInfo(
"fft.fftn",
aten_name="fft_fftn",
decomp_aten_name="_fft_c2c",
ref=np.fft.fftn,
ndimensional=SpectralFuncType.ND,
dtypes=all_types_and_complex_and(torch.bool),
# CUDA supports Half/ComplexHalf Precision FFT only on SM53 or later archs
dtypesIfCUDA=all_types_and_complex_and(
torch.bool,
torch.half,
torch.complex32,
),
sample_inputs_func=partial(sample_inputs_fft_with_min, min_size=(1, 1)),
error_inputs_func=error_inputs_fftn,
# https://github.com/pytorch/pytorch/issues/80411
gradcheck_fast_mode=True,
supports_forward_ad=True,
supports_fwgrad_bwgrad=True,
# See https://github.com/pytorch/pytorch/pull/78358
check_batched_forward_grad=False,
decorators=[precisionOverride({torch.float: 1e-4, torch.cfloat: 1e-4})],
skips=(
# RuntimeError: [srcBuf length] > 0 INTERNAL ASSERT FAILED
DecorateInfo(
unittest.expectedFailure,
"TestCommon",
"test_out",
device_type="mps",
),
# AssertionError: The values for attribute 'shape' do not match: torch.Size([5, 3, 10]) != torch.Size([5, 3, 11]).
DecorateInfo(
unittest.expectedFailure,
"TestCommon",
"test_out_warning",
device_type="mps",
),
),
),
SpectralFuncInfo(
"fft.hfft",
aten_name="fft_hfft",
decomp_aten_name="_fft_c2r",
ref=np.fft.hfft,
ndimensional=SpectralFuncType.OneD,
dtypes=all_types_and_complex_and(torch.bool),
# CUDA supports Half/ComplexHalf Precision FFT only on SM53 or later archs
dtypesIfCUDA=all_types_and_complex_and(
torch.bool,
torch.half,
torch.complex32,
),
sample_inputs_func=partial(sample_inputs_fft_with_min, min_size=2),
error_inputs_func=error_inputs_fft,
# https://github.com/pytorch/pytorch/issues/80411
gradcheck_fast_mode=True,
supports_forward_ad=True,
supports_fwgrad_bwgrad=True,
# See https://github.com/pytorch/pytorch/pull/78358
check_batched_forward_grad=False,
check_batched_gradgrad=False,
skips=(
# Issue with conj and torch dispatch, see https://github.com/pytorch/pytorch/issues/82479
DecorateInfo(
unittest.skip("Skipped!"),
"TestSchemaCheckModeOpInfo",
"test_schema_correctness",
dtypes=(torch.complex64, torch.complex128),
),
),
),
SpectralFuncInfo(
"fft.hfft2",
aten_name="fft_hfft2",
decomp_aten_name="_fft_c2r",
ref=scipy.fft.hfft2 if has_scipy_fft else None,
ndimensional=SpectralFuncType.TwoD,
dtypes=all_types_and_complex_and(torch.bool),
# CUDA supports Half/ComplexHalf Precision FFT only on SM53 or later archs
dtypesIfCUDA=all_types_and_complex_and(
torch.bool,
torch.half,
torch.complex32,
),
sample_inputs_func=partial(sample_inputs_fft_with_min, min_size=(2, 2)),
error_inputs_func=error_inputs_fftn,
# https://github.com/pytorch/pytorch/issues/80411
gradcheck_fast_mode=True,
supports_forward_ad=True,
supports_fwgrad_bwgrad=True,
check_batched_gradgrad=False,
# See https://github.com/pytorch/pytorch/pull/78358
check_batched_forward_grad=False,
decorators=[
DecorateInfo(
precisionOverride({torch.float: 2e-4, torch.cfloat: 2e-4}),
"TestFFT",
"test_reference_nd",
),
],
skips=(
# Issue with conj and torch dispatch, see https://github.com/pytorch/pytorch/issues/82479
DecorateInfo(
unittest.skip("Skipped!"),
"TestSchemaCheckModeOpInfo",
"test_schema_correctness",
),
# FIXME: errors are too large; needs investigation
DecorateInfo(
unittest.skip("Skipped!"),
"TestCommon",
"test_complex_half_reference_testing",
device_type="cuda",
),
),
),
SpectralFuncInfo(
"fft.hfftn",
aten_name="fft_hfftn",
decomp_aten_name="_fft_c2r",
ref=scipy.fft.hfftn if has_scipy_fft else None,
ndimensional=SpectralFuncType.ND,
dtypes=all_types_and_complex_and(torch.bool),
# CUDA supports Half/ComplexHalf Precision FFT only on SM53 or later archs
dtypesIfCUDA=all_types_and_complex_and(torch.bool, torch.half, torch.complex32),
sample_inputs_func=partial(sample_inputs_fft_with_min, min_size=(2, 2)),
error_inputs_func=error_inputs_fftn,
# https://github.com/pytorch/pytorch/issues/80411
gradcheck_fast_mode=True,
supports_forward_ad=True,
supports_fwgrad_bwgrad=True,
check_batched_gradgrad=False,
# See https://github.com/pytorch/pytorch/pull/78358
check_batched_forward_grad=False,
decorators=[
DecorateInfo(
precisionOverride({torch.float: 2e-4, torch.cfloat: 2e-4}),
"TestFFT",
"test_reference_nd",
),
],
skips=(
# Issue with conj and torch dispatch, see https://github.com/pytorch/pytorch/issues/82479
DecorateInfo(
unittest.skip("Skipped!"),
"TestSchemaCheckModeOpInfo",
"test_schema_correctness",
),
),
),
SpectralFuncInfo(
"fft.rfft",
aten_name="fft_rfft",
decomp_aten_name="_fft_r2c",
ref=np.fft.rfft,
ndimensional=SpectralFuncType.OneD,
dtypes=all_types_and(torch.bool),
# CUDA supports Half/ComplexHalf Precision FFT only on SM53 or later archs
dtypesIfCUDA=all_types_and(torch.bool, torch.half),
sample_inputs_func=partial(sample_inputs_fft_with_min, min_size=1),
error_inputs_func=error_inputs_fft,
# https://github.com/pytorch/pytorch/issues/80411
gradcheck_fast_mode=True,
supports_forward_ad=True,
supports_fwgrad_bwgrad=True,
check_batched_grad=False,
check_batched_gradgrad=False,
),
SpectralFuncInfo(
"fft.rfft2",
aten_name="fft_rfft2",
decomp_aten_name="_fft_r2c",
ref=np.fft.rfft2,
ndimensional=SpectralFuncType.TwoD,
dtypes=all_types_and(torch.bool),
# CUDA supports Half/ComplexHalf Precision FFT only on SM53 or later archs
dtypesIfCUDA=all_types_and(torch.bool, torch.half),
sample_inputs_func=partial(sample_inputs_fft_with_min, min_size=(1, 1)),
error_inputs_func=error_inputs_fftn,
# https://github.com/pytorch/pytorch/issues/80411
gradcheck_fast_mode=True,
supports_forward_ad=True,
supports_fwgrad_bwgrad=True,
check_batched_grad=False,
check_batched_gradgrad=False,
decorators=[
precisionOverride({torch.float: 1e-4}),
],
),
SpectralFuncInfo(
"fft.rfftn",
aten_name="fft_rfftn",
decomp_aten_name="_fft_r2c",
ref=np.fft.rfftn,
ndimensional=SpectralFuncType.ND,
dtypes=all_types_and(torch.bool),
# CUDA supports Half/ComplexHalf Precision FFT only on SM53 or later archs
dtypesIfCUDA=all_types_and(torch.bool, torch.half),
sample_inputs_func=partial(sample_inputs_fft_with_min, min_size=(1, 1)),
error_inputs_func=error_inputs_fftn,
# https://github.com/pytorch/pytorch/issues/80411
gradcheck_fast_mode=True,
supports_forward_ad=True,
supports_fwgrad_bwgrad=True,
check_batched_grad=False,
check_batched_gradgrad=False,
decorators=[
precisionOverride({torch.float: 1e-4}),
],
),
SpectralFuncInfo(
"fft.ifft",
aten_name="fft_ifft",
decomp_aten_name="_fft_c2c",
ref=np.fft.ifft,
ndimensional=SpectralFuncType.OneD,
sample_inputs_func=partial(sample_inputs_fft_with_min, min_size=1),
error_inputs_func=error_inputs_fft,
# https://github.com/pytorch/pytorch/issues/80411
gradcheck_fast_mode=True,
supports_forward_ad=True,
supports_fwgrad_bwgrad=True,
# See https://github.com/pytorch/pytorch/pull/78358
check_batched_forward_grad=False,
dtypes=all_types_and_complex_and(torch.bool),
# CUDA supports Half/ComplexHalf Precision FFT only on SM53 or later archs
dtypesIfCUDA=all_types_and_complex_and(torch.bool, torch.half, torch.complex32),
skips=(
# AssertionError: Resizing an out= argument with no elements threw a resize warning!
DecorateInfo(
unittest.expectedFailure,
"TestCommon",
"test_out",
device_type="mps",
),
),
),
SpectralFuncInfo(
"fft.ifft2",
aten_name="fft_ifft2",
decomp_aten_name="_fft_c2c",
ref=np.fft.ifft2,
ndimensional=SpectralFuncType.TwoD,
sample_inputs_func=partial(sample_inputs_fft_with_min, min_size=(1, 1)),
error_inputs_func=error_inputs_fftn,
# https://github.com/pytorch/pytorch/issues/80411
gradcheck_fast_mode=True,
supports_forward_ad=True,
supports_fwgrad_bwgrad=True,
# See https://github.com/pytorch/pytorch/pull/78358
check_batched_forward_grad=False,
dtypes=all_types_and_complex_and(torch.bool),
# CUDA supports Half/ComplexHalf Precision FFT only on SM53 or later archs
dtypesIfCUDA=all_types_and_complex_and(torch.bool, torch.half, torch.complex32),
decorators=[
DecorateInfo(
precisionOverride({torch.float: 1e-4, torch.cfloat: 1e-4}),
"TestFFT",
"test_reference_nd",
)
],
skips=(
# RuntimeError: [srcBuf length] > 0 INTERNAL ASSERT FAILED
DecorateInfo(
unittest.expectedFailure,
"TestCommon",
"test_out",
device_type="mps",
),
# AssertionError: The values for attribute 'shape' do not match: torch.Size([5, 3, 10]) != torch.Size([5, 3, 11]).
DecorateInfo(
unittest.expectedFailure,
"TestCommon",
"test_out_warning",
device_type="mps",
),
),
),
SpectralFuncInfo(
"fft.ifftn",
aten_name="fft_ifftn",
decomp_aten_name="_fft_c2c",
ref=np.fft.ifftn,
ndimensional=SpectralFuncType.ND,
sample_inputs_func=partial(sample_inputs_fft_with_min, min_size=(1, 1)),
error_inputs_func=error_inputs_fftn,
# https://github.com/pytorch/pytorch/issues/80411
gradcheck_fast_mode=True,
supports_forward_ad=True,
supports_fwgrad_bwgrad=True,
# See https://github.com/pytorch/pytorch/pull/78358
check_batched_forward_grad=False,
dtypes=all_types_and_complex_and(torch.bool),
# CUDA supports Half/ComplexHalf Precision FFT only on SM53 or later archs
dtypesIfCUDA=all_types_and_complex_and(
torch.bool,
torch.half,
torch.complex32,
),
decorators=[
DecorateInfo(
precisionOverride({torch.float: 1e-4, torch.cfloat: 1e-4}),
"TestFFT",
"test_reference_nd",
)
],
skips=(
# RuntimeError: [srcBuf length] > 0 INTERNAL ASSERT FAILED
DecorateInfo(
unittest.expectedFailure, "TestCommon", "test_out", device_type="mps"
),
# AssertionError: The values for attribute 'shape' do not match: torch.Size([5, 3, 10]) != torch.Size([5, 3, 11]).
DecorateInfo(
unittest.expectedFailure,
"TestCommon",
"test_out_warning",
device_type="mps",
),
),
),
SpectralFuncInfo(
"fft.ihfft",
aten_name="fft_ihfft",
decomp_aten_name="_fft_r2c",
ref=np.fft.ihfft,
ndimensional=SpectralFuncType.OneD,
sample_inputs_func=partial(sample_inputs_fft_with_min, min_size=(1, 1)),
error_inputs_func=error_inputs_fft,
supports_forward_ad=True,
supports_fwgrad_bwgrad=True,
# See https://github.com/pytorch/pytorch/pull/78358
check_batched_forward_grad=False,
dtypes=all_types_and(torch.bool),
# CUDA supports Half/ComplexHalf Precision FFT only on SM53 or later archs
dtypesIfCUDA=all_types_and(torch.bool, torch.half),
skips=(
# AssertionError: Resizing an out= argument with no elements threw a resize warning!
DecorateInfo(
unittest.expectedFailure, "TestCommon", "test_out", device_type="mps"
),
),
check_batched_grad=False,
),
SpectralFuncInfo(
"fft.ihfft2",
aten_name="fft_ihfft2",
decomp_aten_name="_fft_r2c",
ref=scipy.fft.ihfftn if has_scipy_fft else None,
ndimensional=SpectralFuncType.TwoD,
sample_inputs_func=partial(sample_inputs_fft_with_min, min_size=(1, 1)),
error_inputs_func=error_inputs_fftn,
# https://github.com/pytorch/pytorch/issues/80411
gradcheck_fast_mode=True,
supports_forward_ad=True,
supports_fwgrad_bwgrad=True,
# See https://github.com/pytorch/pytorch/pull/78358
check_batched_forward_grad=False,
dtypes=all_types_and(torch.bool),
# CUDA supports Half/ComplexHalf Precision FFT only on SM53 or later archs
dtypesIfCUDA=all_types_and(torch.bool, torch.half),
check_batched_grad=False,
check_batched_gradgrad=False,
decorators=(
# The values for attribute 'shape' do not match: torch.Size([5, 6, 5]) != torch.Size([5, 6, 6]).
DecorateInfo(unittest.expectedFailure, "TestCommon", "test_out_warning"),
DecorateInfo(
precisionOverride({torch.float: 2e-4}), "TestFFT", "test_reference_nd"
),
# Mismatched elements!
DecorateInfo(unittest.expectedFailure, "TestCommon", "test_out"),
DecorateInfo(unittest.expectedFailure, "TestCommon", "test_out_warnings"),
),
),
SpectralFuncInfo(
"fft.ihfftn",
aten_name="fft_ihfftn",
decomp_aten_name="_fft_r2c",
ref=scipy.fft.ihfftn if has_scipy_fft else None,
ndimensional=SpectralFuncType.ND,
sample_inputs_func=partial(sample_inputs_fft_with_min, min_size=(1, 1)),
error_inputs_func=error_inputs_fftn,
# https://github.com/pytorch/pytorch/issues/80411
gradcheck_fast_mode=True,
supports_forward_ad=True,
supports_fwgrad_bwgrad=True,
# See https://github.com/pytorch/pytorch/pull/78358
check_batched_forward_grad=False,
dtypes=all_types_and(torch.bool),
# CUDA supports Half/ComplexHalf Precision FFT only on SM53 or later archss
dtypesIfCUDA=all_types_and(torch.bool, torch.half),
check_batched_grad=False,
check_batched_gradgrad=False,
decorators=[
# The values for attribute 'shape' do not match: torch.Size([5, 6, 5]) != torch.Size([5, 6, 6]).
DecorateInfo(unittest.expectedFailure, "TestCommon", "test_out_warning"),
# Mismatched elements!
DecorateInfo(unittest.expectedFailure, "TestCommon", "test_out"),
DecorateInfo(
precisionOverride({torch.float: 2e-4}), "TestFFT", "test_reference_nd"
),
],
),
SpectralFuncInfo(
"fft.irfft",
aten_name="fft_irfft",
decomp_aten_name="_fft_c2r",
ref=np.fft.irfft,
ndimensional=SpectralFuncType.OneD,
sample_inputs_func=partial(sample_inputs_fft_with_min, min_size=(1, 2)),
error_inputs_func=error_inputs_fft,
# https://github.com/pytorch/pytorch/issues/80411
gradcheck_fast_mode=True,
supports_forward_ad=True,
supports_fwgrad_bwgrad=True,
# See https://github.com/pytorch/pytorch/pull/78358
check_batched_forward_grad=False,
dtypes=all_types_and_complex_and(torch.bool),
# CUDA supports Half/ComplexHalf Precision FFT only on SM53 or later archs
dtypesIfCUDA=all_types_and_complex_and(torch.bool, torch.half, torch.complex32),
check_batched_gradgrad=False,
),
SpectralFuncInfo(
"fft.irfft2",
aten_name="fft_irfft2",
decomp_aten_name="_fft_c2r",
ref=np.fft.irfft2,
ndimensional=SpectralFuncType.TwoD,
sample_inputs_func=partial(sample_inputs_fft_with_min, min_size=(1, 2)),
error_inputs_func=error_inputs_fftn,
# https://github.com/pytorch/pytorch/issues/80411
gradcheck_fast_mode=True,
supports_forward_ad=True,
supports_fwgrad_bwgrad=True,
# See https://github.com/pytorch/pytorch/pull/78358
check_batched_forward_grad=False,
dtypes=all_types_and_complex_and(torch.bool),
# CUDA supports Half/ComplexHalf Precision FFT only on SM53 or later archs
dtypesIfCUDA=all_types_and_complex_and(torch.bool, torch.half, torch.complex32),
check_batched_gradgrad=False,
decorators=[
DecorateInfo(
precisionOverride({torch.float: 1e-4, torch.cfloat: 1e-4}),
"TestFFT",
"test_reference_nd",
)
],
),
SpectralFuncInfo(
"fft.irfftn",
aten_name="fft_irfftn",
decomp_aten_name="_fft_c2r",
ref=np.fft.irfftn,
ndimensional=SpectralFuncType.ND,
sample_inputs_func=partial(sample_inputs_fft_with_min, min_size=(1, 2)),
error_inputs_func=error_inputs_fftn,
# https://github.com/pytorch/pytorch/issues/80411
gradcheck_fast_mode=True,
supports_forward_ad=True,
supports_fwgrad_bwgrad=True,
# See https://github.com/pytorch/pytorch/pull/78358
check_batched_forward_grad=False,
dtypes=all_types_and_complex_and(torch.bool),
# CUDA supports Half/ComplexHalf Precision FFT only on SM53 or later archs
dtypesIfCUDA=all_types_and_complex_and(torch.bool, torch.half, torch.complex32),
check_batched_gradgrad=False,
decorators=[
DecorateInfo(
precisionOverride({torch.float: 1e-4, torch.cfloat: 1e-4}),
"TestFFT",
"test_reference_nd",
)
],
),
OpInfo(
"fft.fftshift",
dtypes=all_types_and_complex_and(
torch.bool, torch.bfloat16, torch.half, torch.chalf
),
sample_inputs_func=sample_inputs_fftshift,
supports_out=False,
supports_forward_ad=True,
supports_fwgrad_bwgrad=True,
),
OpInfo(
"fft.ifftshift",
dtypes=all_types_and_complex_and(
torch.bool, torch.bfloat16, torch.half, torch.chalf
),
sample_inputs_func=sample_inputs_fftshift,
supports_out=False,
supports_forward_ad=True,
supports_fwgrad_bwgrad=True,
),
]
python_ref_db: list[OpInfo] = [
SpectralFuncPythonRefInfo(
"_refs.fft.fft",
torch_opinfo_name="fft.fft",
skips=(
# AssertionError: Resizing an out= argument with no elements threw a resize warning!
DecorateInfo(
unittest.expectedFailure, "TestCommon", "test_out", device_type="mps"
),
),
),
SpectralFuncPythonRefInfo(
"_refs.fft.ifft",
torch_opinfo_name="fft.ifft",
skips=(
# AssertionError: Resizing an out= argument with no elements threw a resize warning!
DecorateInfo(
unittest.expectedFailure, "TestCommon", "test_out", device_type="mps"
),
),
),
SpectralFuncPythonRefInfo(
"_refs.fft.rfft",
torch_opinfo_name="fft.rfft",
skips=(
# AssertionError: Resizing an out= argument with no elements threw a resize warning!
DecorateInfo(
unittest.expectedFailure, "TestCommon", "test_out", device_type="mps"
),
),
),
SpectralFuncPythonRefInfo(
"_refs.fft.irfft",
torch_opinfo_name="fft.irfft",
skips=(
# AssertionError: Resizing an out= argument with no elements threw a resize warning!
DecorateInfo(
unittest.expectedFailure, "TestCommon", "test_out", device_type="mps"
),
),
),
SpectralFuncPythonRefInfo(
"_refs.fft.hfft",
torch_opinfo_name="fft.hfft",
skips=(
# AssertionError: Resizing an out= argument with no elements threw a resize warning!
DecorateInfo(
unittest.expectedFailure, "TestCommon", "test_out", device_type="mps"
),
),
),
SpectralFuncPythonRefInfo(
"_refs.fft.ihfft",
torch_opinfo_name="fft.ihfft",
skips=(
# AssertionError: Resizing an out= argument with no elements threw a resize warning!
DecorateInfo(
unittest.expectedFailure, "TestCommon", "test_out", device_type="mps"
),
),
),
SpectralFuncPythonRefInfo(
"_refs.fft.fftn",
torch_opinfo_name="fft.fftn",
decorators=[
DecorateInfo(
precisionOverride({torch.float: 1e-4, torch.cfloat: 1e-4}),
"TestFFT",
"test_reference_nd",
)
],
),
SpectralFuncPythonRefInfo(
"_refs.fft.ifftn",
torch_opinfo_name="fft.ifftn",
decorators=[
DecorateInfo(
precisionOverride({torch.float: 1e-4, torch.cfloat: 1e-4}),
"TestFFT",
"test_reference_nd",
)
],
),
SpectralFuncPythonRefInfo(
"_refs.fft.rfftn",
torch_opinfo_name="fft.rfftn",
skips=(
# AssertionError: Resizing an out= argument with no elements threw a resize warning!
DecorateInfo(
unittest.expectedFailure,
"TestCommon",
"test_out",
device_type="mps",
),
),
),
SpectralFuncPythonRefInfo(
"_refs.fft.irfftn",
torch_opinfo_name="fft.irfftn",
decorators=[
DecorateInfo(
precisionOverride({torch.float: 1e-4, torch.cfloat: 1e-4}),
"TestFFT",
"test_reference_nd",
)
],
skips=(
# AssertionError: Resizing an out= argument with no elements threw a resize warning!
DecorateInfo(
unittest.expectedFailure, "TestCommon", "test_out", device_type="mps"
),
),
),
SpectralFuncPythonRefInfo(
"_refs.fft.hfftn",
torch_opinfo_name="fft.hfftn",
decorators=[
DecorateInfo(
precisionOverride({torch.float: 2e-4, torch.cfloat: 2e-4}),
"TestFFT",
"test_reference_nd",
)
],
skips=(
# AssertionError: Resizing an out= argument with no elements threw a resize warning!
DecorateInfo(
unittest.expectedFailure, "TestCommon", "test_out", device_type="mps"
),
),
),
SpectralFuncPythonRefInfo(
"_refs.fft.ihfftn",
torch_opinfo_name="fft.ihfftn",
decorators=[
DecorateInfo(
precisionOverride({torch.float: 2e-4}),
"TestFFT",
"test_reference_nd",
),
# AssertionError: Reference result was farther (0.09746177145360499) from the precise
# computation than the torch result was (0.09111555632069855)
# See https://github.com/pytorch/pytorch/pull/170856 for more details.
DecorateInfo(
unittest.skip("Skipped!"),
"TestCommon",
"test_python_ref_torch_fallback",
dtypes=(torch.float16,),
device_type="cuda",
),
# AssertionError: Reference result was farther (0.0953431016138116) from the precise
# computation than the torch result was (0.09305490684430734)
DecorateInfo(
unittest.skip("Skipped!"),
"TestCommon",
"test_python_ref_executor",
dtypes=(torch.float16,),
device_type="cuda",
),
],
skips=(
# AssertionError: Resizing an out= argument with no elements threw a resize warning!
DecorateInfo(
unittest.expectedFailure, "TestCommon", "test_out", device_type="mps"
),
),
),
SpectralFuncPythonRefInfo(
"_refs.fft.fft2",
torch_opinfo_name="fft.fft2",
),
SpectralFuncPythonRefInfo(
"_refs.fft.ifft2",
torch_opinfo_name="fft.ifft2",
decorators=[
DecorateInfo(
precisionOverride({torch.float: 1e-4, torch.cfloat: 1e-4}),
"TestFFT",
"test_reference_nd",
)
],
),
SpectralFuncPythonRefInfo(
"_refs.fft.rfft2",
torch_opinfo_name="fft.rfft2",
skips=(
# AssertionError: Resizing an out= argument with no elements threw a resize warning!
DecorateInfo(
unittest.expectedFailure, "TestCommon", "test_out", device_type="mps"
),
),
),
SpectralFuncPythonRefInfo(
"_refs.fft.irfft2",
torch_opinfo_name="fft.irfft2",
decorators=[
DecorateInfo(
precisionOverride({torch.float: 1e-4, torch.cfloat: 1e-4}),
"TestFFT",
"test_reference_nd",
)
],
skips=(
# AssertionError: Resizing an out= argument with no elements threw a resize warning!
DecorateInfo(
unittest.expectedFailure, "TestCommon", "test_out", device_type="mps"
),
),
),
SpectralFuncPythonRefInfo(
"_refs.fft.hfft2",
torch_opinfo_name="fft.hfft2",
decorators=[
DecorateInfo(
precisionOverride({torch.float: 2e-4, torch.cfloat: 2e-4}),
"TestFFT",
"test_reference_nd",
)
],
skips=(
# AssertionError: Resizing an out= argument with no elements threw a resize warning!
DecorateInfo(
unittest.expectedFailure, "TestCommon", "test_out", device_type="mps"
),
),
),
SpectralFuncPythonRefInfo(
"_refs.fft.ihfft2",
torch_opinfo_name="fft.ihfft2",
decorators=[
DecorateInfo(
precisionOverride({torch.float: 2e-4}),
"TestFFT",
"test_reference_nd",
),
# FIXME:
# Reference result was farther (0.0953431016138116) from the precise computation
# than the torch result was (0.09305490684430734)!
DecorateInfo(
unittest.skip("Skipped!"),
"TestCommon",
"test_python_ref_executor",
device_type="cuda",
),
],
skips=(
# AssertionError: Resizing an out= argument with no elements threw a resize warning!
DecorateInfo(
unittest.expectedFailure, "TestCommon", "test_out", device_type="mps"
),
),
),
PythonRefInfo(
"_refs.fft.fftshift",
op_db=op_db,
torch_opinfo_name="fft.fftshift",
),
PythonRefInfo(
"_refs.fft.ifftshift",
op_db=op_db,
torch_opinfo_name="fft.ifftshift",
),
]
@@ -0,0 +1,459 @@
# mypy: ignore-errors
import unittest
from collections.abc import Callable
from functools import partial
from itertools import product
import numpy
import torch
from torch.testing._internal.common_dtype import floating_types
from torch.testing._internal.common_utils import TEST_SCIPY
from torch.testing._internal.opinfo.core import (
DecorateInfo,
ErrorInput,
OpInfo,
SampleInput,
)
if TEST_SCIPY:
import scipy.signal
def sample_inputs_window(op_info, device, dtype, requires_grad, *args, **kwargs):
r"""Base function used to create sample inputs for windows.
For additional required args you should use *args, as well as **kwargs for
additional keyword arguments.
"""
# Remove include_conjugated_inputs from kwargs
kwargs.pop("include_conjugated_inputs", None)
# Tests window sizes up to 5 samples.
for size, sym in product(range(6), (True, False)):
yield SampleInput(
size,
*args,
sym=sym,
device=device,
dtype=dtype,
requires_grad=requires_grad,
**kwargs,
)
def reference_inputs_window(op_info, device, dtype, requires_grad, *args, **kwargs):
r"""Reference inputs function to use for windows which have a common signature, i.e.,
window size and sym only.
Implement other special functions for windows that have a specific signature.
See exponential and gaussian windows for instance.
"""
yield from sample_inputs_window(
op_info, device, dtype, requires_grad, *args, **kwargs
)
cases = (8, 16, 32, 64, 128, 256)
for size in cases:
yield SampleInput(size, sym=False)
yield SampleInput(size, sym=True)
def reference_inputs_exponential_window(
op_info, device, dtype, requires_grad, **kwargs
):
yield from sample_inputs_window(op_info, device, dtype, requires_grad, **kwargs)
cases = (
(8, {"center": 4, "tau": 0.5}),
(16, {"center": 8, "tau": 2.5}),
(32, {"center": 16, "tau": 43.5}),
(64, {"center": 20, "tau": 3.7}),
(128, {"center": 62, "tau": 99}),
(256, {"tau": 10}),
)
for size, kw in cases:
yield SampleInput(size, sym=False, **kw)
kw["center"] = None
yield SampleInput(size, sym=True, **kw)
def reference_inputs_gaussian_window(op_info, device, dtype, requires_grad, **kwargs):
yield from sample_inputs_window(op_info, device, dtype, requires_grad, **kwargs)
cases = (
(8, {"std": 0.1}),
(16, {"std": 1.2}),
(32, {"std": 2.1}),
(64, {"std": 3.9}),
(128, {"std": 4.5}),
(256, {"std": 10}),
)
for size, kw in cases:
yield SampleInput(size, sym=False, **kw)
yield SampleInput(size, sym=True, **kw)
def reference_inputs_kaiser_window(op_info, device, dtype, requires_grad, **kwargs):
yield from sample_inputs_window(op_info, device, dtype, requires_grad, **kwargs)
cases = (
(8, {"beta": 2}),
(16, {"beta": 12}),
(32, {"beta": 30}),
(64, {"beta": 35}),
(128, {"beta": 41.2}),
(256, {"beta": 100}),
)
for size, kw in cases:
yield SampleInput(size, sym=False, **kw)
yield SampleInput(size, sym=True, **kw)
def reference_inputs_general_cosine_window(
op_info, device, dtype, requires_grad, **kwargs
):
yield from sample_inputs_window(op_info, device, dtype, requires_grad, **kwargs)
cases = (
(8, {"a": [0.5, 0.5]}),
(16, {"a": [0.46, 0.54]}),
(32, {"a": [0.46, 0.23, 0.31]}),
(64, {"a": [0.5]}),
(128, {"a": [0.1, 0.8, 0.05, 0.05]}),
(256, {"a": [0.2, 0.2, 0.2, 0.2, 0.2]}),
)
for size, kw in cases:
yield SampleInput(size, sym=False, **kw)
yield SampleInput(size, sym=True, **kw)
def reference_inputs_general_hamming_window(
op_info, device, dtype, requires_grad, **kwargs
):
yield from sample_inputs_window(op_info, device, dtype, requires_grad, **kwargs)
cases = (
(8, {"alpha": 0.54}),
(16, {"alpha": 0.5}),
(32, {"alpha": 0.23}),
(64, {"alpha": 0.8}),
(128, {"alpha": 0.9}),
(256, {"alpha": 0.05}),
)
for size, kw in cases:
yield SampleInput(size, sym=False, **kw)
yield SampleInput(size, sym=True, **kw)
def error_inputs_window(op_info, device, *args, **kwargs):
# Tests for windows that have a negative size
yield ErrorInput(
SampleInput(-1, *args, dtype=torch.float32, device=device, **kwargs),
error_type=ValueError,
error_regex="requires non-negative window length, got M=-1",
)
# Tests for window tensors that are not torch.strided, for instance, torch.sparse_coo.
yield ErrorInput(
SampleInput(
3,
*args,
layout=torch.sparse_coo,
device=device,
dtype=torch.float32,
**kwargs,
),
error_type=ValueError,
error_regex="is implemented for strided tensors only, got: torch.sparse_coo",
)
# Tests for window tensors that are not floating point dtypes, for instance, torch.long.
yield ErrorInput(
SampleInput(3, *args, dtype=torch.long, device=device, **kwargs),
error_type=ValueError,
error_regex="expects float32 or float64 dtypes, got: torch.int64",
)
# Tests for window tensors that are bfloat16
yield ErrorInput(
SampleInput(3, *args, dtype=torch.bfloat16, device=device, **kwargs),
error_type=ValueError,
error_regex="expects float32 or float64 dtypes, got: torch.bfloat16",
)
# Tests for window tensors that are float16
yield ErrorInput(
SampleInput(3, *args, dtype=torch.float16, device=device, **kwargs),
error_type=ValueError,
error_regex="expects float32 or float64 dtypes, got: torch.float16",
)
def error_inputs_exponential_window(op_info, device, **kwargs):
# Yield common error inputs
yield from error_inputs_window(op_info, device, **kwargs)
# Tests for negative decay values.
yield ErrorInput(
SampleInput(3, tau=-1, dtype=torch.float32, device=device, **kwargs),
error_type=ValueError,
error_regex="Tau must be positive, got: -1 instead.",
)
# Tests for symmetric windows and a given center value.
yield ErrorInput(
SampleInput(3, center=1, sym=True, dtype=torch.float32, device=device),
error_type=ValueError,
error_regex="Center must be None for symmetric windows",
)
def error_inputs_gaussian_window(op_info, device, **kwargs):
# Yield common error inputs
yield from error_inputs_window(op_info, device, std=0.5, **kwargs)
# Tests for negative standard deviations
yield ErrorInput(
SampleInput(3, std=-1, dtype=torch.float32, device=device, **kwargs),
error_type=ValueError,
error_regex="Standard deviation must be positive, got: -1 instead.",
)
def error_inputs_kaiser_window(op_info, device, **kwargs):
# Yield common error inputs
yield from error_inputs_window(op_info, device, beta=12, **kwargs)
# Tests for negative beta
yield ErrorInput(
SampleInput(3, beta=-1, dtype=torch.float32, device=device, **kwargs),
error_type=ValueError,
error_regex="beta must be non-negative, got: -1 instead.",
)
def error_inputs_general_cosine_window(op_info, device, **kwargs):
# Yield common error inputs
yield from error_inputs_window(op_info, device, a=[0.54, 0.46], **kwargs)
# Tests for negative beta
yield ErrorInput(
SampleInput(3, a=None, dtype=torch.float32, device=device, **kwargs),
error_type=TypeError,
error_regex="Coefficients must be a list/tuple",
)
yield ErrorInput(
SampleInput(3, a=[], dtype=torch.float32, device=device, **kwargs),
error_type=ValueError,
error_regex="Coefficients cannot be empty",
)
def reference_signal_window(fn: Callable):
r"""Wrapper for scipy signal window references.
Discards keyword arguments for window reference functions that don't have a matching signature with
torch, e.g., gaussian window.
"""
def _fn(
*args,
dtype=numpy.float64,
device=None,
layout=torch.strided,
requires_grad=False,
**kwargs,
):
r"""The unused arguments are defined to disregard those values"""
return fn(*args, **kwargs).astype(dtype)
return _fn
def make_signal_windows_opinfo(
name: str,
ref: Callable,
sample_inputs_func: Callable,
reference_inputs_func: Callable,
error_inputs_func: Callable,
*,
skips: tuple[DecorateInfo, ...] = (),
):
r"""Helper function to create OpInfo objects related to different windows."""
return OpInfo(
name=name,
ref=ref if TEST_SCIPY else None,
dtypes=floating_types(),
sample_inputs_func=sample_inputs_func,
reference_inputs_func=reference_inputs_func,
error_inputs_func=error_inputs_func,
supports_out=False,
supports_autograd=False,
skips=(
# TODO: same as this?
# https://github.com/pytorch/pytorch/issues/81774
# also see: arange, new_full
# fails to match any schemas despite working in the interpreter
DecorateInfo(
unittest.expectedFailure,
"TestOperatorSignatures",
"test_get_torch_func_signature_exhaustive",
),
# fails to match any schemas despite working in the interpreter
DecorateInfo(
unittest.expectedFailure, "TestJit", "test_variant_consistency_jit"
),
# skip these tests since we have non tensor input
DecorateInfo(
unittest.skip("Skipped!"), "TestCommon", "test_noncontiguous_samples"
),
DecorateInfo(
unittest.skip("Skipped!"),
"TestCommon",
"test_variant_consistency_eager",
),
DecorateInfo(unittest.skip("Skipped!"), "TestMathBits", "test_conj_view"),
DecorateInfo(
unittest.skip("Skipped!"), "TestMathBits", "test_neg_conj_view"
),
DecorateInfo(unittest.skip("Skipped!"), "TestMathBits", "test_neg_view"),
DecorateInfo(
unittest.skip("Skipped!"),
"TestVmapOperatorsOpInfo",
"test_vmap_exhaustive",
),
DecorateInfo(
unittest.skip("Skipped!"),
"TestVmapOperatorsOpInfo",
"test_op_has_batch_rule",
),
DecorateInfo(
unittest.skip("Buggy on MPS for now (mistakenly promotes to float64)"),
"TestCommon",
"test_numpy_ref_mps",
),
*skips,
),
)
op_db: list[OpInfo] = [
make_signal_windows_opinfo(
name="signal.windows.hamming",
ref=reference_signal_window(scipy.signal.windows.hamming)
if TEST_SCIPY
else None,
sample_inputs_func=sample_inputs_window,
reference_inputs_func=reference_inputs_window,
error_inputs_func=error_inputs_window,
),
make_signal_windows_opinfo(
name="signal.windows.hann",
ref=reference_signal_window(scipy.signal.windows.hann) if TEST_SCIPY else None,
sample_inputs_func=sample_inputs_window,
reference_inputs_func=reference_inputs_window,
error_inputs_func=error_inputs_window,
),
make_signal_windows_opinfo(
name="signal.windows.bartlett",
ref=reference_signal_window(scipy.signal.windows.bartlett)
if TEST_SCIPY
else None,
sample_inputs_func=sample_inputs_window,
reference_inputs_func=reference_inputs_window,
error_inputs_func=error_inputs_window,
),
make_signal_windows_opinfo(
name="signal.windows.blackman",
ref=reference_signal_window(scipy.signal.windows.blackman)
if TEST_SCIPY
else None,
sample_inputs_func=sample_inputs_window,
reference_inputs_func=reference_inputs_window,
error_inputs_func=error_inputs_window,
),
make_signal_windows_opinfo(
name="signal.windows.cosine",
ref=reference_signal_window(scipy.signal.windows.cosine)
if TEST_SCIPY
else None,
sample_inputs_func=sample_inputs_window,
reference_inputs_func=reference_inputs_window,
error_inputs_func=error_inputs_window,
),
make_signal_windows_opinfo(
name="signal.windows.exponential",
ref=reference_signal_window(scipy.signal.windows.exponential)
if TEST_SCIPY
else None,
sample_inputs_func=partial(sample_inputs_window, tau=2.78),
reference_inputs_func=partial(reference_inputs_exponential_window, tau=2.78),
error_inputs_func=error_inputs_exponential_window,
),
make_signal_windows_opinfo(
name="signal.windows.gaussian",
ref=reference_signal_window(scipy.signal.windows.gaussian)
if TEST_SCIPY
else None,
sample_inputs_func=partial(sample_inputs_window, std=1.92),
reference_inputs_func=partial(reference_inputs_gaussian_window, std=1.92),
error_inputs_func=error_inputs_gaussian_window,
skips=(
DecorateInfo(
unittest.skip("Buggy on MPS for now (mistakenly promotes to float64)"),
"TestCommon",
"test_numpy_ref_mps",
),
),
),
make_signal_windows_opinfo(
name="signal.windows.kaiser",
ref=reference_signal_window(scipy.signal.windows.kaiser)
if TEST_SCIPY
else None,
sample_inputs_func=partial(sample_inputs_window, beta=12.0),
reference_inputs_func=partial(reference_inputs_kaiser_window, beta=12.0),
error_inputs_func=error_inputs_kaiser_window,
),
make_signal_windows_opinfo(
name="signal.windows.general_cosine",
ref=reference_signal_window(scipy.signal.windows.general_cosine)
if TEST_SCIPY
else None,
sample_inputs_func=partial(sample_inputs_window, a=[0.54, 0.46]),
reference_inputs_func=partial(
reference_inputs_general_cosine_window, a=[0.54, 0.46]
),
error_inputs_func=error_inputs_general_cosine_window,
),
make_signal_windows_opinfo(
name="signal.windows.general_hamming",
ref=reference_signal_window(scipy.signal.windows.general_hamming)
if TEST_SCIPY
else None,
sample_inputs_func=partial(sample_inputs_window, alpha=0.54),
reference_inputs_func=partial(
reference_inputs_general_hamming_window, alpha=0.54
),
error_inputs_func=error_inputs_window,
),
make_signal_windows_opinfo(
name="signal.windows.nuttall",
ref=reference_signal_window(scipy.signal.windows.nuttall)
if TEST_SCIPY
else None,
sample_inputs_func=sample_inputs_window,
reference_inputs_func=reference_inputs_window,
error_inputs_func=error_inputs_window,
),
]
@@ -0,0 +1,931 @@
# mypy: ignore-errors
import os
import torch
from torch.testing import make_tensor # noqa: F401
from torch.testing._internal.common_dtype import highest_precision_float
from torch.testing._internal.opinfo.core import ( # noqa: F401
BinaryUfuncInfo,
ErrorInput,
generate_elementwise_binary_tensors,
ReductionOpInfo,
sample_inputs_reduction,
SampleInput,
)
def _check_validate(op_info, sample):
def _check_fail(sample):
try:
op_info(
sample.sample_input.input,
*sample.sample_input.args,
**sample.sample_input.kwargs,
)
except sample.error_type:
pass
except Exception as msg:
raise AssertionError( # noqa: B904
f"{op_info.name} on {sample.sample_input=} expected exception "
f"{sample.error_type}: {sample.error_regex}, got {type(msg).__name__}: {msg}"
)
else:
raise AssertionError(
f"{op_info.name} on {sample.sample_input=} expected exception "
f"{sample.error_type}: {sample.error_regex}, got none."
)
def _check_success(sample):
try:
op_info(sample.input, *sample.args, **sample.kwargs)
except Exception as msg:
raise AssertionError( # noqa: B904
f"{op_info.name} on {sample=} expected to succeed "
f", got {type(msg).__name__}: {msg}"
)
if isinstance(sample, ErrorInput):
_check_fail(sample)
else:
_check_success(sample)
def _sample_inputs_sparse(
sample_inputs,
maybe_failing_sample_inputs,
validate_sample_input,
op_info,
*args,
**kwargs,
):
check_validate = (
os.environ.get("PYTORCH_TEST_CHECK_VALIDATE_SPARSE_SAMPLES", "0") == "1"
)
for sample in sample_inputs(op_info, *args, **kwargs):
sample = validate_sample_input(op_info, sample, check_validate=check_validate)
if isinstance(sample, SampleInput):
yield sample
# Error inputs are handled in error_inputs_sparse
for sample in maybe_failing_sample_inputs(op_info, *args, **kwargs):
sample = validate_sample_input(op_info, sample, check_validate=check_validate)
if isinstance(sample, SampleInput):
yield sample
def _error_inputs_sparse(
maybe_failing_sample_inputs, validate_sample_input, op_info, *args, **kwargs
):
check_validate = (
os.environ.get("PYTORCH_TEST_CHECK_VALIDATE_SPARSE_SAMPLES", "0") == "1"
)
for sample in maybe_failing_sample_inputs(op_info, *args, **kwargs):
sample = validate_sample_input(op_info, sample, check_validate=check_validate)
if isinstance(sample, ErrorInput):
yield sample
# Sample inputs are handled in sample_inputs_sparse
def _apply_requires_grad_to_samples(sample_inputs):
"""Decorator to _maybe_failing_sample_inputs_... generator functions
that clones and sets requires_grad argument to tensors in sample
input arguments. This is needed when the generated samples share
tensor instances.
"""
def wrapper(op_info, device, dtype, requires_grad, layout, **kwargs):
def apply_requires_grad(x):
if (
not isinstance(x, torch.Tensor)
or x.requires_grad
or not requires_grad
or not (x.is_floating_point() or x.is_complex())
):
return x
return x.detach().clone().requires_grad_(requires_grad)
if requires_grad:
for sample_input in sample_inputs(
op_info, device, dtype, requires_grad, layout, **kwargs
):
yield sample_input.transform(apply_requires_grad)
else:
yield from sample_inputs(
op_info, device, dtype, requires_grad, layout, **kwargs
)
return wrapper
def sample_inputs_sparse_reduction(
op_info, device, dtype, requires_grad, layout, blocksize=None, **kwargs
):
"""Sample inputs for reduction operations on sparse tensors."""
layout_name = str(layout).split(".", 1)[-1].rsplit("_coo", 1)[0]
op_supports_layout = getattr(op_info, "supports_" + layout_name)
if not op_supports_layout:
return
for sample_input in sample_inputs_reduction(
op_info, device, dtype, requires_grad, **kwargs
):
if sample_input.input.ndim == 0:
# scalar sparse tensors are not supported
continue
if layout in {
torch.sparse_csr,
torch.sparse_csc,
torch.sparse_bsr,
torch.sparse_bsc,
}:
if sample_input.input.ndim < 2:
# conversion to sparse compressed tensors requires at
# least 2 dimensional tensors
continue
if sample_input.input.ndim > 2 and (sample_input.input == 0).any():
# Skip batched sparse compressed samples that contain
# explicit zeros because to_sparse(layout=..) will
# fail, see gh-98495.
# TODO: remove this if-block after gh-98495 is fixed.
continue
if layout in {torch.sparse_bsr, torch.sparse_bsc} and blocksize is None:
blocksize = (1, 1)
yield SampleInput(
sample_input.input.detach()
.to_sparse(layout=layout, blocksize=blocksize)
.requires_grad_(requires_grad),
args=sample_input.args,
kwargs=sample_input.kwargs,
)
if layout is torch.sparse_coo and (dtype.is_floating_point or dtype.is_complex):
# uncoalesced samples
inp = sample_input.input.detach().to_sparse(layout=layout)
inp = torch.sparse_coo_tensor(
inp.indices().repeat(1, 2),
inp.values().repeat(2),
inp.shape,
dtype=inp.dtype,
device=inp.device,
)
if inp.is_coalesced():
raise AssertionError("Expected inp to not be coalesced")
yield SampleInput(
inp.requires_grad_(requires_grad),
args=sample_input.args,
kwargs=sample_input.kwargs,
)
if sample_input.input.ndim > 2:
# hybrid samples
yield SampleInput(
sample_input.input.detach()
.to_sparse(
layout=layout,
blocksize=blocksize,
dense_dim=sample_input.input.ndim - 2,
)
.requires_grad_(requires_grad),
args=sample_input.args,
kwargs=sample_input.kwargs,
)
def _validate_sample_input_sparse_reduction(op_info, sample, check_validate=False):
"""Return the specified sample when it is valid and supported by the
operation. Otherwise, return the sample as ErrorInput instance.
When check_validate is True, the result is validated against
calling the op on the sample.
"""
UNSPECIFIED = object()
if op_info.name == "sum":
sample = _validate_sample_input_sparse_reduction_sum(sample)
if op_info.name == "masked.sum":
mask = sample.kwargs.get("mask", UNSPECIFIED)
if (
mask not in {None, UNSPECIFIED}
and mask.ndim > 2
and mask.layout is torch.strided
and (mask == 0).any()
):
# TODO: remove this if-block after gh-98495 is fixed.
sample = ErrorInput(
sample,
error_regex="Expect the same number of specified elements per batch.",
)
elif not sample.kwargs.get("keepdim"):
sample = ErrorInput(
sample,
error_type=(AssertionError, RuntimeError),
error_regex="reduction operations on (CSR|CSC) tensors with keepdim=False is unsupported",
)
elif mask is UNSPECIFIED:
sample = ErrorInput(
sample,
error_type=ValueError,
error_regex="masked (.*) expects explicit mask for sparse_csr tensor input",
)
elif sample.input.ndim > 2:
sample = ErrorInput(
sample,
error_regex="crow_indices is supposed to be a vector, but got 3 dimensional tensor.",
)
if op_info.name in {"masked.amax", "masked.amin", "masked.mean", "masked.prod"}:
t_inp = sample.input
mask = sample.kwargs.get("mask")
if (
mask is not None
and mask.ndim > 2
and mask.layout is torch.strided
and (mask == 0).any()
):
# TODO: remove this if-block after gh-98495 is fixed.
sample = ErrorInput(
sample,
error_regex="Expect the same number of specified elements per batch.",
)
elif mask is None:
sample = ErrorInput(
sample,
error_type=ValueError,
error_regex="masked (.*) expects explicit mask for sparse_csr tensor input",
)
elif (
mask.layout is sample.input.layout
and mask.ndim > 2
and op_info.name == "masked.mean"
):
sample = ErrorInput(
sample,
error_type=TypeError,
error_regex=(
"where[(][)] received an invalid combination of arguments"
" - got [(]Tensor, Tensor, NoneType[)]"
),
)
elif not sample.kwargs.get("keepdim"):
sample = ErrorInput(
sample,
error_type=(AssertionError, RuntimeError),
error_regex="reduction operations on (CSR|CSC) tensors with keepdim=False is unsupported",
)
elif (
sample.input.ndim > 2
and (sample.kwargs.get("dim") not in {0, 1})
and mask.ndim > 2
and mask.layout is not torch.strided
):
if sample.kwargs.get("dim") == (0, -1):
sample = ErrorInput(
sample,
error_regex="tensor dimensionality must be sum of batch, base, and dense dimensionalities",
)
elif op_info.name == "masked.prod":
sample = ErrorInput(
sample,
error_regex="input_dim == 2 INTERNAL ASSERT FAILED at",
)
else:
sample = ErrorInput(
sample,
error_type=AssertionError,
error_regex="Sparse CSR tensors are 2D and only support reduction along dim 0 or 1.",
)
elif sample.input.ndim > 2:
sample = ErrorInput(
sample,
error_regex="crow_indices is supposed to be a vector, but got 3 dimensional tensor.",
)
elif (
mask.layout is t_inp.layout
and mask._nnz() != t_inp._nnz()
and t_inp.dense_dim() > 0
):
sample = ErrorInput(
sample,
error_regex="Index tensor must have the same number of dimensions as src tensor",
)
if check_validate:
_check_validate(op_info, sample)
return sample
def _validate_sample_input_sparse_reduction_sum(sample, check_validate=False):
# NOTE: When fixing a failing sample case, remove the
# corresponding if-block
t_inp, t_kwargs = sample.input, sample.kwargs
dim = t_kwargs.get("dim")
keepdim = t_kwargs.get("keepdim")
layout = t_inp.layout
if isinstance(dim, (int, list, tuple)):
if layout in {
torch.sparse_csr,
torch.sparse_csc,
torch.sparse_bsr,
torch.sparse_bsc,
}:
if layout in {torch.sparse_csc, torch.sparse_bsr, torch.sparse_bsc}:
return ErrorInput(
sample,
error_regex=(
"Currently the only compressed sparse format supported for sum.dim_IntList is CSR, but got layout"
),
)
if layout in {torch.sparse_csr, torch.sparse_csc} and not keepdim:
return ErrorInput(
sample,
error_regex=(
"reduction operations on CSR tensors with keepdim=False is unsupported"
),
)
if t_inp.dim() != 2:
return ErrorInput(
sample,
error_regex=("input_dim == 2 INTERNAL ASSERT"),
)
if layout == torch.sparse_csr:
if t_inp.dtype == torch.bool:
return ErrorInput(
sample,
error_regex=("_sparse_csr_sum_cpu not implemented for 'Bool'"),
)
if t_inp.dtype == torch.complex32:
return ErrorInput(
sample,
error_regex=(
"_sparse_csr_sum_cuda not implemented for 'ComplexHalf'"
),
)
return sample
def _maybe_failing_sample_inputs_sparse_reduction_sum(
op_info, device, dtype, requires_grad, layout, **kwargs
):
"""Generator of samples that are known to fail or that were failing in past."""
# NOTE: When fixing a failing case, remove the Exception comment
# but keep the `yield sample` statement.
if layout in [
torch.sparse_csr,
torch.sparse_csc,
]:
# NotImplementedError: Could not run 'aten::sum.IntList_out' with arguments from the 'SparseCsrCPU' backend.
yield SampleInput(
torch.tensor([[0, 1], [2, 3]], dtype=dtype)
.to_sparse(layout=layout)
.requires_grad_(requires_grad),
kwargs=dict(dim=0, keepdim=True),
)
yield SampleInput(
torch.tensor([[[0, 1]], [[2, 3]]], dtype=dtype)
.to_sparse(layout=layout, dense_dim=1)
.requires_grad_(requires_grad),
kwargs=dict(dim=0),
)
yield SampleInput(
torch.tensor([[0, 1], [2, 3]], dtype=dtype)
.to_sparse(layout=layout)
.requires_grad_(requires_grad),
kwargs=dict(dim=(0,)),
)
yield SampleInput(
torch.tensor([[0, 1], [2, 3]], dtype=dtype)
.to_sparse(layout=layout)
.requires_grad_(requires_grad),
kwargs=dict(dim=(0,), keepdim=True),
)
yield SampleInput(
torch.tensor([[[0, 1]], [[2, 3]]], dtype=dtype)
.to_sparse(layout=layout, dense_dim=1)
.requires_grad_(requires_grad),
kwargs=dict(dim=(0,)),
)
# RuntimeError: torch.empty: Only batched sparse compressed (non-block) tensors are supported, but got size [2]
yield SampleInput(
torch.tensor([[0, 1], [2, 3]], dtype=dtype)
.to_sparse(layout=layout)
.requires_grad_(requires_grad),
kwargs=dict(dim=0),
)
if layout in [
torch.sparse_bsr,
torch.sparse_bsc,
]:
# RuntimeError: empty_sparse_compressed expected sparse compressed (non-block) tensor layout but got SparseBsr
yield SampleInput(
torch.tensor([[0, 1], [2, 3]], dtype=dtype)
.to_sparse(layout=layout, blocksize=(2, 2))
.requires_grad_(requires_grad),
kwargs=dict(dim=0, keepdim=True),
)
yield SampleInput(
torch.tensor([[[0, 1]], [[2, 3]]], dtype=dtype)
.to_sparse(layout=layout, dense_dim=1, blocksize=(1, 1))
.requires_grad_(requires_grad),
kwargs=dict(dim=0),
)
yield SampleInput(
torch.tensor([[0, 1], [2, 3]], dtype=dtype)
.to_sparse(layout=layout, blocksize=(1, 1))
.requires_grad_(requires_grad),
kwargs=dict(dim=(0,)),
)
yield SampleInput(
torch.tensor([[0, 1], [2, 3]], dtype=dtype)
.to_sparse(layout=layout, blocksize=(1, 1))
.requires_grad_(requires_grad),
kwargs=dict(dim=(0,), keepdim=True),
)
yield SampleInput(
torch.tensor([[[0, 1]], [[2, 3]]], dtype=dtype)
.to_sparse(layout=layout, blocksize=(1, 1), dense_dim=1)
.requires_grad_(requires_grad),
kwargs=dict(dim=(0,)),
)
# RuntimeError: torch.empty: Only batched sparse compressed (non-block) tensors are supported, but got size [2]
yield SampleInput(
torch.tensor([[0, 1], [2, 3]], dtype=dtype)
.to_sparse(layout=layout, blocksize=(1, 1))
.requires_grad_(requires_grad),
kwargs=dict(dim=0),
)
def sample_inputs_sparse_reduction_sum(
op_info, device, dtype, requires_grad, layout, **kwargs
):
"""Sample inputs for sum on sparse tensors."""
yield from _sample_inputs_sparse(
sample_inputs_sparse_reduction,
_maybe_failing_sample_inputs_sparse_reduction_sum,
_validate_sample_input_sparse_reduction,
op_info,
device,
dtype,
requires_grad,
layout,
**kwargs,
)
def error_inputs_sparse_reduction_sum(op_info, device, layout, **kwargs):
"""Error inputs for sum on sparse tensors."""
dtype = torch.float64
requires_grad = False
yield from _error_inputs_sparse(
_maybe_failing_sample_inputs_sparse_reduction_sum,
_validate_sample_input_sparse_reduction,
op_info,
device,
dtype,
requires_grad,
layout,
**kwargs,
)
def sample_inputs_sparse_elementwise_binary_operation(
op_info, device, dtype, requires_grad, layout, **kwargs
):
"""Sample inputs for elementwise binary operations on sparse tensors.
The samples include regular, zero-sized, batched, and hybrid
sparse tensors as well as rhs scalars. All tensors are full tensors.
"""
def _to_sparse(tensor, **kwargs):
return tensor.detach().to_sparse(**kwargs).requires_grad_(requires_grad)
for sample_input in generate_elementwise_binary_tensors(
op_info,
device=device,
dtype=dtype,
requires_grad=requires_grad,
exclude_zero=True,
**kwargs,
):
lhs, rhs = sample_input.input, sample_input.args[0]
min_dense_dim = 0
max_dense_dim = lhs.ndim - 1
if layout in {
torch.sparse_csr,
torch.sparse_csc,
torch.sparse_bsr,
torch.sparse_bsc,
}:
if lhs.ndim < 2:
# sparse compressed tensors sparse_dim must be 2
continue
max_dense_dim = lhs.ndim - 2
for dense_dim in range(min_dense_dim, max_dense_dim + 1):
if layout in {torch.sparse_bsr, torch.sparse_bsc}:
blocksizes = [(1, 1)]
if lhs.numel() > 0:
blocksizes.append(
(
lhs.shape[lhs.ndim - 2 - dense_dim],
lhs.shape[lhs.ndim - 1 - dense_dim],
)
)
else:
blocksizes = [None]
for blocksize in blocksizes:
to_sparse_kwargs = dict(
layout=layout, dense_dim=dense_dim, blocksize=blocksize
)
lhs_sparse = _to_sparse(lhs, **to_sparse_kwargs)
rhs_sparse = _to_sparse(rhs, **to_sparse_kwargs)
# op(sparse, sparse)
yield SampleInput(
lhs_sparse,
args=(rhs_sparse, *sample_input.args[1:]),
kwargs=sample_input.kwargs,
)
# op(sparse, scalar)
yield SampleInput(
lhs_sparse,
args=(
make_tensor(
(), dtype=dtype, device=device, requires_grad=requires_grad
),
*sample_input.args[1:],
),
kwargs=sample_input.kwargs,
)
def _validate_sample_input_elementwise_binary_sparse_mul(sample):
# NOTE: When fixing a failing sample case, remove the
# corresponding if-block
t_inp, t_args = sample.input, sample.args
batch_dim = t_inp.dim() - t_inp.dense_dim() - t_inp.sparse_dim()
layout = t_inp.layout
dtype = t_inp.dtype
if layout is torch.sparse_csr and batch_dim > 0 and t_args[0].ndim > 0:
return ErrorInput(
sample,
error_regex=(
"coo_to_sparse_csr: conversion from Sparse to SparseCsr for input"
" tensors with sparse_dim[(][)]!=2 is not supported"
),
)
elif layout is torch.sparse_csc and t_args[0].ndim > 0:
return ErrorInput(
sample, error_regex="Expected result Tensor to be of format CSR"
)
elif layout is torch.sparse_bsr and t_args[0].ndim > 0:
return ErrorInput(
sample,
error_regex="empty_sparse_compressed expected sparse compressed [(]non-block[)] tensor layout but got SparseBsr",
)
elif layout is torch.sparse_bsc and t_args[0].ndim > 0:
return ErrorInput(
sample,
error_regex="empty_sparse_compressed expected sparse compressed [(]non-block[)] tensor layout but got SparseBsc",
)
elif (
layout is torch.sparse_coo
and dtype is torch.bool
and t_args[0].ndim > 0
and t_inp.is_cpu
and t_inp.numel() > 0
and t_inp.dense_dim() > 0
):
return ErrorInput(
sample, error_regex="\"addcmul_cpu_out\" not implemented for 'Bool'"
)
elif (
layout in {torch.sparse_coo, torch.sparse_csr}
and dtype is torch.bool
and t_inp._nnz() > 0
and t_args[0].ndim > 0
and t_inp.is_cpu
and t_inp.numel() > 0
):
return ErrorInput(
sample, error_regex="\"mul_out_sparse\" not implemented for 'Bool'"
)
elif (
layout is torch.sparse_csr
and t_args[0].layout is torch.strided
and 0 < t_args[0].ndim
and t_args[0].ndim < t_inp.ndim
):
return ErrorInput(
sample, error_regex="sparse_mask_sparse_csr expects self to be 2D"
)
elif layout is torch.sparse_csr and (
(t_args[0].layout is torch.strided and 0 < t_args[0].ndim)
or (t_args[0].layout is layout and t_inp.shape != t_args[0].shape)
):
return ErrorInput(
sample,
error_regex=(
"expects sparse inputs with equal dimensionality, number of sparse dimensions,"
" and shape of sparse dimensions"
),
)
elif (
layout is torch.sparse_csr
and t_inp.dense_dim() > 0
and t_inp._nnz() > 0
and t_inp.is_cpu
and dtype is torch.float16
and t_args[0].ndim > 0
):
return ErrorInput(
sample, error_regex="\"addcmul_cpu_out\" not implemented for 'Half'"
)
return sample
@_apply_requires_grad_to_samples
def _maybe_failing_sample_inputs_sparse_elementwise_binary_mul(
op_info, device, dtype, requires_grad, layout, **kwargs
):
"""Generator of samples that are known to fail or that were failing in past."""
# NOTE: When fixing a failing case, remove the Exception comment
# but keep the `yield sample` statement.
blocksize = (1, 1) if layout in {torch.sparse_bsr, torch.sparse_bsc} else None
regular = torch.tensor([[1, 2], [3, 4]], device=device, dtype=dtype).to_sparse(
layout=layout, dense_dim=0, blocksize=blocksize
)
batch = torch.tensor(
[[[1, 2], [3, 4]], [[4, 5], [6, 7]]], device=device, dtype=dtype
).to_sparse(layout=layout, dense_dim=0, blocksize=blocksize)
hybrid = torch.tensor(
[[[1], [2]], [[3], [4]]], device=device, dtype=dtype
).to_sparse(layout=layout, dense_dim=1, blocksize=blocksize)
if layout is torch.sparse_csr:
# RuntimeError: crow_indices is supposed to be a vector, but got 2 dimensional tensor
yield SampleInput(batch, args=(batch,))
# RuntimeError: Only tensors with two sparse dimensions can be
# converted to the SparseCsr layout, got self with 3 sparse
# dimensions.
yield SampleInput(
torch.zeros_like(hybrid).requires_grad_(requires_grad),
args=(torch.zeros_like(hybrid).requires_grad_(requires_grad),),
)
if dtype is torch.complex32:
# RuntimeError: "mul_out_sparse" not implemented for 'ComplexHalf'
yield SampleInput(regular, args=(regular,))
if dtype is torch.bool and regular.is_cpu:
# RuntimeError: "mul_out_sparse" not implemented for 'Bool'
yield SampleInput(regular, args=(regular,))
if layout is torch.sparse_csc:
# RuntimeError: Expected result Tensor to be of format CSR
yield SampleInput(regular, args=(regular,))
if layout is torch.sparse_bsr:
# RuntimeError: empty_sparse_compressed expected sparse compressed (non-block) tensor layout but got SparseBsr
yield SampleInput(regular, args=(regular,))
if layout is torch.sparse_bsc:
# RuntimeError: empty_sparse_compressed expected sparse compressed (non-block) tensor layout but got SparseBsc
yield SampleInput(regular, args=(regular,))
if layout is torch.sparse_coo:
if dtype is torch.complex32:
# RuntimeError: "mul_out_sparse" not implemented for 'ComplexHalf'
yield SampleInput(regular, args=(regular,))
if dtype is torch.bool and regular.is_cpu:
# RuntimeError: "mul_out_sparse" not implemented for 'Bool'
yield SampleInput(regular, args=(regular,))
if dtype in {torch.bool, torch.float16} and regular.is_cpu:
# RuntimeError: "addcmul_cpu_out" not implemented for '(Bool|Half)'
yield SampleInput(hybrid, args=(hybrid,))
def _validate_sample_input_sparse_elementwise_binary_operation(
op_info, sample, check_validate=False
):
if op_info.name == "mul":
sample = _validate_sample_input_elementwise_binary_sparse_mul(sample)
if check_validate:
_check_validate(op_info, sample)
return sample
def sample_inputs_sparse_mul(op_info, device, dtype, requires_grad, layout, **kwargs):
"""Sample inputs for mul operation on sparse tensors."""
yield from _sample_inputs_sparse(
sample_inputs_sparse_elementwise_binary_operation,
_maybe_failing_sample_inputs_sparse_elementwise_binary_mul,
_validate_sample_input_sparse_elementwise_binary_operation,
op_info,
device,
dtype,
requires_grad,
layout,
**kwargs,
)
def error_inputs_sparse_mul(op_info, device, layout, **kwargs):
"""Error inputs for mul operation on sparse tensors."""
dtype = torch.float64
requires_grad = False
yield from _error_inputs_sparse(
_maybe_failing_sample_inputs_sparse_elementwise_binary_mul,
_validate_sample_input_sparse_elementwise_binary_operation,
op_info,
device,
dtype,
requires_grad,
layout,
**kwargs,
)
def _sample_inputs_sparse_like_fns(
op_info, device, dtype, requires_grad, layout, **kwargs
):
from torch.testing._internal.common_utils import TestCase
for tensor in TestCase().generate_simple_inputs(
layout,
device=device,
dtype=dtype,
enable_batch=True,
enable_hybrid=True,
enable_zero_sized=True,
enable_non_contiguous_indices=False,
enable_non_contiguous_values=False,
):
yield SampleInput(tensor, args=(), kwargs={})
yield SampleInput(
tensor, args=(), kwargs=dict(device=device, dtype=dtype, layout=layout)
)
hpf = highest_precision_float(device)
if dtype is not hpf:
yield SampleInput(tensor, args=(), kwargs=dict(dtype=hpf))
if torch.cuda.is_available():
other_device = "cuda" if tensor.device.type == "cpu" else "cpu"
yield SampleInput(tensor, args=(), kwargs=dict(device=other_device))
if layout is torch.sparse_csr:
other_layout = torch.sparse_csc
elif layout is torch.sparse_csc:
other_layout = torch.sparse_csr
elif layout is torch.sparse_bsr:
other_layout = torch.sparse_bsc
elif layout is torch.sparse_bsc:
other_layout = torch.sparse_bsr
else:
other_layout = torch.strided
yield SampleInput(tensor, args=(), kwargs=dict(layout=other_layout))
if layout is not torch.sparse_coo:
yield SampleInput(tensor, args=(), kwargs=dict(layout=torch.sparse_coo))
def _validate_sample_input_sparse_like_fns(op_info, sample, check_validate=False):
if (
sample.input.layout
in {
torch.sparse_csr,
torch.sparse_csc,
torch.sparse_bsr,
torch.sparse_bsc,
}
and op_info.name != "zeros_like"
):
if sample.kwargs.get("layout", sample.input.layout) != sample.input.layout:
return ErrorInput(
sample,
error_regex=(
"empty_like with different sparse layout is not supported"
" \\(self is Sparse(Csc|Csr|Bsc|Bsr) but you requested Sparse(Csr|Csc|Bsr|Bsc)\\)"
),
)
if sample.input.layout is torch.sparse_coo:
return ErrorInput(
sample,
error_regex=(
"Could not run 'aten::normal_' with arguments from the 'Sparse(CPU|CUDA)' backend."
),
)
if check_validate:
_check_validate(op_info, sample)
return sample
def _maybe_failing_sample_inputs_sparse_like_fns(
op_info, device, dtype, requires_grad, layout, **kwargs
):
if torch.cuda.is_available() and layout is not torch.sparse_coo:
other_device = "cuda" if torch.device(device).type == "cpu" else "cpu"
if layout is torch.sparse_csr:
other_layout = torch.sparse_csc
elif layout is torch.sparse_csc:
other_layout = torch.sparse_csr
elif layout is torch.sparse_bsr:
other_layout = torch.sparse_bsc
elif layout is torch.sparse_bsc:
other_layout = torch.sparse_bsr
else:
other_layout = torch.strided
blocksize = (1, 1) if layout in {torch.sparse_bsr, torch.sparse_bsc} else None
yield SampleInput(
torch.tensor([[0, 1], [2, 3]], dtype=dtype, device=device).to_sparse(
layout=layout, blocksize=blocksize
),
kwargs=dict(device=other_device),
)
yield SampleInput(
torch.tensor([[0, 1], [2, 3]], dtype=dtype, device=device).to_sparse(
layout=layout, blocksize=blocksize
),
kwargs=dict(layout=other_layout),
)
def sample_inputs_sparse_like_fns(
op_info, device, dtype, requires_grad, layout, **kwargs
):
"""Sample inputs for like-functions on sparse tensors."""
yield from _sample_inputs_sparse(
_sample_inputs_sparse_like_fns,
_maybe_failing_sample_inputs_sparse_like_fns,
_validate_sample_input_sparse_like_fns,
op_info,
device,
dtype,
requires_grad,
layout,
**kwargs,
)
def error_inputs_sparse_like_fns(op_info, device, layout, **kwargs):
"""Error inputs for like-functions on sparse tensors."""
dtype = torch.float64
requires_grad = False
yield from _error_inputs_sparse(
_maybe_failing_sample_inputs_sparse_like_fns,
_validate_sample_input_sparse_like_fns,
op_info,
device,
dtype,
requires_grad,
layout,
**kwargs,
)
def _validate_sample_input_sparse_default(op_info, sample, check_validate=False):
if op_info.name == "to_sparse":
if (
sample.input.layout
in {torch.sparse_csr, torch.sparse_csc, torch.sparse_bsr, torch.sparse_bsc}
and len(sample.args) == 1
and isinstance(sample.args[0], int)
and sample.args[0] != 2
):
sample = ErrorInput(
sample,
error_regex="sparse dim argument must be 2 for sparse_compressed_to_sparse",
)
if check_validate:
_check_validate(op_info, sample)
return sample
def validate_sample_input_sparse(op_info, sample, check_validate=False):
"""Return the specified sample when it is valid and supported by the
operation. Otherwise, return the sample as ErrorInput instance.
When check_validate is True, the result is validated against
calling the op on the sample.
"""
if isinstance(op_info, ReductionOpInfo):
return _validate_sample_input_sparse_reduction(
op_info, sample, check_validate=check_validate
)
elif isinstance(op_info, BinaryUfuncInfo):
return _validate_sample_input_sparse_elementwise_binary_operation(
op_info, sample, check_validate=check_validate
)
else:
return _validate_sample_input_sparse_default(
op_info, sample, check_validate=check_validate
)
@@ -0,0 +1,219 @@
# mypy: ignore-errors
from torch.testing._internal.opinfo.core import (
BinaryUfuncInfo,
OpInfo,
ReductionOpInfo,
UnaryUfuncInfo,
)
# NOTE [Python References]
# Python References emulate existing PyTorch operations, but can ultimately
# be expressed in terms of "primitive" operations from torch._prims.
#
# These references are experimental.
# See https://dev-discuss.pytorch.org/t/tracing-with-primitives-update-0/577
# for additional context.
#
# Python Reference OpInfos should be added to the python_ref_db list below.
# Tests can opt-into running on these references by including
# that list in the Sequence they pass to the @ops decorator.
#
# When a Python Reference OpInfo is constructed a pointer to an
# existing OpInfo must be provided using the torch_opinfo_name kwarg.
# The existing OpInfo with that name and no variant will be found
# to inherit from.
#
# Instead of just inheriting the existing OpInfo's metadata, the
# Python Reference OpInfos inherit the existing OpInfo's
# construction arguments. These arguments can be overridden
# by adding kwargs to the constructor.
def _find_referenced_opinfo(referenced_name, variant_name, *, op_db=None):
"""
Finds the OpInfo with the given name that has no variant name.
"""
# NOTE: searching the global op_db doesn't work when OpInfos are split into
# different modules, as otherwise the op_db will not be fully constructed
# yet. So, instead the local op_db must be passed in explicitly.
if op_db is None:
from torch.testing._internal.common_methods_invocations import op_db
for opinfo in op_db:
if opinfo.name == referenced_name and opinfo.variant_test_name == variant_name:
return opinfo
def _inherit_constructor_args(name, op, inherited, overrides):
# inherits metadata
common_kwargs = {
"name": name,
"op": op,
"aliases": None, # TODO add a check for alias coverage
"method_variant": None,
"inplace_variant": None, # TODO: add a check for inplace coverage
"supports_scripting": False,
}
# Acquires inherited kwargs
kwargs = inherited.copy()
# Fixes metadata
if "kwargs" in kwargs:
kwargs.update(kwargs["kwargs"])
del kwargs["kwargs"]
if "self" in kwargs:
del kwargs["self"]
if "__class__" in kwargs:
del kwargs["__class__"]
if "skips" in kwargs:
del kwargs["skips"]
if "decorators" in kwargs:
del kwargs["decorators"]
# Overrides metadata
kwargs.update(common_kwargs)
kwargs.update(overrides)
# At the moment no prims support autograd, so we must not run autograd
# tests e.g. when testing dtype support. Once we start writing autograd
# formulas for prims this can be removed.
kwargs["supports_autograd"] = False
kwargs["supports_gradgrad"] = False
kwargs["supports_fwgrad_bwgrad"] = False
kwargs["supports_inplace_autograd"] = False
kwargs["supports_forward_ad"] = False
return kwargs
class PythonRefInfo(OpInfo):
"""
An OpInfo for a Python reference of an OpInfo base class operation.
"""
def __init__(
self,
name, # the stringname of the callable Python reference
*,
op=None, # the function variant of the operation, populated as torch.<name> if None
op_db=None, # The database of opinfos to search for the parent opinfo
torch_opinfo_name, # the string name of the corresponding torch opinfo
torch_opinfo_variant_name="", # the variant name for corresponding torch opinfo
validate_view_consistency=True,
**kwargs,
): # additional kwargs override kwargs inherited from the torch opinfo
self.torch_opinfo_name = torch_opinfo_name
self.torch_opinfo_variant_name = torch_opinfo_variant_name
self.torch_opinfo = _find_referenced_opinfo(
torch_opinfo_name, torch_opinfo_variant_name, op_db=op_db
)
self.validate_view_consistency = validate_view_consistency
if not isinstance(self.torch_opinfo, OpInfo):
raise AssertionError(
f"Expected torch_opinfo to be OpInfo, got {type(self.torch_opinfo)}"
)
inherited = self.torch_opinfo._original_opinfo_args
ukwargs = _inherit_constructor_args(name, op, inherited, kwargs)
super().__init__(**ukwargs)
class ReductionPythonRefInfo(ReductionOpInfo):
"""
An OpInfo for a Python reference of an elementwise unary operation.
"""
def __init__(
self,
name, # the stringname of the callable Python reference
*,
op=None, # the function variant of the operation, populated as torch.<name> if None
op_db=None, # The database of opinfos to search for the parent opinfo
torch_opinfo_name, # the string name of the corresponding torch opinfo
torch_opinfo_variant_name="", # the variant name for corresponding torch opinfo
**kwargs,
): # additional kwargs override kwargs inherited from the torch opinfo
self.torch_opinfo_name = torch_opinfo_name
self.torch_opinfo_variant_name = torch_opinfo_variant_name
self.torch_opinfo = _find_referenced_opinfo(
torch_opinfo_name, torch_opinfo_variant_name, op_db=op_db
)
if not isinstance(self.torch_opinfo, ReductionOpInfo):
raise AssertionError(
f"Expected torch_opinfo to be ReductionOpInfo, got {type(self.torch_opinfo)}"
)
inherited = self.torch_opinfo._original_reduction_args
ukwargs = _inherit_constructor_args(name, op, inherited, kwargs)
# See https://github.com/pytorch/pytorch/issues/77216
self.validate_view_consistency = False
super().__init__(**ukwargs)
class ElementwiseUnaryPythonRefInfo(UnaryUfuncInfo):
"""
An OpInfo for a Python reference of an elementwise unary operation.
"""
def __init__(
self,
name, # the stringname of the callable Python reference
*,
op=None, # the function variant of the operation, populated as torch.<name> if None
op_db=None, # The database of opinfos to search for the parent opinfo
torch_opinfo_name, # the string name of the corresponding torch opinfo
torch_opinfo_variant_name="", # the variant name for corresponding torch opinfo
validate_view_consistency=True,
**kwargs,
): # additional kwargs override kwargs inherited from the torch opinfo
self.torch_opinfo_name = torch_opinfo_name
self.torch_opinfo_variant_name = torch_opinfo_variant_name
self.torch_opinfo = _find_referenced_opinfo(
torch_opinfo_name, torch_opinfo_variant_name, op_db=op_db
)
self.validate_view_consistency = validate_view_consistency
if not isinstance(self.torch_opinfo, UnaryUfuncInfo):
raise AssertionError(
f"Expected torch_opinfo to be UnaryUfuncInfo, got {type(self.torch_opinfo)}"
)
inherited = self.torch_opinfo._original_unary_ufunc_args
ukwargs = _inherit_constructor_args(name, op, inherited, kwargs)
super().__init__(**ukwargs)
class ElementwiseBinaryPythonRefInfo(BinaryUfuncInfo):
"""
An OpInfo for a Python reference of an elementwise binary operation.
"""
def __init__(
self,
name, # the stringname of the callable Python reference
*,
op=None, # the function variant of the operation, populated as torch.<name> if None
op_db=None, # The database of opinfos to search for the parent opinfo
torch_opinfo_name, # the string name of the corresponding torch opinfo
torch_opinfo_variant_name="", # the variant name for corresponding torch opinfo
**kwargs,
): # additional kwargs override kwargs inherited from the torch opinfo
self.torch_opinfo_name = torch_opinfo_name
self.torch_opinfo_variant_name = torch_opinfo_variant_name
self.torch_opinfo = _find_referenced_opinfo(
torch_opinfo_name, torch_opinfo_variant_name, op_db=op_db
)
if not isinstance(self.torch_opinfo, BinaryUfuncInfo):
raise AssertionError(
f"Expected torch_opinfo to be BinaryUfuncInfo, got {type(self.torch_opinfo)}"
)
inherited = self.torch_opinfo._original_binary_ufunc_args
ukwargs = _inherit_constructor_args(name, op, inherited, kwargs)
super().__init__(**ukwargs)
@@ -0,0 +1,282 @@
# mypy: ignore-errors
import collections
import warnings
from collections.abc import Sequence
from functools import partial, wraps
import numpy as np
import numpy.typing as npt
import torch
from torch.testing._internal.common_cuda import TEST_CUDA
from torch.testing._internal.common_dtype import (
_dispatch_dtypes,
all_types,
all_types_and,
all_types_and_complex,
all_types_and_complex_and,
all_types_and_half,
complex_types,
floating_and_complex_types,
floating_and_complex_types_and,
floating_types,
floating_types_and,
floating_types_and_half,
integral_types,
integral_types_and,
)
from torch.testing._internal.common_utils import torch_to_numpy_dtype_dict
COMPLETE_DTYPES_DISPATCH = (
all_types,
all_types_and_complex,
all_types_and_half,
floating_types,
floating_and_complex_types,
floating_types_and_half,
integral_types,
complex_types,
)
EXTENSIBLE_DTYPE_DISPATCH = (
all_types_and_complex_and,
floating_types_and,
floating_and_complex_types_and,
integral_types_and,
all_types_and,
)
# Better way to acquire devices?
DEVICES = ["cpu"] + (["cuda"] if TEST_CUDA else [])
class _dynamic_dispatch_dtypes(_dispatch_dtypes):
# Class to tag the dynamically generated types.
pass
def get_supported_dtypes(op, sample_inputs_fn, device_type):
# Returns the supported dtypes for the given operator and device_type pair.
if device_type not in ["cpu", "cuda"]:
raise AssertionError(
f"Expected device_type in ['cpu', 'cuda'], got {device_type!r}"
)
if not TEST_CUDA and device_type == "cuda":
warnings.warn(
"WARNING: CUDA is not available, empty_dtypes dispatch will be returned!",
stacklevel=2,
)
return _dynamic_dispatch_dtypes(())
supported_dtypes = set()
for dtype in all_types_and_complex_and(torch.bool, torch.bfloat16, torch.half):
try:
samples = sample_inputs_fn(op, device_type, dtype, False)
except RuntimeError:
# If `sample_inputs_fn` doesn't support sampling for a given
# `dtype`, we assume that the `dtype` is not supported.
# We raise a warning, so that user knows that this was the case
# and can investigate if there was an issue with the `sample_inputs_fn`.
warnings.warn(
f"WARNING: Unable to generate sample for device:{device_type} and dtype:{dtype}",
stacklevel=2,
)
continue
# We assume the dtype is supported
# only if all samples pass for the given dtype.
supported = True
for sample in samples:
try:
op(sample.input, *sample.args, **sample.kwargs)
except RuntimeError:
# dtype is not supported
supported = False
break
if supported:
supported_dtypes.add(dtype)
return _dynamic_dispatch_dtypes(supported_dtypes)
def dtypes_dispatch_hint(dtypes):
# Function returns the appropriate dispatch function (from COMPLETE_DTYPES_DISPATCH and EXTENSIBLE_DTYPE_DISPATCH)
# and its string representation for the passed `dtypes`.
return_type = collections.namedtuple("return_type", "dispatch_fn dispatch_fn_str")
# CUDA is not available, dtypes will be empty.
if len(dtypes) == 0:
return return_type((), "()")
set_dtypes = set(dtypes)
for dispatch in COMPLETE_DTYPES_DISPATCH:
# Short circuit if we get an exact match.
if set(dispatch()) == set_dtypes:
return return_type(dispatch, dispatch.__name__ + "()")
chosen_dispatch = None
chosen_dispatch_score = 0.0
for dispatch in EXTENSIBLE_DTYPE_DISPATCH:
dispatch_dtypes = set(dispatch())
if not dispatch_dtypes.issubset(set_dtypes):
continue
score = len(dispatch_dtypes)
if score > chosen_dispatch_score:
chosen_dispatch_score = score
chosen_dispatch = dispatch
# If user passed dtypes which are lower than the lowest
# dispatch type available (not likely but possible in code path).
if chosen_dispatch is None:
return return_type((), str(dtypes))
return return_type(
partial(dispatch, *tuple(set(dtypes) - set(dispatch()))),
dispatch.__name__ + str(tuple(set(dtypes) - set(dispatch()))),
)
def is_dynamic_dtype_set(op):
# Detect if the OpInfo entry acquired dtypes dynamically
# using `get_supported_dtypes`.
return op.dynamic_dtypes
def str_format_dynamic_dtype(op):
fmt_str = f"""
OpInfo({op.name},
dtypes={dtypes_dispatch_hint(op.dtypes).dispatch_fn_str},
dtypesIfCUDA={dtypes_dispatch_hint(op.dtypesIfCUDA).dispatch_fn_str},
)
"""
return fmt_str
def np_unary_ufunc_integer_promotion_wrapper(fn):
# Wrapper that passes PyTorch's default scalar
# type as an argument to the wrapped NumPy
# unary ufunc when given an integer input.
# This mimics PyTorch's integer->floating point
# type promotion.
#
# This is necessary when NumPy promotes
# integer types to double, since PyTorch promotes
# integer types to the default scalar type.
# Helper to determine if promotion is needed
def is_integral(dtype):
return dtype in [
np.bool_,
bool,
np.uint8,
np.int8,
np.int16,
np.int32,
np.int64,
]
@wraps(fn)
def wrapped_fn(x):
# As the default dtype can change, acquire it when function is called.
# NOTE: Promotion in PyTorch is from integer types to the default dtype
np_dtype = torch_to_numpy_dtype_dict[torch.get_default_dtype()]
if is_integral(x.dtype):
return fn(x.astype(np_dtype))
return fn(x)
return wrapped_fn
def reference_reduction_numpy(f, supports_keepdims=True):
"""Wraps a NumPy reduction operator.
The wrapper function will forward dim, keepdim, mask, and identity
kwargs to the wrapped function as the NumPy equivalent axis,
keepdims, where, and initiak kwargs, respectively.
Args:
f: NumPy reduction operator to wrap
supports_keepdims (bool, optional): Whether the NumPy operator accepts
keepdims parameter. If it does not, the wrapper will manually unsqueeze
the reduced dimensions if it was called with keepdim=True. Defaults to True.
Returns:
Wrapped function
"""
@wraps(f)
def wrapper(x: npt.NDArray, *args, **kwargs):
# Copy keys into a set
keys = set(kwargs.keys())
dim = kwargs.pop("dim", None)
keepdim = kwargs.pop("keepdim", False)
if "dim" in keys:
dim = tuple(dim) if isinstance(dim, Sequence) else dim
# NumPy reductions don't accept dim=0 for scalar inputs
# so we convert it to None if and only if dim is equivalent
if x.ndim == 0 and dim in {0, -1, (0,), (-1,)}:
kwargs["axis"] = None
else:
kwargs["axis"] = dim
if "keepdim" in keys and supports_keepdims:
kwargs["keepdims"] = keepdim
if "mask" in keys:
mask = kwargs.pop("mask")
if mask is not None:
if mask.layout != torch.strided:
raise AssertionError(
f"Expected mask.layout == torch.strided, got {mask.layout}"
)
kwargs["where"] = mask.cpu().numpy()
if "identity" in keys:
identity = kwargs.pop("identity")
if identity is not None:
if identity.dtype is torch.bfloat16:
identity = identity.cpu().to(torch.float32)
else:
identity = identity.cpu()
kwargs["initial"] = identity.numpy()
result = f(x, *args, **kwargs)
# Unsqueeze reduced dimensions if NumPy does not support keepdims
if keepdim and not supports_keepdims and x.ndim > 0:
dim = list(range(x.ndim)) if dim is None else dim
result = np.expand_dims(result, dim)
return result
return wrapper
def prod_numpy(a, *args, **kwargs):
"""
The function will call np.prod with type as np.int64 if the input type
is int or uint64 if is uint. This is necessary because windows np.prod uses by default
int32 while on linux it uses int64.
This is for fixing integer overflow https://github.com/pytorch/pytorch/issues/77320
Returns:
np.prod of input
"""
if "dtype" not in kwargs:
if np.issubdtype(a.dtype, np.signedinteger):
a = a.astype(np.int64)
elif np.issubdtype(a.dtype, np.unsignedinteger):
a = a.astype(np.uint64)
fn = reference_reduction_numpy(np.prod)
return fn(a, *args, **kwargs)
@@ -0,0 +1,7 @@
# mypy: ignore-errors
from .make_fx import make_fx_check
from .aot_autograd import aot_autograd_check, _test_aot_autograd_forwards_backwards_helper
from .fake_tensor import fake_check
from .autograd_registration import autograd_registration_check
from .generate_tests import generate_opcheck_tests, opcheck, OpCheckError, dontGenerateOpCheckTests, is_inside_opcheck_mode
@@ -0,0 +1,176 @@
# mypy: ignore-errors
import torch
import torch.utils._pytree as pytree
from torch.testing._utils import wrapper_set_seed
from functorch.compile import compiled_function, min_cut_rematerialization_partition, default_partition, nop
from .make_fx import randomize
import re
class assert_raises_regex:
def __init__(self, exception_cls, regex):
self.exception_cls = exception_cls
self.regex = regex
def __enter__(self):
pass
def __exit__(self, exc_type, exc_val, traceback):
if exc_type == self.exception_cls:
msg = str(exc_val)
if not re.search(self.regex, msg):
raise AssertionError(
f"Expected exception to match regex. regex: {self.regex}, exception: {msg}")
return True # Squashes the exception
if exc_type is not None:
raise AssertionError(
f"Expected {self.exception_cls} to be raised, instead got exception {exc_type}")
raise AssertionError("Expected exception to be raised but none was")
def aot_autograd_check(
func,
args,
kwargs,
dynamic,
assert_raises_regex_fn=assert_raises_regex,
assert_equals_fn=torch.testing.assert_close,
check_gradients=True,
try_check_data_specialization=False,
skip_correctness_check=False,
disable_functionalization=False):
"""Compares func(*args, **kwargs) in eager-mode to under AOTAutograd.
Compares outputs and (if check_gradients=True) gradients produced by
AOTAutograd against eager-mode PyTorch.
We assume that func(*args, **kwargs) succeeds in eager-mode PyTorch.
"""
flat_args, args_spec = pytree.tree_flatten((args, kwargs))
args = [arg for arg in flat_args if isinstance(arg, torch.Tensor)]
# We construct a new function that only accepts Tensors as inputs
def func_no_tensors(args):
reconstructed_flat_args = []
args = iter(args)
for v in flat_args:
if isinstance(v, torch.Tensor):
reconstructed_flat_args.append(next(args))
else:
reconstructed_flat_args.append(v)
c_args, c_kwargs = pytree.tree_unflatten(reconstructed_flat_args, args_spec)
return func(*c_args, **c_kwargs)
# cannot use the min cut partitioner without functionalization
if disable_functionalization:
compiled_f = compiled_function(
func_no_tensors,
nop,
nop,
dynamic=dynamic,
partition_fn=default_partition,
keep_inference_input_mutations=True,
disable_functionalization=True
)
else:
compiled_f = compiled_function(
func_no_tensors,
nop,
nop,
dynamic=dynamic,
partition_fn=min_cut_rematerialization_partition,
keep_inference_input_mutations=True,
disable_functionalization=False
)
out = wrapper_set_seed(func_no_tensors, args)
if check_gradients == "auto":
any_tensor_requires_grad = pytree.tree_any_only(torch.Tensor, lambda x: x.requires_grad, args)
any_output_requires_grad = pytree.tree_any_only(torch.Tensor, lambda x: x.requires_grad, out)
check_gradients = any_tensor_requires_grad and any_output_requires_grad
if not check_gradients:
compiled_out = wrapper_set_seed(compiled_f, args)
if not skip_correctness_check:
assert_equals_fn(compiled_out, out, msg=outputs_msg)
return
_test_aot_autograd_forwards_backwards_helper(
func_no_tensors, compiled_f, args, assert_raises_regex_fn, assert_equals_fn,
try_check_data_specialization, skip_correctness_check)
outputs_msg = (
"Outputs of the operator are different in eager-mode PyTorch vs "
"AOTDispatcher tracing. This means the operator will have incorrect output "
"underneath torch.compile. This could be because the operator's "
"implementation not traceable."
)
def _test_aot_autograd_forwards_backwards_helper(
f, compiled_f, args, assert_raises_regex_fn, assert_equals_fn,
try_check_data_specialization, skip_correctness_check=False):
# Verify grads are equal between compiled and non-compiled versions of f.
def call_forwards_backwards(f, args):
flat_args = pytree.arg_tree_leaves(*args)
diff_args = [arg for arg in flat_args if isinstance(arg, torch.Tensor) and
arg.requires_grad]
out = wrapper_set_seed(f, args)
flat_out = pytree.tree_leaves(out)
sm = 0
for i in flat_out:
if isinstance(i, torch.Tensor):
# We need to call .abs() because it is possible that the output of the
# operator is a complex Tensor and autograd will yell at autograd.grad
# on a complex Tensor unless we manually provide the grad_output flag.
sm += i.sum().abs()
if not isinstance(sm, torch.Tensor):
raise AssertionError(f"Expected sm to be a Tensor, got {type(sm)}")
return out, torch.autograd.grad(sm, diff_args, allow_unused=True)
def check(args, ignore_failure=False):
try:
orig_out, orig_grad = call_forwards_backwards(f, args)
except Exception:
if ignore_failure:
return
raise
# See https://github.com/pytorch/pytorch/pull/98960#issuecomment-1505962215
tensor_args = [x for x in pytree.tree_flatten(args)[0] if isinstance(x, torch.Tensor)]
any_non_leaves = any(x.grad_fn is not None for x in tensor_args)
if all(x is None for x in orig_grad) and any_non_leaves:
with assert_raises_regex_fn(RuntimeError, 'does not require grad and does not have a grad_fn'):
call_forwards_backwards(compiled_f, args)
return
msg = (
"Gradients of the operator are different in eager-mode PyTorch vs "
"AOTDispatcher. This means the operator will have incorrect gradients "
"underneath torch.compile. This could be because the operator's "
"backward is incorrectly registered or not traceable."
)
compiled_out, compiled_grad = call_forwards_backwards(compiled_f, args)
if not skip_correctness_check:
try:
assert_equals_fn(compiled_out, orig_out)
except Exception as e:
raise type(e)(outputs_msg) from e
try:
assert_equals_fn(compiled_grad, orig_grad)
except Exception as e:
raise type(e)(msg) from e
check(args, ignore_failure=False)
# Randomize the data and run the traced graph with it, to catch bugs
# where we may have baked in Tensor data into the trace.
# This is not guaranteed to succeed, because `f` might have preconditions
# on the values of the inputs, so we just ignore if this test fails.
if try_check_data_specialization:
args = randomize(args)
check(args, ignore_failure=True)
@@ -0,0 +1,135 @@
# mypy: ignore-errors
import contextlib
import torch
import torch.utils._pytree as pytree
@contextlib.contextmanager
def set_autograd_fallback_mode(mode):
prev = torch._C._get_autograd_fallback_mode()
try:
torch._C._set_autograd_fallback_mode(mode)
yield
finally:
torch._C._set_autograd_fallback_mode(prev)
def autograd_registration_check(op, args, kwargs):
"""Check if autograd was registered correctly (for the operator).
Operators should have "autograd support" registered directly to an
autograd dispatch key.
An incorrect registration may lead to unexpected silent incorrectness.
Note that this check won't catch all problems but will catch
the most common ones.
Example usage:
>>> x = torch.randn(3, requires_grad=True)
>>> autograd_registration_check(torch.ops.aten.sin.default, (x,), {})
Here are some best practices if you do find your autograd is
registered incorrectly:
- If the operator is composite (i.e. consists of other PyTorch ops)
and you wish the operator to decompose and get autograd support
that way, then please register the implementation to
DispatchKey::CompositeImplicitAutograd
- If you're adding an autograd formula for the operator, the correct
thing to do is to register an autograd.Function to
DispatchKey::Autograd (preferred) or one of the
DispatchKey::Autograd<BACKEND> keys. It is NOT OK to register
an autograd.Function to a backend (e.g. CPU/CUDA) key.
- If your operator is non-differentiable, then you should register
an implementation to the Autograd key that uses
AutoDispatchBelowAutograd and re-invokes the operator.
"""
if not isinstance(op, torch._ops.OpOverload):
raise AssertionError(f"Expected op to be OpOverload, got {type(op)}")
# Implementation details
# -----------------------------------------------
# If an operator doesn't have an autograd kernel at an autograd key,
# and the operator does not return inputs as-is, then all of
# the outputs should have requires_grad=False before we apply
# special behaviors of our default autograd fallback.
# (The default autograd fallback may set requires_grad=True on output
# tensors in certain modes so that when they are backpropped through,
# they raise an error).
#
# Our strategy for detecting if an operator doesn't have an autograd
# kernel at the autograd key is:
# - set the autograd fallback mode to "nothing" (so it does not change
# the required-gradness of outputs)
# - run the operator
# - Check if any outputs of the operator (that are not inputs) require
# grad. This would only happen if the user calls regular PyTorch
# operations in their backend key (this op should instead be
# CompositeImplicitAutograd or not an op) or if the user invokes
# an autograd.Function in the backend key.
#
# Note that it's already likely a bug if the operator directly returns
# an input as output (because custom ops don't have a good way of
# constructing true in-place or out variants), but we defer that
# responsibility to a different test (schema_check).
flat_args = pytree.arg_tree_leaves(*args, **kwargs)
all_tensors = [arg for arg in flat_args if isinstance(arg, torch.Tensor)]
if not any(t.requires_grad for t in all_tensors):
raise RuntimeError(
"autograd_registration_check: no inputs have requires_grad=True so "
"we are unable to actually perform this test. Please pass inputs "
"that do require grad."
)
# Determine which AutogradBACKEND key to check
all_device_types = {arg.device.type for arg in all_tensors}
if not all_device_types.issubset(["cpu", "cuda", "xpu"]):
# Don't want to support other keys yet
raise NotImplementedError(
f"autograd_registration_check: NYI devices other than CPU/CUDA/XPU, got {all_device_types}"
)
if "cuda" in all_device_types:
key = "AutogradCUDA"
elif "cpu" in all_device_types:
key = "AutogradCPU"
elif "xpu" in all_device_types:
key = "AutogradXPU"
if torch._C._dispatch_has_kernel_for_dispatch_key(op.name(), key):
return
if torch._C._dispatch_has_kernel_for_dispatch_key(op.name(), "Autograd"):
return
if torch._C._dispatch_has_kernel_for_dispatch_key(
op.name(), "CompositeImplicitAutograd"
):
return
# At this point, we know the operator doesn't have a kernel registered to an
# autograd key. Let's proceed with our test.
with set_autograd_fallback_mode("nothing"):
all_outs = op(*args, **kwargs)
inp_ids = {id(arg) for arg in flat_args}
def not_an_input_and_requires_grad(tensor):
if not tensor.requires_grad:
return False
if id(tensor) in inp_ids:
return False
return True
if not pytree.tree_any_only(torch.Tensor, not_an_input_and_requires_grad, all_outs):
return
raise AssertionError(
f"{op.name()}: at least one output of this operator has requires_grad=True "
f"but the operator does not have an autograd kernel defined at an autograd "
f"key (e.g. DispatchKey::Autograd). This could mean that you have "
f"incorrectly registered an autograd kernel to a non-Autograd DispatchKey, "
f"which may lead to silently incorrect results. If your operator consists "
f"of regular PyTorch operations, consider not using an operator at all "
f"or registering your operator as CompositeImplicitAutograd. If you have "
f"an autograd.Function registered to a backend (CPU/CUDA/XPU) key, the correct "
f"location for it is the Autograd key."
)
@@ -0,0 +1,12 @@
# mypy: ignore-errors
import torch._subclasses
def is_builtin(op):
return op.namespace in ('aten', 'prims', 'prim')
def fake_check(op, args, kwargs):
with torch._subclasses.CrossRefFakeMode(ignore_op_fn=is_builtin):
op(*args, **kwargs)
@@ -0,0 +1,866 @@
# mypy: ignore-errors
import datetime
import difflib
import functools
import inspect
import json
import os
import re
import tempfile
import threading
import unittest
from collections.abc import Callable, Sequence
from typing import Any
import torch
import torch._dynamo
import torch.utils._pytree as pytree
from torch._dynamo.utils import clone_input
from torch._library.custom_ops import CustomOpDef
from torch._subclasses.schema_check_mode import SchemaCheckMode
from torch._utils_internal import get_file_path_2
from torch.overrides import TorchFunctionMode
from torch.testing._internal.optests import (
aot_autograd_check,
autograd_registration_check,
fake_check,
)
def dontGenerateOpCheckTests(reason: str):
def inner(fun):
fun._torch_dont_generate_opcheck_tests = True
return fun
return inner
def is_abstract(tensor: torch.Tensor) -> bool:
if tensor.is_meta:
return True
if torch._subclasses.fake_tensor.is_fake(tensor):
return True
return False
def safe_schema_check(
op: torch._ops.OpOverload,
args: tuple[Any, ...],
kwargs: dict[str, Any],
*,
copy_inputs: bool = True,
rtol: float | None = None,
atol: float | None = None,
) -> Any:
if copy_inputs:
args, kwargs = deepcopy_tensors((args, kwargs))
if pytree.tree_any_only(torch.Tensor, is_abstract, (args, kwargs)):
return None
with SchemaCheckMode():
result = op(*args, **kwargs)
return result
def safe_autograd_registration_check(
op: torch._ops.OpOverload,
args: tuple[Any, ...],
kwargs: dict[str, Any],
*,
copy_inputs: bool = True,
rtol: float | None = None,
atol: float | None = None,
) -> None:
if pytree.tree_any_only(torch.Tensor, is_abstract, (args, kwargs)):
return
if copy_inputs:
args, kwargs = deepcopy_tensors((args, kwargs))
# Don't perform autograd_registration_check if none of the inputs require grad.
if not pytree.tree_any_only(
torch.Tensor, lambda x: x.requires_grad, (args, kwargs)
):
return
return autograd_registration_check(op, args, kwargs)
def safe_fake_check(
op: torch._ops.OpOverload,
args: tuple[Any, ...],
kwargs: dict[str, Any],
*,
copy_inputs: bool = True,
rtol: float | None = None,
atol: float | None = None,
) -> None:
if pytree.tree_any_only(torch.Tensor, is_abstract, (args, kwargs)):
return None
if copy_inputs:
args, kwargs = deepcopy_tensors((args, kwargs))
return fake_check(op, args, kwargs)
def safe_aot_autograd_check(
op: torch._ops.OpOverload,
args: tuple[Any, ...],
kwargs: dict[str, Any],
dynamic: bool,
*,
copy_inputs: bool = True,
rtol: float | None = None,
atol: float | None = None,
) -> Any:
# NB: copy_inputs does nothing for aot_autograd_check: it always needs to copy
# inputs.
if pytree.tree_any_only(torch.Tensor, is_abstract, (args, kwargs)):
return None
def func(*args, **kwargs):
args, kwargs = pytree.tree_map_only(torch.Tensor, torch.clone, (args, kwargs))
return op(*args, **kwargs)
# aot_autograd_check runs func(*args, **kwargs) multiple times
# and assumes `func` does not modify its inputs.
if rtol and atol:
assert_equals_fn = functools.partial(
torch.testing.assert_close, rtol=rtol, atol=atol
)
else:
assert_equals_fn = torch.testing.assert_close
return aot_autograd_check(
func,
args,
kwargs,
dynamic,
check_gradients="auto",
assert_equals_fn=assert_equals_fn,
)
def deepcopy_tensors(inputs: Any) -> Any:
return pytree.tree_map_only(torch.Tensor, clone_input, inputs)
# Test util requirements
# - The test util must have signature (op: OpOverload, args, kwargs)
# - The test util must NOT mutate args, kwargs.
# - The test utils in this list must not be prefixes of each other. For example,
# having both "test_schema" and "test_schema_is_functional" is NOT OK.
# - The order of items in this dict matters (for opcheck), we'll run them
# in order.
ALL_TEST_UTILS = {
"test_schema": safe_schema_check,
"test_autograd_registration": safe_autograd_registration_check,
"test_faketensor": safe_fake_check,
"test_aot_dispatch_static": functools.partial(
safe_aot_autograd_check,
dynamic=False,
),
"test_aot_dispatch_dynamic": functools.partial(
safe_aot_autograd_check,
dynamic=True,
),
}
GDOC = "https://docs.google.com/document/d/1Pj5HRZvdOq3xpFpbEjUZp2hBovhy7Wnxw14m6lF2154/edit"
DEFAULT_TEST_UTILS = [
"test_schema",
"test_autograd_registration",
"test_faketensor",
"test_aot_dispatch_dynamic",
]
DEPRECATED_DEFAULT_TEST_UTILS = DEFAULT_TEST_UTILS + [
"test_aot_dispatch_static",
]
def generate_opcheck_tests(
testcase: Any,
namespaces: list[str],
failures_dict_path: str | None = None,
additional_decorators: dict[str, Callable] | None = None,
test_utils: list[str] = DEFAULT_TEST_UTILS,
) -> None:
"""Given an existing TestCase, use the existing tests to generate
additional validation tests for custom operators.
For {all existing tests in the TestCase} x {all test utils},
we will generate one new test. The new test runs a TorchFunctionMode
that intercepts ``op(*args, **kwargs)`` calls and invokes
``test_util(op, *args, **kwargs)``, where ``op`` is an operator.
The test_util that we support are in ALL_TEST_UTILS. They are:
- test_schema: This runs SchemaCheckMode.
- test_autograd_registration: This runs autograd_registration_check.
- test_faketensor: This runs CrossRefFakeMode.
- test_aot_dispatch_static: This runs aot_autograd_check, which:
checks that the outputs (and gradients, if they are computable)
are the same under eager-mode PyTorch and using AOTAutograd.
- test_aot_dispatch_dynamic: Same as aot_dispatch_static, but
runs AOTAutograd using dynamic shapes instead of static shapes.
The generated test will have name ``{test_util}__{original_name}``.
For example, if there is a method named ``test_cumsum``, then
we will generate a ``test_schema__test_cumsum``,
``test_faketensor__test_cumsum``, etc.
For more details, see https://docs.google.com/document/d/1Pj5HRZvdOq3xpFpbEjUZp2hBovhy7Wnxw14m6lF2154/edit
Args:
testcase: The testcase we will modify and generate additional tests for.
namespaces: We will only intercept calls to custom operators with these
namespaces.
failures_dict_path: See ``validate_failures_dict_structure`` for more details
test_utils: a list of test_utils to generate. Example: ["test_schema", "test_faketensor"]
"""
if additional_decorators is None:
additional_decorators = {}
test_methods = [
m
for m in dir(testcase)
if m.startswith("test_") and callable(getattr(testcase, m))
]
if failures_dict_path is None:
# The default failures_dict_path is failures_dict.json in
# the same directory as the test file.
prev_frame = inspect.currentframe().f_back
filename = inspect.getframeinfo(prev_frame)[0]
failures_dict_path = get_file_path_2(
os.path.dirname(filename), "failures_dict.json"
)
failures_dict = FailuresDict.load(
failures_dict_path, create_file=should_update_failures_dict()
)
validate_failures_dict_structure(failures_dict, test_utils, testcase)
validate_failures_dict_formatting(failures_dict_path)
def construct_method(attr, prefix, tester):
method = getattr(testcase, attr)
if getattr(method, "_torch_dont_generate_opcheck_tests", False):
return
new_method_name = prefix + "__" + attr
@functools.wraps(method)
def new_method(*args, **kwargs):
with OpCheckMode(
namespaces,
prefix,
tester,
failures_dict,
f"{testcase.__name__}.{new_method_name}",
failures_dict_path,
):
result = method(*args, **kwargs)
return result
if pytestmark := new_method.__dict__.get("pytestmark"):
import pytest
# check if we need to simplify the parametrize marks
# NB: you need to add this mark to your pytest.ini
opcheck_only_one = False
for mark in pytestmark:
if isinstance(mark, pytest.Mark) and mark.name == "opcheck_only_one":
opcheck_only_one = True
if opcheck_only_one:
new_pytestmark = []
for mark in pytestmark:
if isinstance(mark, pytest.Mark) and mark.name == "parametrize":
argnames, argvalues = mark.args
if mark.kwargs:
raise AssertionError("NYI: mark.kwargs is not empty")
# Special case for device, we want to run on all
# devices
if argnames != "device":
new_pytestmark.append(
pytest.mark.parametrize(
argnames, (next(iter(argvalues)),)
)
)
continue
new_pytestmark.append(mark)
new_method.__dict__["pytestmark"] = new_pytestmark
if new_method_name in additional_decorators:
for dec in additional_decorators[new_method_name]:
new_method = dec(new_method)
if hasattr(testcase, new_method_name):
raise RuntimeError(
f"Tried to autogenerate {new_method_name} but {testcase} already "
f"has method named {new_method_name}. Please rename the original "
f"method on the TestCase."
)
setattr(testcase, new_method_name, new_method)
test_utils = {name: ALL_TEST_UTILS[name] for name in test_utils}
for attr in test_methods:
for prefix, tester in test_utils.items():
construct_method(attr, prefix, tester)
generate_tag_tests(testcase, failures_dict, additional_decorators)
def generate_tag_tests(testcase, failures_dict, additional_decorators):
def generate_test(qualname, definitely_not_pt2_compliant, xfailed_tests):
def inner(self):
try:
op = torch._library.utils.lookup_op(qualname)
except AttributeError as e:
# Operator not importable in this test file
raise unittest.SkipTest(f"Can't import operator {qualname}") from e
op_marked_as_compliant = torch.Tag.pt2_compliant_tag in op.tags
if not op_marked_as_compliant:
return
if not definitely_not_pt2_compliant:
return
raise AssertionError(
f"op '{qualname}' was tagged with torch.Tag.pt2_compliant_tag "
f"but it failed some of the generated opcheck tests "
f"({xfailed_tests}). This may lead to silent correctness issues, "
f"please fix this."
)
return inner
for qualname, test_dict in failures_dict.data.items():
xfailed_tests = [
test
for test, status_dict in test_dict.items()
# We're about to delete the following test after Ed's PR
# to specialize on C++ .size() calls
if "test_aot_dispatch_static" not in test
and status_dict["status"] == "xfail"
]
definitely_not_pt2_compliant = len(xfailed_tests) > 0
generated = generate_test(qualname, definitely_not_pt2_compliant, xfailed_tests)
# Could result in collisions, but unlikely. We'll raise if we see one below.
mangled_qualname = qualname.replace("::", "_").replace(".", "_")
test_name = "test_pt2_compliant_tag_" + mangled_qualname
# You can skip this test via the additional_decorators argument
# in generate_opcheck_tests
if test_name in additional_decorators:
for decorator in additional_decorators[test_name]:
generated = decorator(generated)
if hasattr(testcase, test_name):
raise RuntimeError(
f"Tried to generate a test named {test_name}, but it exists "
f"already. This could be because of a name collision (where "
f"we generated two tests with the same name), or where we "
f"generated a test with the same name as an existing test."
)
setattr(testcase, test_name, generated)
TEST_OPTIONS = ("xfail", "skip", "xsuccess")
def validate_failures_dict_formatting(failures_dict_path: str) -> None:
with open(failures_dict_path) as fp:
actual = fp.read()
failures_dict = FailuresDict.load(failures_dict_path)
expected = failures_dict._save(to_str=True)
if actual == expected:
return
if should_update_failures_dict():
failures_dict = FailuresDict.load(failures_dict_path)
failures_dict.save()
return
expected = expected.splitlines(1)
actual = actual.splitlines(1)
diff = difflib.unified_diff(actual, expected)
diff = "".join(diff)
raise RuntimeError(
f"\n{diff}\n\nExpected the failures dict to be formatted "
f"a certain way. Please see the above diff; you can correct "
f"this either manually or by re-running the test with "
f"PYTORCH_OPCHECK_ACCEPT=1"
)
def validate_failures_dict_structure(
failure_dict: "FailuresDict", test_utils: list[str], testcase: Any
) -> None:
"""Validates the failures dict.
The failure dict looks something like the following.
It maps operator name (qualname) to a list of autogenerated tests.
Each autogenerated test may have a check for the operator (if the operator is
called by the test); the dictionary specifies if we should skip the check,
or if we expect some check to fail.
{
"fbgemm::split_lengths": {
"test_schema__test_split_lengths": {
"comment": "you can put whatever you want into the comment section",
"status": "xfail",
}
"test_schema__test_split_lengths_empty": {
"comment": "",
"status": "skip",
},
},
"fbgemm::gather_lengths": {
"test_schema__test_gather_lengths": {
"comment": "",
"status": "skip",
},
},
}
"""
failure_dict = failure_dict.data
for test_to_option in failure_dict.values():
for test_name, test_dict in test_to_option.items():
if set(test_dict.keys()) != set({"comment", "status"}):
raise RuntimeError(
"in failures_dict, expected sub-dict to have keys 'comment' and 'status'"
)
test_option = test_dict["status"]
if test_option not in TEST_OPTIONS:
raise RuntimeError(
f"In failures_dict, got status={test_option} but it needs to be in {TEST_OPTIONS}"
)
test_class, actual_test_name = test_name.split(".")
if not any(actual_test_name.startswith(test) for test in test_utils):
raise RuntimeError(
f"In failures_dict, test name '{test_name}' should begin with one of {test_utils}"
)
for test in test_utils:
if not actual_test_name.startswith(test):
continue
base_test_name = actual_test_name[len(test) + 2 :]
# remove potential pytest parametrization suffix
base_test_name = re.sub(r"\[.*\]", "", base_test_name)
if testcase.__name__ != test_class:
continue
if hasattr(testcase, base_test_name):
continue
raise RuntimeError(
f"In failures dict, got test name '{test_name}'. We parsed this as "
f"running test '{test}' on '{base_test_name}', but "
f"{base_test_name} does not exist on the TestCase '{testcase.__name__}]. "
f"Maybe you need to change the test name?"
)
def should_update_failures_dict() -> bool:
key = "PYTORCH_OPCHECK_ACCEPT"
return key in os.environ and os.environ[key] == "1"
_is_inside_opcheck_mode = threading.local()
_is_inside_opcheck_mode.value = False
def is_inside_opcheck_mode():
return _is_inside_opcheck_mode.value
class OpCheckMode(TorchFunctionMode):
"""
For a given test, OpCheckMode intercepts calls to operators and runs
test_util(op, args, kwargs) for each intercepted (op, args, kwargs).
"""
def __init__(
self,
namespaces: list[str],
test_util_name: str,
test_util: Callable,
failures_dict: "FailuresDict",
test_name: str,
failures_dict_path: str,
):
# We will intercept calls to ops with these namespaces
self.namespaces = namespaces
# The test utility function. Its signature should be (op, args, kwargs) -> None.
# Examples of test utilities are: schema_check, make_fx_check
self.test_util = test_util
self.test_util_name = test_util_name
# The name of the test that is running this OpCheckMode.
self.test_name = test_name
# Maps qualname -> test_name -> skip/xfail
# Tells us if we should skip a test or assert that there is a failure.
self.failures_dict = failures_dict
# Location of the failures dict. Makes it so that the error message is better.
self.failures_dict_path = failures_dict_path
# OpCheckMode suppresses errors, collects them here, and then raises them on exit.
# Maps qualname -> List[(Exception, func, maybe args, maybe kwargs)]
self.seen_ops_to_errors = {}
def maybe_raise_errors_on_exit(self) -> None:
# Check expected failures first
for qualname in self.seen_ops_to_errors:
option = self.failures_dict.get_status(qualname, self.test_name)
if len(self.seen_ops_to_errors[qualname]) == 0:
if should_update_failures_dict():
self.failures_dict.set_status(
qualname, self.test_name, "xsuccess", comment=""
)
else:
if option == "xfail":
raise OpCheckError(
f"generate_opcheck_tests: Unexpected success for operator "
f"{qualname} on test {self.test_name}. This may mean that "
f"you have fixed this test failure. Please rerun the test with "
f"PYTORCH_OPCHECK_ACCEPT=1 to automatically update the test runner "
f"or manually remove the "
f"expected failure in the failure dict at "
f"{self.failures_dict_path}"
f"For more details, see "
f"{GDOC}"
)
continue
failed_ops = []
for qualname in self.seen_ops_to_errors:
option = self.failures_dict.get_status(qualname, self.test_name)
if option != "xsuccess":
continue
if len(self.seen_ops_to_errors[qualname]) == 0:
continue
failed_ops.append(qualname)
if not failed_ops:
return
if should_update_failures_dict():
for op in failed_ops:
self.failures_dict.set_status(op, self.test_name, "xfail")
return
# Raise from the first error but also report about all of them to make
# recording xfails easier.
ex, op, args, kwargs = self.seen_ops_to_errors[failed_ops[0]][0]
repro_command = generate_repro(
self.test_util_name, op, args, kwargs, save_data=should_print_better_repro()
)
raise OpCheckError(
f"Test generated by `generate_opcheck_tests`, {self.test_name}, "
f"failed on operators {failed_ops}. This usually means that the "
f"operators are not implemented correctly and may lead to silently "
f"incorrect behavior. Set PYTORCH_OPCHECK_PRINT_BETTER_REPRO=1 for a standalone repro, "
f"or please see "
f"{GDOC} "
f"for more recommendations. "
f"To reproduce this problem locally, try to run the following:\n{repro_command}"
) from ex
def __enter__(self, *args, **kwargs):
self.prev_is_opcheck_mode = _is_inside_opcheck_mode.value
self.prev_dynamo_disable = os.environ.get("TORCHDYNAMO_DISABLE", "")
_is_inside_opcheck_mode.value = True
os.environ["TORCHDYNAMO_DISABLE"] = "1"
# When running this test mode, we want to disable
# default torch.compile custom op checker
self.prev_functorch_config_for_checking_custom_op = (
torch._functorch.config.check_custom_op_aliasing
)
torch._functorch.config.check_custom_op_aliasing = False
return super().__enter__(*args, **kwargs)
def __exit__(self, *args, **kwargs):
_is_inside_opcheck_mode.value = self.prev_is_opcheck_mode
os.environ["TORCHDYNAMO_DISABLE"] = self.prev_dynamo_disable
torch._functorch.config.check_custom_op_aliasing = (
self.prev_functorch_config_for_checking_custom_op
)
try:
self.maybe_raise_errors_on_exit()
if should_update_failures_dict():
self.failures_dict.save()
finally:
result = super().__exit__(*args, **kwargs)
return result
def run_test_util(self, op, args, kwargs):
try:
self.test_util(op, args, kwargs, copy_inputs=False)
except torch._subclasses.fake_tensor.UnsupportedFakeTensorException:
# We might get here if the input is already a FakeTensor
# or if we're in a torch.compile block. Just ignore these
# since we can't handle them and reporting them as failures
# is too noisy.
pass
def __torch_function__(self, func, types, args=(), kwargs=None):
kwargs = kwargs if kwargs else {}
# Only intercept calls to operators
if not isinstance(func, (torch._ops.OpOverloadPacket, torch._ops.OpOverload)):
return func(*args, **kwargs)
if (
torch.jit.is_tracing()
or torch.jit.is_scripting()
or torch._dynamo.is_compiling()
):
return func(*args, **kwargs)
# Pre-existing code may not use the .default overload. If we see an
# OpOverloadPacket and we cannot resolve the overload, then we just throw
# and ask the user to clarify. Otherwise, we attempt to resolve the overload.
if isinstance(func, torch._ops.OpOverloadPacket):
func = resolve_unique_overload_or_throw(func)
qualname = func.name()
ns = qualname.split("::")[0]
if ns not in self.namespaces:
return func(*args, **kwargs)
args_c, kwargs_c = deepcopy_tensors((args, kwargs))
result = func(*args, **kwargs)
option = self.failures_dict.get_status(qualname, self.test_name)
if option == "xsuccess" or option == "xfail":
# Suppress all errors during execution. Raise them during __exit__.
try:
if qualname not in self.seen_ops_to_errors:
self.seen_ops_to_errors[qualname] = []
self.run_test_util(func, args_c, kwargs_c)
except Exception as ex:
if should_print_better_repro():
self.seen_ops_to_errors[qualname].append((ex, func, args, kwargs))
else:
self.seen_ops_to_errors[qualname].append((ex, func, None, None))
elif option == "skip":
pass
return result
def should_print_better_repro() -> None:
"""If set, the tests generated by `generate_opcheck_tests` will print a
repro command on failure.
In order to print the repro command, we need to save some tensors to disk.
These will be saved under the following directory:
{tempfile.gettempdir()}/pytorch_opcheck_safe_to_delete/.
Although this is a temp folder, it will usually not automatically get cleaned
up, so you'll need to manually delete it.
"""
key = "PYTORCH_OPCHECK_PRINT_BETTER_REPRO"
if key not in os.environ:
return False
value = os.environ[key]
return value == "1" or value == 1
def opcheck(
op: torch._ops.OpOverload | torch._ops.OpOverloadPacket | CustomOpDef,
args: tuple[Any, ...],
kwargs: dict[str, Any] | None = None,
*,
test_utils: str | Sequence[str] = DEFAULT_TEST_UTILS,
raise_exception: bool = True,
rtol: float | None = None,
atol: float | None = None,
) -> dict[str, str]:
"""See torch.library.opcheck for docstring"""
if (rtol is None) ^ (atol is None):
raise ValueError(
"opcheck(op, ...): if you specify one of rtol/atol, you must specify both"
)
if kwargs is None:
kwargs = {}
if isinstance(op, CustomOpDef):
op = op._opoverload
if isinstance(op, torch._ops.OpOverloadPacket):
op = resolve_unique_overload_or_throw(op)
if not isinstance(op, torch._ops.OpOverload):
raise ValueError(
f"opcheck(op, ...): op must be instance of torch._ops.OpOverload, "
f"e.g. torch.ops.aten.sin.default, got {type(op)}"
)
if test_utils == "ALL":
test_utils = tuple(ALL_TEST_UTILS.keys())
if isinstance(test_utils, str):
test_utils = (test_utils,)
if not isinstance(test_utils, (tuple, list)) or not set(test_utils).issubset(
ALL_TEST_UTILS.keys()
):
raise ValueError(
f"opcheck(op, ..., test_utils={test_utils}), expected test_utils "
f"to be subset of {tuple(ALL_TEST_UTILS.keys())} but it was not"
)
results_dict = {}
for test_util in test_utils:
tester = ALL_TEST_UTILS[test_util]
try:
tester(op, args, kwargs, rtol=rtol, atol=atol)
results_dict[test_util] = "SUCCESS"
except Exception as ex:
if raise_exception:
raise OpCheckError(
f"opcheck(op, ...): {test_util} failed with {ex} "
f"(scroll up for stack trace)"
) from ex
results_dict[test_util] = ex
return results_dict
class OpCheckError(Exception):
pass
def generate_repro(
test: str,
op: torch._ops.OpOverload,
args: tuple[Any, ...],
kwargs: dict[str, Any],
*,
save_data: bool,
dry_run: bool = False,
) -> str:
if save_data:
now = datetime.datetime.now()
path = os.path.join(tempfile.gettempdir(), "pytorch_opcheck_safe_to_delete")
unix_timestamp = datetime.datetime.timestamp(now) * 100000
filepath = os.path.join(path, f"repro_{unix_timestamp}.pt")
if not dry_run:
os.makedirs(path, exist_ok=True)
torch.save((args, kwargs), filepath)
args_kwargs = f'args, kwargs = torch.load("{filepath}")'
else:
args_kwargs = (
"# If you rerun your test with PYTORCH_OPCHECK_PRINT_BETTER_REPRO=1\n"
"# we will fill them in same (args, kwargs) as in your test\n"
"args = () # args to the operator\n"
"kwargs = {} # kwargs to the operator"
)
ns, name = op._schema.name.split("::")
overload = op._overloadname
repro_command = (
f"# =========================================================\n"
f"# BEGIN REPRO SCRIPT\n"
f"# =========================================================\n"
f"import torch\n"
f"from torch.testing._internal.optests import opcheck\n"
f"\n"
f"# Make sure you have loaded the library that contains the op\n"
f"# via an import or torch.ops.load_library(...)\n"
f"op = torch.ops.{ns}.{name}.{overload}\n"
f"\n"
f"{args_kwargs}\n"
f'opcheck(op, args, kwargs, test_utils="{test}")\n'
f"# =========================================================\n"
f"# END REPRO SCRIPT\n"
f"# =========================================================\n"
)
return repro_command
def resolve_unique_overload_or_throw(
op: torch._ops.OpOverloadPacket,
) -> torch._ops.OpOverload:
all_schemas = torch._C._jit_get_schemas_for_operator(op._qualified_op_name)
if len(all_schemas) != 1:
raise RuntimeError(
f"opcheck can only test operators without overloads. "
f"Got the following overloads for {op._qualified_op_name}: "
f"{[schema.overload_name for schema in all_schemas]}"
)
overload_name = all_schemas[0].overload_name
if overload_name == "":
return op.default
return getattr(op, overload_name)
DUMP_OPTIONS = {"indent": 2, "sort_keys": True}
FailuresDictData = dict[str, dict[str, dict[str, str]]]
VERSION = 1
DESCRIPTION = (
f"This is a dict containing failures for tests autogenerated by "
f"generate_opcheck_tests. "
f"For more details, please see {GDOC}"
)
class FailuresDict:
def __init__(self, path: str, data: FailuresDictData):
self.path = path
self.data = data
@staticmethod
def load(path, *, create_file=False) -> "FailuresDict":
if create_file and not os.path.exists(path):
result = FailuresDict(path, {})
FailuresDict.save()
return result
with open(path) as fp:
contents = fp.read()
if contents.strip() == "":
dct = {
"_description": DESCRIPTION,
"data": {},
"_version": VERSION,
}
else:
dct = json.loads(contents)
if "data" not in dct:
raise AssertionError("Expected 'data' in dct")
if "_version" not in dct or dct["_version"] != VERSION:
raise AssertionError(
f"Expected '_version' in dct with value {VERSION}"
)
return FailuresDict(path, dct["data"])
def _save(self, to_str=False) -> str | None:
to_dump = {
"_description": DESCRIPTION,
"data": self.data,
"_version": VERSION,
}
# json.dumps doesn't end with a newline. Let's add one because files
# should end in newlines.
serialized = json.dumps(to_dump, **DUMP_OPTIONS) + "\n"
if to_str:
return serialized
with open(self.path, "w") as fp:
fp.write(serialized)
return None
def save(self) -> None:
return self._save()
def get_status(self, qualname: str, test_name: str) -> str:
if qualname not in self.data:
return "xsuccess"
dct = self.data[qualname]
if test_name not in dct:
return "xsuccess"
return dct[test_name]["status"]
def set_status(
self,
qualname: str,
test_name: str,
status: str,
*,
comment: str | None = None,
):
if qualname not in self.data:
self.data[qualname] = {}
dct = self.data[qualname]
if test_name not in dct:
dct[test_name] = {"status": None, "comment": ""}
if status == "xsuccess":
# The default status is "xsuccess".
del dct[test_name]
else:
dct[test_name]["status"] = status
if comment is not None:
dct[test_name]["comment"] = comment

Some files were not shown because too many files have changed in this diff Show More