8000 Implement `np.diff` for single order differences by soulitzer · Pull Request #50569 · pytorch/pytorch · GitHub
[go: up one dir, main page]

Skip to content

Implement np.diff for single order differences #50569

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions aten/src/ATen/core/aten_interned_strings.h
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ _(aten, diag_embed) \
_(aten, diagflat) \
_(aten, diagonal) \
_(aten, fill_diagonal_) \
_(aten, diff) \
_(aten, digamma) \
_(aten, dim) \
_(aten, dist) \
Expand Down
84 changes: 84 additions & 0 deletions aten/src/ATen/native/ReduceOps.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,90 @@ Tensor cummaxmin_backward(const Tensor& grad, const Tensor& input, const Tensor&
return result.scatter_add_(dim, indices, grad);
}

static Tensor prepend_append_on_dim(const Tensor& self, const c10::optional<Tensor>& prepend, const c10::optional<Tensor>& append, int64_t dim) {
// Helper for diff that handles prepending and appending when at least one is present
TORCH_INTERNAL_ASSERT(prepend.has_value() || append.has_value(), "either prepend or append must be have value");
if (!prepend.has_value() && append.has_value()) {
return at::cat({self, append.value()}, dim);
} else if (prepend.has_value() && !append.has_value()) {
return at::cat({prepend.value(), self}, dim);
} else {
return at::cat({prepend.value(), self, append.value()}, dim);
}
}

static inline void diff_check_compatible_shape(const Tensor& self, const c10::optional<Tensor>&other, int64_t dim) {
// Helper for diff that checks whether the shape of the tensor to prepend or append
// is compatible with that of input
if (other.has_value()) {
int64_t wrapped_dim = maybe_wrap_dim(dim, self.dim(), false);

TORCH_CHECK(
other.value().dim() == self.dim(),
"diff expects prepend or append to be the same dimension as input");

for (int i = 0; i < other.value().dim(); i++) {
TORCH_CHECK(
other.value().size(i) == self.size(i) || i == wrapped_dim,
"diff expects the shape of tensor to prepend or append to match that of"
" input except along the differencing dimension;"
" input.size(", i, ") = ", self.size(i), ", but got"
" tensor.size(", i, ") = ", other.value().size(i));
}
}
}

static inline void diff_check(const Tensor& self, int64_t n, int64_t dim, const c10::optional<Tensor>&prepend, const c10::optional<Tensor>& append) {
// Helper for diff that checks whether its parameters are valid
TORCH_CHECK(
n == 1,
"diff only supports n = 1 currently. Please file an issue at"
" https://github.com/pytorch/pytorch/issues/new?assignees=&labels=&template=feature-request.md"
" if your use case requires supporting higher-order differences");

TORCH_CHECK(
self.dim() >= 1,
"diff expects input to be at least one-dimensional");

diff_check_compatible_shape(self, prepend, dim);
diff_check_compatible_shape(self, append, dim);
}

static inline Tensor diff_helper(const Tensor& self, int64_t n, int64_t dim) {
auto out_len = self.size(dim) - 1;
if (self.dtype() == at::kBool) {
return at::logical_xor(at::narrow(self, dim, 1, out_len), at::narrow(self, dim, 0, out_len));
}
return at::narrow(self, dim, 1, out_len) - at::narrow(self, dim, 0, out_len);
}

Tensor diff(const Tensor& self, int64_t n, int64_t dim, const c10::optional<Tensor>& prepend, const c10::optional<Tensor>& append) {
diff_check(self, n, dim, prepend, append);
if (!prepend.has_value() && !append.has_value()) {
return diff_helper(self, n, dim);
} else {
auto a = prepend_append_on_dim(self, prepend, append, dim);
return diff_helper(a, n, dim);
}
}

static inline Tensor& diff_out_helper(const Tensor& self, int64_t n, int64_t dim, Tensor& result) {
auto out_len = self.size(dim) - 1;
if (self.dtype() == at::kBool) {
return at::logical_xor_out(result, at::narrow(self, dim, 1, out_len), at::narrow(self, dim, 0, out_len));
}
return at::sub_out(result, at::narrow(self, dim, 1, out_len), at::narrow(self, dim, 0, out_len));
}

Tensor& diff_out(const Tensor& self, int64_t n, int64_t dim, const c10::optional<Tensor>& prepend, const c10::optional<Tensor>& append, Tensor& result) {
diff_check(self, n, dim, prepend, append);
if (!prepend.has_value() && !append.has_value()) {
return diff_out_helper(self, n, dim, result);
} else {
auto a = prepend_append_on_dim(self, prepend, append, dim);
return diff_out_helper(a, n, dim, result);
}
}

/ 8000 / ALL REDUCE #################################################################

Expand Down
10 changes: 10 additions & 0 deletions aten/src/ATen/native/native_functions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1365,6 +1365,16 @@
- func: fill_diagonal_(Tensor(a!) self, Scalar fill_value, bool wrap=False) -> Tensor(a!)
variants: method

- func: diff(Tensor self, int n=1, int dim=-1, Tensor? prepend=None, Tensor? append=None) -> Tensor
Copy link
Collaborator
@mruberry mruberry Jan 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @rgommers for a possible issue with NumPy's signature

np.diff has both prepend and append default to "no value", which is represented by:

https://github.com/numpy/numpy/blob/1de46fe2feee9d3c500a83331ac9b75af5aef947/numpy/_globals.py#L57

The comment indicates it's intended to be used with "deprecated keywords." However, it appears these defaults come from the PR adding the kwargs: numpy/numpy#8206. @rgommers, seems like the NumPy signature would ideally have prepend=None and append=None?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That comment is a little misleading, or at least incomplete. _NoValue is mainly used when keywords are added to a function, and the function forwards its keywords somewhere. See for example np.sum which forwards to the .sum method of its input (which can be a torch.Tensor). Adding keywords to the numpy function that are not yet present in all other/downstream libraries implementing the same functionality would then break previously working code. I'll go fix the code comment.

Wherever you see _NoValue, just read None.

The PR gives this example that would not work with prepend=None for object arrays:

>>> data = np.array([2, 3], dtype=object)
>>> np.diff(data, prepend=None)
array([None, 1], dtype=object)  # expected
array([1], dtype=object)  # actual

A little far-fetched, but unfortunately this works:

>>> np.array(None)                                                          
array(None, dtype=object)

Object arrays are very annoying.

variants: function, method
dispatch:
Math: diff

- func: diff.out(Tensor self, int n=1, int dim=-1, Tensor? prepend=None, Tensor? append=None, *, Tensor(a!) out) -> Tensor(a!)
variants: function
dispatch:
Math: diff_out

- func: div.Tensor(Tensor self, Tensor other) -> Tensor
variants: function, method
dispatch:
Expand Down
1 change: 1 addition & 0 deletions docs/source/tensors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ view of a storage and defines numeric operations on it.
.. automethod:: fill_diagonal_
.. automethod:: fmax
.. automethod:: fmin
.. automethod:: diff
.. automethod:: digamma
.. automethod:: digamma_
.. automethod:: dim
Expand Down
1 change: 1 addition & 0 deletions docs/source/torch.rst
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,7 @@ Other Operations
diag_embed
diagflat
diagonal
diff
einsum
flatten
flip
Expand Down
83 changes: 82 additions & 1 deletion test/test_torch.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
do_test_dtypes, IS_SANDCASTLE, IS_FBCODE, IS_REMOTE_GPU, load_tests, slowTest,
skipCUDAMemoryLeakCheckIf, BytesIOContext,
skipIfRocm, skipIfNoSciPy, TemporaryFileName, TemporaryDirectoryName,
wrapDeterministicFlagAPITest, DeterministicGuard)
wrapDeterministicFlagAPITest, DeterministicGuard, make_tensor)
from multiprocessing.reduction import ForkingPickler
from torch.testing._internal.common_device_type import (
instantiate_device_type_tests,
Expand Down Expand Up @@ -4077,6 +4077,87 @@ def logcumsumexp(a, axis):
'expected scalar_type Double but found Float'):
torch.logcumsumexp(b, axis, out=inplace_out)

def _test_diff_numpy(self, t, dims=None):
# Helper for test_diff to compare with NumPy reference implementation
def to_np(t):
if t.dtype == torch.bfloat16:
return t.to(dtype=torch.float, device="cpu").numpy()
else:
return t.cpu().numpy()

for dim in dims if dims else range(t.dim()):
prepend = t.narrow(dim, 0, 1)
append = t.narrow(dim, 0, 1)
np_t = to_np(t)

# test when prepend and append's size along dim is 1
actual = torch.diff(t, dim=dim, prepend=prepend, append=append)
expected = torch.from_numpy(np.diff(np_t, axis=dim, prepend=to_np(prepend), append=to_np(append)))
self.assertEqual(actual, expected.to(t.dtype))

# test when prepend and append's size along dim != 1
actual = torch.diff(t, dim=dim, prepend=t, append=t)
expected = torch.from_numpy(np.diff(np_t, axis=dim, prepend=np_t, append=np_t))
self.assertEqual(actual, expected.to(t.dtype))

# All tensors appear contiguous on XLA
@onlyOnCPUAndCUDA
@dtypes(*torch.testing.get_all_dtypes())
def test_diff_noncontig(self, device, dtype):
shapes = (
(1,),
(1, 5),
(3, 5),
(1, 5, 1),
(2, 3, 5))

for shape in shapes:
contig = make_tensor(shape, device, dtype, low=-9, high=9)

non_contig = torch.empty(shape + (2, 2), device=device, dtype=dtype)[..., 0]
non_contig = non_contig.select(-1, -1)
non_contig.copy_(contig)
self.assertTrue(not non_contig.is_contiguous() or shape == (1,))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool testing of both contiguous and discontiguous use cases.


self._test_diff_numpy(non_contig)

# RngNormal not implemented for type f16 for XLA
@dtypes(*torch.testing.get_all_dtypes(include_half=False))
@dtypesIfCPU(*torch.testing.get_all_dtypes())
@dtypesIfCUDA(*torch.testing.get_all_dtypes())
def test_diff(self, device, dtype):
shapes = (
(1,),
(1, 5),
(3, 5),
(1, 5, 1),
(2, 3, 5))

for shape in shapes:
contig = make_tensor(shape, device, dtype, low=-9, high=9)
self._test_diff_numpy(contig)

t = torch.ones(2, 3)

with self.assertRaisesRegex(
RuntimeError, 'diff expects prepend or append to be the same dimension as input'):
invalid_prepend = torch.tensor([1, 2, 3], device=device, dtype=dtype)
t.diff(dim=0, prepend=invalid_prepend)

with self.assertRaisesRegex(
RuntimeError, 'diff expects the shape of tensor to prepend or append to match that of input'):
invalid_prepend = torch.tensor([[0, 1]], device=device, dtype=dtype)
t.diff(dim=0, prepend=invalid_prepend)

with self.assertRaisesRegex(
RuntimeError, 'diff only supports n = 1 currently'):
torch.diff(t, n=2)

with self.assertRaisesRegex(
RuntimeError, 'diff expects input to be at least one-dimensional'):
scalar = torch.tensor(2, device=device, dtype=dtype)
torch.diff(scalar)

def _test_large_cum_fn_helper(self, x, fn):
x_cpu = x.cpu().float()
expected = fn(x_cpu)
Expand Down
7 changes: 7 additions & 0 deletions torch/_tensor_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -1158,6 +1158,13 @@ def add_docstr_all(method, docstr):
In-place version of :meth:`~Tensor.floor_divide`
""")

add_docstr_all('diff',
r"""
diff(n=1, dim=-1, prepend=None, append=None) -> Tensor

See :func:`torch.diff`
""")

add_docstr_all('digamma',
r"""
digamma() -> Tensor
Expand Down
39 changes: 39 additions & 0 deletions torch/_torch_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2638,6 +2638,45 @@ def merge_dicts(*dicts):
[ 1.0500, 0.7336, -0.3836, -1.1015]]])
""".format(**common_args))

add_docstr(torch.diff, r"""
diff(input, n=1, dim=-1, prepend=None, append=None) -> Tensor

Computes the n-th forward difference along the given dimension.

The first-order differences are given by `out[i] = input[i + 1] - input[i]`. Higher-order
differences are calculated by using :func:`torch.diff` recursively.

.. note:: Only `n = 1` is currently supported

Args:
input (Tensor): the tensor to compute the differences on
n (int, optional): the number of times to recursively compute the difference
dim (int, optional): the dimension to compute the difference along.
Default is the last dimension.
prepend, append (Tensor, optional): values to prepend or append to
:attr:`input` along :attr:`dim` before computing the difference.
Their dimensions must be equivalent to that of input, and their shapes
must match input's shape except on :attr:`dim`.

Keyword args:
{out}

Example::

>>> a = torch.tensor([1, 3, 2])
>>> torch.diff(a)
tensor([ 2, -1])
>>> b = torch.tensor([4, 5])
>>> torch.diff(a, append=b)
tensor([ 2, -1, 2, 1])
>>> c = torch.tensor([[1, 2, 3], [3, 4, 5]])
>>> torch.diff(c, dim=0)
tensor([[2, 2, 2]])
>>> torch.diff(c, dim=1)
tensor([[1, 1],
[1, 1]])
""".format(**common_args))

add_docstr(torch.digamma, r"""
digamma(input, *, out=None) -> Tensor

Expand Down
1 change: 1 addition & 0 deletions torch/overrides.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ def get_testing_overrides() -> Dict[Callable, Callable]:
torch.diag: lambda input, diagonal=0, out=None: -1,
torch.diag_embed: lambda input, diagonal=0, out=None: -1,
torch.diagflat: lambda input, offset=0: -1,
torch.diff: lambda input, n=1, dim=-1, prepend=None, append=None, out=None: -1,
torch.diagonal: lambda input, offset=0, dim1=0, dim2=1: -1,
torch.digamma: lambda input, out=None: -1,
torch.dist: lambda input, other, p=2: -1,
Expand Down
35 changes: 35 additions & 0 deletions torch/testing/_internal/common_methods_invocations.py
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,33 @@ def sample_inputs_gather(op_info, device, dtype, requires_grad):
0, torch.tensor(0, dtype=torch.int64, device=device))),
)

def sample_inputs_diff(op_info, device, dtype, requires_grad):
test_cases = (
((1,), 0, None, None),
((S,), 0, None, None),
((S, 1), 0, None, None),
((S, 1), 1, None, None),
((S, S), 0, None, None),
((S, S), 1, None, None),
((S, S), 0, (1, S), (2, S)),
((S, S), 0, None, (2, S)),
((S, S, S), 1, None, None),
((S, S, S), 1, (S, 1, S), (S, 1, S)),)

sample_inputs = []
for size, dim, size_prepend, size_append in test_cases:
args = (make_tensor(size, device, dtype,
low=None, high=None,
requires_grad=requires_grad), 1, dim,
make_tensor(size_prepend, device, dtype,
low=None, high=None,
requires_grad=requires_grad) if size_prepend else None,
make_tensor(size_append, device, dtype,
low=None, high=None,
requires_grad=requires_grad) if size_append else None)
sample_inputs += [SampleInput(args)]

return tuple(sample_inputs)

def sample_inputs_index_select(op_info, device, dtype, requires_grad):
return (SampleInput((make_tensor((S, S, S), device, dtype,
Expand Down Expand Up @@ -1179,6 +1206,11 @@ def sample_inputs_fliplr_flipud(op_info, device, dtype, requires_grad):
SkipInfo('TestCommon', 'test_variant_consistency_jit',
device_type='cuda', dtypes=[torch.float16]),
)),
OpInfo('diff',
op=torch.diff,
dtypes=all_types_and_complex_and(torch.bool, torch.float16, torch.bfloat16),
sample_inputs_func=sample_inputs_diff,
test_inplace_grad=False),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for this PR, but the test for inplace grads should be skipped automatically if the op doesn't have an inplace variant.

UnaryUfuncInfo('exp',
ref=np_unary_ufunc_integer_promotion_wrapper(np.exp),
dtypes=all_types_and_complex_and(torch.bool, torch.half),
Expand Down Expand Up @@ -2005,6 +2037,9 @@ def __len__(self):
def ident(x):
return x

# Do NOT add to this list. Method tests are being DEPRECATED and replaced by OpInfos.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

# See https://github.com/pytorch/pytorch/wiki/Writing-tests-in-PyTorch-1.8
#
# (
# method name,
# input size/constructing fn,
Expand Down
0