pytorch/torch/_numpy/_normalizations.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

260 lines
8.1 KiB
Python
Raw Normal View History

Replace follow_imports = silent with normal (#118414) This is a lot of files changed! Don't panic! Here's how it works: * Previously, we set `follow_imports = silent` for our mypy.ini configuration. Per https://mypy.readthedocs.io/en/stable/running_mypy.html#follow-imports, what this does is whenever we have an import to a module which is not listed as a file to be typechecked in mypy, we typecheck it as normal but suppress all errors that occurred in that file. * When mypy is run inside lintrunner, the list of files is precisely the files covered by the glob in lintrunner.toml, but with files in excludes excluded. * The top-level directive `# mypy: ignore-errors` instructs mypy to typecheck the file as normal, but ignore all errors. * Therefore, it should be equivalent to set `follow_imports = normal`, if we put `# mypy: ignore-errors` on all files that were previously excluded from the file list. * Having done this, we can remove the exclude list from .lintrunner.toml, since excluding a file from typechecking is baked into the files themselves. * torch/_dynamo and torch/_inductor were previously in the exclude list, because they were covered by MYPYINDUCTOR. It is not OK to mark these as `# mypy: ignore-errors` as this will impede typechecking on the alternate configuration. So they are temporarily being checked twice, but I am suppressing the errors in these files as the configurations are not quite the same. I plan to unify the configurations so this is only a temporary state. * There were some straggler type errors after these changes somehow, so I fixed them as needed. There weren't that many. In the future, to start type checking a file, just remove the ignore-errors directive from the top of the file. The codemod was done with this script authored by GPT-4: ``` import glob exclude_patterns = [ ... ] for pattern in exclude_patterns: for filepath in glob.glob(pattern, recursive=True): if filepath.endswith('.py'): with open(filepath, 'r+') as f: content = f.read() f.seek(0, 0) f.write('# mypy: ignore-errors\n\n' + content) ``` Signed-off-by: Edward Z. Yang <ezyang@meta.com> Pull Request resolved: https://github.com/pytorch/pytorch/pull/118414 Approved by: https://github.com/thiagocrepaldi, https://github.com/albanD
2024-01-26 23:42:31 +00:00
# mypy: ignore-errors
""" "Normalize" arguments: convert array_likes to tensors, dtypes to torch dtypes and so on.
"""
from __future__ import annotations
import functools
import inspect
import operator
import typing
import torch
from . import _dtypes, _dtypes_impl, _util
ArrayLike = typing.TypeVar("ArrayLike")
Scalar = typing.Union[int, float, complex, bool]
ArrayLikeOrScalar = typing.Union[ArrayLike, Scalar]
DTypeLike = typing.TypeVar("DTypeLike")
AxisLike = typing.TypeVar("AxisLike")
NDArray = typing.TypeVar("NDArray")
CastingModes = typing.TypeVar("CastingModes")
KeepDims = typing.TypeVar("KeepDims")
# OutArray is to annotate the out= array argument.
#
# This one is special is several respects:
# First, It needs to be an NDArray, and we need to preserve the `result is out`
# semantics. Therefore, we cannot just extract the Tensor from the out array.
# So we never pass the out array to implementer functions and handle it in the
# `normalizer` below.
# Second, the out= argument can be either keyword or positional argument, and
# as a positional arg, it can be anywhere in the signature.
# To handle all this, we define a special `OutArray` annotation and dispatch on it.
#
OutArray = typing.TypeVar("OutArray")
try:
from typing import NotImplementedType
except ImportError:
NotImplementedType = typing.TypeVar("NotImplementedType")
def normalize_array_like(x, parm=None):
from ._ndarray import asarray
return asarray(x).tensor
def normalize_array_like_or_scalar(x, parm=None):
if _dtypes_impl.is_scalar_or_symbolic(x):
return x
return normalize_array_like(x, parm)
def normalize_optional_array_like_or_scalar(x, parm=None):
if x is None:
return None
return normalize_array_like_or_scalar(x, parm)
def normalize_optional_array_like(x, parm=None):
# This explicit normalizer is needed because otherwise normalize_array_like
# does not run for a parameter annotated as Optional[ArrayLike]
return None if x is None else normalize_array_like(x, parm)
def normalize_seq_array_like(x, parm=None):
return tuple(normalize_array_like(value) for value in x)
def normalize_dtype(dtype, parm=None):
# cf _decorators.dtype_to_torch
torch_dtype = None
if dtype is not None:
dtype = _dtypes.dtype(dtype)
torch_dtype = dtype.torch_dtype
return torch_dtype
def normalize_not_implemented(arg, parm):
if arg != parm.default:
raise NotImplementedError(f"'{parm.name}' parameter is not supported.")
def normalize_axis_like(arg, parm=None):
from ._ndarray import ndarray
if isinstance(arg, ndarray):
arg = operator.index(arg)
return arg
def normalize_ndarray(arg, parm=None):
# check the arg is an ndarray, extract its tensor attribute
if arg is None:
return arg
from ._ndarray import ndarray
if not isinstance(arg, ndarray):
raise TypeError(f"'{parm.name}' must be an array")
return arg.tensor
def normalize_outarray(arg, parm=None):
# almost normalize_ndarray, only return the array, not its tensor
if arg is None:
return arg
from ._ndarray import ndarray
Handle some numpy functions with out arguments correctly in dynamo (#118248) Dynamo creates Tensors when tracing through numpy ufuncs like np.sin, np.minimum etc. When running, np functions generally return Tensors when run with `torch.compile`. However, we currently require when normalizing `out` arguments that the input is an ndarray. This creates assertion errors when running torch.compile on any numpy function with an out argument: ``` def test_numpy_ufunc_out(self): @torch.compile(backend="eager") def foo(): x = np.arange(5) out = np.empty((x.shape[0], x.shape[0])) res_out = np.sin(x, out=out) assert res_out is out foo() ``` Failure with stack trace: https://gist.github.com/jamesjwu/68e217638d735678b3de968584dba23f Instead, we can wrap tensors in an ndarray in normalize_outarray to handle the case correctly. Fixing this resolves ~220 tests under dynamo_test_failures, but also exposes a followup bug. In the presence of a graph break, ndarrays don't preserve their id, which can affect assertions and `is` checks between numpy arrays: ``` def test_x_and_out_broadcast(self, ufunc): x = self.get_x(ufunc) out = np.empty((x.shape[0], x.shape[0])) x_b = np.broadcast_to(x, out.shape) # ufunc is just np.sin here res_out = ufunc(x, out=out) res_bcast = ufunc(x_b) # passes assert res_out is out graph_break() # fails assert res_out is out ``` Regular tensors preserve their id because Dynamo caches their example tensor values across a graph break. However, with ndarrays, we only store their converted tensor values, and construct new ndarrays around those values: https://github.com/pytorch/pytorch/blob/eebe7e1d37f1baa995c694d540cc2fc98884fa18/torch/_dynamo/variables/builder.py#L1083 Added a test with expected failure to showcase this — we can then fix that issue separately. Pull Request resolved: https://github.com/pytorch/pytorch/pull/118248 Approved by: https://github.com/lezcano
2024-01-29 03:02:17 +00:00
# Dynamo can pass torch tensors as out arguments,
# wrap it in an ndarray before processing
if isinstance(arg, torch.Tensor):
arg = ndarray(arg)
if not isinstance(arg, ndarray):
raise TypeError(f"'{parm.name}' must be an array")
return arg
def normalize_casting(arg, parm=None):
if arg not in ["no", "equiv", "safe", "same_kind", "unsafe"]:
raise ValueError(
f"casting must be one of 'no', 'equiv', 'safe', 'same_kind', or 'unsafe' (got '{arg}')"
)
return arg
normalizers = {
"ArrayLike": normalize_array_like,
"ArrayLikeOrScalar": normalize_array_like_or_scalar,
"Optional[ArrayLike]": normalize_optional_array_like,
"Sequence[ArrayLike]": normalize_seq_array_like,
"Optional[ArrayLikeOrScalar]": normalize_optional_array_like_or_scalar,
"Optional[NDArray]": normalize_ndarray,
"Optional[OutArray]": normalize_outarray,
"NDArray": normalize_ndarray,
"Optional[DTypeLike]": normalize_dtype,
"AxisLike": normalize_axis_like,
"NotImplementedType": normalize_not_implemented,
"Optional[CastingModes]": normalize_casting,
}
def maybe_normalize(arg, parm):
"""Normalize arg if a normalizer is registered."""
normalizer = normalizers.get(parm.annotation, None)
return normalizer(arg, parm) if normalizer else arg
# ### Return value helpers ###
def maybe_copy_to(out, result, promote_scalar_result=False):
# NB: here out is either an ndarray or None
if out is None:
return result
elif isinstance(result, torch.Tensor):
if result.shape != out.shape:
can_fit = result.numel() == 1 and out.ndim == 0
if promote_scalar_result and can_fit:
result = result.squeeze()
else:
raise ValueError(
f"Bad size of the out array: out.shape = {out.shape}"
f" while result.shape = {result.shape}."
)
out.tensor.copy_(result)
return out
elif isinstance(result, (tuple, list)):
return type(result)(
maybe_copy_to(o, r, promote_scalar_result) for o, r in zip(out, result)
)
else:
raise AssertionError # We should never hit this path
def wrap_tensors(result):
from ._ndarray import ndarray
if isinstance(result, torch.Tensor):
return ndarray(result)
elif isinstance(result, (tuple, list)):
result = type(result)(wrap_tensors(x) for x in result)
return result
def array_or_scalar(values, py_type=float, return_scalar=False):
if return_scalar:
return py_type(values.item())
else:
from ._ndarray import ndarray
return ndarray(values)
# ### The main decorator to normalize arguments / postprocess the output ###
def normalizer(_func=None, *, promote_scalar_result=False):
def normalizer_inner(func):
@functools.wraps(func)
def wrapped(*args, **kwds):
sig = inspect.signature(func)
params = sig.parameters
first_param = next(iter(params.values()))
# NumPy's API does not have positional args before variadic positional args
if first_param.kind == inspect.Parameter.VAR_POSITIONAL:
args = [maybe_normalize(arg, first_param) for arg in args]
else:
# NB: extra unknown arguments: pass through, will raise in func(*args) below
args = (
tuple(
maybe_normalize(arg, parm)
for arg, parm in zip(args, params.values())
)
+ args[len(params.values()) :]
)
kwds = {
name: maybe_normalize(arg, params[name]) if name in params else arg
for name, arg in kwds.items()
}
result = func(*args, **kwds)
# keepdims
bound_args = None
if "keepdims" in params and params["keepdims"].annotation == "KeepDims":
# keepdims can be in any position so we need sig.bind
bound_args = sig.bind(*args, **kwds).arguments
if bound_args.get("keepdims", False):
# In this case the first arg is the initial tensor and
# the second arg is (optionally) the axis
tensor = args[0]
axis = bound_args.get("axis")
result = _util.apply_keepdims(result, axis, tensor.ndim)
# out
if "out" in params:
# out can be in any position so we need sig.bind
if bound_args is None:
bound_args = sig.bind(*args, **kwds).arguments
out = bound_args.get("out")
result = maybe_copy_to(out, result, promote_scalar_result)
result = wrap_tensors(result)
return result
return wrapped
if _func is None:
return normalizer_inner
else:
return normalizer_inner(_func)