-
-
Notifications
You must be signed in to change notification settings - Fork 10.8k
ENH: Refactor updateifcopy #9269
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
Oh, dangerous ;) I'm worried about downstream projects using updateifcopy arrays. Do we want to deprecate that? |
it is all very backward-compatible for CPython. |
Is the issue with nditer that on each iteration we can call I guess the reason this feels dangerous is because it changes the behavior of the existing flag, rather than making a new flag and using a deprecation cycle to migrate to that. Whether it's actually dangerous is less clear. I guess those are the two options to consider though, so let's think it through. I'm assuming that there are downstream projects out there using UPDATEIFCOPY and relying on the GC, and we would like them to eventually stop doing that and stop supporting it ourselves. Option 1:
Option 2:
So my concern about option 1 is: how should we do the deprecation? The problem is that you can't raise an error from OTOH with option 2, we can detect up front when they create the array whether they're using UPDATEIFCOPY or UPDATEIFCOPY_FIXED, so we can issue a DeprecationWarning warning then, and eventually raise an error on UPDATEIFCOPY. So I'm leaning towards option 2. Does that logic magic sense? Is there anything I'm missing? The other migration we need to think about is for nditer as a context manager. My suggestion is that Question: should nditer enforce the context manager thing in all cases, or only in cases where it detects that UPDATEIFCOPY{,_FIXED} is actually needed? The argument for the latter is that it's less disruptive; the argument for the former is that it's hard to know whether UPDATEIFCOPY will be needed (could depend on obscure things like the strides of input arrays), so it's better to enforce it uniformly from the start instead of leaving an unexploded landmine to be discovered in production. I don't feel like I have a good intuition here; I'm really not very familiar with the nditer code or how it's used. |
@njs Thanks for the reply, you gave this alot of thought. I would prefer option 2 I would prefer merging this pull request (with the addition of an nditer warning as below for PyPy only), since it is totally backward compatible and does solve issues with PyPy and the functions in TestUpdateIfCopy. I am +1 for warning on creating an |
Just to throw it in, are you sure we can't deprecate the usage in nditer as well? For all I know there might be only a couple of uses for that flag in nditer and possible not a single one in real live.... NpyIter is a pretty unused beast mostly. |
It turns out the ndarray under the iterator operand is created (in
The ndarray can also use the So it is not as simple as removing the |
My mail to numpy-discussion got no response. How should I proceed to get this merged? Is there a POC for |
@@ -1027,6 +1027,8 @@ typedef void (NpyIter_GetMultiIndexFunc)(NpyIter *iter, | |||
* eliminate overlap. | |||
*/ | |||
#define NPY_ITER_COPY_IF_OVERLAP 0x00002000 | |||
/* At least one operand uses UPDATEIFCOPY semantics, use context manager */ | |||
#define NPY_ITER_USE_CONTEXT_MANAGER 0x00004000 |
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.
Some questions/comments:
- Just so you know the nditer/NpyIter is mostly a C-Api beast, the python side is not used by far as much.
- Is the flag really necessary? I guess you would have to check every single operand array, but that does not seem too bad? The flag may not be bad either though, so not sure what I think yet.
- Why is there an explicit
ResolveUpdateIfCopy
in thenditer
pywrap code? It seems to me that on the C-Side this should be triggered by theNpyIter_Deallocate
call, so it would be more obvious to me if you would just call that. (Hopefully guarding all the other methods against the iterator beingNULL
or so is not too bad, but it should be done anyway). - Regarding above point: Right now it seems to me like C-Code using NpyIter is broken in PyPy, because the resolve code is never called.
- In cpython, if I write
for i in np.nditer(...):
I know the nditer will be deallocated by the time the for loop ends, I could imagine trying to give a warning when this might not work, and we know it from the reference count, since it should be higher if there is any chance of the object living longer. (I don't care whether this warning is a ) - I suppose PyPy has no way of knowing if a temporary iterator object only exists within the scope of the single for loop? (in which case you could do the same thing as above)
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.
The warning in cpython would be produced on (the first) call to next
. If there was any way to detect that the code looks like for i in np.nditer(...)
(and that iteration is finished/aborted) in PyPy that would be cool since it would make the context manager normally unnecessary, but I guess there is not.
A short mention in the documentation would be nice of course.
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 also think about doing an automatic copy-back when the iteration end is reached. But since this is a rather low-level api, maybe better not.
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.
Arrg, I guess it might not possible to guess for sure if the iterator is limited to a for-block from the code (though it would be good to be sure/check). Also C-Api usage/cython might make a refcount check impossible I guess.
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.
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.
Well, but does it call array_dealloc
or does it just reduce the reference count of those arrays? Because it should do the Resolve step specifically.
Yes, it would destroy the state of the iterator, but first, on the C-side, that is how you would achieve copy-back I think and second I don't see much use entering the same nditer object twice.
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.
HHmmmpf, I guess I have no clue how exactly pypy works anyway, and yeah for the python side and updateifcopy things get strange anyway if someone looks at the operand arrays I guess. Will have to think about it some more (and actually figure out the exact logic).
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.
So, even on PyPy, if we stay fully within C, NpyIter_Dealloc
s Py_DECREF
s will actually trigger array_dealloc
immediatly? I just glanced over http://docs.cython.org/en/latest/src/userguide/pypy.html
and I don't quite see that this happens? Only the C-Api "view" should be cleaned up, or does it know that there can't be a Python side object (because there was no access or because it never left the C-api)?
If for some reason this works even with python code in between, we could possibly check for access to the operand arrays as well? But probably not worth the trouble, right now I am mostly worried about the C-Api usage being correct for sure.
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 will add "updating cython/docs/src/userguide/pypy.rst" to my TODO list. While not wrong, that page is slightly out of date and it should mention that only certain C-API functions will actually create a python-level object linked to the C one. Looking at the implementation of Py_DECREF
one can see that if the refcount is 0 (which means no python-level object is created, otherwise the refcount would be >= REFCNT_FROM_PYPY
) then obj.tp_dealloc
is called immediately.
I did try to naively add a call to PyArray_ResolveUpdateIfCopy
inside NpyIter_Deallocate
just before the call to Py_XDECREF(*object)
, but it causes a crash in the test_nditer.py
tests. It seems there are cases where the operand is not solely owned by the iterator, i.e. in any of the places NpyIter_GetOperandArray
, NpyIter_Copy
or others are called.
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.
As far as nditer is concerned, the context manager completely resolves any needed PyArray_ResolveUpdateIfCopy
call without relying on refcount semantics or modifying NpyIter_Deallocate
Now that I issue a warning when PyArray_ResolveUpdateIfCopy
is called during deallocation, it turns out I was misreading the actual place needing conversion to a context manager. Since I did not convert all the nditer() calls to context managers, and there were places in tests that reuse the same variable name, triggering the deallocation, I actually needed to convert the previous test to a context manager.
Again, once I reworked the python-level tests, no warnings of a call to PyArray_ResolveUpdateIfCopy
are emitted during nditer tests. However, there are a few places it seems PyArray_ResolveUpdateIfCopy
is still needed, see my comment below
added documentation, including doctest - how do I know if the doctest passes? |
We don't actually use doctests, so don't worry about it. Some other C-style stuff in there too (and probably more), but I would like to figure out that the context manager this way is right. |
I feel like either we're in the bad old mode of relying on the GC, in which case it's fine to wait for the array itself to be deallocated, it's worked so far and no need to mess with it, or else we're in the new good mode that does the explicit copy back, in which case there's again no reason to mess with functions named |
@njsmith I agree. If the solution of a context manager for python-level
It turns out that by adding the warning, I found a few more places that emit it. I am now trying to re-factor those to use the new
Once I can pass the test suite without raising the Warning I will update the pull request |
darn. I ironed out all the Python2 warnings by carefully looking at ufuncs and such that create nditers in C, but moving to Python3 it turns out
into code like this will be acceptable ...
Although on second thought the fact that changing c will only affect b and a when c is deleted is not really clear, and explicit trumps implicit? |
Not sure what you're seeing, but I see it on python 2 as well: Python 2.7.13 (default, Dec 18 2016, 20:19:42) [GCC 6.2.1 20161215] on linux2
>>> import numpy as np
>>> np.arange(10)[::2].flat.__array__().flags["UPDATEIFCOPY"]
True |
Do I understand correctly from #9269 (comment) that you (a) want to add a new flag that acts like UPDATEIFCOPY except for requiring explicit resolution, but (b) you want to start by adding explicit resolution to the existing UPDATEIFCOPY and then add the new flag in a future PR? If that's the plan, then I don't think we can start issuing DeprecationWarnings until after the new flag has landed, because the point of a DeprecationWarning is to nag downstream users to fix their code, and if the fix is for them to switch to the new flag, then we need the new flag before they can fix things :-) |
@njs Somewhere along the line I decided to deprecate the idea of a new flag, since in most cases such arrays are created with a direct call to
I am willing to consider a fallback position where all the above is still implemented (and required on PyPy), but the actual warning is only emitted if a environment/compliation flag is turned on, for full backward compatibility |
@mattip: UPDATEIFCOPY is also part of numpy's public C API -- there's probably third-party code that's calling Our rule is that we have to use noisy
|
OK, after reconsideration I propose we only lay the infrastructure in this pull request, interally refactor NumPy code where needed, and recommend using nditer as a context manager. So far all this is backward compatible on CPython but will raise Errors and Warnings on PyPy or if compiled with DEPRECATE_UPDATEIFCOPY defined. AFAICT this will be sufficient for me to work with downstream projects to fix their code for PyPy. Future enhancements will (each point is a separate enhancement/pull request/discussion):
Breaking this into pieces will make it all more managable. |
(edited comment to add " outside a context manager" in point about nditer) |
(edited comment to qualify 'backward compatible' WRT PyPy) |
Note that this pull request modified some (not all) of the tests in test_nditer.py to use a context manager so that they mostly pass on PyPy. Should I (1) revert this file, and make these changes part of future pull requests (2) completely rework the rest of the nditer calls to use a context manager since that is now recommended practice or (3) fix tests that are still causing warnings/errors on PyPy? |
I reworked much of this as pull request #9451, but things seem very quiet over there :( |
ping on pull request #9451which replaces this one? Anyone? |
Replaced by #9639 |
Fixes issue #7054 by refactoring the resolution of
UPDATEIFCOPY
into a new private API functionPyArray_ResolveUpdateIfCopy
. Add a call to the function wherever a temporaryndarray
with that flag is created, and added tests for calls that exercise the new function.In the second commit I made nditer into a context manager, where
PyArray_ResolveUpdateIfCopy
is called on__exit__()
, and updated the nditer tests to use the context manager wherever theupdateifcopy
flag is used.The third commit 4effdfb makes sure
clip
is called with amode
keyword, which seems to have been forgotten.The last commit f012769 explicitly uses the
__exit__
method to resolveupdateifcopy
for iterators created withnested_iters
, how can I make this look nicer?