Skip to content

TST: add test that wrapping preserves view/copy semantics #333

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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

ev-br
Copy link
Member

@ev-br ev-br commented Jun 1, 2025

If a bare library returns a copy, so does the wrapped library; if the bare library returns a view, so does the wrapped library.
Unary functions only for now.

cross-ref gh-331

@ev-br ev-br force-pushed the views_vs_copies branch 6 times, most recently from 204a42f to 35c2d67 Compare June 1, 2025 13:20
@ev-br
Copy link
Member Author

ev-br commented Jun 1, 2025

Okay, CI is green now across numpy from 1.22 to -dev. Will likely merge gh-334 first to separate the CI tweaks from -compat tweaks (currently, this PR contains both).

@ev-br
Copy link
Member Author

ev-br commented Jun 1, 2025

What were other "unpleasantly surprising" cases, if you happen to remember @crusaderky ?

@ev-br ev-br changed the title TST: add test that wrapping preserves a view/copy semantics TST: add test that wrapping preserves view/copy semantics Jun 1, 2025
@crusaderky
Copy link
Contributor

What were other "unpleasantly surprising" cases, if you happen to remember @crusaderky ?

there's the table at #331 (comment); there's sum(axis=0), and probably quite a few more. I didn't do a thorough review of all array API functions but I do expect many edge cases where it is possible (and, very arguably, reasonable) to return a view.

@ev-br
Copy link
Member Author

ev-br commented Jun 2, 2025

Views are not a problem per se; returning a view when a bare library returns a copy is.
Regarding the sum example, I actually don't see how sum(x, axis=0) can ever return a view into x. Mind expanding?

@crusaderky
Copy link
Contributor

Regarding the sum example, I actually don't see how sum(x, axis=0) can ever return a view into x. Mind expanding?

sum, prod, mean, and possibly a few more reductions could return a view when all reduced axes have size 1.

@ev-br
Copy link
Member Author

ev-br commented Jun 2, 2025

sum, prod, mean, and possibly a few more reductions could return a view when all reduced axes have size 1.

Cannot repro, what am I missing?

In [1]: import numpy as np

In [2]: a = np.asarray([[1, 2, 3]])

In [3]: from array_api_compat import array_namespace

In [4]: xp = array_namespace(a)

In [5]: b = xp.sum(a, axis=0)

In [6]: b
Out[6]: array([1, 2, 3])

In [7]: np.shares_memory(a, b)
Out[7]: False

In [8]: b[0] = 42

In [9]: b
Out[9]: array([42,  2,  3])

In [10]: a
Out[10]: array([[1, 2, 3]])

@ev-br ev-br force-pushed the views_vs_copies branch from 35c2d67 to de39ee0 Compare June 2, 2025 12:46
@crusaderky
Copy link
Contributor

These functions could return a view. They don't return a view in NumPy. I did not test the other backends wrapped by array-api-compat.

@ev-br
Copy link
Member Author

ev-br commented Jun 2, 2025

Thanks for the clarification! So it's a theoretical concern at this point.

IMO the reasonable thing to declare is #331 (comment)
And this position is what this PR tests for, at least partially.

Comment on lines 532 to 534
if result.dtype != x.dtype:
# numpy < 2: ceil(int array) is float
result = xp.asarray(result, dtype=x.dtype)
Copy link
Contributor

Choose a reason for hiding this comment

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

This leads to loss of precision for very large ints.

Copy link
Member Author

Choose a reason for hiding this comment

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

Indeed, thanks. Reworked to avoid round-tripping int->float->int.

…ry functions

If a bare library returns a copy, so does the wrapped library; if the bare
library returns a view, so does the wrapped library.
ev-br and others added 3 commits June 4, 2025 10:53
Remove these functions from common/_aliases.py, add specific
implementations for numpy < 2 and cupy.
@ev-br ev-br force-pushed the views_vs_copies branch from 1805aaa to 118ae2d Compare June 4, 2025 08:55
Copy link
Contributor

@crusaderky crusaderky left a comment

Choose a reason for hiding this comment

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

Just a cosmetic nit

Comment on lines +125 to +140
def ceil(x: Array, /) -> Array:
if cp.issubdtype(x.dtype, cp.integer):
return x.copy()
return cp.ceil(x)


def floor(x: Array, /) -> Array:
if cp.issubdtype(x.dtype, cp.integer):
return x.copy()
return cp.floor(x)


def trunc(x: Array, /) -> Array:
if cp.issubdtype(x.dtype, cp.integer):
return x.copy()
return cp.trunc(x)
Copy link
Contributor

@crusaderky crusaderky Jun 4, 2025

Choose a reason for hiding this comment

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

int arguments are outside of the scope of the Array API.
So I'm not sure this should be here at all?

For numpy is a bit more nuanced - on one hand it makes sense to unify numpy 1.x behaviour to match 2.x.
On the other hand it's a array-api-compat tweak specifically to cover a change in behaviour that is already outside of the Array API.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think it's just backwards compat. On main, all backends try to return integers for integer inputs (via common/_aliases.py), so this PR tries to preserve that, while fixing the views/copies issue.

@ev-br ev-br force-pushed the views_vs_copies branch from 9f43599 to b0eed55 Compare June 4, 2025 13:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants