cuda_example builds one pybind11 module with a CPU and a CUDA path
Example pybind11 module built with a CMake-based build system
At a glance
- What is it?
- The pybind cuda_example project renders the Mandelbrot set twice, once with a plain nested loop and once with a CUDA kernel, so the two can be compared side by side. It is a teaching repository, and its build insists on the CUDA Toolkit even when you only want the CPU path.
- Who is it for?
- cuda_example earns its place as a reference for people wiring pybind11 into a CMake build that must produce a CUDA extension, and for teams who want a minimal example that pairs a CPU loop with a kernel on purpose rather than by accident.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 4 days ago.
- What is it written in?
- Mainly C++, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Two implementations of the same pixels, written to be compared
The pybind cuda_example project renders the Mandelbrot set in two ways, once on the CPU and once on the GPU, so a reader can run both and compare them. The README is explicit that the two implementations are written the same way on purpose. The CPU version in src/mandelbrot_cpu.cpp is a plain nested loop over every pixel, and the GPU version in src/mandelbrot.cu is the same logic expressed as a CUDA kernel with one thread per pixel. Both return a height by width int32 NumPy array of escape counts, which is what makes the comparison meaningful rather than decorative: identical outputs mean any difference in runtime comes from the execution model. The project is built with pybind11, CUDA and scikit-build-core, and it requires Python 3.11 or newer. It is a reference repository rather than a library, and the file list in the README makes the minimal set explicit, from pyproject.toml and CMakeLists.txt through the shared declarations in src/mandelbrot.h to the bindings in src/main.cpp that turn results into NumPy arrays.
CUDA is a required language, so configuration fails without nvcc
The most consequential line in the build description is that CUDA is declared as a required language in the CMake project, which means configuration fails outright on a machine without the CUDA Toolkit. This is worth flagging because it is unusual. Many mixed projects treat CUDA as an optional component so that a pure CPU build still works. Here the CMake configuration refuses to proceed, and the README states plainly that the CUDA Toolkit with nvcc must be installed and discoverable by CMake. The practical effect is that you cannot use this repository as a drop in pybind11 example on a laptop with no NVIDIA driver installed, unless you change the CMake language requirement yourself, and the README does not document how. The dependency that makes this possible is scikit-build-core, which the project lists at 0.11 or newer, alongside pybind11 3.0 or newer, with the scikit_build_core.build backend.
A statically linked CUDA runtime keeps the wheel importable without a GPU
The build links the CUDA runtime statically, using the CUDA_RUNTIME_LIBRARY Static setting, so the resulting wheels do not depend on libcudart. The consequence is stated directly in the README: the wheel stays importable on machines without a GPU, calling mandelbrot_gpu there raises, and cuda_available lets you check first. This is a deliberate design choice for distribution, and it is the reason the project can ship a wheel that many users install and import successfully even though only a fraction of them have a CUDA device. It also means the wheel carries the runtime inside itself, which has a size cost the README does not quantify. On the Python side the module is a normal package with numpy as its only runtime dependency, and the module root needs to sit at the package name, inside src, or inside python for auto discovery, which the README lists as one of the necessary files along with the __init__.py.
Installing locally and calling both paths
Installation is a clone followed by a pip install of the local directory, and it assumes the CUDA Toolkit is present and discoverable by CMake.
pip install ./cuda_exampleThe test call from the README imports the module and runs the CPU version first, then checks for a GPU before attempting the CUDA path. Because a blank line would be ambiguous to copy, the essential lines are shown here.
import cuda_example
# (height, width) int32 array of escape counts
image = cuda_example.mandelbrot_cpu(width=800, height=600, max_iterations=100)
if cuda_example.cuda_available():
image = cuda_example.mandelbrot_gpu(width=800, height=600, max_iterations=100)Both functions accept width, height and max_iterations as keyword arguments, and the result can be displayed with any plotting library. The README shows a matplotlib example that renders the array with the twilight_shifted colormap over the usual Mandelbrot coordinate extent, which is a quick way to confirm that the escape counts arrived in the right order.
Timing the two paths relies on identical arguments and identical outputs
The comparison section of the README shows why the two implementations are written the same way. Because both functions take the same arguments and return identical arrays, you can run them back to back and time them on a machine with a GPU.
import time
import cuda_example
size = {"width": 2000, "height": 1500, "max_iterations": 200}The snippet in the README then times a CPU call and a GPU call with that same size dictionary, printing an elapsed time for each, and finishes with an assertion that the two arrays are element for element identical. The final assertion is the interesting part, because it turns the demo into a correctness check: if the kernel and the loop ever disagree on an escape count, the example fails loudly instead of printing a plausible looking picture. Note that this script only works on a machine with a CUDA device, since the GPU call has no guard here.
Wheels come from manylinux CUDA images, and the image is the version knob
The Wheels workflow builds CUDA enabled Linux wheels with cibuildwheel, using the custom manylinux images that ship the CUDA Toolkit. The images are configured in pyproject.toml.
[tool.cibuildwheel]
manylinux-x86_64-image = "quay.io/manylinux_cuda/manylinux_2_28_x86_64_cuda13_1:latest"
manylinux-aarch64-image = "quay.io/manylinux_cuda/manylinux_2_28_aarch64_cuda13_1:latest"If you need a different CUDA version, for example an older one to support older drivers, the README shows how to override the images through environment variables instead of editing the file.
export CIBW_MANYLINUX_X86_64_IMAGE=quay.io/manylinux_cuda/manylinux_2_28_x86_64_cuda12_9:latest
export CIBW_MANYLINUX_AARCH64_IMAGE=quay.io/manylinux_cuda/manylinux_2_28_aarch64_cuda12_9:latest
cibuildwheelThe available images are listed in the cibuildwheel documentation, and the manylinux_2_28 and manylinux_2_34 bases can be mixed with the cuda12_9 and cuda13_1 versions. The configuration also restricts builds to CPython on manylinux by skipping the musllinux tags, because musllinux and PyPy have no matching CUDA image. That is a real ceiling on the wheel matrix: no PyPy build and no musl based distribution is possible with this setup.
Docker lets you compile and import the CUDA build with no GPU present
The manylinux images include the CUDA Toolkit, so nvcc runs inside the container. That means you can compile the extension and import the wheel on a machine with no GPU at all, and the README notes that the kernels are compiled and the wheel is imported while execution on a GPU is skipped, with those tests marked as skipped rather than failed. The flow picks the image matching your host architecture, where the aarch64 image runs natively on Apple Silicon and x86_64 hosts use the x86_64 image.
# Apple Silicon / arm64 host:
IMAGE=quay.io/manylinux_cuda/manylinux_2_28_aarch64_cuda13_1:latest
# x86_64 host:
mkdir -p wheelhouse
docker run --rm \
-v "$PWD":/io:ro \
-v "$PWD/wheelhouse":/wheelhouse \
"$IMAGE" bash -lc '
PY=/opt/python/cp312-cp312/bin/python
cp -r /io /tmp/src && cd /tmp/src
$PY -m pip install --upgrade pip build pytest
$PY -m build --wheel --outdir /wheelhouse . # compiles src/mandelbrot.cu with nvcc
$PY -m pip install /wheelhouse/*.whl
$PY -m pytest # GPU tests skip (no device)
'The wheel lands in the wheelhouse directory on the host, so you can inspect or install it afterwards. Because the container has no GPU, cuda_available returns False inside it and the mandelbrot_gpu test is skipped while the mandelbrot_cpu tests still run. The README notes that the same flow runs in CI in the cuda job of the pip workflow.
The package metadata is still a template, and the licence signals disagree
The repository is listed with a custom licence that GitHub cannot classify, pointing to the LICENSE file, while the package metadata in pyproject.toml declares license as MIT with the LICENSE file listed under license-files. Two signals in the same repository disagree, and the LICENSE file is the one that decides. Beyond that, the metadata is clearly a starting template rather than a released package. The version is 0.0.1, the authors field still reads My Name with a placeholder email, and the classifiers include Private :: Do Not Upload, which is a marker that this example is not meant to reach PyPI. The build system requirements pin scikit-build-core 0.11 or newer and pybind11 3.0 or newer, the test group is pytest with numpy, and the pytest configuration turns warnings into errors with a strict marker and config mode. There are no GitHub releases published, so there is no tagged artifact to point at, although the last push was on 2026-10-19 and the CI badges for pip builds and wheels are wired to the repository workflows.
Editorial conclusion
cuda_example earns its place as a reference for people wiring pybind11 into a CMake build that must produce a CUDA extension, and for teams who want a minimal example that pairs a CPU loop with a kernel on purpose rather than by accident. It does not serve as a starting point for a package you intend to publish to PyPI as is, because the project metadata still carries the placeholder author name, a private-use classifier, a version of 0.0.1, and no GitHub releases, and because the repository licence classification does not match the MIT identifier written in the package metadata. Verify three things: that the CUDA Toolkit is discoverable by CMake on the machine that builds the extension, that your target drivers match the CUDA version in the manylinux image you select, and that the terms in the LICENSE file match what the MIT string in the package metadata implies. The last push was on 2026-10-19, so the CI recipes move faster than the tag history.
Frequently asked questions
Do I need an NVIDIA GPU to build cuda_example?
You need the CUDA Toolkit with nvcc, because the CMake project declares CUDA as a required language and configuration fails without it. The README adds that the manylinux images ship the toolkit, so a Docker run can compile and import the build without a GPU, with the kernel tests skipped.
How do I install cuda_example from source?
Clone the repository and run pip install ./cuda_example from the project directory. The CUDA Toolkit must already be installed and discoverable by CMake, and Python 3.11 or newer is required.
Will the wheel import on a machine without a GPU?
Yes. The CUDA runtime is linked statically, so the wheels do not depend on libcudart and stay importable without a GPU. Calling mandelbrot_gpu there raises, so cuda_available is provided to check first.
How do I build wheels against an older CUDA version?
Override the cibuildwheel images through the CIBW_MANYLINUX_X86_64_IMAGE and CIBW_MANYLINUX_AARCH64_IMAGE environment variables instead of editing pyproject.toml, pointing them at a manylinux_2_28 or manylinux_2_34 image with the cuda12_9 tag. The configuration also skips musllinux, since musllinux and PyPy have no matching CUDA image.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/pybind-cuda-example)Was this useful?