Improve SVD consistency and small array handling#6616
Improve SVD consistency and small array handling#6616TomAugspurger merged 4 commits intodask:masterfrom
Conversation
dc1306d to
aa42a8d
Compare
aa42a8d to
078d4af
Compare
dask/array/linalg.py
Outdated
| # Short-and-fat case | ||
| # Single-chunk case | ||
| if nb[0] == nb[1] == 1: | ||
| u, s, v = map(asarray, np.linalg.svd(np.asarray(a), full_matrices=False)) |
There was a problem hiding this comment.
This will trigger computation. I think that instead we probably want to keep things lazy, and instead return three single-chunked dask arrays. Thoughts?
There was a problem hiding this comment.
I gave it a shot with 2d7194f#diff-d190ab15173165ce6615a5df4e3067ebR862-R868. That look ok?
TomAugspurger
left a comment
There was a problem hiding this comment.
Thanks, just a couple comments / questions.
dask/array/linalg.py
Outdated
| coerce_signs : bool | ||
| Whether or not to apply sign correction to singular vectors in | ||
| order to maintain deterministic results, by default True. |
There was a problem hiding this comment.
This should go under the Parameters section.
There was a problem hiding this comment.
dask/array/linalg.py
Outdated
| # Short-and-fat case | ||
| # Single-chunk case | ||
| if nb[0] == nb[1] == 1: | ||
| u, s, v = map(asarray, np.linalg.svd(np.asarray(a), full_matrices=False)) |
There was a problem hiding this comment.
Is the np.asarray here to avoid dispatching back to dask's svd?
We might instead consider
| u, s, v = map(asarray, np.linalg.svd(np.asarray(a), full_matrices=False)) | |
| a = a.compute() | |
| u, s, v = map(asarray, np.linalg.svd(a, full_matrices=False)) |
For the case where we have a Dask array backed by a cupy array on the GPU, this would (I think) keep things on the GPU the whole time.
There was a problem hiding this comment.
I had originally just done that to avoid dispatch to the same function but tried to make it all lazy instead (as in @mrocklin's comment above).
|
Thanks @mrocklin / @TomAugspurger, a few other notes:
|
| if nb[0] == nb[1] == 1: | ||
| m, n = a.shape | ||
| k = min(a.shape) | ||
| mu, ms, mv = np.linalg.svd(np.ones((1, 1), dtype=a.dtype)) |
There was a problem hiding this comment.
We might still have the issue of a being on a GPU here. IIUC, this is to determine the appropriate dtypes after the SVD for the meta later on. Hopefully this suffices
| mu, ms, mv = np.linalg.svd(np.ones((1, 1), dtype=a.dtype)) | |
| mu, ms, mv = np.linalg.svd(a._meta) |
That's a size-0 array, but at least for ndarrays np.linalg.svd seems to be OK with it.
There was a problem hiding this comment.
Ah interesting, that does clear all the tests (change committed). Good to know.
What's the concern with a being on a GPU? Does anything in that code look like it would pull it back onto the CPU?
There was a problem hiding this comment.
Actually that doesn't work on Python 3.6 apparently (https://github.com/dask/dask/pull/6616/checks?check_run_id=1099031906):
dask\array\linalg.py:864: in svd
mu, ms, mv = np.linalg.svd(a._meta)
C:\Miniconda3\envs\test-environment\lib\site-packages\numpy\linalg\linalg.py:1542: in svd
_assertNoEmpty2d(a)
On linux https://travis-ci.org/github/dask/dask/jobs/726090041:
E numpy.linalg.linalg.LinAlgError: Arrays cannot be empty
Any objections to me changing it back? I'm not sure how to work around that otherwise.
There was a problem hiding this comment.
Hey @TomAugspurger I reverted it just to give CI time to run in case you're OK with it as it was. Let me know if there's something else that can be done to avoid any issues on a GPU (I don't understand what they are or I'd happily experiment with other solutions).
There was a problem hiding this comment.
Yeah, I'm fine with changing it back for now.
I'm not 100% sure what would happen if a were on the GPU. u, s, and v will be fine, since they're returning CuPy arrays. But I don't know what from_delayed does when it gets GPU memory and meta is a NumPy array rather than a CuPy array.
cc @jakirkham or @pentschev if you think there's any followup needed here.
There was a problem hiding this comment.
Ah I see, maybe something like this would make more sense?
np.linalg.svd(np.ones_like(a._meta, shape=(1,1)))
There was a problem hiding this comment.
Ah I see, maybe something like this would make more sense?
np.linalg.svd(np.ones_like(a._meta, shape=(1,1)))
Yeah, we should do that, otherwise mu, ms, mv will all be NumPy-arrays, and using them below, e.g. meta=mu, becomes completely irrelevant, as it will default anyway to a NumPy array.
There was a problem hiding this comment.
It should actually use ones_like_safe from
Lines 314 to 323 in 519b62f
There was a problem hiding this comment.
Ok thanks @pentschev. To clarify on this:
using them below, e.g. meta=mu, becomes completely irrelevant, as it will default anyway to a NumPy array.
I was only after the dtype on the mu, ms, mv arrays. I'll submit another PR that just sets the dtype in from_delayed instead of using meta so that the array backend shouldn't matter.
Co-authored-by: Tom Augspurger <TomAugspurger@users.noreply.github.com>
| if nb[0] == nb[1] == 1: | ||
| m, n = a.shape | ||
| k = min(a.shape) | ||
| mu, ms, mv = np.linalg.svd(np.ones((1, 1), dtype=a.dtype)) |
There was a problem hiding this comment.
Yeah, I'm fine with changing it back for now.
I'm not 100% sure what would happen if a were on the GPU. u, s, and v will be fine, since they're returning CuPy arrays. But I don't know what from_delayed does when it gets GPU memory and meta is a NumPy array rather than a CuPy array.
cc @jakirkham or @pentschev if you think there's any followup needed here.
* SVD single chunk bypass and sign coercion
black dask/flake8 daskThis addresses #6612, builds on #6599, and is related to #3576.
For #6612, this calls numpy.linalg.svd for single-chunk arrays which avoids performance issues and result shape inconsistencies that occur when the array has more columns than rows.
This uses the svd_flip function added for #6599 by default and adds a
coerce_signsargument to disable that behavior.For #3576, this ensures that all results are consistent with
np.linalg.svdwhenfull_matrices=False.Note: The code I added here related to the
truncateflag is a bit of a band-aid on top of not knowing what's going on in tsqr. To be clear that code is addressing a corner case that should never happen on real datasets though. It only occurs when an array has column blocks but is still wider than it is tall (e.g. as shown in this example: #3576 (comment)). Let me know if anybody thinks that is worthy of a separate tsqr-specific issue. I'm on the fence about it but leaning towards no, that this is actually more of an SVD-specific problem so having the logic here may not be a bad solution anyways.