-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
ENH: signal: add array API support / delegation to lfilter et al #21713
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
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noticed 213 new failures on GPU testing vs. main
via SCIPY_DEVICE=cuda python dev.py test -j 8 -b all
.
Many can be circumvented by skipping GPU tests with torch
as usual, but I commented on a sample of one of the more interesting cases.
zi = sosfilt_zi(sos) | ||
|
||
y, zf = sosfilt(sos, np.ones(40, dt), zi=zi) | ||
assert_allclose_cast(zf, zi, rtol=1e-13) | ||
y, zf = sosfilt(sos, xp.ones(40, dtype=dt), zi=zi) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something interesting is happening with the test failure here via i.e., python dev.py test -t scipy/signal/tests/test_signaltools.py::TestSOSFilt::test_sosfilt_zi -b cupy
I noticed that zf
in CuPy is the transpose of the result provided by NumPy and torch
; i.e., this causes the above incantation to pass:
--- a/scipy/signal/tests/test_signaltools.py
+++ b/scipy/signal/tests/test_signaltools.py
@@ -3954,6 +3954,7 @@ class TestSOSFilt:
with pytest.raises(ValueError, match='Invalid zi shape'):
sosfilt(sos, x, zi=zi, axis=1)
+ @skip_xp_backends(cpu_only=True, exceptions=['cupy'])
def test_sosfilt_zi(self, dt, xp):
dt = getattr(xp, dt)
sos = signal.butter(6, 0.2, output='sos')
@@ -3961,7 +3962,7 @@ class TestSOSFilt:
zi = sosfilt_zi(sos)
y, zf = sosfilt(sos, xp.ones(40, dtype=dt), zi=zi)
- xp_assert_close(zf, zi, rtol=1e-13, check_dtype=False)
+ xp_assert_close(zf.T, zi, rtol=1e-13, check_dtype=False)
# Expected steady state value of the step response of this filter:
ss = xp.prod(xp.sum(sos[:, :3], axis=-1) / xp.sum(sos[:, 3:], axis=-1))
We obviously can't/shouldn't use a patch like that, filtered to CuPy case. At first glance, I can't imagine this would be an intentional change on the CuPy side based on the docs. I do see that the zi
shape convention is a bit different between SciPy and CuPy, but upon closer inspection I wasn't entirely convinced that was the cause since sosfilt_zi
should compensate by preserving the larger zi
convention in CuPy anyway.
Hm. Apparently,
scipy:
and
Before opening a cupy issue, let me ping @andfoy : as the author of cupy's |
The difference is that the state is not being represented as a sum of delays: Given IIR coefficients
Such that when computing the output
This kind of sum carrying is possible given that SciPy filter computation occurs serially. When computing the filter output in GPU, the previous state values are required as they are, that is |
Thanks Edgar! This should probably be great to document, for both scipy and cupy. That said, this does not explain the need to transpose noted in #21713 (comment) though? |
Now that you mention it, maybe this line comes to mind w.r.t output shape manipulation? https://github.com/cupy/cupy/blob/fb6ffd71d3da200d4300c5760327a278cd8d9ee3/cupyx/scipy/signal/_iir_utils.py#L730 |
I've just opened a PR in CuPy that fixes the issue at hand: cupy/cupy#8677 |
46eac28
to
58f4b2b
Compare
Added a commit with a slew of skips for CuPy, and a separate commit to avoid delegating for functions which deliberately deviate in scipy and cupy. |
Note: Some CuPy functions deliberately deviate from scipy: {lfilter,sosfilt}_zi return arrays of different shapes. Skip dispatching for these cases; a user will have to be explicit on whether they want the 'original' SciPy output (then call lfilter_zi with NumPy array arguments) or the CuPy output (then call cupyx.scipy.signal.lfilter_zi with CuPy array arguments).
e9ad517
to
5a4876e
Compare
b = xp.concat((b, xp.zeros(n - b.shape[0], dtype=b.dtype))) | ||
|
||
dt = xp.result_type(a, b) | ||
IminusA = np.eye(n - 1) - linalg.companion(a).T |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this work, passing a
to linalg.companion
for non-NumPy arrays?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose this goes through __array__
. As long as it works, I'd keep it as is, until at some point scipy.linalg.companion
gets array api compatible, and we'll fix this in one go.
The replacement for `np.atleast_1d(x)` is `xpx.atleast_nd(xp.asarray(x), ndim=1, xp=xp)`, no less.
9b61c03
to
bb0cf02
Compare
Updated to address review comments, add some array-like tests, improve a link to a CuPy fix to investigate after a CuPy makes a 14.0 release, and added enough skips for tests to pass locally with SCIPY_DEVICE=cuda. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks pretty good, thanks!
9351b41
to
b062440
Compare
b062440
to
40a626e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks Evgeni!
Is on top of #20772Add array API / delegation support to more routines from
signaltools
: lfilter, sosfilt, filtfilt, sosfiltfilt. Alsolfiltic
andlfilt_zi
. While at it, also adddeconvolve
since it's only a small thing on top oflfilter
anyway.