Skip to content

Conversation

lucascolley
Copy link
Member

@lucascolley lucascolley commented Jun 13, 2024

@github-actions github-actions bot added scipy._lib CI Items related to the CI tools such as CircleCI, GitHub Actions or Azure array types Items related to array API support and input array validation (see gh-18286) enhancement A new feature or improvement labels Jun 13, 2024
@lucascolley lucascolley removed the CI Items related to the CI tools such as CircleCI, GitHub Actions or Azure label Jun 13, 2024
@lucascolley
Copy link
Member Author

cc @tomwhite @jakirkham

@lucascolley

This comment was marked as outdated.

@lucascolley
Copy link
Member Author

lucascolley commented Aug 8, 2024

@mdhaber FYI there are a few places in _chandrupatla and related infra that used xp.full_like with passing an array to fill_value. As per the spec, fill_value should be a Python scalar. array-api-strict does not catch this, but Dask does, albeit indirectly via a FutureWarning about np.copyto being used along the way.

(I'll fix it here, but just in case you were going on to write something similar before this is merged)

@lucascolley
Copy link
Member Author

@ev-br could you suggest how to fix

# lists/tuples
return type(result)(
xp.asarray(x) if isinstance(x, np.ndarray) else x
for x in result
)

for

TypeError: Array.__new__() missing 2 required positional arguments: 'name' and 'chunks'

?

@ev-br
Copy link
Member

ev-br commented Aug 8, 2024

What is result and is type(result)?

@lucascolley
Copy link
Member Author

What is result and is type(result)?

result is a dask array, type(result) is dask.array.Array

@lucascolley
Copy link
Member Author

_lazywhere is tripped up by Dask's limited boolean indexing capability:

In [19]: da.asarray(0.)[True]
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Cell In[19], line 1
----> 1 da.asarray(0.)[True]

File ~/dev/pixi-dev-scipystack/scipy/.pixi/envs/default/lib/python3.12/site-packages/dask/array/core.py:1975, in Array.__getitem__(self, index)
   1967     index = (index,)
   1969 from dask.array.slicing import (
   1970     normalize_index,
   1971     slice_with_bool_dask_array,
   1972     slice_with_int_dask_array,
   1973 )
-> 1975 index2 = normalize_index(index, self.shape)
   1976 dependencies = {self.name}
   1977 for i in index2:

File ~/dev/pixi-dev-scipystack/scipy/.pixi/envs/default/lib/python3.12/site-packages/dask/array/slicing.py:789, in normalize_index(idx, shape)
    787 idx = idx + (slice(None),) * (len(shape) - n_sliced_dims)
    788 if len([i for i in idx if i is not None]) > len(shape):
--> 789     raise IndexError("Too many indices for array")
    791 none_shape = []
    792 i = 0

IndexError: Too many indices for array

@ev-br
Copy link
Member

ev-br commented Aug 9, 2024

result is a dask array, type(result) is dask.array.Array

Then the fix is to wrap the # lists/tuples stanza in an explicit if isinstance(result, (list, tuple)

@jakirkham
Copy link
Contributor

cc @phofl (for awareness)

@lucascolley
Copy link
Member Author

@mdhaber does the diff in e63b914 look okay to you? I've been experimenting with Dask and a SciPy nightly, and this diff allows e.g. special.rel_entr to stay native to Dask via the generic implementation, so things can be kept parallel.

If so, I think I'd like to merge that separately before this PR.

@lucascolley
Copy link
Member Author

@mdhaber does the diff in e63b914 look okay to you? I've been experimenting with Dask and a SciPy nightly, and this diff allows e.g. special.rel_entr to stay native to Dask via the generic implementation, so things can be kept parallel.

If so, I think I'd like to merge that separately before this PR.

Just to follow up here @mdhaber - that change isn't specific to Dask.

@lucascolley lucascolley force-pushed the dask branch 3 times, most recently from 20dbdf8 to 2ea1c63 Compare September 18, 2024 14:22
@lucascolley
Copy link
Member Author

@ev-br do you have any idea why we are hitting "dask.array has no argsort" errors for some ndimage.{maximum, minimum} tests? As far as I understand, we shouldn't be calling dask.array.argsort anywhere? Are we accidentally calling np.argsort on a Dask array maybe when we should be converting to np first?

@ev-br
Copy link
Member

ev-br commented Sep 27, 2024

Looks like indeed some np.asarray / np.asanyarray calls are missing and internal functions receive mixtures of numpy and dask arrays:

917  ->	def _select(input, labels=None, index=None, find_min=False, find_max=False,
918  	            find_min_positions=False, find_max_positions=False,
919  	            find_median=False):
920  	    """Returns min, max, or both, plus their positions (if requested), and
921  	    median."""
922  	
(Pdb) n
> /home/ev-br/repos/scipy/build-install/lib/python3.11/site-packages/scipy/ndimage/_measurements.py(923)_select()
-> input = np.asanyarray(input)
(Pdb) p input
dask.array<array, shape=(3, 4), dtype=int8, chunksize=(3, 4), chunktype=numpy.ndarray>

...

(Pdb) a
input = array([[5, 4, 2, 5],      # have been converted, but lables and index have not been
       [3, 7, 8, 2],
       [1, 5, 1, 1]], dtype=int8)
labels = dask.array<array, shape=(4,), dtype=float64, chunksize=(4,), chunktype=numpy.ndarray>
index = dask.array<array, shape=(2,), dtype=float64, chunksize=(2,), chunktype=numpy.ndarray>
find_min = False
find_max = False
find_min_positions = False
find_max_positions = True
find_median = False

@lucascolley
Copy link
Member Author

Makes sense. I wonder why this doesn't trip up PyTorch CPU but does for Dask.

@lithomas1
Copy link
Contributor

lithomas1 commented Oct 25, 2024

Planning on helping give this a little nudge.

Using the newly wrapped fft module in array-api-compat, we can get down to 11 failures on fft at least - mainly regarding dtype differences.
(some updates to the tests were required though, I'll try to push something up tomorrow)

I haven't looked at any of the other failures yet.

EDIT 1: Have gotten special to work (other than the issue with suppressing the runtime warnings from dask/numpy)

@lucascolley
Copy link
Member Author

feel free to make a PR to my branch @lithomas1 !

@lithomas1
Copy link
Contributor

I made a draft PR here
lucascolley#14
(I'll add some more comments/insights on why I changed what lines of code later)

Could you pull main on your branch later if you have more time?
(the diff on that PR is quite large since this PR is a bit behind main)

@lucascolley
Copy link
Member Author

thanks @lithomas1 , that definitely helped to bring the number of errors down. Of the remaining errors, some look trivial, others I'm not so sure.

Comment on lines +205 to +208
# TODO: we shouldn't be mutating in-place here unless we make a copy
# dask arrays do not copy before this somehow
#a[i_max] = -xp.inf
a = xp.where(i_max, -xp.asarray(xp.inf, dtype=a.dtype), a)
Copy link
Member Author

Choose a reason for hiding this comment

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

just flagging this as a review comment so it doesn't get lost

Copy link
Member

Choose a reason for hiding this comment

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

A copy is always made, due to this line near the top of the logsumexp implementation:

a, b = xp_broadcast_promote(a, b, ensure_writeable=True, ...

So this will yield correct semantics. It may be slower (didn't check) since a copy is always made now, while the previous implementation (e.g., in v1.14.1) only made a copy if np.any(b == 0) is True.

Potentially, not copying and avoiding any in-place mutation but using xp.where is therefore beneficial also for performance with numpy in the common case where b isn't used or doesn't have elements that are 0.

@lucascolley
Copy link
Member Author

@lithomas1 do you think you could resolve the merge conflicts? @ev-br also updated array-api-compat in gh-21796

@lithomas1
Copy link
Contributor

Apologies for the long silence here.

I did some digging into the remaining fft/special failures and other than some small issues with extra warnings coming from dask, I think I have those two modules passing locally.

One source of failures that I've noticed is due to us passing the "naked" dask.array namespace (as opposed to the array-api-compat wrapped version) in as the xp fixture in tests even though it is not really array API compatible. This is something that we do for the other libraries too (e.g. torch), even though those libraries also seem not to be fully array API compatible.

The problem is worse for dask since oftentimes in the there is an xp.asarray(..., copy=True) call, and I think the default dask asarray doesn't support copy as a parameter (it accepts it as a kwarg silently and then ignores it I think).

This is probably why we had the issue with the copy not happening in logsumexp I think too I think.
(I haven't verified this particular case, but I've tested fft and using array_api_compat.dask.array in the xp_available_backends variable solves the remaining failing tests in fft which were previously failing due to copies not happening/dtype difference)

Comment on lines +157 to +183
import dask.array # type: ignore[import-not-found]
xp_available_backends.update({'dask.array': dask.array})
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
import dask.array # type: ignore[import-not-found]
xp_available_backends.update({'dask.array': dask.array})
import array_api_compat.dask.array as da # type: ignore[import-not-found]
xp_available_backends.update({'dask.array': da})

This is the change that I'm talking about that fixes a lot of things.

Assuming making this change looksgood, I'll cleanup previous unneeded fixes locally and send in another PR to add this + fix merge conflicts.

Copy link
Member Author

Choose a reason for hiding this comment

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

We don't want to do this, actually. We want to pass arrays from the unwrapped namespace in tests to check that we correctly coerce with array_namespace. If you need to use standard functions in tests, you can write xp_test = array_namespace(x). It's good to add a comment about what's missing from the unwrapped namespace.

Copy link
Contributor

Choose a reason for hiding this comment

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

Got it, I'll try to add individual array_namespace calls and report back on what the diff looks like after.

Copy link
Member Author

Choose a reason for hiding this comment

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

If you grep for xp_test you'll see examples of the pattern. There is another pattern like acos = array_namespace(x).acos - either is fine.

Comment on lines +72 to +82
if is_dask(xp):
x.compute()
y.compute()
Copy link
Contributor

Choose a reason for hiding this comment

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

Think these should be assigned to variables (possibly the same variables they already occupy like x = x.compute())

Also the id check at the end makes me wonder if this should use .persist() instead of .compute() (the previous point would still apply)

@lithomas1
Copy link
Contributor

Now, I should have all of fft/linalg/special passing.

stats looks do-able, the main changes seem to be silencing/ignoring the RuntimeWarnings from dask and figuring out how to make _lazy_where not use dynamic shapes for dask/jax and co.

I've fixed up the merge conflicts in lucascolley#15, however the diff is very large, so I've split that out of my dask changes.

(Maybe this is because I do merge commits instead of rebasing to keep up with main?)

@lucascolley
Copy link
Member Author

thanks @lithomas1, I rebased on main here. The CI log is quite messy due to an error in TestLazywhere in _lib.tests.test__util:

TypeError: unsupported operand type(s) for -: 'NoneType' and 'int'

@lithomas1
Copy link
Contributor

thanks @lithomas1, I rebased on main here. The CI log is quite messy due to an error in TestLazywhere in _lib.tests.test__util:

Thanks for the update.

I've attempted to cherry-pick my changes on top of your branch in lucascolley#16.

(The history is still a little messed up - but I think it should be OK if you're fine with squashing my PR into a single commit).

@lucascolley lucascolley mentioned this pull request Dec 5, 2024
9 tasks
@lucascolley
Copy link
Member Author

continued in #22240

@lucascolley lucascolley closed this Jan 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
array types Items related to array API support and input array validation (see gh-18286) enhancement A new feature or improvement scipy._lib
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants