Skip to content

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

Closed
wants to merge 8 commits into from
15 changes: 15 additions & 0 deletions numpy/add_newdocs.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,21 @@ def luf(lamdaexpr, *args, **kwargs):
>>> luf(lambda i,j:i*i + j/2, a, b)
array([ 0.5, 1.5, 4.5, 9.5, 16.5])

If operand flags such as "updateifcopy" or "readwrite" are used, or in
other edge cases, the operands may be views into the original data with the
UPDATEIFCOPY flag. In this case it is important to use the nditer as a
context manager. The temporary data will be written back to the original
data when the context manager's __exit__ function is called::

>>> a = np.arange(6, dtype='i4')[::-2]
>>> with nditer(a, [],
... [['writeonly', 'updateifcopy']],
... casting='unsafe',
... op_dtypes=[np.dtype('f4')]) as i:
... i.operands[0][:] = [-1, -2, -3]
>>> a
array([-1, -2, -3])

""")

# nditer methods
Expand Down
2 changes: 1 addition & 1 deletion numpy/core/code_generators/cversions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,5 @@
0x0000000a = 9b8bce614655d3eb02acddcb508203cb

# Version 11 (NumPy 1.13) Added PyArray_MapIterArrayCopyIfOverlap
# Version 11 (NumPy 1.14) No Change
# Version 11 (NumPy 1.14) Added PyArray_ResolveUpdateIfCopy
0x0000000b = edb1ba83730c650fd9bc5772a919cda7
2 changes: 2 additions & 0 deletions numpy/core/code_generators/numpy_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,8 @@
# End 1.10 API
'PyArray_MapIterArrayCopyIfOverlap': (301,),
# End 1.13 API
'PyArray_ResolveUpdateIfCopy': (302,),
# End 1.14 API
}

ufunc_types_api = {
Expand Down
2 changes: 2 additions & 0 deletions numpy/core/include/numpy/ndarraytypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

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 the nditer pywrap code? It seems to me that on the C-Side this should be triggered by the NpyIter_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 being NULL 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)

Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

@seberg "Why is there an explicit ResolveUpdateIfCopy in the nditer pywrap code? It seems to me that on the C-Side this should be triggered by the NpyIter_Deallocate call, ..."

Do you mean the call on line 2397 here in npyiter_exit?

Copy link
Member

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.

Copy link
Member

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).

Copy link
Member

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_Deallocs Py_DECREFs 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.

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 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_deallocis 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.

Copy link
Member Author

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


/*** Per-operand flags that may be passed to the iterator constructors ***/

Expand Down
82 changes: 61 additions & 21 deletions numpy/core/src/multiarray/arrayobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,53 @@ PyArray_TypeNumFromName(char *str)
return NPY_NOTYPE;
}

/*NUMPY_API
*
* If UPDATEIFCOPY and self has data, reset the base WRITEABLE flag,
* copy the local data to base, release the local data, and set flags
* appropriately. Return 0 if not relevant, 1 if success, < 0 on failure
*/
NPY_NO_EXPORT int
PyArray_ResolveUpdateIfCopy(PyArrayObject * self)
{
PyArrayObject_fields *fa = (PyArrayObject_fields *)self;
if (fa && fa->base) {
if (fa->flags & NPY_ARRAY_UPDATEIFCOPY) {
/*
* UPDATEIFCOPY means that fa->base's data
* should be updated with the contents
* of self.
* fa->base->flags is not WRITEABLE to protect the relationship
* unlock it.
*/
int retval = 0;
PyArray_ENABLEFLAGS(((PyArrayObject *)fa->base),
NPY_ARRAY_WRITEABLE);
PyArray_CLEARFLAGS(self, NPY_ARRAY_UPDATEIFCOPY);
retval = PyArray_CopyAnyInto((PyArrayObject *)fa->base, self);
if (retval < 0) {
/* this should never happen, how did the two copies of data
* get out of sync?
*/
return retval;
}
if ((fa->flags & NPY_ARRAY_OWNDATA) && fa->data) {
/* Free internal references if an Object array */
if (PyDataType_FLAGCHK(fa->descr, NPY_ITEM_REFCOUNT)) {
Py_INCREF(self); /*hold on to self */
PyArray_XDECREF(self);
Py_DECREF(self);
}
npy_free_cache(fa->data, PyArray_NBYTES(self));
fa->data = NULL;
PyArray_CLEARFLAGS(self, NPY_ARRAY_OWNDATA);
}
return 1;
}
}
return 0;
}

/*********************** end C-API functions **********************/

/* array object functions */
Expand All @@ -386,27 +433,21 @@ array_dealloc(PyArrayObject *self)
PyObject_ClearWeakRefs((PyObject *)self);
}
if (fa->base) {
/*
* UPDATEIFCOPY means that base points to an
* array that should be updated with the contents
* of this array upon destruction.
* fa->base->flags must have been WRITEABLE
* (checked previously) and it was locked here
* thus, unlock it.
*/
if (fa->flags & NPY_ARRAY_UPDATEIFCOPY) {
PyArray_ENABLEFLAGS(((PyArrayObject *)fa->base),
NPY_ARRAY_WRITEABLE);
Py_INCREF(self); /* hold on to self in next call */
if (PyArray_CopyAnyInto((PyArrayObject *)fa->base, self) < 0) {
PyErr_Print();
PyErr_Clear();
}
/*
* Don't need to DECREF -- because we are deleting
*self already...
*/
int retval;
Py_INCREF(self); /* hold on to self in next call */
retval = PyArray_ResolveUpdateIfCopy(self);
if (retval < 0)
{
PyErr_Print();
PyErr_Clear();
}
#if defined(PPY_VERSION) || defined(DEPRECATE_UPDATIFCOPY)
if (retval > 0) {
DEPRECATE("UPDATEIFCOPY resolution in array_dealloc is "
"incompatible with PyPy and will be removed in "
"the future");
}
#endif
/*
* In any case base is pointing to something that we need
* to DECREF -- either a view or a buffer object
Expand All @@ -426,7 +467,6 @@ array_dealloc(PyArrayObject *self)
}
npy_free_cache(fa->data, PyArray_NBYTES(self));
}

/* must match allocation in PyArray_NewFromDescr */
npy_free_cache_dim(fa->dimensions, 2 * fa->nd);
Py_DECREF(fa->descr);
Expand Down
3 changes: 3 additions & 0 deletions numpy/core/src/multiarray/calculation.c
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ PyArray_ArgMax(PyArrayObject *op, int axis, PyArrayObject *out)
Py_DECREF(ap);
/* Trigger the UPDATEIFCOPY if necessary */
if (out != NULL && out != rp) {
PyArray_ResolveUpdateIfCopy(rp);
Py_DECREF(rp);
rp = out;
Py_INCREF(rp);
Expand Down Expand Up @@ -251,6 +252,7 @@ PyArray_ArgMin(PyArrayObject *op, int axis, PyArrayObject *out)
Py_DECREF(ap);
/* Trigger the UPDATEIFCOPY if necessary */
if (out != NULL && out != rp) {
PyArray_ResolveUpdateIfCopy(rp);
Py_DECREF(rp);
rp = out;
Py_INCREF(rp);
Expand Down Expand Up @@ -1153,6 +1155,7 @@ PyArray_Clip(PyArrayObject *self, PyObject *min, PyObject *max, PyArrayObject *o
Py_XDECREF(maxa);
Py_DECREF(newin);
/* Copy back into out if out was not already a nice array. */
PyArray_ResolveUpdateIfCopy(newout);
Py_DECREF(newout);
return (PyObject *)out;

Expand Down
1 change: 1 addition & 0 deletions numpy/core/src/multiarray/cblasfuncs.c
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,7 @@ cblas_matrixproduct(int typenum, PyArrayObject *ap1, PyArrayObject *ap2,
Py_DECREF(ap2);

/* Trigger possible copyback into `result` */
PyArray_ResolveUpdateIfCopy(out_buf);
Py_DECREF(out_buf);

return PyArray_Return(result);
Expand Down
1 change: 1 addition & 0 deletions numpy/core/src/multiarray/compiled_base.c
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@ arr_insert(PyObject *NPY_UNUSED(self), PyObject *args, PyObject *kwdict)

Py_XDECREF(values);
Py_XDECREF(mask);
PyArray_ResolveUpdateIfCopy(array);
Py_DECREF(array);
Py_RETURN_NONE;

Expand Down
1 change: 1 addition & 0 deletions numpy/core/src/multiarray/ctors.c
Original file line number Diff line number Diff line change
Expand Up @@ -1801,6 +1801,7 @@ PyArray_FromAny(PyObject *op, PyArray_Descr *newtype, int min_depth,
}
else {
ret = (PyArrayObject *)PyArray_FromArray(arr, newtype, flags);
PyArray_ResolveUpdateIfCopy(arr); /* prevent spurious warnings */
Py_DECREF(arr);
}
}
Expand Down
4 changes: 4 additions & 0 deletions numpy/core/src/multiarray/item_selection.c
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ PyArray_TakeFrom(PyArrayObject *self0, PyObject *indices0, int axis,
Py_XDECREF(self);
if (out != NULL && out != obj) {
Py_INCREF(out);
PyArray_ResolveUpdateIfCopy(obj);
Py_DECREF(obj);
obj = out;
}
Expand Down Expand Up @@ -406,6 +407,7 @@ PyArray_PutTo(PyArrayObject *self, PyObject* values0, PyObject *indices0,
Py_XDECREF(values);
Py_XDECREF(indices);
if (copied) {
PyArray_ResolveUpdateIfCopy(self);
Py_DECREF(self);
}
Py_RETURN_NONE;
Expand Down Expand Up @@ -523,6 +525,7 @@ PyArray_PutMask(PyArrayObject *self, PyObject* values0, PyObject* mask0)
Py_XDECREF(values);
Py_XDECREF(mask);
if (copied) {
PyArray_ResolveUpdateIfCopy(self);
Py_DECREF(self);
}
Py_RETURN_NONE;
Expand Down Expand Up @@ -768,6 +771,7 @@ PyArray_Choose(PyArrayObject *ip, PyObject *op, PyArrayObject *out,
PyDataMem_FREE(mps);
if (out != NULL && out != obj) {
Py_INCREF(out);
PyArray_ResolveUpdateIfCopy(obj);
Py_DECREF(obj);
obj = out;
}
Expand Down
1 change: 1 addition & 0 deletions numpy/core/src/multiarray/iterators.c
Original file line number Diff line number Diff line change
Expand Up @@ -1151,6 +1151,7 @@ iter_richcompare(PyArrayIterObject *self, PyObject *other, int cmp_op)
return NULL;
}
ret = array_richcompare(new, other, cmp_op);
PyArray_ResolveUpdateIfCopy(new);
Py_DECREF(new);
return ret;
}
Expand Down
1 change: 1 addition & 0 deletions numpy/core/src/multiarray/multiarraymodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -1062,6 +1062,7 @@ PyArray_MatrixProduct2(PyObject *op1, PyObject *op2, PyArrayObject* out)
Py_DECREF(ap2);

/* Trigger possible copy-back into `result` */
PyArray_ResolveUpdateIfCopy(out_buf);
Py_DECREF(out_buf);

return (PyObject *)result;
Expand Down
1 change: 1 addition & 0 deletions numpy/core/src/multiarray/nditer_constr.c
Original file line number Diff line number Diff line change
Expand Up @@ -2927,6 +2927,7 @@ npyiter_allocate_arrays(NpyIter *iter,
Py_DECREF(temp);
return 0;
}
op_flags[iop] |= NPY_ITER_USE_CONTEXT_MANAGER;
}

Py_DECREF(op[iop]);
Expand Down
80 changes: 78 additions & 2 deletions numpy/core/src/multiarray/nditer_pywrap.c
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ struct NewNpyArrayIterObject_tag {
NpyIter *iter;
/* Flag indicating iteration started/stopped */
char started, finished;
/* inside context manager
0 == NO, not needed
1 == YES
2 == needed, (UPDATEIFCOPY flag is true). If this value is
reached and nditer API function called, fail on PyPy
3 == YES, and needed, all is good*/
char managed;
/* Child to update for nested iteration */
NewNpyArrayIterObject *nested_child;
/* Cached values from the iterator */
Expand Down Expand Up @@ -710,6 +717,12 @@ npyiter_convert_ops(PyObject *op_in, PyObject *op_flags_in,
*nop_out = 0;
return 0;
}
{
PyArrayObject_fields * fa = (PyArrayObject_fields*)ao;
if (fa->base && (fa->flags & NPY_ARRAY_UPDATEIFCOPY)) {
op_flags[iop] |= NPY_ITER_USE_CONTEXT_MANAGER;
}
}
Py_DECREF(op[iop]);
op[iop] = ao;
}
Expand Down Expand Up @@ -771,6 +784,13 @@ npyiter_init(NewNpyArrayIterObject *self, PyObject *args, PyObject *kwds)
goto fail;
}

self->managed = 0;
for (iop = 0; iop < nop; ++iop) {
if (op_flags[iop] & NPY_ITER_USE_CONTEXT_MANAGER)
self->managed |= 2; /* context manager recommended/required */
op_flags[iop] &= ~NPY_ITER_USE_CONTEXT_MANAGER;
}

/* op_request_dtypes */
if (op_dtypes_in != NULL && op_dtypes_in != Py_None &&
npyiter_convert_dtypes(op_dtypes_in,
Expand Down Expand Up @@ -1063,6 +1083,14 @@ NpyIter_NestedIters(PyObject *NPY_UNUSED(self),
Py_DECREF(ret);
goto fail;
}
#ifdef PYPY_VERSION
if (iter->managed == 2)
{
PyErr_SetString(PyExc_ValueError,
"'updateifcopy/copy' not supported on PyPy");
goto fail;
}
#endif

if (inest < nnest-1) {
iter->iter = NpyIter_AdvancedNew(nop, op, flags, order,
Expand Down Expand Up @@ -1404,7 +1432,13 @@ static PyObject *npyiter_value_get(NewNpyArrayIterObject *self)
"Iterator is past the end");
return NULL;
}

#ifdef PYPY_VERSION
if (self->managed == 2) {
PyErr_SetString(PyExc_ValueError,
"'updateifcopy', 'readwrite' flags must be used in a context manager");
return NULL;
}
#endif
nop = NpyIter_GetNOp(self->iter);

/* Return an array or tuple of arrays with the values */
Expand Down Expand Up @@ -1441,7 +1475,14 @@ static PyObject *npyiter_operands_get(NewNpyArrayIterObject *self)
"Iterator is invalid");
return NULL;
}

#ifdef PYPY_VERSION
if (self->managed == 2) {
PyErr_SetString(PyExc_ValueError,
"'updateifcopy', 'readwrite' flags must be used in a context manager");
return NULL;
}
#endif

nop = NpyIter_GetNOp(self->iter);
operands = self->operands;

Expand Down Expand Up @@ -2329,6 +2370,37 @@ npyiter_ass_subscript(NewNpyArrayIterObject *self, PyObject *op,
return -1;
}

static PyObject *
npyiter_self(NewNpyArrayIterObject *self)
{
if (self->iter == NULL)
{
PyErr_SetString(PyExc_ValueError, "operation on non-initialized iterator");
return NULL;
}
self->managed |= 1;
Py_INCREF(self);
return (PyObject *)self;
}

static PyObject *
npyiter_exit(PyObject *self, PyObject *args)
{
int iop, nop;
PyArrayObject **object;
NpyIter *iter = ((NewNpyArrayIterObject*)self)->iter;
if (iter == NULL)
Py_RETURN_NONE;
nop = NpyIter_GetNOp(iter);
object = NpyIter_GetOperandArray(iter);
for(iop = 0; iop < nop; ++iop, ++object) {
if (PyArray_ResolveUpdateIfCopy(*object) < 0)
return NULL;
}
Py_RETURN_NONE;
}


static PyMethodDef npyiter_methods[] = {
{"reset",
(PyCFunction)npyiter_reset,
Expand All @@ -2354,6 +2426,10 @@ static PyMethodDef npyiter_methods[] = {
{"debug_print",
(PyCFunction)npyiter_debug_print,
METH_NOARGS, NULL},
{"__enter__", (PyCFunction)npyiter_self,
METH_NOARGS, NULL},
{"__exit__", (PyCFunction)npyiter_exit,
METH_VARARGS, NULL},
{NULL, NULL, 0, NULL},
};

Expand Down
Loading