Repository navigation
BUG: Add promoter to ldexp for python integers to prevent overflow - #29597
MaanasArora wants to merge 17 commits into
Conversation
|
What will happen with complex input? |
|
@mattip this only promotes integer inputs (Python scalar integer, actually), so the rest would stay the same? |
|
Thanks a for looking at this, this is very cool to see this use! I am curious if we can push this further and actually use the promoter always. That is, we:
|
|
Thanks, that extension makes a lot of sense! I tried implementing this, but it causes a segmentation fault. It seems to be due to the fact that Edit: that does seem to be it; reverting the operation for the first dtype and setting the others in this way doesn't crash (though of course the promotion doesn't work properly) |
|
Oh, hmmm, the pyfloat one should work ok probably, although the abstract version woukd be nicer. |
|
|
|
Making the abstract and PyFloat do the same thing probably makes sense, but the fact that it doesn't work here seems like a bug somewhere that I (or we) need track down to make this work (and a couple of other cases, because we should use this for other float only functions). |
|
To be clear it doesn't crash, it just doesn't promote further up than In line 223-226 of abstractdtypes.c we do in else if (other == &PyArray_PyLongDType) {
Py_INCREF(cls);
return cls;
}This is equivalent to doing no promotion as we already know it's a float, but looking at NEP 50, this actually seems correct behavior - Python ints can promote to the lowest float (given we need a float) right? So for |
|
Yes, we promote to numpy/numpy/_core/src/multiarray/abstractdtypes.c Lines 217 to 226 in 583993f |
|
Ah, that makes sense (if I am getting the right picture). We are getting the abstract version as a result. In other paths that is OK because we only need a concrete dtype instance (via default_descr), but here we need to convert the abstract one to the concrete Float64 one. |
|
Ah, I see, thanks! Yes, I think that's right, we're getting the |
|
Yeah, I suppose it probably isn't a concern in practice, so the choice we have may be the pragmatic one, even if it is slightly weird/reversed if we use it here. Reverse or not, until we have a case where this fails, it may be a reasonable to path to "promote to a concrete DType". |
|
Makes sense! If it's a missing step, I suppose we should add it to (I realize this is an unrelated change to this PR, so completely happy to revert and open another PR if you prefer.) |
Yeah, I am not sure I want that function to ensure no abstract result. We could add a mini internal helper for now that ensures we have a concrete dtype, also means we can refactor it in a single place if we ever have to. (Also could check if you need the PyFloat rather than the abstract float version, since that didn't actually help initially.) |
|
Makes sense! I actually meant that Using the |
Hmmmmmmm, we had some reason for changing that. FWIW, it's probably fine then, we should be using the abstract version. |
|
We can just pass the Edit: unless there is a reason to not define the slots (I guess not, since the only other two are |
Basically except that may return one of the |
|
Hmm, there doesn't seem to be a reference to the I've added two slots to the abstract float dtype (I can add if you want them on the other abstract dtypes as well). The internal helper, should it be only for promoters? Edit: sorry, I suppose if it's a helper it can just be used directly in ldexp? Pushed this. |
|
The new test seems to reliably fail on armhf (will investigate). |
|
A rebase wouldn't hurt |
f26c334 to
246adb3
Compare
|
Pushed a small fixup mostly for additional error checks. I also broadened it up to all integer inputs (including int32, etc.). NumPy promotion should go to float64 for integers of any type by default for functions that do not support integers normally, I think. Since we add the machinery to do that, might as well do that now I think. Let's see if tests pass now, although I am not sure what that |
Nothing, the promoter will never be called and even if it was, it wouldn't do anything. There are no complex loops anyway. |
| Py_CLEAR(new_op_dtypes[0]); | ||
| return -1; | ||
| } | ||
| if (new_op_dtypes[2] == NULL) { |
There was a problem hiding this comment.
Note to solve (or Maanas), forgot an INCREF. Also not sure if we should check signature, although... I suppose it doesn't really matter, the promotion above should be always OK and if it causes a signature mismatch I think the later machinery would raise an error.
The test failures are all on platforms with int being 32bit integers, although I am not sure that is the underlying reason.
I somewhat suspect it may be an issue with the SIMD code, a way forward may to ignore this warning/FPE and check what the result type (and value, but if that fails just the type to see if the promotion is right).
There was a problem hiding this comment.
Thanks - added the INCREF.
|
Thanks for the cleanup!
Yes, likely some downstream bug, since it's only on a few platforms. I've added a test to check the result dtype is |
Well, this is should be in the SIMD code path I think, but let's see. So it may or may not be the system math being at fault for spuriously giving a warning. |
| warnings.filterwarnings('ignore', message='overflow encountered in ldexp', | ||
| category=RuntimeWarning) | ||
| warnings.filterwarnings('ignore', message='overflow encountered in cast', | ||
| category=RuntimeWarning) |
There was a problem hiding this comment.
There may be a test not liking this. We can use pytest.mark.filterwarnings here, I think.
(But let's see what is going on first, I guess...
There was a problem hiding this comment.
All tests pass now, but I can still push this if it's better!
There was a problem hiding this comment.
Ah nice. I think it would be a bit nicer, since it is not relevant to the test (and removes the indentation).
Just to understand, the overflow is actually real on these platforms? I.e. they give the incorrect result?
We should open an issue for this if that is the case, as I think it indicates a bug in the SIMD code (or so).
(For the cherry on top, we add a comment that links to the issue.)
There was a problem hiding this comment.
Ah, two other small things if you touch things, but I can do it also:
- I guess it's now big enough that parametrization may be nice.
- The dtype check would be nice to always do I think. While the result check is OK if it fails (due to an issue).
There was a problem hiding this comment.
Makes sense! I've pushed a refactored test with parametrization and pytest.mark.filterwarnings.
I'm not sure if the overflow is real - I only saw the warning for sure in CI and I don't have access to one of these (armhf is the main one, IIRC). I've pushed it without the try, so let's see if it catches.
There was a problem hiding this comment.
Yes quite strange! It seems the dtype is being promoted incorrectly on the failing Windows MSVC and armhf:
FAILED numpy/_core/tests/test_umath.py::TestLdexp::test_ldexp_integer_scalar[int32-int] - AssertionError: assert dtype('float16') == <class 'numpy.float64'>
FAILED numpy/_core/tests/test_umath.py::TestLdexp::test_ldexp_integer_scalar[int32-int8] - AssertionError: assert dtype('float16') == <class 'numpy.float64'>
FAILED numpy/_core/tests/test_umath.py::TestLdexp::test_ldexp_integer_scalar[int32-int16] - AssertionError: assert dtype('float32') == <class 'numpy.float64'>
I'm not sure why the promotion is platform dependent either. (Not to mention, it seems assert_almost_equal actually passed? That is odd, given the result is clearly overflown to infinity.)
There was a problem hiding this comment.
The test failures are all on platforms with int being 32bit integers, although I am not sure that is the underlying reason.
I think this is relevant - the overflow only happens when the first integer is int32. In fact it seems we call the third branch of float_common_dtype (from inspecting the error outputs), so perhaps the first condition is skipped for some reason?
numpy/numpy/_core/src/multiarray/abstractdtypes.c
Lines 214 to 227 in 0ecf780
There was a problem hiding this comment.
Yeah, taking the last branch here could explain it, but I can't say I see a way that this branch can be taken really unless other->type_num is corrupted and I don't really see that happening either!?
As to why it passed so oddly (and why emscripten passed before), it did float16(inf) == large_number, but float16(large_number) was also inf and that gives the same warning, but if you ignore the warnings it passes.
There was a problem hiding this comment.
Yeah, I don't really see how it could be taken either! The outputs are weirdly consistent with it still. Perhaps the promoter is just not being called? Also seems unlikely, as I don't see why int32 would not be under abstract int. That said, int32 does seems 'special' on these platforms.
There was a problem hiding this comment.
True, but if the promoter wasn't used, I would expect it to pick the first loop that supports safe casting, and int32 -> float16 isn't safe casting. Feels like we are missing something "obvious" maybe, but maybe hard to figure out without a 32bit system.
Fixes #29182.
Adds a promoter for first argument
PyArray_PyLongDTypetoldexp. It casts straight todouble, not sure if that is more arbitrary than needed.Thanks for reviewing!