Skip to content

Support non-constant exception values in JIT - #8134

Merged
sklam merged 52 commits into
numba:mainfrom
guilhermeleobas:guilhermeleobas/exception-handling
Mar 27, 2023
Merged

sklam merged 52 commits into
numba:mainfrom
guilhermeleobas:guilhermeleobas/exception-handling

Conversation

@guilhermeleobas

Copy link
Copy Markdown
Contributor

Fixes #7834

@guilhermeleobas
guilhermeleobas force-pushed the guilhermeleobas/exception-handling branch from 7354c1c to 6cc1ffe Compare June 7, 2022 03:41
@guilhermeleobas
guilhermeleobas force-pushed the guilhermeleobas/exception-handling branch from 6cc1ffe to fcb75e1 Compare June 7, 2022 14:10
@guilhermeleobas
guilhermeleobas force-pushed the guilhermeleobas/exception-handling branch from fb091ed to fcecc05 Compare June 9, 2022 03:33

@njriasan njriasan 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.

Thanks @guilhermeleobas this looks really good.

Comment thread a.py Outdated
Comment thread numba/core/callconv.py Outdated
Comment thread numba/core/callconv.py Outdated
Comment thread numba/core/consts.py Outdated
Comment thread numba/core/ir.py
"""
Raise an exception class and some argument *values* unknown at compile-time.
Note that if *exc_class* is None, a bare "raise" statement is implied
(i.e. re-raise the current exception).

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.

What are the situations in which someone can reraise an exception? I see that code like this below won't work because we can't do as e

import numba

@numba.njit
def f(lst):
    try:
        idx = lst[10]
    except KeyError as e:
        raise e

f([1])

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.

This sort of thing is permissible:

from numba import njit

@njit
def reraise():
    raise

def foo():
    try:
        raise ZeroDivisionError
    except ZeroDivisionError:
        reraise()

foo()

Comment thread numba/core/serialize.py Outdated
for i, runtime_arg in enumerate(runtime_args)]
obj = (exc, tuple(real_args), locinfo)
data = dumps(obj)
# Does it worth computing the hash?

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.

This is a good question. Adding this comment for visibility in future reviews.

@guilhermeleobas
guilhermeleobas marked this pull request as ready for review June 14, 2022 18:36

@stuartarchibald stuartarchibald 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.

Thanks for the patch @guilhermeleobas, it's good to see this feature added. I've given this patch an initial review, there's a lot of use cases that currently fail that I think should be addressed ahead of a more thorough review of the code/implementation. Thanks again!

Comment thread numba/_helperlib.c Outdated
Comment thread numba/core/callconv.py Outdated
Comment on lines +441 to +443
malloc_fn = cgutils.get_or_insert_function(builder.module, alloc_fnty, name="malloc")
struct_size = int32_t(self.context.get_abi_sizeof(excinfo_t))
ret = builder.call(malloc_fn, [struct_size])

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.

What free's this allocation?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've changed the code to use the NRT allocator, which I believe should manage the memory. Correct?

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.

The NRT allocator is this:

@_check_null_result
def allocate(self, builder, size):
"""
Low-level allocate a new memory area of `size` bytes. The result of the
call is checked and if it is NULL, i.e. allocation failed, then a
MemoryError is raised.
"""
return self.allocate_unchecked(builder, size)
def allocate_unchecked(self, builder, size):
"""
Low-level allocate a new memory area of `size` bytes. Returns NULL to
indicate error/failure to allocate.
"""
self._require_nrt()
mod = builder.module
fnty = ir.FunctionType(cgutils.voidptr_t, [cgutils.intp_t])
fn = cgutils.get_or_insert_function(mod, fnty, "NRT_Allocate")
fn.return_value.add_attribute("noalias")
return builder.call(fn, [size])

which calls NRT_Allocate and ends up in here:
extern "C" void* NRT_Allocate(size_t size) {
return NRT_Allocate_External(size, NULL);
}
extern "C" void* NRT_Allocate_External(size_t size, NRT_ExternalAllocator *allocator) {
void *ptr = NULL;
if (allocator) {
ptr = allocator->malloc(size, allocator->opaque_data);
NRT_Debug(nrt_debug_print("NRT_Allocate_External custom bytes=%zu ptr=%p\n", size, ptr));
} else {
ptr = TheMSys.allocator.malloc(size);
NRT_Debug(nrt_debug_print("NRT_Allocate_External bytes=%zu ptr=%p\n", size, ptr));
}
if (TheMSys.stats.enabled)
{
TheMSys.stats.alloc++;
}
return ptr;
}

which is essentially just a call to malloc. @sklam do you have a view on what to do with this? Perhaps a meminfo could be used.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

So, the memory is still leaking?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hi @stuartarchibald, once you have the chance, can you review this PR? I believe I have addressed the memory leak issue

Comment thread numba/core/pythonapi.py Outdated
Comment thread numba/tests/test_exceptions.py Outdated
Comment thread numba/core/callconv.py Outdated
Comment thread numba/core/callconv.py Outdated

v = builder.extract_value(builder.load(struct_gv_ptr), 0)
_len = builder.extract_value(builder.load(struct_gv_ptr), 1)
pybytes = pyapi.bytes_from_string_and_size(

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.

The following code makes a lot of use of the pyapi without checking if the function calls were successful?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed

Comment thread numba/core/callconv.py Outdated
Comment thread numba/core/callconv.py Outdated
no_pyobj_flags.nrt = True
self.check_raise_instance_with_runtime_args(flags=no_pyobj_flags)
no_pyobj_flags.nrt = False

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.

Examples of things that are currently not working correctly:

Unhashable:

from numba import njit
import numpy as np

@njit
def foo(z):
    raise ValueError('a', [1,])

foo(1)

Unhashable:

from numba import njit
import numpy as np

@njit
def foo(z):
    raise ValueError('a', ValueError('a'))

foo(1)

Incorrect arg reported: ValueError: i8* null

from numba import njit
import numpy as np

@njit
def foo(pred):
    raise ValueError({'a': 1, 'b': np.ones(4)})

foo(1)

Incorrect arg reported: ValueError: i8* null

from numba import njit
import numpy as np

@njit
def foo():
    raise ValueError(range(3))

foo()

Segfaults

from numba import njit
import numpy as np

@njit
def foo():
    raise ValueError({'a': 1, 'b': 2})

foo()

Segfaults:

from numba import njit
import numpy as np

@njit
def foo(rng):
    raise ValueError(rng.bit_generator)

rng = np.random.default_rng()
foo(rng)

Unhashable:

from numba import njit
import numpy as np

thing = np.ones(10)

@njit
def foo():
    raise ValueError(thing)

foo()

Unhashable:

from numba import njit, objmode
import numpy as np

@njit
def foo():
    raise ValueError(slice(1, 2))


foo()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The cases you marked as unhashable are not related to this PR as Numba type them as static raise. As for the other four cases, the errors are correctly treated.

Comment thread numba/core/callconv.py Outdated
@guilhermeleobas
guilhermeleobas marked this pull request as draft July 9, 2022 00:29

@stuartarchibald stuartarchibald 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.

Thanks for the updates @guilhermeleobas, unfortunately I've found an issue with boxing and environments (noted inline) that needs addressing. Otherwise this looks good once the few other comments/suggestions are addressed. Thanks again.

Comment thread numba/core/ir.py
Comment on lines 828 to 832
Similar to ``StaticRaise`` but does not terminate.
"""
Kind = 'static'

def __init__(self, exc_class, exc_args, loc):

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.

Why does DynamicTryRaise inherit from StaticTryRaise? Should DynamicTryRaise and StaticTryRaise both inherit from a _TryRaise ABC instead?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

DynamicTryRaise has the same methods and values as StaticTryRaise. If needed, I can just copy instead of inheriting.

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.

I think copy or create a common base, either is fine. Was just trying to avoid what I think would seem to be an unexpected inheritance relationship.

@guilhermeleobas guilhermeleobas Mar 23, 2023 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'll just copy. It's better than creating a forth *Raise class in ir.py.

Comment thread numba/tests/test_exceptions.py Outdated
Comment on lines +433 to +447

def test_try_raise(self):

@njit

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.

This fails if the argument to try_raise is a np.array (I tried a = np.ones(3)). There is an AttributeError in numba.core.boxing.box_array, line 429:

AttributeError: 'NoneType' object has no attribute 'read_cost'

(suspect this is because the env_manager is None?)

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.

The same thing also happens with the more simple:

@njit
def raise_(x):
    raise ValueError(x)

@njit
def foo(x):
    raise_(x)

foo(np.ones(3))

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.

I've got to what I think is the cause of this. Essentially when the code for unwrapping the dynamic exception is generated it has to emit code to box native representations into python objects, but the "environment manager" is missing as kwarg "env_manager=" here:

obj = pyapi.from_native_value(typ, val)

You can get an environment manager by calling self.context.get_env_manager(builder) but this isn't enough though because the emitted environment sentry needs to be aware that this is a Python context code region. I made this change locally to be able to wire this through:

@@ -123,13 +123,14 @@ class CPUContext(BaseContext):
             builder, envptr, _dynfunc._impl_info['offsetof_env_body'])
         return EnvBody(self, builder, ref=body_ptr, cast_ref=True)
 
-    def get_env_manager(self, builder):
+    def get_env_manager(self, builder, return_pyobject=False):
         envgv = self.declare_env_global(builder.module,
                                         self.get_env_name(self.fndesc))
         envarg = builder.load(envgv)
         pyapi = self.get_python_api(builder)
         pyapi.emit_environment_sentry(
             envarg, debug_msg=self.fndesc.env_name,
+            return_pyobject=return_pyobject
         )
         env_body = self.get_env_body(builder, envarg)
         return pyapi.get_env_manager(self.environment, env_body, envarg)

and then did this:

@@ -576,10 +576,12 @@ class CPUCallConv(BaseCallConv):
         nb_types = [typ for typ in nb_types if typ is not None]
 
         # convert native values into CPython objects
+        env_manager = self.context.get_env_manager(builder,
+                                                   return_pyobject=True)
         objs = []
         for i, typ in enumerate(nb_types):
             val = builder.extract_value(builder.load(st_ptr), i)
-            obj = pyapi.from_native_value(typ, val)
+            obj = pyapi.from_native_value(typ, val, env_manager=env_manager)

thoughts?

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.

oh, that's fine

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Thanks @stuartarchibald

No problem.

Comment thread numba/core/lowering.py
Comment thread numba/core/callconv.py
with cgutils.if_unlikely(builder, failed):
msg = ('Error creating Python tuple from runtime exception '
'arguments')
pyapi.err_set_string("PyExc_RuntimeError", msg)

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.

Suggested change
pyapi.err_set_string("PyExc_RuntimeError", msg)
pyapi.err_set_string("PyExc_RuntimeError", msg)
builder.ret(pyapi.get_null_object())

I'm not sure that this has any effect unless the error state is communicated through a return value of NULL.

Comment thread numba/core/callconv.py Outdated
Comment thread numba/core/callconv.py

@stuartarchibald stuartarchibald 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.

Two minor things, else looks good for a buildfarm test, thanks for all your efforts on this @guilhermeleobas.

Comment thread numba/core/ir.py Outdated
Comment thread numba/core/callconv.py Outdated
@stuartarchibald

Copy link
Copy Markdown
Contributor

I've checked/reviewed up to b1eb24b, looks good. Given this enables "dynamic" exceptions, last task is to remove the restriction note from the docs:

* ``raise SomeException(<arguments>)``: in :term:`nopython mode`, constructor
arguments must be :term:`compile-time constants <compile-time constant>`

(suggest just making it read raise SomeException(<arguments>), i.e. no restriction clause)

@stuartarchibald stuartarchibald 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.

@guilhermeleobas It's great to see this feature make it into Numba, thanks for all your work on it and for your persistence in getting it completed!

Patch approved pending buildfarm testing!

@guilhermeleobas

Copy link
Copy Markdown
Contributor Author

Thanks for all the help. It was a good journey to work on this feature.

@stuartarchibald stuartarchibald added the 4 - Waiting on CI Review etc done, waiting for CI to finish label Mar 23, 2023

@stuartarchibald stuartarchibald 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.

Thanks for the fixes @guilhermeleobas.

@stuartarchibald stuartarchibald removed the 4 - Waiting on author Waiting for author to respond to review label Mar 27, 2023
@stuartarchibald

Copy link
Copy Markdown
Contributor

Buildfarm ID: numba_smoketest_cpu_yaml_170.

@gmarkall

Copy link
Copy Markdown
Member

gpuci run tests

@gmarkall gmarkall 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.

I haven't reviewed the code, but this has my approval based on the gpuCI pass (i.e. it's not breaking CUDA exceptions).

@sklam

sklam commented Mar 27, 2023

Copy link
Copy Markdown
Member

numba_smoketest_cpu_yaml_170 passed

@sklam sklam added 5 - Ready to merge Review and testing done, is ready to merge and removed 4 - Waiting on CI Review etc done, waiting for CI to finish labels Mar 27, 2023
@sklam
sklam merged commit 201a2f8 into numba:main Mar 27, 2023
gmarkall added a commit to gmarkall/numba that referenced this pull request Apr 4, 2023
…eobas/exception-handling"

This reverts commit 201a2f8, reversing
changes made to e520da8.
@guilhermeleobas
guilhermeleobas deleted the guilhermeleobas/exception-handling branch April 19, 2023 17:35
@dlee992

dlee992 commented Dec 8, 2023 •

Copy link
Copy Markdown
Contributor

Hi, just leave a comment: this PR definitely helps us for debugging. Thanks! However, in some cases, it does hurt the execution time when with this PR. In principle, you think in which way this PR can affect the performance, compared with the old constant way?

The code changes in this PR seem fundamentally huge, but can we create a Numba envvar to turn it on/off?
cc @guilhermeleobas , btw, thanks again! An extremely useful PR!

@guilhermeleobas

Copy link
Copy Markdown
Contributor Author

Hi @dlee992, glad this work is helping you.

In principle, you think in which way this PR can affect the performance, compared with the old constant way?

Cannot think of an example of why this can cause a performance bottleneck. It may increase compilation time, but it shouldn't affect the execution. Do you have a reproducer that I can take a look at?

@dlee992

dlee992 commented Dec 8, 2023 •

Copy link
Copy Markdown
Contributor

Ah, I retried. When applying this PR locally, I also brought in the changes in #8535 in callconv.py. After I removed the changes from 8535 (sth related to changing the _return_errcode_raw), the execution time seems back to normal.

So accidentally, maybe I found the root cause of my another issue #9186. Maybe it's not llvm14 or this PR's fault, changes in _return_errcode_raw could be.

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 Effort - long Long size effort needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support passing non-constant values to Exceptions in JIT

6 participants