Skip to content

ENH,API: Make the errstate/extobj a contextvar #23936

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

Merged
merged 12 commits into from
Jun 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions doc/release/upcoming_changes/23936.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
* Being fully context and thread-safe, ``np.errstate`` can only
be entered once now. It is possible to enter it again after
exiting first.
* ``np.setbufsize`` is now tied to ``np.errstate()``: Leaving an
``np.errstate`` context will also reset the ``bufsize``.
5 changes: 5 additions & 0 deletions doc/release/upcoming_changes/23936.improvement.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
``np.errstate()`` is now faster and context safe
------------------------------------------------
The `np.errstate` context manager/decorator is now faster and
safer. Previously, it was not context safe and had (rarely)
issues with thread-safety.
25 changes: 0 additions & 25 deletions doc/source/reference/c-api/ufunc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,7 @@ UFunc API
Constants
---------

``UFUNC_ERR_{HANDLER}``
.. c:macro:: UFUNC_ERR_IGNORE

.. c:macro:: UFUNC_ERR_WARN

.. c:macro:: UFUNC_ERR_RAISE

.. c:macro:: UFUNC_ERR_CALL

``UFUNC_{THING}_{ERR}``
.. c:macro:: UFUNC_MASK_DIVIDEBYZERO

.. c:macro:: UFUNC_MASK_OVERFLOW

.. c:macro:: UFUNC_MASK_UNDERFLOW

.. c:macro:: UFUNC_MASK_INVALID

.. c:macro:: UFUNC_SHIFT_DIVIDEBYZERO

.. c:macro:: UFUNC_SHIFT_OVERFLOW

.. c:macro:: UFUNC_SHIFT_UNDERFLOW

.. c:macro:: UFUNC_SHIFT_INVALID

.. c:macro:: UFUNC_FPE_DIVIDEBYZERO

.. c:macro:: UFUNC_FPE_OVERFLOW
Expand Down
16 changes: 0 additions & 16 deletions numpy/__init__.cython-30.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -875,26 +875,10 @@ cdef extern from "numpy/ufuncobject.h":
PyUFunc_Zero
PyUFunc_One
PyUFunc_None
UFUNC_ERR_IGNORE
UFUNC_ERR_WARN
UFUNC_ERR_RAISE
UFUNC_ERR_CALL
UFUNC_ERR_PRINT
UFUNC_ERR_LOG
UFUNC_MASK_DIVIDEBYZERO
UFUNC_MASK_OVERFLOW
UFUNC_MASK_UNDERFLOW
UFUNC_MASK_INVALID
UFUNC_SHIFT_DIVIDEBYZERO
UFUNC_SHIFT_OVERFLOW
UFUNC_SHIFT_UNDERFLOW
UFUNC_SHIFT_INVALID
UFUNC_FPE_DIVIDEBYZERO
UFUNC_FPE_OVERFLOW
UFUNC_FPE_UNDERFLOW
UFUNC_FPE_INVALID
UFUNC_ERR_DEFAULT
UFUNC_ERR_DEFAULT2

object PyUFunc_FromFuncAndData(PyUFuncGenericFunction *,
void **, char *, int, int, int, int, char *, char *, int)
Expand Down
16 changes: 0 additions & 16 deletions numpy/__init__.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -833,26 +833,10 @@ cdef extern from "numpy/ufuncobject.h":
PyUFunc_Zero
PyUFunc_One
PyUFunc_None
UFUNC_ERR_IGNORE
UFUNC_ERR_WARN
UFUNC_ERR_RAISE
UFUNC_ERR_CALL
UFUNC_ERR_PRINT
UFUNC_ERR_LOG
UFUNC_MASK_DIVIDEBYZERO
UFUNC_MASK_OVERFLOW
UFUNC_MASK_UNDERFLOW
UFUNC_MASK_INVALID
UFUNC_SHIFT_DIVIDEBYZERO
UFUNC_SHIFT_OVERFLOW
UFUNC_SHIFT_UNDERFLOW
UFUNC_SHIFT_INVALID
UFUNC_FPE_DIVIDEBYZERO
UFUNC_FPE_OVERFLOW
UFUNC_FPE_UNDERFLOW
UFUNC_FPE_INVALID
UFUNC_ERR_DEFAULT
UFUNC_ERR_DEFAULT2

object PyUFunc_FromFuncAndData(PyUFuncGenericFunction *,
void **, char *, int, int, int, int, char *, char *, int)
Expand Down
13 changes: 0 additions & 13 deletions numpy/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -3154,19 +3154,6 @@ infty: Final[float]
nan: Final[float]
pi: Final[float]

ERR_IGNORE: L[0]
ERR_WARN: L[1]
ERR_RAISE: L[2]
ERR_CALL: L[3]
ERR_PRINT: L[4]
ERR_LOG: L[5]
ERR_DEFAULT: L[521]

