-
-
Notifications
You must be signed in to change notification settings - Fork 10.9k
API: Introduce optional (and partial) NEP 50 weak scalar logic #21626
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
I think this should actually be usable, now (not final, but usable for first tests). SciPy only shows 244 failures (most of them are clustered in either sparse or KDE tests; the KDE tests go to longdouble when they did not before and then fail because they use linalg which barfs on longdouble. With warnings enabled, there are ~2400 failures, but even those should be very heavily clustered on parametrized tests. It may be that some of those should just be ignored (even in NumPy itself). There are only a few serious ones (e.g. due to low-precision integers being used where they probably should not be used), I suspect the serious ones are easy to fix. (EDIT: I am not even sure the 2400 warnings are even a lot of churn, they seem very clustered to a few heavily parametrized tests.) |
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.
Could you add annotations for the new functions to the main __init__.pyi
file?
I do believe that contextmanager
still has to be imported from contextlib
.
@contextmanager
def no_nep50_warning() -> Generator[None, None, None]: ...
def get_promotion_state() -> str: ...
def set_promotion_state(state: str, /) -> None: ...
2186a2b
to
f89aa56
Compare
I think this should be reaching a merge-able state now. I would like to still add brief docs (either in global state, or the NEP itself). There are a few changes, so to summarize:
A bit more "indirect" changes are that I fixed up a few tests to at least include The promotion of |
Even the new promotion has to use the min-scalar logic to avoid picking up a float16 loop for `np.int8(3) * 3.`.
We need to be able to query the state for testing, probably should be renamed before the end, but need to have something for now.
Promotion in percentile will now more aggressively preserve the input dtype for floating point types (rather than upgrading the type to at least float64).
This ensures that the precision is not downcast, which could make a small value zero (for float16 mostly). This lets tests pass that check whether `np.float16(0)` is almost equal to 0, which otherwise fail (because `float16(0.00000001)` will evaluate to 0 exactly.
Also make the warning message sane :)
The issue is that bools are subclasses of ints, which triggers the more general problem that we cannot use the `int` abstract dtype if the input has a concrete dtype associated (e.g. bools, but also general user defined DTypes in principle)
Forcing the output dtype does not work here, since it actually can be integral (not that this is usually a good idea). In practice, the change we are doing here is forcing 64bit (or 32bit depending on platform) of precision for the calculation. This means that the change will only ever increase precision mildly compared to the current situation.
If all are scalars, then legacy promotion is not forced but we would use weak promotion internally suddenly (which we must not!).
Co-authored-by: Bas van Beek <[email protected]>
This follows the tests (and actually goes hand in hand with them). There are still some apparent issues here though, I suspect (but am not sure), the that the legacy promotion may need to kick in more agressively when errors occur. Also, surprisingly this fixes things that maybe should fail in legacy promotion, and I am not yet sure why...
…re robust It seems that the (weird and probably non-existing in practice) case of uint8 vs. int8 promotion when the input is a single integer was broken at some point and this fixes it again. This is only really relevant for rational, which defines only a very selective number of integer promotions. This fixes up the previous chunk that relaxes promotion fallbacks a lot for legacy dtypes.
this also effectively fixes some corner cases in np.result_type
I'm testing this with SciPy and pre-emptively fixing up some things as needed. For one test failure ( >>> np.uint32(1) < np.iinfo(np.int32).min
True
>>> type(np.iinfo(np.int32).min)
<class 'int'>
>>> np.uint32(1) < -214783648
True |
Very cool, thanks! That result should really not be the final result there. Right now, my expectation for the final result is that it will raise an error. I.e. raising errors (and many We could try to be smarter (for comparisons), but I have to think about it more. Maybe comparisons could always use the
|
Thanks. That makes sense to me I think - and it's certainly better than the current result.
It sounds logical, but perhaps it'll turn out later that special-casing casting behavior for comparisons was a mistake - hard to predict. So perhaps starting with an exception is better? |
One more in the same vein: >>> np.uint32(10) % 2**32 # expected answer: 10 (or an exception)
<ipython-input-5-e66c79a8614e>:1: RuntimeWarning: divide by zero encountered in remainder
np.uint32(10) % 2**32
0 After fixing pretty much all of the failures in SciPy that show up with
|
The warnings exist as |
Ah yes, thanks. That does help, when combined with exact test selection. Typically I run all tests in a file or submodule, but that's too noisy here. I just tried on
gives a root cause for the failure of interest:
|
@seberg does the affect the array_api work? |
This may be their biggest blocker. But its not set up to be e.g. usable with a context manage, so it is important but doesn't affect them right now (as it doesn't add any new API to use the new behavior). |
Could you add a release note for this so that folks know how to test against it? |
Good point, I added a brief release note pointing to the NEP. The NEP has the actual note on how to test (seems more discoverable to me, and better to udpate). |
Thanks Sebastian. |
This is still a work-in-progress, but I also do not mean it to be quite complete (for PR size reasons). There is a reason this is all opt-in for now.
The current state introduces the new NEP 50 "weak scalar" logic to replace value-based casting. There may still be holes in the logic at this point.
The main points are:
NPY_PROMOTION_STATE
and functionsnp._set_promotion_state(state)
andnp._get_promotion_state()
with the following options:NPY_PROMOTION_STATE=legacy
no change in behavior (default)NPY_PROMOTION_STATE=weak
new NEP 50 behaviorNPY_PROMOTION_STATE=weak_and_warn
additional gives a warning in many cases where a change occurs (see details below).np._no_nep50_warning()
as a context manager to locally suppress the optional warning. UnlikeNPY_PROMOTION_STATE
this is thread- and context-safe.Missing things are:
np.can_cast
does not take into account that technicallynp.can_cast(1, np.uint8)
should probably returnTrue
!Note on warnings:
np.float32(3.1) < 3.1
changes behavior is ignored by the warnings: The behavior only changes with respect to floating point precision! Similarly, I ignore the warnings that would be triggered insidenp.isclose
.Painpoints:
np.arange
juggles Python integers internally, which leads to warnings (even if it rarely changes things). Similarlynp.linspace
runs into warnings easily, although it probably also doesn't change things there (or only very little).np.quantile(float16_arr, 0.5)
will return afloat16
?This is hopefully test-runnable on other projects though. The missing integer handling should hopefully break loudly on many good test-suites and you actually will get the transition warning for those cases.