Skip to content

Commit 8f94b57

Browse files
Add blogpost about Numba dynamic exceptions (#747)
1 parent 627e84d commit 8f94b57

File tree

2 files changed

+174
-0
lines changed

2 files changed

+174
-0
lines changed
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
---
2+
title: 'Numba Dynamic Exceptions'
3+
published: June 27, 2023
4+
author: guilherme-leobas
5+
description: 'In the following blogpost, we will explore the newly added feature in Numba: Dynamic exception support. We will discuss the previous limitations and explain how Numba was enhanced to handle runtime exceptions.'
6+
category: [PyData ecosystem]
7+
featuredImage:
8+
src: /posts/enhancements-to-numba-guvectorize-decorator/blog_feature_var1.svg
9+
alt: 'An illustration of a brown and a dark brown hand coming towards each other to pass a business card with the logo of Quansight Labs.'
10+
hero:
11+
imageSrc: /posts/enhancements-to-numba-guvectorize-decorator/blog_hero_org.svg
12+
imageAlt: 'An illustration of a brown hand holding up a microphone, with some graphical elements highlighting the top of the microphone.'
13+
---
14+
15+
16+
[Numba 0.57](https://numba.readthedocs.io/en/stable/release-notes.html#version-0-57-0-1-may-2023) was recently released, and it added an important feature: dynamic exceptions. Numba now supports exceptions with runtime arguments. Since [version 0.13.2](https://numba.readthedocs.io/en/stable/release-notes.html#version-0-13-2), Numba had limited support for exceptions: arguments had to be compile-time constants.
17+
18+
Although Numba's focus is on compiling Python into fast machine code, there is still value in providing better support for exceptions. Improving support means that exception messages can now include more comprehensive content - for example, an `IndexError` can now include the index in the exception message.
19+
20+
## Past, present and future
21+
22+
Before Numba 0.57, exceptions were limited to compile-time constants only. This means that users could only raise exceptions in the following form:
23+
24+
```python
25+
from numba import njit
26+
27+
@njit
28+
def getitem(lst: list[int], idx: int):
29+
if idx >= len(lst):
30+
raise IndexError('list index out of range')
31+
return lst[idx]
32+
```
33+
34+
Attempting to raise an exception with runtime values in versions prior to 0.57 would result in a compilation error:
35+
36+
```python
37+
from numba import njit
38+
39+
@njit
40+
def getitem(lst: list[int], index: int):
41+
if index >= len(lst):
42+
raise IndexError(f'list index "{index}" out of range')
43+
return lst[index]
44+
```
45+
46+
```bash
47+
$ python -c 'import numba; print(numba.__version__)'
48+
0.56.4
49+
50+
$ python example.py
51+
Traceback (most recent call last):
52+
File "/Users/guilhermeleobas/git/blog/example.py", line 13, in <module>
53+
print(getitem(lst, index))
54+
File "/Users/guilhermeleobas/miniconda3/envs/numba056/lib/python3.10/site-packages/numba/core/dispatcher.py", line 480, in _compile_for_args
55+
error_rewrite(e, 'constant_inference')
56+
File "/Users/guilhermeleobas/miniconda3/envs/numba056/lib/python3.10/site-packages/numba/core/dispatcher.py", line 409, in error_rewrite
57+
raise e.with_traceback(None)
58+
numba.core.errors.ConstantInferenceError: Failed in nopython mode pipeline (step: nopython rewrites)
59+
Constant inference not possible for: $24build_string.6 + $const22.5
60+
61+
File "example.py", line 7:
62+
def getitem(lst: list[int], index: int):
63+
<source elided>
64+
if index >= len(lst):
65+
raise IndexError(f'list index "{index}" out of range')
66+
^
67+
```
68+
69+
This example works just fine in the latest release.
70+
71+
```python
72+
$ python -c 'import numba; print(numba.__version__)'
73+
0.57.0
74+
75+
$ python example.py
76+
Traceback (most recent call last):
77+
File "/Users/guilhermeleobas/git/blog/example.py", line 13, in <module>
78+
print(getitem(lst, index))
79+
File "/Users/guilhermeleobas/git/blog/example.py", line 7, in getitem
80+
raise IndexError(f'list index "{index}" out of range')
81+
IndexError: list index "4" out of range
82+
```
83+
84+
In the future, Numba users can expect better exception messages raised from Numba overloads and compiled code.
85+
86+
## How does it work?
87+
88+
Numba is a JIT compiler that translates a subset of Python into machine code. This translation step is done using [LLVM](https://llvm.org/). When Numba compiled code raises an exception, it must signal to the interpreter and propagate any required information back. The calling convention for **CPU** targets specifies how signaling is done:
89+
90+
```c
91+
retcode_t (<Python return type>*, excinfo_t **, ... <Python arguments>)
92+
```
93+
94+
The return code is one of the `RETCODE_*` constants in the [callconv.py](https://github.com/numba/numba/blob/main/numba/core/callconv.py#L47-L55) file.
95+
96+
<p align="center">
97+
<img
98+
alt="Control flow of execution when an exception is raised"
99+
src="/posts/numba-dynamic-exceptions/diagram.png" />
100+
<br /><i>Figure contains a high-level illustration of the control flow
101+
when a Numba function raises an exception.</i>
102+
</p>
103+
104+
### Static Exceptions
105+
106+
When an exception is raised, the struct `excinfo_t**` is filled with a pointer to a struct describing the raised exception. Before Numba 0.57, this struct contained three fields:
107+
108+
- A pointer (`i8*`) to a pickled string.
109+
- String size (`i32`).
110+
- Hash (`i8*`) of this same string.
111+
112+
Take for instance the following snippet of code:
113+
114+
```python
115+
@jit(nopython=True)
116+
def func():
117+
raise ValueError('exc message')
118+
```
119+
120+
The triple `(ValueError, 'exc message', location)` is pickled and serialized to the [LLVM module](https://llvm.org/docs/LangRef.html#module-structure) as a constant string. When the exception is raised, this same serialized string is unpickled by the interpreter (1) and a frame is created for the exception (2).
121+
122+
### Dynamic Exceptions
123+
124+
To support dynamic exceptions, we reuse all the existing fields and introduce two new ones.
125+
126+
- A pointer (`i8*`) to a pickled string containing static information.
127+
- String size (`i32`).
128+
- The third argument (`i8*`), which was previously used for hashing is now used to hold a list of native values.
129+
- A pointer to a function (`i8*`) that knows how to convert native values back to Python values. This is called [boxing](https://numba.pydata.org/numba-doc/dev/extending/interval-example.html#boxing-and-unboxing).
130+
- A flag (`i32`) to signal whether an exception is static or dynamic. A value greater than zero not only indicates whether it is a dynamic exception, but also the number of runtime arguments.
131+
132+
Using Python code, dynamic exceptions work as follows:
133+
134+
```python
135+
@jit(nopython=True)
136+
def dyn_exc_func(s: str):
137+
raise TypeError('error', s, len(s))
138+
```
139+
140+
For each dynamic exception, Numba will generate a function that boxes native values into Python types. In the example above, `__exc_conv` will be generated automatically:
141+
142+
```python
143+
def __exc_conv(s: native_string, i: int64) -> Tuple[str, int]:
144+
# convert
145+
py_string: str = box(s)
146+
py_int: int = box(i)
147+
return (py_string, py_int)
148+
```
149+
150+
The code mentioned earlier is used for illustrative purposes. However, in practice, `__exc_conv` is implemented as native code.
151+
152+
The `excinfo` struct will be filled with:
153+
154+
- Pickled string of compile-time information: (exception type, static arguments, location).
155+
- String size.
156+
- A list of dynamic arguments: `[native string, int64]`.
157+
- A pointer to `__exc_conv`.
158+
- Number of dynamic arguments: `2`.
159+
160+
During runtime, just before the control flow is returned to the interpreter, function `__exc_conv` is invoked to convert native `string/int` values into their equivalent Python `str/int` types. At this stage, the interpreter also unpickles constant information, and both static and dynamic arguments are combined into a unified list (3).
161+
162+
I encourage anyone interested in further details to read the comments left on `callconv.py::CPUCallConv` ([ref](https://github.com/numba/numba/blob/c9cc06ba1410aff242764ffde8387a1bef2180ae/numba/core/callconv.py#L411-L444)).
163+
164+
## Limitations and future work
165+
166+
Numba has a [page](https://numba.readthedocs.io/en/stable/reference/pysupported.html#exception-handling) describing what is supported in exception handling. Some work still needs to be done to support exceptions to their full extent.
167+
168+
We would like to thank [Bodo](https://bodo.ai) for sponsoring this work and the Numba core developers and community for reviewing this work and the useful insights given during code review.
169+
170+
## References
171+
172+
* (1) [`numba/core/serialize.py::_numba_unpickle`](https://github.com/numba/numba/blob/82d3cbb8818b43dc66e5dd4bb38355eaf25131be/numba/core/serialize.py#L30-L49)
173+
* (2) [`numba/_helperlib.c::numba_do_raise`](https://github.com/numba/numba/blob/39fc546dda0a21b90432e60f3c5e8c34f7892024/numba/_helperlib.c#L995-L1025)
174+
* (3) [`numba/core/serialize.py::runtime_build_excinfo_struct`](https://github.com/numba/numba/blob/82d3cbb8818b43dc66e5dd4bb38355eaf25131be/numba/core/serialize.py#L64-L73)
52.5 KB
Loading

0 commit comments

Comments
 (0)