Cuda Parallel Marching Blocks
cupmb extracts the zero level set of a scalar field on the GPU from Python: root finding in 1D, marching squares in 2D and marching cubes in 3D. It implements the Parallel Marching Blocks (PMB) algorithm, see Parallel Marching Blocks: A Practical Isosurfacing Algorithm for Large Data on Many-Core Architectures.
- Indexed meshes: vertices are shared between the simplices of a block, no triangle soup.
- Your scalar field, compiled: an analytic CUDA device function or a sampled array, compiled at runtime with NVRTC and cached on disk.
- Skip empty space: an optional block predicate (for instance with interval or affine arithmetic) discards the blocks that cannot contain the surface, the cost then scales with the surface instead of the volume.
- Built for real time: autotuned block sizes, a single device to host copy per call, no recompilation when the grid size or the extra parameters change, output buffers can be OpenGL buffers.
- CuPy or PyCUDA backends.
Performance
Median time of a call on a RTX 5090 (CUDA 12.9, CuPy 13.6, single precision, outputs kept on the device), as reported by the benchmarks of the examples:
| Example | Grid | Output | Without predicate | With predicate |
|---|---|---|---|---|
7_predicate.py |
2048³ | 27.4 M triangles | 44.5 ms | 4.1 ms |
8_levelset.py (animated scene) |
2560 × 732 × 2284 | 15.1 to 17.7 M triangles | 30.9 ms | 21.5 ms |
9_gamma.py (1D) |
2²⁶ | 117 roots | 6.0 ms | 1.7 ms |
Requirements
- Linux, NVIDIA GPU and driver.
- Python >= 3.10.
- CUDA 12.x (tested with CUDA 12.9 on a sm_120 GPU). The CUDA 13 cupy wheel (
cupy-cuda13x) can be selected with thecupy13extra. - One of the two backends: CuPy (default) or PyCUDA.
- Header only device libraries: trove (git submodule, shipped with the package) and cusvna (
cusvnapackage on PyPI, installed as a dependency).
Getting started
cupmb is available on PyPI, pick a backend extra (cupy12, cupy13 for CUDA 13 or pycuda):
python3 -m pip install "cupmb[cupy12]"
To run the examples, clone the repository with its submodules:
git clone --recursive https://github.com/keckj/cupmb.git
cd cupmb
python3 -m venv .venv && source .venv/bin/activate
python3 -m pip install ".[cupy12,examples]" # cupmb, its CuPy backend and the dependencies of the examples
python3 examples/0_simple.py # meshes a sphere and displays it with trimesh
If the repository was cloned without --recursive, run git submodule update --init --recursive before installing.
Available extras, combine them with commas:
| Extra | Content |
|---|---|
cupy12 |
CuPy backend for CUDA 12 (cupy-cuda12x) |
cupy13 |
CuPy backend for CUDA 13 (cupy-cuda13x) |
pycuda |
PyCUDA backend (pycuda, cuda-python for NVRTC) |
gl |
Cuda/OpenGL interoperability (cuda-python, PyOpenGL) |
examples |
Dependencies of the examples (trimesh, open3d, matplotlib, glfw, ...) |
test |
Test suite (pytest, scipy) |
dev |
Test suite, build, pre-commit, black, ruff, scikit-image |
All the dependencies of the examples (both backends included) can also be installed with python3 -m pip install -r requirements_examples.txt.
Minimal usage
from cupmb import ParallelMarchingBlocks
fdef = r"""
namespace cupmb {
template <typename T, typename I, I dim>
__device__ __forceinline__ T scalar(
const trove::array<T,dim>& p,
const trove::array<I,dim>& i) {
const T& x = trove::get<0>(p);
const T& y = trove::get<1>(p);
const T& z = trove::get<2>(p);
return x*x + y*y + z*z - T(0.45)*T(0.45);
}
}
"""
pmb = ParallelMarchingBlocks(3, fdef, N=64, x0=-0.5, L=1.0)
V, I = pmb(to_numpy=True) # vertices (nv, 3) float32, triangles (nt, 3) uint32
- The scalar function receives the position
pand the integer coordinatesiof each grid point. The grid size is a kernel argument and not a template parameter (formertemplate <typename T, typename I, I dim, I... N>signatures are still accepted,Nis then empty). - Without
to_numpy=Truethe output buffers stay on the device. When the zero level set is empty,VandIareNone. - Verbosity is controlled by the
CUPMB_LOGLEVELenvironment variable (DEBUG,INFO,WARNING, ...).
Features
Output
- PMB outputs an indexed mesh: vertices are shared between the simplices of a block, no triangle soup as with other marching cubes algorithms. Vertices lying on the faces shared by two blocks are duplicated, weld them by position if you need a fully connected mesh.
- 1D (zero crossings, single vertex simplices), 2D (marching squares, segments) and 3D (marching cubes, triangles) support, single or double precision for vertices. In 1D,
VandIhave shape(n, 1)and simplexkreferences vertexk, vertices are never duplicated on block boundaries. uint32oruint64simplex indices.- Consistently oriented simplices: normals point from negative to positive scalar values (along the gradient). Triangles are counterclockwise when seen from the positive side (closed surfaces have a positive signed volume) and segments are counterclockwise around negative regions (positive signed area). 1D simplices (single vertices) carry no orientation, the normal is the sign of the derivative and can be computed as a vertex property.
- Custom user defined vertex properties (normals, colors, ...) computed on the GPU alongside the vertices.
- Vertices and simplices are stored as array of structures, vector loads and stores are optimized with trove.
Input
- The scalar field can be analytic (function computed at runtime on the GPU) or stored in an array/texture. The sampled scalar field can be exported (
export_scalar_field=True), it is stored x-fastest and indexed asS[iz,iy,ix](shapeN[::-1]+1,S[iy,ix]in 2D as expected bymatplotlib.imshoworcontour). - User provided extra kernel parameters (constants or Python callables evaluated at each call).
- Dynamic domains (axis-aligned bounding box) and dynamic kernel parameters.
Performance
- Runtime KD-tree-pyramid domain decomposition (binary tree in 1D, quadtree in 2D and octtree in 3D) from analytic block predicates (
pdef). Predicates can be computed with range analysis by using interval arithmetic or affine arithmetic, for instance with the cusvna headers (#include <cusvna/af1.cuh>, requiresenable_cuda_std_lib=True). - Once the buffers are large enough, a call performs a single device to host copy: kernels read the active block, vertex and simplex counts from device counters and grids are sized from the buffer capacities. When a count exceeds a capacity, the buffers are reallocated and the kernels launched again step by step.
- Low memory consumption with user configurable allocation policies (and possibly user provided buffers).
Integration
- Compatible with both CuPy and PyCUDA backends.
- Cuda/OpenGL interoperability: vertices and simplices can be written directly into OpenGL buffers.
Kernel cache
Kernels are compiled at runtime with NVRTC for each configuration (dimension, block sizes, types, user code, ...), autotuning compiles one module per candidate block size. The grid size is a kernel argument: changing N does not trigger any compilation. Compiled modules are cached on disk so that subsequent runs skip the compilation (example 8 starts in about 1.5 s instead of 25 s):
- the cache directory is
$CUPMB_CACHE_DIR/kernels, it defaults to~/.cache/cupmb/kernels($XDG_CACHE_HOME/cupmb/kernels), seecupmb.kernel_cache_dir(); - set
CUPMB_DISABLE_KERNEL_CACHE=1to disable it and callcupmb.clear_kernel_cache()(or remove the directory) to clear it; - entries are keyed by the cupmb version, the backend and its NVRTC version, the device compute capability, the kernel source code, the build options and the content of the cupmb kernel headers (including trove and cusvna). Headers included from other directories through
extra_build_optionsare not part of the key: clear the cache when they change.
Examples
All the examples run with python3 -m pip install ".[cupy12,examples]", some of them need an additional extra:
| Example | Dim | Topic | Extra |
|---|---|---|---|
0_simple.py |
3D | Analytic sphere, mesh displayed with trimesh | |
1_animation.py |
3D | Extra kernel parameter, mesh recomputed at each frame | |
2_normals.py |
3D | Normals as a vertex property | |
3_glinterop_cupy.py |
3D | Cuda/OpenGL interoperability with CuPy | gl |
3_glinterop_pycuda.py |
3D | Cuda/OpenGL interoperability with PyCUDA | pycuda |
4_isolines.py |
2D | Marching squares compared to matplotlib.contour |
|
5_moving_sphere.py |
3D | Extra parameters given as Python callables | |
6_moving_bbox.py |
3D | Dynamic bounding box | |
7_predicate.py |
3D | Block predicate benchmark (affine arithmetic) | |
8_levelset.py |
3D | Real time meshing of an animated scene, OpenGL viewer | gl |
9_gamma.py |
1D | Root finding on 2²⁶ cells |
0_simple.pyThis simple example will show you how to execute the marching cubes algorithm to find the zero levelset of a scalar field representing a sphere. The scalar field is defined as $\phi(x,y,z) = x^2 + y^2 + z^2 - r^2$ where $r=0.45$ is a constant. The function is analytic and implemented directly as a device function. The default bounding box is $[0,1]^3$ so we offset it by $x_0=-0.5$ to find the zeros for $(x,y,z) \in [-0.5,+0.5]^3$. The output mesh (verticesV, simplicesI) is copied to the host (numpy arrays) and is then displayed by using the trimesh module. ParameterNcontrols the resolution of the marching cubes algorithm.1_animation.pySphere with a radius given as an extra kernel parameter (extra_params=[("r", "T")], passed aspmb(r=...)), the mesh is recomputed at each frame and displayed with open3d.2_normals.pyUser defined vertex properties (VertexProperty): normals are computed on the GPU alongside the vertices and used for shading in an open3d GUI.3_glinterop_cupy.py,3_glinterop_pycuda.pyCuda/OpenGL interoperability: vertices and simplices are written directly into OpenGL buffers (no host round trip) and rendered with glfw/PyOpenGL (lock_buffer/replace_buffer), with the CuPy and PyCUDA backends respectively. The PyCUDA version requires PyCUDA to be built with OpenGL support (pycuda.gl), which is not the case of a defaultpython3 -m pip install pycuda.4_isolines.pyMarching squares in 2D: isolines of an analytic scalar field, the sampled scalar field is exported (export_scalar_field=True) and displayed with matplotlib, the cupmb segments (red) are compared tomatplotlib.contour(black).5_moving_sphere.pyExtra kernel parameters (ExtraParameter) given as Python callables that are evaluated before each call, here the center of a moving sphere.6_moving_bbox.pyDynamic domain: the bounding box follows the moving sphere through a user providedupdate_bboxcallback.7_predicate.pyBenchmark of the block predicate based domain decomposition against the naive block extraction, the predicate is written with cusvna affine arithmetic. Runpython3 examples/7_predicate.py --helpfor the options (-Ngrid size,--rigorautotuning rigor,--runs), large grids require a lot of device memory. The mesh is then displayed with open3d at a lower resolution (--display-N, default 512,--no-displayto disable), shaded with normals computed on the GPU as a vertex property.8_levelset.pyReal time meshing of the animated scene of the last cusvna example (gyroid ball surrounded by a ring of heart, Kummer, Barth, tetrahedral, twisted box and metaballs level sets): the shapes are written once as templates evaluated on floats for the scalar field and on cusvna affine forms for the block predicate, vertices, triangles, normals and colors are written directly into OpenGL buffers and rendered with glfw/PyOpenGL. The predicate scales with the surface instead of the volume, it is slower at smallNand pays off at high resolution.- Controls (shown in an overlay when pyimgui is installed, logged at startup otherwise):
SPACEpause,+/-(orUP/DOWN) animation speed,*//(orPAGE UP/PAGE DOWN) multiply or divide the resolution by two between 64 and 4096 cells along x,Wwireframe, mouse to rotate and zoom,Q/ESCquit. The viewer uses a power of two number of cells on each axis, the domain is extended around the scene. When the resolution changes, the marching blocks are rebuilt with theESTIMATEautotuning rigor, the grid size being a kernel argument the kernels are not recompiled. - Options:
-Ngrid size (default 2560),--no-predicate,--rigorautotuning rigor,--framesnumber of frames,--no-displayheadless benchmark with and without predicate (median meshing times, triangle counts are checked to be identical).
- Controls (shown in an overlay when pyimgui is installed, logged at startup otherwise):
9_gamma.pyMarching blocks in 1D: root finding on a very fine grid ($N=2^{26}$ by default). The gamma function has no zero, the example finds instead the solutions of $\Gamma(x)=e^{k\pi}$, $k \in \mathbb{Z}$, i.e. the zeros of $\sin(\ln\Gamma(x))$ for $x>0$, with a cusvna affine arithmetic predicate (binary tree). The roots are checked against a host bisection and displayed with matplotlib (--no-plotto disable).
Testing
python3 -m pip install ".[cupy12,test]"
python3 -m pytest # requires a CUDA device, GPU tests are skipped otherwise
python3 -m pytest --runslow # also runs the large grid and large index tests (several GB of device memory)
If scikit-image is installed (dev extra), the 3D meshes are also cross-checked against skimage.measure.marching_cubes.
Development
python3 -m pip install -e ".[cupy12,dev]"
pre-commit install
python3 -m build # sdist and wheel in dist/
License
cupmb is released under the MIT License. The device header libraries keep their own licenses: trove (BSD 3-clause, NVIDIA, shipped with the package) and cusvna (MIT).
Metadata
Release files for cupmb 1.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 | |
|---|---|---|---|
| cupmb-1.1.0.tar.gz | 117.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| cupmb-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 213.3 kB
Release files / cupmb-1.1.0.tar.gz
| Download URL | cupmb-1.1.0.tar.gz |
|---|---|
| Size | 117.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3128dafbf986470eed6737dc191a12fdd98e965776bf7369c38ff14e2397b2ef
|
|
BLAKE2b-256 checksum How to use checksums |
fc1c50a1be070ed01ce698f599c188b7deca71eb970fc93c8ebef0253f27d076
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|
Release files / cupmb-1.1.0-py3-none-any.whl
| Download URL | cupmb-1.1.0-py3-none-any.whl |
|---|---|
| Size | 95.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f8e9bbb32dbcd106f9c4f30c7cd9a749c23072c838d6809813670665d75b6256
|
|
BLAKE2b-256 checksum How to use checksums |
5643973ae35bf0e5fd20577df2afcb23e14d9d2bb08b9143c2c12b7309a3b06b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.10.12
|