Skip to content

Allow libraries that implement __array_ufunc__ to override DUFunc.__c… - #8995

Merged
sklam merged 4 commits into
numba:mainfrom
jpivarski:nep13-for-untyped-DUFunc
Jul 24, 2023
Merged

sklam merged 4 commits into
numba:mainfrom
jpivarski:nep13-for-untyped-DUFunc

Conversation

@jpivarski

Copy link
Copy Markdown
Contributor

This PR addresses a long-standing issue in which array-like objects that define an __array_ufunc__ method (NEP-18) can't be used with ufuncs created by np.vectorize unless a signature is provided. (I should have done this years ago!)

For instance, in Awkward Array,

>>> import awkward as ak
>>> import numba as nb

>>> nb.vectorize([nb.float64(nb.float64)])(lambda x: x + 1)(ak.Array([[1.1, 2.2, 3.3], [], [4.4, 5.5]]))
<Array [[2.1, 3.2, 4.3], [], [5.4, 6.5]] type='3 * var * float64'>

but

>>> nb.vectorize()(lambda x: x + 1)(ak.Array([[1.1, 2.2, 3.3], [], [4.4, 5.5]]))
<stdin>:1: NumbaWarning: 
Compilation is falling back to object mode WITHOUT looplifting enabled because Function "<lambda>" failed type inference due to: non-precise type pyobject
During: typing of argument at <stdin> (1)

File "<stdin>", line 1:
<source missing, REPL/exec in use?>

/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/core/object_mode_passes.py:151: NumbaWarning: Function "<lambda>" was compiled in object mode without forceobj=True.

File "<stdin>", line 1:
<source missing, REPL/exec in use?>

  warnings.warn(errors.NumbaWarning(warn_msg,
/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/core/object_mode_passes.py:161: NumbaDeprecationWarning: 
Fall-back from the nopython compilation path to the object mode compilation path has been detected. This is deprecated behaviour that will be removed in Numba 0.59.0.

For more information visit https://numba.readthedocs.io/en/stable/reference/deprecation.html#deprecation-of-object-mode-fall-back-behaviour-when-using-jit

File "<stdin>", line 1:
<source missing, REPL/exec in use?>

  warnings.warn(errors.NumbaDeprecationWarning(msg,
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/np/ufunc/dufunc.py", line 206, in _compile_for_args
    return self._compile_for_argtys(tuple(argtys))
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/np/ufunc/dufunc.py", line 225, in _compile_for_argtys
    actual_sig = ufuncbuilder._finalize_ufunc_signature(
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/np/ufunc/ufuncbuilder.py", line 189, in _finalize_ufunc_signature
    raise TypeError("return type must be specified for object mode")
TypeError: return type must be specified for object mode

and even if we pass nopython=True,

>>> nb.vectorize(nopython=True)(lambda x: x + 1)(ak.Array([[1.1, 2.2, 3.3], [], [4.4, 5.5]]))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/np/ufunc/dufunc.py", line 206, in _compile_for_args
    return self._compile_for_argtys(tuple(argtys))
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/np/ufunc/dufunc.py", line 223, in _compile_for_argtys
    cres, argtys, return_type = ufuncbuilder._compile_element_wise_function(
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/np/ufunc/ufuncbuilder.py", line 176, in _compile_element_wise_function
    cres = nb_func.compile(sig, **targetoptions)
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/np/ufunc/ufuncbuilder.py", line 124, in compile
    return self._compile_core(sig, flags, locals)
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/np/ufunc/ufuncbuilder.py", line 157, in _compile_core
    cres = compiler.compile_extra(typingctx, targetctx,
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/core/compiler.py", line 742, in compile_extra
    return pipeline.compile_extra(func)
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/core/compiler.py", line 460, in compile_extra
    return self._compile_bytecode()
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/core/compiler.py", line 528, in _compile_bytecode
    return self._compile_core()
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/core/compiler.py", line 507, in _compile_core
    raise e
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/core/compiler.py", line 494, in _compile_core
    pm.run(self.state)
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/core/compiler_machinery.py", line 368, in run
    raise patched_exception
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/core/compiler_machinery.py", line 356, in run
    self._runPass(idx, pass_inst, state)
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/core/compiler_lock.py", line 35, in _acquire_compile_lock
    return func(*args, **kwargs)
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/core/compiler_machinery.py", line 311, in _runPass
    mutated |= check(pss.run_pass, internal_state)
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/core/compiler_machinery.py", line 273, in check
    mangled = func(compiler_state)
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/core/typed_passes.py", line 110, in run_pass
    typemap, return_type, calltypes, errs = type_inference_stage(
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/core/typed_passes.py", line 88, in type_inference_stage
    errs = infer.propagate(raise_errors=raise_errors)
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/core/typeinfer.py", line 1086, in propagate
    raise errors[0]
numba.core.errors.TypingError: Failed in nopython mode pipeline (step: nopython frontend)
non-precise type pyobject
During: typing of argument at <stdin> (1)

File "<stdin>", line 1:
<source missing, REPL/exec in use?>

Similarly, with the Sparse library,

>>> import numpy as np
>>> import sparse
>>> x = np.random.random((100, 100, 100))
>>> x[x < 0.9] = 0
>>> s = sparse.COO(x)

>>> nb.vectorize([nb.float64(nb.float64)], nopython=True)(lambda x: x + 100)(s)
<COO: shape=(100, 100, 100), dtype=float64, nnz=99450, fill_value=100.0>

>>> nb.vectorize([nb.float64(nb.float64)], nopython=True)(lambda x: x + 100)(s)[:2, :5, :8].todense()
array([[[100.        , 100.        , 100.        , 100.        ,
         100.        , 100.        , 100.        , 100.        ],
        [100.        , 100.        , 100.        , 100.        ,
         100.        , 100.        , 100.        , 100.        ],
        [100.        , 100.        , 100.        , 100.        ,
         100.        , 100.        , 100.        , 100.98842074],
        [100.        , 100.        , 100.        , 100.        ,
         100.        , 100.        , 100.        , 100.        ],
        [100.        , 100.        , 100.        , 100.        ,
         100.        , 100.99039874, 100.        , 100.        ]],

       [[100.        , 100.        , 100.        , 100.        ,
         100.        , 100.        , 100.        , 100.        ],
        [100.        , 100.        , 100.        , 100.        ,
         100.        , 100.        , 100.        , 100.        ],
        [100.        , 100.        , 100.        , 100.        ,
         100.        , 100.        , 100.99464796, 100.        ],
        [100.        , 100.        , 100.        , 100.        ,
         100.        , 100.        , 100.        , 100.        ],
        [100.        , 100.        , 100.        , 100.        ,
         100.        , 100.        , 100.95169938, 100.        ]]])

but

>>> nb.vectorize()(lambda x: x + 100)(s)
<stdin>:1: NumbaWarning: 
Compilation is falling back to object mode WITHOUT looplifting enabled because Function "<lambda>" failed type inference due to: non-precise type pyobject
During: typing of argument at <stdin> (1)

File "<stdin>", line 1:
<source missing, REPL/exec in use?>

/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/core/object_mode_passes.py:151: NumbaWarning: Function "<lambda>" was compiled in object mode without forceobj=True.

File "<stdin>", line 1:
<source missing, REPL/exec in use?>

  warnings.warn(errors.NumbaWarning(warn_msg,
/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/core/object_mode_passes.py:161: NumbaDeprecationWarning: 
Fall-back from the nopython compilation path to the object mode compilation path has been detected. This is deprecated behaviour that will be removed in Numba 0.59.0.

For more information visit https://numba.readthedocs.io/en/stable/reference/deprecation.html#deprecation-of-object-mode-fall-back-behaviour-when-using-jit

File "<stdin>", line 1:
<source missing, REPL/exec in use?>

  warnings.warn(errors.NumbaDeprecationWarning(msg,
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/np/ufunc/dufunc.py", line 206, in _compile_for_args
    return self._compile_for_argtys(tuple(argtys))
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/np/ufunc/dufunc.py", line 225, in _compile_for_argtys
    actual_sig = ufuncbuilder._finalize_ufunc_signature(
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/np/ufunc/ufuncbuilder.py", line 189, in _finalize_ufunc_signature
    raise TypeError("return type must be specified for object mode")
TypeError: return type must be specified for object mode

and

>>> nb.vectorize(nopython=True)(lambda x: x + 100)(s)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/np/ufunc/dufunc.py", line 206, in _compile_for_args
    return self._compile_for_argtys(tuple(argtys))
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/np/ufunc/dufunc.py", line 223, in _compile_for_argtys
    cres, argtys, return_type = ufuncbuilder._compile_element_wise_function(
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/np/ufunc/ufuncbuilder.py", line 176, in _compile_element_wise_function
    cres = nb_func.compile(sig, **targetoptions)
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/np/ufunc/ufuncbuilder.py", line 124, in compile
    return self._compile_core(sig, flags, locals)
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/np/ufunc/ufuncbuilder.py", line 157, in _compile_core
    cres = compiler.compile_extra(typingctx, targetctx,
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/core/compiler.py", line 742, in compile_extra
    return pipeline.compile_extra(func)
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/core/compiler.py", line 460, in compile_extra
    return self._compile_bytecode()
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/core/compiler.py", line 528, in _compile_bytecode
    return self._compile_core()
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/core/compiler.py", line 507, in _compile_core
    raise e
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/core/compiler.py", line 494, in _compile_core
    pm.run(self.state)
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/core/compiler_machinery.py", line 368, in run
    raise patched_exception
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/core/compiler_machinery.py", line 356, in run
    self._runPass(idx, pass_inst, state)
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/core/compiler_lock.py", line 35, in _acquire_compile_lock
    return func(*args, **kwargs)
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/core/compiler_machinery.py", line 311, in _runPass
    mutated |= check(pss.run_pass, internal_state)
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/core/compiler_machinery.py", line 273, in check
    mangled = func(compiler_state)
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/core/typed_passes.py", line 110, in run_pass
    typemap, return_type, calltypes, errs = type_inference_stage(
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/core/typed_passes.py", line 88, in type_inference_stage
    errs = infer.propagate(raise_errors=raise_errors)
  File "/home/jpivarski/mambaforge/lib/python3.9/site-packages/numba/core/typeinfer.py", line 1086, in propagate
    raise errors[0]
numba.core.errors.TypingError: Failed in nopython mode pipeline (step: nopython frontend)
non-precise type pyobject
During: typing of argument at <stdin> (1)

File "<stdin>", line 1:
<source missing, REPL/exec in use?>

However, with this PR, ufuncs created with @nb.vectorize and no type signature are recognized by Awkward Array:

>>> nb.vectorize()(lambda x: x + 1)(ak.Array([[1.1, 2.2, 3.3], [], [4.4, 5.5]]))
<Array [[2.1, 3.2, 4.3], [], [5.4, 6.5]] type='3 * var * float64'>

>>> nb.vectorize(nopython=True)(lambda x: x + 1)(ak.Array([[1.1, 2.2, 3.3], [], [4.4, 5.5]]))
<Array [[2.1, 3.2, 4.3], [], [5.4, 6.5]] type='3 * var * float64'>

and Sparse:

>>> nb.vectorize()(lambda x: x + 100)(s)
<COO: shape=(100, 100, 100), dtype=float64, nnz=100594, fill_value=100.0>

>>> nb.vectorize()(lambda x: x + 100)(s)[:2, :5, :8].todense()
array([[[100.        , 100.        , 100.        , 100.        ,
         100.        , 100.        , 100.        , 100.        ],
        [100.        , 100.        , 100.        , 100.        ,
         100.        , 100.        , 100.        , 100.96245334],
        [100.        , 100.        , 100.        , 100.        ,
         100.        , 100.        , 100.        , 100.        ],
        [100.        , 100.        , 100.        , 100.        ,
         100.        , 100.        , 100.        , 100.        ],
        [100.        , 100.        , 100.        , 100.        ,
         100.        , 100.        , 100.        , 100.        ]],

       [[100.        , 100.        , 100.        , 100.        ,
         100.        , 100.99515858, 100.        , 100.        ],
        [100.        , 100.        , 100.        , 100.        ,
         100.        , 100.        , 100.        , 100.        ],
        [100.        , 100.        , 100.95034604, 100.        ,
         100.        , 100.        , 100.        , 100.        ],
        [100.        , 100.96199577, 100.        , 100.        ,
         100.        , 100.        , 100.        , 100.        ],
        [100.        , 100.        , 100.        , 100.        ,
         100.        , 100.        , 100.        , 100.        ]]])

It's because __array_ufunc__ methods are usually implemented by pre- and post-processing the result of applying the ufunc to some underlying NumPy array. If an __array_ufunc__ method is implemented some other way, without Numba-compatible objects at any level, then @nb.vectorize-decorated functions wouldn't work with it, anyway.

This is a draft PR because:

  1. Please ensure that you have written units tests for the changes made/features added.

Some thought will be needed to know what kinds of tests would be appropriate. This mostly affects libraries that use Numba. Maybe a little class that implements __array_ufunc__?

  1. If you are closing an issue please use one of the automatic closing words as noted here: https://help.github.com/articles/closing-issues-using-keywords/

It could be related to #6678, #4625, and/or #2979. For completeness, the PR in its current state only addresses ufunc's __call__ method; perhaps we want accumulate, at, outer, reduce, and reduceat as well. Also, it only addresses np.vectorize; maybe something similar is needed for np.guvectorize.

I'm leaving it in this simple state so that I can get feedback about whether this is the right direction before proceeding to generalize it. (Also, which generalizations are important?)

@sklam

sklam commented Jun 6, 2023

Copy link
Copy Markdown
Member

@jpivarski, Thank you for the effort. This PR is definitely in the right direction.

Some thought will be needed to know what kinds of tests would be appropriate. This mostly affects libraries that use Numba. Maybe a little class that implements array_ufunc?

Yes, that would do. A minimal class to exercise the new code path in Dufunc would be sufficient.

It could be related to #6678, #4625, and/or #2979. For completeness, the PR in its current state only addresses ufunc's call method; perhaps we want accumulate, at, outer, reduce, and reduceat as well. Also, it only addresses np.vectorize; maybe something similar is needed for np.guvectorize.

I think we should keep this PR small and focus on __array_ufunc__ __call__ support and defer the support for other ufunc methods and guvectorize to different PRs.

In summary, I think all that's remaining for this PR are tests and a doc update.

@jpivarski
jpivarski marked this pull request as ready for review June 7, 2023 21:50
@jpivarski

Copy link
Copy Markdown
Contributor Author

I added a test (we'll see what happens in CI), though I'm not sure what to say where for a doc update. I never saw any documentation that says that ufuncs made by @vectorize aren't recognized by NEP13-overloaded arrays; it's something I just ran into. Do you have a suggestion of what text should be added?

Thanks!

@sklam

sklam commented Jun 9, 2023

Copy link
Copy Markdown
Member

For docs, I think it's worth adding a note in https://github.com/numba/numba/blob/main/docs/source/reference/jit-compilation.rst#vectorized-functions-ufuncs-and-dufuncs to say what level of NEP13 is supported.

@jpivarski

Copy link
Copy Markdown
Contributor Author

Okay, I think that's it: the assertions are unittest-style and I've added a paragraph at the end of the jit-compilation.rst reference.

@jpivarski

Copy link
Copy Markdown
Contributor Author

Is this waiting on anything from me? As far as I'm aware, it's done.

@sklam sklam added 4 - Waiting on reviewer Waiting for reviewer to respond to author and removed 2 - In Progress labels Jul 19, 2023

@sklam sklam left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the contribution!

@sklam sklam added 5 - Ready to merge Review and testing done, is ready to merge and removed 4 - Waiting on reviewer Waiting for reviewer to respond to author labels Jul 19, 2023
@sklam sklam added this to the Numba 0.58 RC milestone Jul 19, 2023
@sklam
sklam merged commit fc0371c into numba:main Jul 24, 2023
@gmarkall gmarkall mentioned this pull request Aug 1, 2023

@ianna ianna left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@jpivarski - I think the issue scikit-hep/awkward#3179 should be solved in Numba by adding similar functionality.

Comment thread numba/np/ufunc/dufunc.py
return output
else:
return super().__call__(*args, **kws)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@jpivarski - this PR implemented:

>>> import numba as nb
>>> @nb.vectorize()
... def _square(x):
...     return x * x
... 
>>> _square        
<numba._DUFunc '_square'>

we need to add similar functionality to a CUDAUFuncDispatcher that handles ufuncs on CUDA to allow the following:

>>> @nb.vectorize(
...,
... )
... def _square_cuda(x):
...     return x * x
... 
>>> _square_cuda
<numba.cuda.vectorizers.CUDAUFuncDispatcher object at 0x7259f82fa790>

@jpivarski

Copy link
Copy Markdown
Contributor Author

Maybe this function:

@classmethod
def call(cls, typemap, args, kws):
"""Perform the entire ufunc call mechanism.
"""
# Handle keywords
stream = kws.pop('stream', cls.DEFAULT_STREAM)
out = kws.pop('out', None)
if kws:
warnings.warn("unrecognized keywords: %s" % ', '.join(kws))
# Begin call resolution
cr = cls(typemap, args)
args = cr.get_arguments()
resty, func = cr.get_function()
outshape = args[0].shape
# Adjust output value
if out is not None and cr.is_device_array(out):
out = cr.as_device_array(out)
def attempt_ravel(a):
if cr.SUPPORT_DEVICE_SLICING:
raise NotImplementedError
try:
# Call the `.ravel()` method
return a.ravel()
except NotImplementedError:
# If it is not a device array
if not cr.is_device_array(a):
raise
# For device array, retry ravel on the host by first
# copying it back.
else:
hostary = cr.to_host(a, stream).ravel()
return cr.to_device(hostary, stream)
if args[0].ndim > 1:
args = [attempt_ravel(a) for a in args]
# Prepare argument on the device
devarys = []
any_device = False
for a in args:
if cr.is_device_array(a):
devarys.append(a)
any_device = True
else:
dev_a = cr.to_device(a, stream=stream)
devarys.append(dev_a)
# Launch
shape = args[0].shape
if out is None:
# No output is provided
devout = cr.allocate_device_array(shape, resty, stream=stream)
devarys.extend([devout])
cr.launch(func, shape[0], stream, devarys)
if any_device:
# If any of the arguments are on device,
# Keep output on the device
return devout.reshape(outshape)
else:
# Otherwise, transfer output back to host
return devout.copy_to_host().reshape(outshape)
elif cr.is_device_array(out):
# If output is provided and it is a device array,
# Return device array
if out.ndim > 1:
out = attempt_ravel(out)
devout = out
devarys.extend([devout])
cr.launch(func, shape[0], stream, devarys)
return devout.reshape(outshape)
else:
# If output is provided and it is a host array,
# Return host array
assert out.shape == shape
assert out.dtype == resty
devout = cr.allocate_device_array(shape, resty, stream=stream)
devarys.extend([devout])
cr.launch(func, shape[0], stream, devarys)
return devout.copy_to_host(out, stream=stream).reshape(outshape)

is missing __array_ufunc__ handling?

@gmarkall, I didn't see anything like that in the issues or PRs. Should @ianna open an issue?

(Should it go to this codebase or the new one, in which Numba-CUDA is an external project?)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

5 - Ready to merge Review and testing done, is ready to merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants