Skip to content
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

Added computing matrix condition numbers (linalg.cond) #45832

Closed
wants to merge 53 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
7ce8a20
Added linalg_cond
IvanYashchuk Oct 5, 2020
dcff9ef
Updated the implementation.
IvanYashchuk Oct 5, 2020
d179001
Merge remote-tracking branch 'upstream/master' into linalg_cond
IvanYashchuk Nov 3, 2020
c88ccc3
Updated non-invertible case implementation
IvanYashchuk Nov 3, 2020
1e49003
Updated docs
IvanYashchuk Nov 3, 2020
db2de7c
Added entry to common_methods_invocations.py
IvanYashchuk Nov 3, 2020
843c07f
Added overrides.py entry
IvanYashchuk Nov 3, 2020
fd82325
flake8
IvanYashchuk Nov 3, 2020
ed1b7de
Implement a trick for replacing FLT_MAX or DBL_MAX to INFINITY
IvanYashchuk Nov 3, 2020
7be03c7
Added out= variant
IvanYashchuk Nov 3, 2020
f36e732
Updated implementation and tests. Complex input is now supported.
IvanYashchuk Nov 3, 2020
de025dc
Fix typo
IvanYashchuk Nov 4, 2020
43f52bc
Trying to fix initialization of 'dim'
IvanYashchuk Nov 4, 2020
39cc93f
Merge remote-tracking branch 'upstream/master' into linalg_cond
IvanYashchuk Nov 4, 2020
6bded45
Trying to fix initialization of 'dim'
IvanYashchuk Nov 4, 2020
7a4dac6
Small changes in the implementation structure
IvanYashchuk Nov 9, 2020
5705429
Use the explicit shape for the result instead of dummy norm computations
IvanYashchuk Nov 9, 2020
0d9d49f
Fixed conversion FLT_MAX / DBL_MAX -> INFINITY
IvanYashchuk Nov 10, 2020
ae8878f
Added a warning for batched singular input; slightly refactored the t…
IvanYashchuk Nov 10, 2020
8dc29e0
Updated documentation
IvanYashchuk Nov 10, 2020
02f43eb
Added non-square input test cases
IvanYashchuk Nov 10, 2020
36385d9
Updated norms description
IvanYashchuk Nov 10, 2020
abf8a29
Merge remote-tracking branch 'upstream/master' into linalg_cond
IvanYashchuk Nov 10, 2020
0b214da
Merge remote-tracking branch 'upstream/master' into linalg_cond
IvanYashchuk Nov 12, 2020
49a1577
Raise error for batched input with at least one non-invertible matrix
IvanYashchuk Nov 12, 2020
8b0cfcd
Merge remote-tracking branch 'upstream/master' into linalg_cond
IvanYashchuk Nov 17, 2020
655a03d
Updated documentations
IvanYashchuk Nov 17, 2020
27142ba
Use enums to dispatch on norm type
IvanYashchuk Nov 17, 2020
195463f
Add missing imports
IvanYashchuk Nov 17, 2020
ea95b08
Enums are redundant here, use only c10::variant for norm dispatch
IvanYashchuk Nov 17, 2020
fbe025a
Simplify error message for batched non-invertibel case
IvanYashchuk Nov 17, 2020
0056ca9
Use at::full instead of empty + fill_
IvanYashchuk Nov 17, 2020
47da625
Make error message more specific about the norm types
IvanYashchuk Nov 17, 2020
6eb461a
Removed unneeded conversions to infinity
IvanYashchuk Nov 18, 2020
725cc17
Merge remote-tracking branch 'upstream/master' into linalg_cond
IvanYashchuk Nov 18, 2020
e8b05da
Merge remote-tracking branch 'upstream/master' into linalg_cond
IvanYashchuk Nov 20, 2020
26651b1
Merge remote-tracking branch 'upstream/master' into linalg_cond
IvanYashchuk Nov 29, 2020
bacdc65
Fix typo norm -> cond
IvanYashchuk Nov 29, 2020
c7b52de
Rename ord -> p
IvanYashchuk Nov 29, 2020
873f93e
Fix typo
IvanYashchuk Nov 29, 2020
5813333
Remove 2-norm mentions in the docs, leaving just ratio of singular va…
IvanYashchuk Nov 29, 2020
c082881
Use IntArrayRef for constructing result_shape
IvanYashchuk Nov 29, 2020
c1e4bd9
Allow batched input with zero dimensions; singular values are sorted …
IvanYashchuk Nov 29, 2020
f5296cb
Added empty batch size test inputs
IvanYashchuk Nov 29, 2020
b452379
Revert changes to common_methods_invocations.py
IvanYashchuk Nov 29, 2020
b15fedd
Return 0 for 0x0 matrices
IvanYashchuk Nov 30, 2020
73046d1
Added tests case for 0x0 matrices
IvanYashchuk Nov 30, 2020
5ed8a77
Updated note for p = 2, -2 to include reference to svd
IvanYashchuk Nov 30, 2020
e407098
Added checks for norm type
IvanYashchuk Nov 30, 2020
9008c10
Updated documentation
IvanYashchuk Dec 2, 2020
f615332
Changed the note on non-invertible input
IvanYashchuk Dec 2, 2020
994cca6
flake8
IvanYashchuk Dec 3, 2020
898f855
Merge remote-tracking branch 'upstream/master' into linalg_cond
IvanYashchuk Dec 3, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
156 changes: 156 additions & 0 deletions aten/src/ATen/native/LinearAlgebra.cpp
Expand Up @@ -18,6 +18,8 @@
#include <limits>
#include <ATen/NamedTensorUtils.h>

#include <c10/util/variant.h>

namespace at {
namespace native {

Expand Down Expand Up @@ -1729,6 +1731,160 @@ Tensor& linalg_norm_out(Tensor& result, const Tensor& self, std::string ord, opt
return linalg_norm_out_impl(result, self, c10::nullopt, ord, opt_dim, keepdim, opt_dtype);
}

Tensor _linalg_cond_exception_helper(const Tensor& self) {
// For batched input if at least one matrix in the batch is not invertible,
// we can't get the result for all other (possibly) invertible matrices in the batch without an explicit for loop.
// This should change when at::inverse works with silent errors
if (self.dim() > 2) {
TORCH_CHECK(false,
"One or more matrices in the batch was not invertible! "
"linalg_cond does not support yet this case.");
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's remove this second line.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Follow-up question, if the intent is to turn inf for every value, should this be a warning and not a check?

Maybe something like...

TORCH_WARN("One or more matrices in the batch was not invertible! This will cause the entire result to be filled with infs.");

?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We've decided here #45832 (comment) that since returning inf for every matrix is not correct we should raise an error. The batched case will be properly implemented in the future once it's possible to call at::inverse with silent errors (this should be possible after #48261 is ready, similarly to how it's now possible to call at::solve_out_info without error checks here.

Copy link
Collaborator

Choose a reason for hiding this comment

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

OK; I'm conviced.

}
auto result_shape = IntArrayRef(self.sizes().cbegin(), self.sizes().cend()-2);
Tensor result = at::full(result_shape, INFINITY, self.options());
return result;
}

// This function helps to dispatch norm computations depending on 'ord' of variant type
Tensor _linalg_cond_helper(const Tensor& self, c10::variant<Scalar, std::string> ord_variant) {
// Ignore errors if not invertible, result is INFINITY in this case
// Currently checking for error in at::inverse causes cross-device data movement
// For batched input if at least one matrix in the batch is not invertible,
// then the result for all other (possibly) invertible matrices will be infinity as well
// since there is currently no way to use at::inverse with silent errors
Tensor self_inverse;
try {
self_inverse = at::inverse(self);
} catch (const std::exception& e) {
if (strstr(e.what(), "singular")) {
return _linalg_cond_exception_helper(self);
} else {
TORCH_CHECK(false, "linalg_cond got an unexpected error:\n", e.what());
}
}
std::array<int64_t, 2> dim_arr = {-2, -1};
optional<IntArrayRef> dim = IntArrayRef(dim_arr);

return c10::visit([&](auto&& ord) {
Tensor norm_self = at::linalg_norm(self, ord, dim);
Tensor norm_inverse = at::linalg_norm(self_inverse, ord, dim);
Tensor result = norm_self * norm_inverse;
return result;
}, ord_variant);
}

// Return zero for each matrix in the batch
Tensor _linalg_cond_empty_matrix(const Tensor& self, c10::ScalarType dtype) {
auto result_shape = IntArrayRef(self.sizes().cbegin(), self.sizes().cend()-2);
return at::zeros(result_shape, self.options().dtype(dtype));
}

void _linalg_cond_check_ord(c10::variant<Scalar, std::string> ord_variant) {
if (ord_variant.index() == 0) {
Scalar* ord = c10::get_if<Scalar>(&ord_variant);
double abs_ord = std::abs(ord->toDouble());
TORCH_CHECK(abs_ord == 2.0 || abs_ord == 1.0 || abs_ord == INFINITY,
"linalg_cond got an invalid norm type: ", ord->toDouble());
} else if (ord_variant.index() == 1) {
std::string* ord = c10::get_if<std::string>(&ord_variant);
TORCH_CHECK(*ord == "fro" || *ord == "nuc",
"linalg_cond got an invalid norm type: ", *ord);
} else {
TORCH_CHECK(false,
"linalg_cond: something went wrong while checking the norm type");
}
}

// Numerical or None norms
Tensor linalg_cond(const Tensor& self, optional<Scalar> opt_ord) {
TORCH_CHECK(self.dim() >= 2, "linalg_cond only supports matrices or batches of matrices, but got a tensor with ",
self.dim(), " dimensions.");

// The default case is using 2-norm
Scalar ord = opt_ord.has_value() ? opt_ord.value() : 2;

c10::variant<Scalar, std::string> ord_variant = ord;
_linalg_cond_check_ord(ord_variant);

// NumPy doesn't define the condition number for 0x0 matrices, we return 0.0 for such input
if (self.numel() == 0) {
auto real_dtype = toValueType(typeMetaToScalarType(self.dtype()));
auto expected_dtype = std::abs(ord.toDouble()) == 2.0 ? real_dtype : self.scalar_type();
return _linalg_cond_empty_matrix(self, expected_dtype);
}

// If ord == None or ord == ±2
if (std::abs(ord.toDouble()) == 2.0) {
auto singular_values = std::get<1>(at::svd(self));
// singular values are sorted in descending order
auto s_max = at::narrow(singular_values, /*dim=*/-1, /*start=*/0, /*length=*/1);
auto s_min = at::narrow(singular_values, /*dim=*/-1, /*start=*/-1, /*length=*/1);
Tensor result;
if (ord.toDouble() == -2.0) {
result = s_min / s_max;
} else {
result = s_max / s_min;
}
return result;
}

// ord == ±1 ord == ±inf
// since at::inverse is used in the implementation, self has to be a tensor consisting of square matrices
// the same check as squareCheckInputs(self) but with a slightly more informative error message
TORCH_CHECK(self.size(-1) == self.size(-2),
IvanYashchuk marked this conversation as resolved.
Show resolved Hide resolved
"linalg_cond with ±1 or ±inf norm types only supports square matrices or batches of square matrices "
"but got ", self.size(-1), " by ", self.size(-2), " matrices");

return _linalg_cond_helper(self, ord_variant);
}

Tensor& linalg_cond_out(Tensor& result, const Tensor& self, optional<Scalar> opt_ord) {
// If ord == None or ord == ±2 then SVD is used to compute the condition number
// the result is always real-valued, for other cases it is complex-valued for the complex-valued input.
ScalarType real_dtype = toValueType(typeMetaToScalarType(self.dtype()));
IvanYashchuk marked this conversation as resolved.
Show resolved Hide resolved
Scalar ord = opt_ord.has_value() ? opt_ord.value() : 2;
auto expected_dtype = std::abs(ord.toDouble()) == 2.0 ? real_dtype : self.scalar_type();
Copy link
Collaborator

@mruberry mruberry Nov 26, 2020

Choose a reason for hiding this comment

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

Technically instead of casting ord to double unconditionally we should first check that it's not complex and not an integer that will overflow, but that seems onerous.

Not for this PR, of course, but I wonder if the Scalar type could/does do that for us automatically? It might catch some errors with the handling of complex scalars.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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


TORCH_CHECK(result.scalar_type() == expected_dtype,
"result dtype ", result.scalar_type(), " does not match the expected dtype ", expected_dtype);

Tensor result_tmp = at::linalg_cond(self, opt_ord);
at::native::resize_output(result, result_tmp.sizes());
result.copy_(result_tmp);
mruberry marked this conversation as resolved.
Show resolved Hide resolved
return result;
}

// Frobenius or nuclear norms
Tensor linalg_cond(const Tensor& self, std::string ord) {
// the same checks as squareCheckInputs(self) but with a slightly more informative error message
TORCH_CHECK(self.dim() >= 2, "linalg_cond only supports matrices or batches of matrices, but got a tensor with ",
self.dim(), " dimensions.");
TORCH_CHECK(self.size(-1) == self.size(-2),
"linalg_cond with frobenius or nuclear norm types only supports square matrices or batches of square matrices "
"but got ", self.size(-1), " by ", self.size(-2), " matrices");

c10::variant<Scalar, std::string> ord_variant = ord;
_linalg_cond_check_ord(ord_variant);

// NumPy doesn't define the condition number for 0x0 matrices, we return 0.0 for such input
if (self.numel() == 0) {
return _linalg_cond_empty_matrix(self, self.scalar_type());
}

return _linalg_cond_helper(self, ord_variant);
}

// TODO: implement _out variant avoiding copy and using already allocated storage directly
Tensor& linalg_cond_out(Tensor& result, const Tensor& self, std::string ord) {
IvanYashchuk marked this conversation as resolved.
Show resolved Hide resolved
TORCH_CHECK(result.scalar_type() == self.scalar_type(),
"result dtype ", result.scalar_type(), " does not match the expected dtype ", self.scalar_type());

Tensor result_tmp = at::linalg_cond(self, ord);
at::native::resize_output(result, result_tmp.sizes());
result.copy_(result_tmp);
return result;
}

Tensor linalg_tensorinv(const Tensor& self, int64_t ind) {
/*
The idea is to reduce the problem to 2D square matrix inversion.
Expand Down
24 changes: 24 additions & 0 deletions aten/src/ATen/native/native_functions.yaml
Expand Up @@ -9479,6 +9479,30 @@
python_module: linalg
variants: function

- func: linalg_cond(Tensor self, Scalar? p=None) -> Tensor
python_module: linalg
variants: function
dispatch:
Math: linalg_cond

- func: linalg_cond.out(Tensor self, Scalar? p=None, *, Tensor(a!) out) -> Tensor(a!)
python_module: linalg
variants: function
dispatch:
Math: linalg_cond_out

- func: linalg_cond.p_str(Tensor self, str p) -> Tensor
python_module: linalg
variants: function
dispatch:
Math: linalg_cond

- func: linalg_cond.p_str_out(Tensor self, str p, *, Tensor(a!) out) -> Tensor(a!)
python_module: linalg
variants: function
dispatch:
Math: linalg_cond_out

- func: linalg_tensorinv(Tensor self, int ind=2) -> Tensor
python_module: linalg
variants: function
Expand Down
2 changes: 1 addition & 1 deletion test/test_jit.py
Expand Up @@ -15732,7 +15732,7 @@ def fn(*inputs, **kwargs):
check_types=check_types)

# alias annotation testing
if not is_magic_method and test_name not in EXCLUDE_SCRIPT:
if not is_magic_method and test_name not in EXCLUDE_SCRIPT and not exclude_tensor_method(name, test_name):
check_alias_annotation(name, (self_variable,) + args_variable, kwargs_variable)

check(name)
Expand Down
116 changes: 116 additions & 0 deletions test/test_linalg.py
Expand Up @@ -945,6 +945,122 @@ def run_test_case(input, p, dim, keepdim):
for ord in ord_settings:
run_test_case(input, ord, dim, keepdim)

@skipCPUIfNoLapack
@skipCUDAIfNoMagma
@dtypes(torch.float32, torch.float64, torch.complex64, torch.complex128)
@precisionOverride({torch.float32: 1e-3})
mruberry marked this conversation as resolved.
Show resolved Hide resolved
def test_cond(self, device, dtype):
def run_test_case(input, p):
result = torch.linalg.cond(input, p)
result_numpy = np.linalg.cond(input.cpu().numpy(), p)
self.assertEqual(result, result_numpy, rtol=1e-2, atol=self.precision)

# test out= variant
out = torch.empty_like(result)
ans = torch.linalg.cond(input, p, out=out)
self.assertEqual(ans, out)
self.assertEqual(ans, result)

norm_types = [1, -1, 2, -2, inf, -inf, 'fro', 'nuc', None]
input_sizes = [(32, 32), (2, 3, 3, 3)]
for input_size in input_sizes:
input = torch.randn(*input_size, dtype=dtype, device=device)
for p in norm_types:
# frobenius norm not supported for complex tensors
if dtype.is_complex and p == 'fro':
with self.assertRaisesRegex(RuntimeError, "frobenius norm not supported for complex tensors"):
torch.linalg.cond(input, p)
continue
IvanYashchuk marked this conversation as resolved.
Show resolved Hide resolved
run_test_case(input, p)

# test empty batch sizes
input_sizes = [(0, 3, 3), (0, 2, 5, 5)]
for input_size in input_sizes:
input = torch.randn(*input_size, dtype=dtype, device=device)
for p in norm_types:
run_test_case(input, p)

# test non-square input
input_sizes = [(16, 32), (32, 16), (2, 3, 5, 3), (2, 3, 3, 5)]
IvanYashchuk marked this conversation as resolved.
Show resolved Hide resolved
for input_size in input_sizes:
input = torch.randn(*input_size, dtype=dtype, device=device)
for p in [2, -2, None]:
run_test_case(input, p)

# test for singular input
a = torch.eye(3, dtype=dtype, device=device)
a[-1, -1] = 0 # make 'a' singular
for p in norm_types:
run_test_case(a, p)

# test for 0x0 matrices. NumPy doesn't work for such input, we return 0
input_sizes = [(0, 0), (2, 5, 0, 0)]
for input_size in input_sizes:
input = torch.randn(*input_size, dtype=dtype, device=device)
for p in ['fro', 2]:
expected_dtype = a.real.dtype if dtype.is_complex and p == 2 else dtype
expected = torch.zeros(input_size[:-2], dtype=expected_dtype, device=device)
actual = torch.linalg.cond(input, p)
self.assertEqual(actual, expected)

@skipCPUIfNoLapack
@skipCUDAIfNoMagma
@dtypes(torch.float32, torch.float64, torch.complex64, torch.complex128)
@precisionOverride({torch.float32: 1e-3})
def test_cond_errors_and_warnings(self, device, dtype):
norm_types = [1, -1, 2, -2, inf, -inf, 'fro', 'nuc', None]

# cond expects the input to be at least 2-dimensional
a = torch.ones(3, dtype=dtype, device=device)
for p in norm_types:
with self.assertRaisesRegex(RuntimeError, r'supports matrices or batches of matrices'):
torch.linalg.cond(a, p)

# for some norm types cond expects the input to be square
a = torch.ones(3, 2, dtype=dtype, device=device)
norm_types = [1, -1, inf, -inf, 'fro', 'nuc']
for p in norm_types:
with self.assertRaisesRegex(RuntimeError, r'supports square matrices or batches of square matrices'):
torch.linalg.cond(a, p)

# if non-empty out tensor with wrong shape is passed a warning is given
a = torch.ones((2, 2), dtype=dtype, device=device)
for p in ['fro', 2]:
real_dtype = a.real.dtype if dtype.is_complex and p == 2 else dtype
out = torch.empty(a.shape, dtype=real_dtype, device=device)
with warnings.catch_warnings(record=True) as w:
# Trigger warning
torch.linalg.cond(a, p, out=out)
# Check warning occurs
self.assertEqual(len(w), 1)
self.assertTrue("An output with one or more elements was resized" in str(w[-1].message))

# dtypes should match
out = torch.empty_like(a).to(torch.int)
for p in ['fro', 2]:
with self.assertRaisesRegex(RuntimeError, "result dtype Int does not match"):
torch.linalg.cond(a, p, out=out)

# for batched input if at least one matrix in the batch is not invertible,
# we can't get the result for all other (possibly) invertible matrices in the batch without an explicit for loop.
# this should change when at::inverse works with silent errors
# NumPy works fine in this case because it's possible to silence the error and get the inverse matrix results
# possibly filled with NANs
batch_dim = 3
a = torch.eye(3, 3, dtype=dtype, device=device)
a = a.reshape((1, 3, 3))
a = a.repeat(batch_dim, 1, 1)
a[0, -1, -1] = 0 # now a[0] is singular
for p in [1, -1, inf, -inf, 'fro', 'nuc']:
with self.assertRaisesRegex(RuntimeError, "linalg_cond does not support yet"):
mruberry marked this conversation as resolved.
Show resolved Hide resolved
torch.linalg.cond(a, p)

# check invalid norm type
a = torch.ones(3, 3, dtype=dtype, device=device)
for p in ['wrong_norm', 5]:
with self.assertRaisesRegex(RuntimeError, f"linalg_cond got an invalid norm type: {p}"):
torch.linalg.cond(a, p)

# Test autograd and jit functionality for linalg functions.
# TODO: Once support for linalg functions is added to method_tests in common_methods_invocations.py,
# the `test_cases` entries below should be moved there. These entries are in a similar format,
Expand Down