numba-vulkan
A Vulkan compute target for Numba: write a kernel in Python, run it on any GPU with a Vulkan driver.
Background: numba/numba#10116 »
Documentation
·
Usage
·
Benchmarks
·
Roadmap
Table of Contents
About The Project
Numba's only maintained GPU target is CUDA, which ties GPU-accelerated Numba code to NVIDIA hardware. Supporting every vendor separately is a lot of work; Vulkan is implemented by nearly all of them.
numba-vulkan registers a vulkan target through Numba's target extension API
and compiles numba.cuda-style kernels to SPIR-V compute shaders. The same
kernel has been run on an NVIDIA GPU, an Intel integrated GPU and a CPU
rasteriser (llvmpipe), with results checked against NumPy.
This is alpha software. Its test suite passes on NVIDIA and Intel GPUs and llvmpipe on Linux, on an Apple M3 Pro through MoltenVK, and on Windows with lavapipe, and the benchmarks run on an Intel Arc GPU under Windows; but its interfaces may still change and it has not yet been proven on real workloads.
Built With
- Numba: bytecode frontend, type inference, lowering and the target extension API
- libclc, LLVM's OpenCL math library, linked into kernels for accurate and double-precision math
- llvmlite 0.50+ (LLVM 22), whose SPIR-V backend emits Vulkan-flavoured SPIR-V
- vulkan: Python bindings for the Vulkan API
- devenv and uv for the development environment
How It Works
@nv.jitcreates a dispatcher for thevulkantarget. Numba's own pipeline (bytecode analysis, type inference, lowering) produces LLVM IR.- Arrays are a custom Numba type that carries the descriptor binding of its storage buffer, because shaders have no general pointers. Element access is emitted as a placeholder call taking a binding and an index.
- Everything is inlined into one entry point and simplified with a few LLVM passes. Only then are the placeholders expanded into buffer accesses.
- LLVM's SPIR-V backend turns the result into a compute shader. It runs in a child process, so a backend failure raises an exception instead of aborting Python.
- The runtime uploads the arguments (binding 0: error status, binding
1 + k: array argumentk; scalars and array shapes as push constants), dispatches, and copies written buffers back. Device arrays are used in place.
Getting Started
Prerequisites
- libclc's
clspv--.bcfrom LLVM 22 or older, for float64 and accurate float32 math. Wheels bundle it and the devenv shell provides it; only a bare source checkout needs it installed (see the math library docs). - A Vulkan 1.2 driver. lavapipe (Mesa's CPU implementation) is enough to try it without a GPU.
- Python 3.11 or newer.
- Optional:
spirv-valfrom SPIRV-Tools to validate generated shaders.
Installation
From PyPI, with the Vulkan loader (libvulkan1 on Debian and Ubuntu,
vulkan-loader on Fedora and Arch) and a Vulkan driver for your GPU
installed:
pip install numba-vulkan # or: uv add numba-vulkan
python -c "import numba_vulkan as nv; print(nv.list_devices())"
The wheel bundles libclc for accurate and float64 math. On macOS it also
needs MoltenVK and an llc of LLVM 22 named by NUMBA_VULKAN_LLC (KI-32);
see below.
To work on numba-vulkan itself: with devenv (NixOS or any system with Nix), everything including the Vulkan loader and SPIRV-Tools is provided by the shell:
devenv shell
uv run pytest
Without Nix, install the Vulkan loader from your distribution, then:
uv sync
uv run pytest
On macOS, Vulkan runs on MoltenVK, and llvmlite's SPIR-V backend has to be
replaced by an llc of LLVM 22 (KI-32). The devenv shell sets up both
(devenv shell -- uv run pytest); the test suite passes on an Apple M3 Pro.
See Getting started
for the details and a Homebrew alternative.
The test suite runs every test on every Vulkan device it finds.
Documentation
The Sphinx documentation is published at https://arunoruto.github.io/numba-vulkan/ and covers usage, the design, the limitations, the known issues and the benchmarks. To build it locally:
uv sync --group docs
cd docs && uv run sphinx-build -M html ./source ./build
Usage
A kernel looks like a numba.cuda kernel. global_id(axis) plays the role of
cuda.grid, and forall launches it over a 1-d to 3-d grid:
import numpy as np
import numba_vulkan as nv
@nv.jit
def saxpy(a, x, y):
i = nv.global_id(0)
if i < x.shape[0]:
y[i] = a * x[i] + y[i]
x = np.arange(1000, dtype=np.float32)
y = np.ones(1000, dtype=np.float32)
saxpy.forall(x.size)(np.float32(2.0), x, y)
NumPy arrays are copied to the device and back on every call. Device arrays
stay there, as with numba.cuda:
dx, dy = nv.to_device(x), nv.to_device(y)
saxpy.forall(x.size)(np.float32(2.0), dx, dy) # no copies
saxpy.forall(x.size)(np.float32(0.5), dx, dy)
y = dy.copy_to_host()
Functions compiled with @nv.jit can be called from kernels, may return
values or tuples, and can take arrays. Functions compiled with numba.njit
are recompiled for Vulkan when a kernel calls them:
import math
from numba import njit
@njit
def norm(x, y):
return math.sqrt(x * x + y * y)
@nv.jit
def radius(x, y, out):
i = nv.global_id(0)
if i < out.size:
out[i] = norm(x[i], y[i])
Numba's ufunc decorators work too, without writing a kernel:
from numba import vectorize, guvectorize
@vectorize(["float32(float32, float32)"], target="vulkan")
def hypot(a, b):
return math.sqrt(a * a + b * b)
@guvectorize(["void(float32[:], float32[:], float32[:])"], "(n),(n)->()",
target="vulkan")
def dot(a, b, out):
out[0] = np.dot(a, b)
hypot(x, y) # element-wise, with broadcasting
dot(matrix, vector) # one dot product per row
Cooperating kernels use the same building blocks as in numba.cuda:
workgroup-shared arrays, barriers, atomics and an explicit launch
configuration:
@nv.jit
def block_sums(x, out):
partial = nv.shared.array(256, np.float32)
t, i = nv.local_id(0), nv.global_id(0)
partial[t] = x[i] if i < x.shape[0] else np.float32(0)
nv.barrier()
step = 128
while step > 0:
if t < step:
partial[t] += partial[t + step]
nv.barrier()
step //= 2
if t == 0:
nv.atomic.add(out, 0, partial[0])
block_sums[groups, 256](x, out) # like kernel[blocks, threads] in CUDA
Because it is a real Numba target, @overload works with it:
from numba.extending import overload
@overload(my_function, target="vulkan")
def ol_my_function(x):
return lambda x: x * 2
Choosing a device:
nv.list_devices() # [<0: NVIDIA ... (discrete)>, <1: ...>, ...]
nv.select_device("intel") # by name, index, or NUMBA_VULKAN_DEVICE
saxpy.forall(n, device="llvmpipe")(a, x, y)
Inspecting what was generated:
from numba import float32
compiled = saxpy.compile((float32, float32[::1], float32[::1]))
compiled.llvm_ir, compiled.spirv, compiled.capabilities
Set NUMBA_VULKAN_VALIDATE=1 to run every generated shader through
spirv-val before it reaches a driver.
Benchmarks
Two suites compare numba-vulkan with Numba's CPU target and numba-cuda: the
same scalar function compiled for every backend (benchmarks/bench.py), and
the same CUDA-style kernels with shared memory, barriers and atomics
(benchmarks/kernels.py). Results are collected in the repository, one file
per run, and charted in the
documentation,
including every backend, device and variant and how results change over
time. The fastest result per backend, GPUs with data on the device (log
scale; further left is faster):
In the first collected run (Intel i9-9900K, NVIDIA TITAN X, Intel UHD 630):
- With data on the device, numba-vulkan takes 25 to 55 % longer than numba-cuda on the same card on most workloads, and is about level on the tiled matrix product (1.85 against 1.75 ms). Repeated runs vary by 10 to 20 %, so small differences are not meaningful.
- With NumPy arrays as arguments, the copies dominate: the memory-bound
saxpytakes 7 to 10 ms on every GPU backend and 1.8 ms on one CPU thread. - A kernel's first call, which compiles it, takes 0.04 to 0.25 s on Vulkan, comparable to numba-cuda.
On an Apple M3 Pro (MoltenVK 1.4.2, kernels translated by llc, KI-32),
with data on the device:
- The compute-bound Mandelbrot set takes 7 to 9 ms, about level with the TITAN X; within a full run the GPU shares the chip's power budget with the CPU benchmarks before it, which costs up to 20 %.
- A launch that waits for its result costs about 0.2 ms, so the small
workloads mostly measure that:
saxpytakes 0.61 ms, about the same as the CPU. - The tiled matrix product reaches only about 185 GFLOP/s (11.7 ms), the clearest candidate for tuning.
Please add a run from your machine; it takes a few minutes:
uv run python benchmarks/collect.py --user <your GitHub name>
What Works and What Does Not
Works:
int32/int64/float32/float64/boolscalars and N-d arrays ofbool, 8 to 64-bit integers,float32andfloat64- indexing with integers and slices; slices are views, as in NumPy, and can
be iterated over, passed to functions, assigned to and reduced
(
a[i, 1:-1].sum(),for v in a[i],out[i, :3] = 0,np.dot,.T) - arithmetic, comparisons, bit operations, casts, tuples, complex numbers,
if/while/for ... in range(...), earlyreturns,min/max/abs - the
mathmodule in float32 and float64 (includinghypot,log1p,erf,gamma,isnan...), accurate to the last digit and consistent across devices, and the**operator - NumPy functions on scalars, such as
np.sqrt(x[i])ornp.maximum(a, b) - calling other
@nv.jitand@njitfunctions;@overload(target="vulkan")
Does not work:
- float64 math without libclc. Vulkan's math library is 32-bit only, so
sin,exp,powand friends in float64 come from libclc, LLVM's OpenCL math library. Wheels bundle it; in a source checkout without it such calls raise, unless you opt in to float32 precision with@nv.jit(narrow_math=True). - Arrays of run-time size in kernels (
a[mask],a.copy(),np.zeros(n)),try/except, recursion. Arrays of constant shape, array expressions such asa * 2 + band reductions along an axis such asa.sum(axis=0)work; on the host, device arrays support masks and index arrays. - float64 precision on devices without float64. Many mobile GPUs and Apple devices lack float64 or int64. Kernels are narrowed to 32-bit types there, with a warning; float narrowing is tested on an Apple M3 Pro, integer narrowing only on desktop GPUs with the feature switched off.
- Selecting between array elements by reference in ways the optimiser turns into pointer selects is rejected.
Things found along the way:
- NVIDIA's driver returns wrong results for 64-bit signed remainders with negative operands; remainders are computed from the quotient instead.
- NVIDIA's shader compiler segfaults on some invalid SPIR-V, which is why modules are checked before being handed to a driver.
- float32
sinon Intel/Mesa is only accurate to about 1e-4. - LLVM's SPIR-V backend miscompiles nested aggregate inserts and aborts on several unsupported inputs.
- LLVM's SPIR-V structurizer emits invalid shaders for early returns and short-circuit conditions; control flow is restructured beforehand.
- All three drivers reassociate float arithmetic unless told not to, so every float operation is marked exact.
- LLVM's SPIR-V backend mistakes
x = a * b; (x < 0) ? -y : yfor GLSL'sfaceforwardand crashes; such comparisons are rewritten.
Known Issues
The full list, with causes, workarounds and pointers to where each would be fixed, is in known issues. The ones most likely to bite:
| Issue | Workaround | |
|---|---|---|
| KI-01 | float64 math.sin(x), x ** 2.5... need libclc, which a source checkout lacks |
install a wheel, or set NUMBA_VULKAN_LIBCLC |
| KI-04 | no arrays of run-time size in kernels: a[mask], a.copy(), np.zeros(n) |
constant-shape arrays, array expressions, axis= reductions |
| KI-10 | indices and divisions by zero are only checked on request | @nv.jit(boundscheck=True, error_model="python") while debugging |
| KI-17 | running without int64 (mobile) is tested only with the feature switched off on desktop GPUs | report what you find |
| KI-32 | llvmlite's SPIR-V backend segfaults on macOS arm64 | set NUMBA_VULKAN_LLC to an LLVM 22 llc; the devenv shell does |
tests/test_known_issues.py reproduces the coverage gaps as expected
failures, so the list stays honest:
uv run pytest tests/test_known_issues.py -rxX
To continue the work, start with the development guide.
Roadmap
- Vulkan target registered with Numba's target extension API
- Kernels, device functions, overloads
- Test suite running on every available Vulkan device
- Sphinx documentation and benchmark suite
- Math functions, NumPy ufuncs on scalars, complex numbers
- Early returns and short-circuit conditions (control-flow restructuring)
- Loops with
break/returnanywhere,whileloops (fuzz-tested) - Device arrays and buffer reuse, to avoid copying on every call
- Asynchronous launches on device arrays (
nv.synchronize()) - Indexing (basic and advanced), views and NumPy ufuncs on device arrays
- Structured (record) arrays
- Autotuning of kernel variants (
nv.autotune) - Device-side timing with events (
nv.event) - Streams overlapping copies and kernels (
nv.stream,nv.pinned_array) - Launch plans: repeated launches skip typing
- Scalars as push constants
- Slices, array views, iteration and reductions, also along an axis
- Array expressions and local arrays
- Kernels narrowed to 32-bit types on devices without float64/int64
- float64 math, through libclc
- Bundle libclc in wheels
- Publish to PyPI
-
numba.vectorize/numba.guvectorizewithtarget="vulkan" -
reduceofvectorizefunctions -
print()in kernels,float16arrays,error_model="python" - Other ufunc methods (
accumulate,outer) - Shared memory, atomics, barriers and CUDA-style launch configuration
- Subgroup operations: reductions, scans, votes, ballots, shuffles
- On-disk caching of compiled kernels
-
cache=True: compiled kernels reused without recompiling - Testing on Apple (MoltenVK)
- Testing on AMD and mobile GPUs
Authorship and AI Disclosure
| Who | |
|---|---|
| Idea | Mirza Arnaut, in numba/numba#10116 |
| Direction and design decisions | Mirza Arnaut |
| Implementation: code, tests, benchmarks, documentation | Claude (Anthropic's Claude Fable 5.1, and Claude Opus 5.5 from 2 October 2026 on, running in Claude Code) |
| Review | Mirza Arnaut |
Every line here was generated by a language model in October 2026; the human author steered the work and reviewed it but did not write the code. Many implementation-level choices were made by the model while solving problems it ran into, and should be judged on their merits. Benchmark numbers and hardware observations come from real runs on the author's machine.
This is an independent project and is not endorsed by the Numba maintainers. Numba has an AI tools policy that requires attributing LLM-generated contributions; it applies if any part of this is ever proposed upstream.
Contributing
Issues and pull requests are welcome, in particular reports of how the test suite behaves on other GPUs and drivers:
uv run pytest
uv run python benchmarks/bench.py
Contributions written with AI tools are welcome on the same terms as the project itself: say which tool produced them, and have a human review them.
License
Distributed under the BSD 2-Clause License, the same license as Numba. See LICENSE.
Contact
Mirza Arnaut - @arunoruto
Acknowledgments
- Numba and numba-cuda, whose CUDA target is the template for this one
- Taichi, for showing that Python kernels on Vulkan work
- The LLVM SPIR-V backend developers
- Best-README-Template
Metadata
Release files for numba-vulkan 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| numba_vulkan-0.1.0.tar.gz | 809.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| numba_vulkan-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.6 MB
Release files / numba_vulkan-0.1.0.tar.gz
| Download URL | numba_vulkan-0.1.0.tar.gz |
|---|---|
| Size | 809.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
cf7978cc5320823a297373e417414889314bd69b08505b2b72ab9a3791524993
|
|
BLAKE2b-256 checksum How to use checksums |
c58ab191ded099f5e6c3fac222f35e1c617cdb51557211b273b04108e573ac90
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.
Transparency logRelease files / numba_vulkan-0.1.0-py3-none-any.whl
| Download URL | numba_vulkan-0.1.0-py3-none-any.whl |
|---|---|
| Size | 820.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1bf0f309370f775f3fd0304737f9e8cace0f6070a0c58a51f4b39f38bb6a9eb2
|
|
BLAKE2b-256 checksum How to use checksums |
e0c6f9f5a83a929e64ac3ccba2080400642bee4c8840c38db1b3f42351447cb6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.
Transparency log