Files
pytorch/torch/_numpy/random.py
Edward Z. Yang 9bce208dfb 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-27 02:44:11 +00:00

192 lines
4.5 KiB
Python

# mypy: ignore-errors
"""Wrapper to mimic (parts of) np.random API surface.
NumPy has strict guarantees on reproducibility etc; here we don't give any.
Q: default dtype is float64 in numpy
"""
from __future__ import annotations
import functools
from math import sqrt
from typing import Optional
import torch
from . import _dtypes_impl, _util
from ._normalizations import array_or_scalar, ArrayLike, normalizer
__all__ = [
"seed",
"random_sample",
"sample",
"random",
"rand",
"randn",
"normal",
"choice",
"randint",
"shuffle",
"uniform",
]
def use_numpy_random():
# local import to avoid ref cycles
import torch._dynamo.config as config
return config.use_numpy_random_stream
def deco_stream(func):
@functools.wraps(func)
def inner(*args, **kwds):
if not use_numpy_random():
return func(*args, **kwds)
else:
import numpy
from ._ndarray import ndarray
f = getattr(numpy.random, func.__name__)
# numpy funcs accept numpy ndarrays, unwrap
args = tuple(
arg.tensor.numpy() if isinstance(arg, ndarray) else arg for arg in args
)
kwds = {
key: val.tensor.numpy() if isinstance(val, ndarray) else val
for key, val in kwds.items()
}
value = f(*args, **kwds)
# `value` can be either numpy.ndarray or python scalar (or None)
if isinstance(value, numpy.ndarray):
value = ndarray(torch.as_tensor(value))
return value
return inner
@deco_stream
def seed(seed=None):
if seed is not None:
torch.random.manual_seed(seed)
@deco_stream
def random_sample(size=None):
if size is None:
size = ()
dtype = _dtypes_impl.default_dtypes().float_dtype
values = torch.empty(size, dtype=dtype).uniform_()
return array_or_scalar(values, return_scalar=size == ())
def rand(*size):
if size == ():
size = None
return random_sample(size)
sample = random_sample
random = random_sample
@deco_stream
def uniform(low=0.0, high=1.0, size=None):
if size is None:
size = ()
dtype = _dtypes_impl.default_dtypes().float_dtype
values = torch.empty(size, dtype=dtype).uniform_(low, high)
return array_or_scalar(values, return_scalar=size == ())
@deco_stream
def randn(*size):
dtype = _dtypes_impl.default_dtypes().float_dtype
values = torch.randn(size, dtype=dtype)
return array_or_scalar(values, return_scalar=size == ())
@deco_stream
def normal(loc=0.0, scale=1.0, size=None):
if size is None:
size = ()
dtype = _dtypes_impl.default_dtypes().float_dtype
values = torch.empty(size, dtype=dtype).normal_(loc, scale)
return array_or_scalar(values, return_scalar=size == ())
@deco_stream
def shuffle(x):
# no @normalizer because we do not cast e.g. lists to tensors
from ._ndarray import ndarray
if isinstance(x, torch.Tensor):
tensor = x
elif isinstance(x, ndarray):
tensor = x.tensor
else:
raise NotImplementedError("We do not random.shuffle lists in-place")
perm = torch.randperm(tensor.shape[0])
xp = tensor[perm]
tensor.copy_(xp)
@deco_stream
def randint(low, high=None, size=None):
if size is None:
size = ()
if not isinstance(size, (tuple, list)):
size = (size,)
if high is None:
low, high = 0, low
values = torch.randint(low, high, size=size)
return array_or_scalar(values, int, return_scalar=size == ())
@deco_stream
@normalizer
def choice(a: ArrayLike, size=None, replace=True, p: Optional[ArrayLike] = None):
# https://stackoverflow.com/questions/59461811/random-choice-with-pytorch
if a.numel() == 1:
a = torch.arange(a)
# TODO: check a.dtype is integer -- cf np.random.choice(3.4) which raises
# number of draws
if size is None:
num_el = 1
elif _util.is_sequence(size):
num_el = 1
for el in size:
num_el *= el
else:
num_el = size
# prepare the probabilities
if p is None:
p = torch.ones_like(a) / a.shape[0]
# cf https://github.com/numpy/numpy/blob/main/numpy/random/mtrand.pyx#L973
atol = sqrt(torch.finfo(p.dtype).eps)
if abs(p.sum() - 1.0) > atol:
raise ValueError("probabilities do not sum to 1.")
# actually sample
indices = torch.multinomial(p, num_el, replacement=replace)
if _util.is_sequence(size):
indices = indices.reshape(size)
samples = a[indices]
return samples