SHIFT_DIVIDEBYZERO: L[0]
SHIFT_OVERFLOW: L[3]
SHIFT_UNDERFLOW: L[6]
SHIFT_INVALID: L[9]

FPE_DIVIDEBYZERO: L[1]
FPE_OVERFLOW: L[2]
FPE_UNDERFLOW: L[4]
Expand Down
141 changes: 55 additions & 86 deletions numpy/core/_ufunc_config.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,21 @@
"""
Functions for changing global ufunc configuration

This provides helpers which wrap `umath._geterrobj` and `umath._seterrobj`
This provides helpers which wrap `_get_extobj_dict` and `_make_extobj`, and
`_extobj_contextvar` from umath.
"""
import collections.abc
import contextlib
import contextvars

from .._utils import set_module
from .umath import (
UFUNC_BUFSIZE_DEFAULT,
ERR_IGNORE, ERR_WARN, ERR_RAISE, ERR_CALL, ERR_PRINT, ERR_LOG, ERR_DEFAULT,
SHIFT_DIVIDEBYZERO, SHIFT_OVERFLOW, SHIFT_UNDERFLOW, SHIFT_INVALID,
)
from . import umath
from .umath import _make_extobj, _get_extobj_dict, _extobj_contextvar

__all__ = [
"seterr", "geterr", "setbufsize", "getbufsize", "seterrcall", "geterrcall",
"errstate", '_no_nep50_warning'
]

_errdict = {"ignore": ERR_IGNORE,
"warn": ERR_WARN,
"raise": ERR_RAISE,
"call": ERR_CALL,
"print": ERR_PRINT,
"log": ERR_LOG}

_errdict_rev = {value: key for key, value in _errdict.items()}


@set_module('numpy')
def seterr(all=None, divide=None, over=None, under=None, invalid=None):
Expand Down Expand Up @@ -106,25 +93,14 @@ def seterr(all=None, divide=None, over=None, under=None, invalid=None):

"""

pyvals = umath._geterrobj()
old = geterr()

if divide is None:
divide = all or old['divide']
if over is None:
over = all or old['over']
if under is None:
under = all or old['under']
if invalid is None:
invalid = all or old['invalid']

maskvalue = ((_errdict[divide] << SHIFT_DIVIDEBYZERO) +
(_errdict[over] << SHIFT_OVERFLOW) +
(_errdict[under] << SHIFT_UNDERFLOW) +
(_errdict[invalid] << SHIFT_INVALID))

pyvals[1] = maskvalue
umath._seterrobj(pyvals)
old = _get_extobj_dict()
# The errstate doesn't include call and bufsize, so pop them:
old.pop("call", None)
old.pop("bufsize", None)

extobj = _make_extobj(
all=all, divide=divide, over=over, under=under, invalid=invalid)
_extobj_contextvar.set(extobj)
return old


Expand Down Expand Up @@ -168,17 +144,10 @@ def geterr():
>>> oldsettings = np.seterr(**oldsettings) # restore original

"""
maskvalue = umath._geterrobj()[1]
mask = 7
res = {}
val = (maskvalue >> SHIFT_DIVIDEBYZERO) & mask
res['divide'] = _errdict_rev[val]
val = (maskvalue >> SHIFT_OVERFLOW) & mask
res['over'] = _errdict_rev[val]
val = (maskvalue >> SHIFT_UNDERFLOW) & mask
res['under'] = _errdict_rev[val]
val = (maskvalue >> SHIFT_INVALID) & mask
res['invalid'] = _errdict_rev[val]
res = _get_extobj_dict()
# The "geterr" doesn't include call and bufsize,:
res.pop("call", None)
res.pop("bufsize", None)
return res


Expand All @@ -187,23 +156,19 @@ def setbufsize(size):
"""
Set the size of the buffer used in ufuncs.

.. versionchanged:: 2.0
The scope of setting the buffer is tied to the `np.errstate` context.
Exiting a ``with errstate():` will also restore the bufsize.

Parameters
----------
size : int
Size of buffer.

"""
if size > 10e6:
raise ValueError("Buffer size, %s, is too big." % size)
if size < 5:
raise ValueError("Buffer size, %s, is too small." % size)
if size % 16 != 0:
raise ValueError("Buffer size, %s, is not a multiple of 16." % size)

pyvals = umath._geterrobj()
old = getbufsize()
pyvals[0] = size
umath._seterrobj(pyvals)
old = _get_extobj_dict()["bufsize"]
extobj = _make_extobj(bufsize=size)
_extobj_contextvar.set(extobj)
return old


Expand All @@ -218,7 +183,7 @@ def getbufsize():
Size of ufunc buffer in bytes.

"""
return umath._geterrobj()[0]
return _get_extobj_dict()["bufsize"]


@set_module('numpy')
Expand Down Expand Up @@ -303,14 +268,9 @@ def seterrcall(func):
{'divide': 'log', 'over': 'log', 'under': 'log', 'invalid': 'log'}

"""
if func is not None and not isinstance(func, collections.abc.Callable):
if (not hasattr(func, 'write') or
not isinstance(func.write, collections.abc.Callable)):
raise ValueError("Only callable can be used as callback")
pyvals = umath._geterrobj()
old = geterrcall()
pyvals[2] = func
umath._seterrobj(pyvals)
old = _get_extobj_dict()["call"]
extobj = _make_extobj(call=func)
_extobj_contextvar.set(extobj)
return old


Expand Down Expand Up @@ -359,7 +319,7 @@ def geterrcall():
>>> old_handler = np.seterrcall(None) # restore original

"""
return umath._geterrobj()[2]
return _get_extobj_dict()["call"]


class _unspecified:
Expand Down Expand Up @@ -428,29 +388,38 @@ class errstate(contextlib.ContextDecorator):
>>> olderr = np.seterr(**olderr) # restore original state

"""

def __init__(self, *, call=_Unspecified, **kwargs):
self.call = call
self.kwargs = kwargs
__slots__ = [
"_call", "_all", "_divide", "_over", "_under", "_invalid", "_token"]

def __init__(self, *, call=_Unspecified,
all=None, divide=None, over=None, under=None, invalid=None):
self._token = None
self._call = call
self._all = all
self._divide = divide
self._over = over
self._under = under
self._invalid = invalid

def __enter__(self):
self.oldstate = seterr(**self.kwargs)
if self.call is not _Unspecified:
self.oldcall = seterrcall(self.call)
if self._token is not None:
raise TypeError("Cannot enter `np.errstate` twice.")
if self._call is _Unspecified:
extobj = _make_extobj(
all=self._all, divide=self._divide, over=self._over,
under=self._under, invalid=self._invalid)
else:
extobj = _make_extobj(
call=self._call,
all=self._all, divide=self._divide, over=self._over,
under=self._under, invalid=self._invalid)

self._token = _extobj_contextvar.set(extobj)

def __exit__(self, *exc_info):
seterr(**self.oldstate)
if self.call is not _Unspecified:
seterrcall(self.oldcall)


def _setdef():
defval = [UFUNC_BUFSIZE_DEFAULT, ERR_DEFAULT, None]
umath._seterrobj(defval)


# set the default values
_setdef()
_extobj_contextvar.reset(self._token)
# Allow entering twice, so long as it is sequential:
self._token = None


NO_NEP50_WARNING = contextvars.ContextVar("_no_nep50_warning", default=False)
Expand Down
24 changes: 0 additions & 24 deletions numpy/core/include/numpy/ufuncobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -231,34 +231,10 @@ typedef struct _tagPyUFuncObject {
/* flags inferred during execution */
#define UFUNC_CORE_DIM_MISSING 0x00040000

#define UFUNC_ERR_IGNORE 0
#define UFUNC_ERR_WARN 1
#define UFUNC_ERR_RAISE 2
#define UFUNC_ERR_CALL 3
#define UFUNC_ERR_PRINT 4
#define UFUNC_ERR_LOG 5

/* Python side integer mask */

#define UFUNC_MASK_DIVIDEBYZERO 0x07
#define UFUNC_MASK_OVERFLOW 0x3f
#define UFUNC_MASK_UNDERFLOW 0x1ff
#define UFUNC_MASK_INVALID 0xfff

#define UFUNC_SHIFT_DIVIDEBYZERO 0
#define UFUNC_SHIFT_OVERFLOW 3
#define UFUNC_SHIFT_UNDERFLOW 6
#define UFUNC_SHIFT_INVALID 9


#define UFUNC_OBJ_ISOBJECT 1
#define UFUNC_OBJ_NEEDS_API 2

/* Default user error mode */
#define UFUNC_ERR_DEFAULT \
(UFUNC_ERR_WARN << UFUNC_SHIFT_DIVIDEBYZERO) + \
(UFUNC_ERR_WARN << UFUNC_SHIFT_OVERFLOW) + \
(UFUNC_ERR_WARN << UFUNC_SHIFT_INVALID)

#if NPY_ALLOW_THREADS
#define NPY_LOOP_BEGIN_THREADS do {if (!(loop->obj & UFUNC_OBJ_NEEDS_API)) _save = PyEval_SaveThread();} while (0);
Expand Down
2 changes: 2 additions & 0 deletions numpy/core/src/common/umathmodule.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

#include "ufunc_object.h"
#include "ufunc_type_resolution.h"
#include "extobj.h" /* for the python side extobj set/get */


NPY_NO_EXPORT PyObject *
get_sfloat_dtype(PyObject *NPY_UNUSED(mod), PyObject *NPY_UNUSED(args));
Expand Down
Loading