Repository navigation
BUG: where= in gufuncs is slightly broken with out= and casts #18700
Description
Activity
I am both surprised that it changed, and that it ever "retained" the old values :(.
The only way it can work to begin with is by both filling the buffer and then writing the buffer back again, which is exactly what happens since this can't possibly retain the values (and does not):
out = np.full_like(x, 9.123, dtype=np.float16) func(x, out)giving:
array([[1., 9., 9., ..., 9., 1., 9.], [1., 9., 9., ..., 9., 1., 9.], [1., 9., 9., ..., 9., 1., 9.], ..., [1., 9., 9., ..., 9., 1., 9.], [1., 9., 9., ..., 9., 1., 9.], [1., 9., 9., ..., 9., 1., 9.]], dtype=float16)casting anything. So in your int example, things only works out because casting uint to int to uint happens to retain the values.
I think everything should pan out fine if the second array was flagged for read-write (instead of readonly), although I am not sure if you can do that right now. If it was flagged readwrite, the question is whether the ufunc machinery does/should already refuse to do the operation though, since the cast is not safe!
(It does point me to the fact that maybe
ArrayMethodshould use the flags instead ofninandnoutlike ufuncs do usually/currently.)OK, should have guessed that this was the change, I missed the point that this was limited to gufuncs a bit, I guess:
Seems like I probably had the wrong impression that this
READWRITEserved the only purpose of fixing broadcasting. There is a comment alluring thatREADWRITEwas set for more reasons, which I must have missed. But I also don't understand why that comment exists. I would think that the ufunc can indicateREADWRITEif actually needed, rather than assuming that it is and doing completely unnecessary casts?@mattip do you remember the reason for this comment (that mismatches currently with the code):
numpy/numpy/core/src/umath/ufunc_object.c
Lines 2394 to 2405 in a115369
/* * We don't write to all elements, and the iterator may make * UPDATEIFCOPY temporary copies. The output arrays (unless they are * allocated by the iterator itself) must be considered READWRITE by the * iterator, so that the elements we don't write to are copied to the * possible temporary array. */ _ufunc_setup_flags(ufunc, NPY_ITER_COPY | NPY_UFUNC_DEFAULT_INPUT_FLAGS, NPY_ITER_UPDATEIFCOPY | NPY_ITER_WRITEONLY | NPY_UFUNC_DEFAULT_OUTPUT_FLAGS, op_flags); Maybe there are cases where the array iterator makes temporary copies for intermediate values when the output and input overlap. I think the comment is hinting that in order for that to work properly the iter must be marked
READWRITE, but it was changed here in #15162. If that conjecture is true, I wonder why there are no overlapped memory test failures after #15162.Hmmmgrrrr. I think the comment probably originates from
wherehandling (although that bleeds a lot ofnditerimplmenetation details IMO). Now gufuncs don't actually supportwherehandling, so that would explain why no tests can fail.That doesn't really help me figure out if the change is right here or not (it seems right to me), or whether we want it even if it is correct. Also, it begs to question whether the
whereimplementation that probably relies on this for some code paths isn't just broken to begin with.OK, I am pretty sure you are right your note on overlapping input and output. When that happens and
whereis used, we will definitely run into this. That type of behaviour seems to be the origin of the comment (although it is older than overlap detection, there are definitely other ways to trigger the (updateif)copy. I am just not sure if the ufunc machinery will ever do it in its current state).This is ridiculous, but shows the loss of precision in the masked values:
In [21]: out = np.full(8, 1.25, dtype=np.float64) In [22]: out Out[22]: array([1.25, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25, 1.25]) In [23]: arr = out.view(np.int64) # overlaps In [24]: np.add(arr, arr) Out[24]: array([9216616637413720064, 9216616637413720064, 9216616637413720064, 9216616637413720064, 9216616637413720064, 9216616637413720064, 9216616637413720064, 9216616637413720064]) In [25]: np.add(arr, arr, out=out, where=[True, False]*4) Out[25]: array([9.21661664e+18, 1.00000000e+00, 9.21661664e+18, 1.00000000e+00, 9.21661664e+18, 1.00000000e+00, 9.21661664e+18, 1.00000000e+00])You should be able to create slightly less ridiculous things if you add a
float64array in-place, but forcedtype=float32.
It still seems to me that this change is strictly speaking correct (happy to be convinced to undo it though) and additionally that checking the
WRITEMASKEDflag inside nditer would be a better solution. But, that example with the loss of precision is still confusing me.
Of course it is almost impossible to trigger it...PR #11580 that added the comment does mention where
Yeah, in particular in the original position of that comment,
NPY_ITER_READWRITEwas only set ifwherewas used. But going before that diff, the generalized ufuncs still always useREADWRITEon outputs, even though the comment (i.e.where) doesn't apply. I still think that was probably to make broadcasting work.In theory, I think the correct thing would be:
- Here, everything is fine. The gufunc should be declared as taking two inputs, but manually flag the second input to
READWRITE. (considering that the output is garbage withoutoutbeing passed in, it is probably OK to require it to be passed) where=seems slightly broken, although that is very hard to trigger. I wonder if the correct fix would actually be to have aPyArray_ResolveWritebackIfCopyMasked, so that the iterator can copy back selectively (and doesn't have to copy the original data). (In principle, that dance is only necessary if an actual cast is necessary so loss of data can occur! In most cases no cast is necessary for theoutargument – or buffering is used – so that copying everything "back and forth" is a valid option; The difference basically replacing a full copy back and forth, with a selective copy of only the result values.)
- Here, everything is fine. The gufunc should be declared as taking two inputs, but manually flag the second input to
sounds like a plan
- changed the title
[-]values in ufunc output array not always passed into the ufunc kernel in np1.20[/-][+]BUG: `where=` in gufuncs is slightly broken with `out=` and casts[/+]on Jul 11, 2024
EDIT (seberg): The original behavior change described here seems OK. However, another issue was identified: #18700 (comment)
When the
outparameter on ufunc is given, numpy 1.19 (and older versions) would always pass the values of theoutarray to the kernel. In numpy 1.20, that changed and a fresh uninitialized array is given when the type does not match exactly.In other words... given a no-op ufunc (kernel does nothing) that takes 1 input and 1 output. And, it is called as:
Is the output guaranteed to retain the original values?
Reproducing code example:
Reproducer from numba/numba#6864
with Numba 0.53 and Numpy 1.20.1, the result is:
In np1.20, the skipped slots are containing random values.
with Numba 0.53 and Numpy 1.19.5, the result is:
In np1.19, the skipped slots are retaining the original zero values.
Error message:
No error message. The problem is a change in behavior.
NumPy/Python version information: