Repository navigation
Allow Basix functions to be called from Numba kernels - #1096
garth-wells wants to merge 2 commits into
Conversation
Tabulation and the push-forward/pull-back maps are exposed as native functions that a Numba kernel compiled in nopython mode can call directly, so that loops over large numbers of points run at native speed rather than paying a Python call per point. The entry points use no Python API and let no C++ exception escape into JIT-compiled code, reporting failure through their return value instead. They are registered with LLVM by symbol name, so kernels that call them can still be cached by Numba. The map entry point writes into a buffer supplied by the caller and performs no allocation, and derives the reference and physical value sizes from the element and the geometric dimension, which differ when gdim is not equal to tdim. An element is passed to a kernel as the address of the underlying C++ object. ElementHandle holds that address together with a reference to the element, so that the element cannot be collected while a kernel is using it. FiniteElement.tabulate_shape is added to the Python interface, since the size of the tabulation buffer cannot otherwise be computed from Python. Fixes #201. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The demo runs in a subprocess, so importing Numba in the test process only loaded its shared libraries into the process that valgrind traces. glibc's loader leaks a small allocation per dlopen, which the valgrind job reports as definitely lost and fails on. Check for Numba with importlib.util.find_spec, which finds the module without executing it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| # ================================== | ||
| # | ||
| # Tabulating an element one point at a time from Python is dominated by | ||
| # the cost of the Python call, not by the mathematics. A loop that |
There was a problem hiding this comment.
not by the mathematics - not by the calculations performed within the call.
| # cells are affine, so the pull-back is the inverse of the Jacobian | ||
| # ``J = [v1 - v0, v2 - v0]`` applied to ``x - v0``. | ||
| # | ||
| # Because the tabulation function does not touch the Python interpreter, |
There was a problem hiding this comment.
This is a bit roundabout and I'm not sure about the GIL because this doesn't appear to be threaded?
I would propose "the numba code directly calls the Basix C++ function without passing back through the Python interpreter, leading to no Python function call overhead."
| - ``out`` is the address of the output buffer, shape | ||
| ``(num_jacobians, num_points, vs)``. | ||
|
|
||
| The value size ``vs`` is the reference value size on the reference |
There was a problem hiding this comment.
This comment is 'standard knowledge'.
| # application would locate each point in the mesh with a bounding box | ||
| # tree; here we pick a cell and a position within it, so that the cell |
There was a problem hiding this comment.
Not just a bounding box tree, it would also have to check for containment within the cell by using a pull back or closest point projection.
| J = np.zeros((num_mapped_cells, 2, 2)) | ||
| for c in range(num_mapped_cells): | ||
| v0, v1, v2 = points[cells[c]] | ||
| J[c] = np.column_stack([v1 - v0, v2 - v0]) | ||
| detJ = np.array([np.linalg.det(J[c]) for c in range(num_mapped_cells)]) | ||
| K = np.array([np.linalg.inv(J[c]) for c in range(num_mapped_cells)]) |
There was a problem hiding this comment.
Shouldn't this rather use the basis functions to compute the jacobian, so this generalizes to higher order or non-affien geometries?
Tabulation and the push-forward/pull-back maps are exposed as native functions that a Numba kernel compiled in
nopythonmode can call directly, so that loops over large numbers of points run at native speed instead of paying a Python call per point.Until now
basix.numba_helpersonly provided DOF transformations, reimplemented in Python. Anything else had to be reimplemented the same way, which is what #201 asks to avoid.Interface
numba.types.ExternalFunction, so kernels that call them are still cacheable. Binding the raw address throughctypesinstead makes Numba refuse to cache the kernel (Cannot cache compiled function ... uses dynamic globals).ElementHandleholds the address of the C++ element together with a reference to the element, so the element cannot be collected while a kernel is using it.push_forward/pull_back. It derives the reference and physical value sizes from the element and the geometric dimension, which differ whengdim != tdim— a contravariant Piola mapped element on a triangle in 3D has reference value size 2 and physical value size 3.FiniteElement.tabulate_shapeis added to the Python interface, since the size of the tabulation buffer cannot otherwise be computed from Python.Demo
demo/python/demo_numba_kernels.pyassembles the interpolation matrix from a degree 2 Lagrange space to a set of scattered points, which is the use case raised in #201. One kernel pulls each point back to the reference cell, tabulates, and scatters the basis values into a matrix row. On 200,000 points:Its demo test is guarded with
importorskip, since Numba is not installed on every platform Basix supports.Testing
43 cases in
test/test_numba.py: tabulation againstFiniteElement.tabulatefor float32 and float64, derivative orders 0 to 2 and five elements; maps againstpush_forward/pull_backincluding three cases wheregdim != tdim; a cache test that promotesNumbaWarningto an error; handle lifetime and dtype rejection; and an error path using a custom L2 Piola element, where an unsupported map is reported rather than letting the C++ exception escape.Closes #201.
🤖 Generated with Claude Code