Skip to content

Conversation

ev-br
Copy link
Member

@ev-br ev-br commented Jul 1, 2024

Reference issue

cross-ref #21012, #20772

What does this implement/fix?

This is a POC of the layer of dispatch/delegation from scipy.ndimage to cupyx.scipy.ndimage, for a full scipy submodule. ndimage is probably clearer than signal in that it's all compiled code.

Several things to note:

  1. There are no changes to the ndimage code itself, the diff is tweaking tests and adding a the delegation layer only.
  2. Test changes are mechanical: port the test suite to the xp_assert framework, add xfails for CuPy issues
  3. _support_alternative_backends.py is for now copy-pasted from ENH: array types, signal: delegate to CuPy and JAX for correlations and convolutions #20772; the next step would be to deduplicate with the machinery from ENH: linalg/signal/special: unify approach for array API dispatching #21012; I think some simplifications are possible, too.
  4. One difference from ENH: linalg/signal/special: unify approach for array API dispatching #21012 is that instead of a mapping dictionary here we use a dedicated private module, _dispatchers.py.
  5. In general, the delegation machinery is this:
    • _ndimage_api.py collects imports from implementer modules instead of __init__.py
    • _supports_alternative_backends.py decorates things from _ndimage_api with the cupy dispatch
    • __init__.py imports decorated names from _supports_alternative_backends.py
  6. Delegation is based on _dispatcher functions. The full set is worked out for the whole submodule, so we can assess if we can simplify and possibly avoid dispatching on a full signature (I don't think we can, but am open to pleasant surprises).
  7. I am not hooking it to any CI yet, because we do not test CuPy on CI anyway.

Technical TODOs:

  • work out the delegation to jax.scipy.ndimage --- unlike cupyx.scipy.ndimage, it only implements map_coordinates?
  • make it run gracefully under dev.py -b all

Additional information

@ev-br ev-br requested review from larsoner and rgommers as code owners July 1, 2024 10:55
@github-actions github-actions bot added scipy.ndimage scipy._lib Meson Items related to the introduction of Meson as the new build system for SciPy array types Items related to array API support and input array validation (see gh-18286) labels Jul 1, 2024
@ev-br ev-br requested review from lucascolley and mdhaber and removed request for rgommers, larsoner and lucascolley July 1, 2024 10:55
@lucascolley lucascolley changed the title ndimage cupy delegation ENH/POC: ndimage: delegate to CuPy Jul 1, 2024
@lucascolley lucascolley added the enhancement A new feature or improvement label Jul 1, 2024
@ev-br ev-br force-pushed the ndimage_array_api_tests_and_dispatch branch from 21290a1 to eaa9a0a Compare July 1, 2024 11:19
@@ -107,25 +129,25 @@ def _validate_complex(self, array, kernel, type2, mode='reflect', cval=0):
# test correlate output dtype
output = correlate(array, kernel, output=type2)
assert_array_almost_equal(expected, output)
assert_equal(output.dtype.type, type2)
assert output.dtype.type == type2
Copy link
Contributor

Choose a reason for hiding this comment

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

IIUC, the dtypes passed in here will always be NumPy dtypes, but this takes advantage of the fact that CuPy dtypes can be compared with NumPy dtypes and this PR is only for CuPy backend?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah. CuPy dtypes in fact are numpy dtypes, so this works for cupy. Won't work for dtypes from array-api-strict though, so if we want to make it work in this PR, the diff is going to grow by another ~500 mechanical changes :-).


# test correlate with pre-allocated output
output = np.zeros_like(array, dtype=type2)
output = xp.zeros_like(array, dtype=type2)
correlate(array, kernel, output=output)
assert_array_almost_equal(expected, output)
Copy link
Contributor

Choose a reason for hiding this comment

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

Does assert_array_almost_equal work for CuPy arrays?

Copy link
Member Author

Choose a reason for hiding this comment

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

No. This is a shim from _lib.

@ev-br
Copy link
Member Author

ev-br commented Jul 3, 2024

speaking of dtypes: what is the recommended way to to declare a dtype across all array api compatible backends?

The pattern is

@pytest.mark.parametrize(dt, [np.float32, np.float64])
def test_somethin(xp):
    arr = xp.zeros((3, 4), dtype=dt)
    ...

and it fails to understand the dtype under array_api_strict. I tried specifying strings, and do xp.dtype("float64") but that fails, too.

@ev-br ev-br force-pushed the ndimage_array_api_tests_and_dispatch branch from 07c51a0 to fd28566 Compare July 3, 2024 09:28
@lucascolley
Copy link
Member

lucascolley commented Jul 3, 2024

xp.float64 should work

(However, since we are dealing with the raw namespaces rather than the array-api-compat wrapped versions initially in tests, this is what is blocking us from adding dask, as there is no dask.array.float64. Maybe we will need to invent our own solution for robustly parametrising dtypes, but I'm going to ask in the Dask community meeting if they can help us out)

@ev-br
Copy link
Member Author

ev-br commented Jul 3, 2024

xp.float64 should work

In the test itself yes, but how to @parametrize: xp is not known outside of the test function.

@lucascolley
Copy link
Member

Oops 🤦‍♂️ try getattr(xp, dtype) and parametrize with strings like "float64".

@ev-br
Copy link
Member Author

ev-br commented Jul 3, 2024

No dice:

diff --git a/scipy/ndimage/tests/test_interpolation.py b/scipy/ndimage/tests/test_interpolation.py
index db3479dbb1..63e816d7d8 100644
--- a/scipy/ndimage/tests/test_interpolation.py
+++ b/scipy/ndimage/tests/test_interpolation.py
@@ -194,11 +194,12 @@ class TestNdimageInterpolation:
         assert_array_almost_equal(out, [0, 4, 1, 3])
 
     @pytest.mark.parametrize('order', range(0, 6))
-    @pytest.mark.parametrize('dtype', [np.float64, np.complex128])
+    @pytest.mark.parametrize('dtype', ["float64", "complex128"])
     def test_geometric_transform05(self, order, dtype, xp):
         if is_cupy(xp):
             pytest.xfail("CuPy does not have geometric_transform")
 
+        dtype = getattr(xp, "dtype")
         data = xp.asarray([[1, 1, 1, 1],
                          [1, 1, 1, 1],
                          [1, 1, 1, 1]], dtype=dtype)

produces

$ python dev.py test -t scipy/ndimage/tests/test_interpolation.py -b array_api_strict -- -k 'test_geometric_transform05'
...
_____________________________________________________________ TestNdimageInterpolation.test_geometric_transform05[float64-0-array_api_strict] _____________________________________________________________
scipy/ndimage/tests/test_interpolation.py:202: in test_geometric_transform05
    dtype = getattr(xp, "dtype")
E   AttributeError: module 'array_api_strict' has no attribute 'dtype'. Did you mean: '_dtypes'?
        dtype      = 'float64'

@j-bowhay
Copy link
Member

j-bowhay commented Jul 3, 2024

diff --git a/scipy/ndimage/tests/test_interpolation.py b/scipy/ndimage/tests/test_interpolation.py
index db3479dbb1..63e816d7d8 100644
--- a/scipy/ndimage/tests/test_interpolation.py
+++ b/scipy/ndimage/tests/test_interpolation.py
@@ -194,11 +194,12 @@ class TestNdimageInterpolation:
         assert_array_almost_equal(out, [0, 4, 1, 3])
 
     @pytest.mark.parametrize('order', range(0, 6))
-    @pytest.mark.parametrize('dtype', [np.float64, np.complex128])
+    @pytest.mark.parametrize('dtype', ["float64", "complex128"])
     def test_geometric_transform05(self, order, dtype, xp):
         if is_cupy(xp):
             pytest.xfail("CuPy does not have geometric_transform")
 
+        dtype = getattr(xp, "dtype")
         data = xp.asarray([[1, 1, 1, 1],
                          [1, 1, 1, 1],
                          [1, 1, 1, 1]], dtype=dtype)

I think there is a type in your patch, should it not be dtype = getattr(xp, dtype) instead of dtype = getattr(xp, "dtype")

@ev-br
Copy link
Member Author

ev-br commented Jul 3, 2024

Ah, that's it. Thanks Jake!

@ev-br ev-br force-pushed the ndimage_array_api_tests_and_dispatch branch from 0761236 to 9b013f4 Compare July 3, 2024 13:22
Copy link
Member

@lucascolley lucascolley left a comment

Choose a reason for hiding this comment

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

I've finished one of the big test files - there are a lot of occurrences of indentation being thrown off by adding xp.asarray. A few comments otherwise.

@lucascolley lucascolley removed request for andyfaff and ilayn July 12, 2024 12:32
Copy link
Member Author

@ev-br ev-br left a comment

Choose a reason for hiding this comment

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

Review comments addressed.

@ev-br ev-br force-pushed the ndimage_array_api_tests_and_dispatch branch from 4fd7284 to 2a6944f Compare July 13, 2024 14:12
@ev-br
Copy link
Member Author

ev-br commented Jul 13, 2024

OK, I hopefully reformatted all instances of misaligned arrays.

Copy link
Member

@lucascolley lucascolley left a comment

Choose a reason for hiding this comment

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

getting there! this is a huge diff 😅

@ev-br
Copy link
Member Author

ev-br commented Jul 13, 2024

this is a huge diff

This is all going to be mechanical, they said :-).

@ev-br ev-br force-pushed the ndimage_array_api_tests_and_dispatch branch from 1bae880 to 021addf Compare July 14, 2024 09:52
@ev-br
Copy link
Member Author

ev-br commented Jul 14, 2024

All right, all the test formatting etc should be in shape now.

@ev-br ev-br force-pushed the ndimage_array_api_tests_and_dispatch branch 2 times, most recently from 44406fd to 705d9c9 Compare July 14, 2024 14:25
@ev-br
Copy link
Member Author

ev-br commented Jul 14, 2024

The last round of comments is addressed.

@ev-br ev-br force-pushed the ndimage_array_api_tests_and_dispatch branch 2 times, most recently from 05c5e55 to 37655f9 Compare July 14, 2024 15:06
@ev-br
Copy link
Member Author

ev-br commented Jul 15, 2024

FYI @lucascolley turning other backends on requires further test tweaks. I'll sync tests here when #21150 reaches a natural sync point.

@ev-br ev-br force-pushed the ndimage_array_api_tests_and_dispatch branch from 37655f9 to f4e5d46 Compare July 16, 2024 08:57
@ev-br
Copy link
Member Author

ev-br commented Jul 16, 2024

All tests are in sync now. Sorry for the back-and-forth @lucascolley

@lucascolley
Copy link
Member

could you take a look at the CI failures here? I assume most of it is adding the appropriate skips, to be removed in the next PR.

@ev-br
Copy link
Member Author

ev-br commented Jul 17, 2024

Ha, this one is a funny one. TL;DR: skip_xp_backends do not add up.

Consider test_white_tophat01 test in test_morphology.py, which fails here. The test module is

from scipy.conftest import array_api_compatible
skip_xp_backends = pytest.mark.skip_xp_backends
pytestmark = [array_api_compatible, pytest.mark.usefixtures("skip_xp_backends"),
              # XXX: only CuPy delegation is implemented
              skip_xp_backends("jax.numpy", "torch", "array_api_strict"),
]

<snip>

   @skip_xp_backends("jax.numpy", reason="output array is read-only.")
   def test_white_tophat01(self, xp):
        .....

The decorator clears other skips, so the test is only skipped on jax --- and fails miserably on torch and array-api-strict.


<stepping away from the keyboard for 1/2 hour>


It seems to me that the attempt to break a large PR into a "stack" of this one and #21150, which works great on some other projects, simply does not work in SciPy. Or at least does not work here anyway.

I'm open to suggestions on how to move this forward. Preferably in a way that won't require me adding and immediately removing ~100 skips.

@lucascolley
Copy link
Member

lucascolley commented Jul 17, 2024

Would you like to just consolidate into 1 PR? The diff is so big anyway that making it a bit bigger doesn't really make it any more difficult to review.

RE: multi-level skips - yes, this has caught me out before. I haven't rushed to work around this, though, as I think the mental model of 'the source of truth for skips is the closest decorator to the function's level, if one exists' is okay to grasp, once you've been told about it. It just isn't the most intuitive model.

@ev-br
Copy link
Member Author

ev-br commented Jul 17, 2024

OK, let's continue in #21150 indeed.

The bulk of the diff is just the same:

  • test tweaks are the same
  • the decoration/delegation story is mostly the same, the main difference is that the full version just needs a small additional dance to take care of various non-array return values.

'the source of truth for skips is the closest decorator to the function's level, if one exists' is okay to grasp, once you've been told about it. It just isn't the most intuitive model.

The main footgun IME is that repeated skips tend to be fall into cracks: if you skip X in pytestmark, need to skip Y for a particular test, you've got to skip both X and Y --- and then when you undo the pytestmark, the test keeps skipping both.

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 scipy.ndimage
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants