diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c648c36..4931ab5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,10 +2,13 @@ name: Build (CPU) on: push: branches: [master, dev] + pull_request: + branches: [master, dev] jobs: build: strategy: + fail-fast: false matrix: os: - ubuntu-latest @@ -16,18 +19,34 @@ jobs: - "3.10" - "3.11" - "3.12" + - "3.13" runs-on: ${{ matrix.os }} steps: - name: Check out repository code - uses: actions/checkout@v4 + uses: actions/checkout@v5 + - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.version }} - - name: Install requirements + + - name: Install PyTorch (CPU) run: | python -m pip install --upgrade pip - python -m pip install numpy "pybind11[global]" + python -m pip install numpy python -m pip install torch --index-url https://download.pytorch.org/whl/cpu + + - name: Install build dependencies + run: python -m pip install scikit-build-core pybind11 + - name: Build - run: pip install . + # Same command as documented in README: build against the PyTorch + # installed above instead of an isolated build environment. + run: pip install --no-build-isolation . + - name: Import test + # Run outside the repository root so the installed package is imported, + # not the source directory. + working-directory: ${{ runner.temp }} + run: | + python -c "import torchmcubes; print(torchmcubes.__file__, torchmcubes.__version__)" + python -c "import importlib.metadata as m; print('Requires-Dist:', m.requires('torchmcubes'))" diff --git a/.gitignore b/.gitignore index c3d108a..4d26f49 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ __pycache__ *.pyd *.so *.egg-info +.venv +uv.lock diff --git a/CMakeLists.txt b/CMakeLists.txt index 3433480..986a96f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,7 @@ if(CMAKE_CUDA_COMPILER) add_definitions(-DWITH_CUDA) find_package(CUDAToolkit REQUIRED) - set(CMAKE_CUDA_STANDARD 17) + set(CMAKE_CUDA_STANDARD 20) set(CMAKE_CUDA_STANDARD_REQUIRED ON) message(STATUS "INSTALLING EXTENSIONS WITH CUDA!") @@ -25,7 +25,10 @@ else() message(WARNING "NO CUDA INSTALLATION FOUND, TRYING TO INSTALL CPU VERSION ONLY!") endif() -set(CMAKE_CXX_STANDARD 17) +# Recent PyTorch headers require C++20 (torch/all.h and ATen/ATen.h emit +# #error otherwise; torch 2.14 requires it, torch 2.9 still accepted C++17). +# Older torch versions compile fine with C++20 as well. +set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_POSITION_INDEPENDENT_CODE ON) diff --git a/README.md b/README.md index 3c48f78..a0d7bb1 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,9 @@ ### Requirements - Python (3.9 or later) -- NumPy (1.x is preferable) - PyTorch +- C++20 compiler (GCC 10+, Clang 12+, or Visual Studio 2019 16.11+), required by recent PyTorch headers +- CUDA Toolkit 12 or later (optional, only for GPU support; nvcc needs CUDA 12 for C++20) - CMake (3.18 or later) Make sure that you have nvcc CUDA compiler with the following command. @@ -19,7 +20,7 @@ Make sure that you have nvcc CUDA compiler with the following command. nvcc --version ``` -If you have CUDA installed but not able to run nvcc, you migth need to add it to your path: +If you have CUDA installed but not able to run nvcc, you might need to add it to your path: ```shell export CUDA_HOME=/usr/local/cuda/ @@ -28,14 +29,24 @@ export PATH=$CUDA_HOME/bin:$PATH ### Pip installation +torchmcubes is compiled against the PyTorch installed in your environment. Install PyTorch first, then install the build dependencies and torchmcubes **without build isolation**. + ```shell -# Make sure that your environment meets the requirements above -pip install git+https://github.com/tatsy/torchmcubes.git +# 1. Install PyTorch (if you need GPU support, choose the correct CUDA version) +pip install torch + +# 2. Install build dependencies +pip install scikit-build-core pybind11 + +# 3. Build and install torchmcubes against the PyTorch installed above +pip install --no-build-isolation git+https://github.com/tatsy/torchmcubes.git ``` +To build from a local checkout, run `pip install --no-build-isolation .` in the repository root instead of the last command. + ## Usage -See [mcubes.py](./mcubes.py) for more details. +See [mcubes.py](./mcubes.py) for more details (the example additionally needs `numpy` and `matplotlib`). ```python import time @@ -85,4 +96,4 @@ visualize(verts, faces, colors) ## Copyright -MIT License 2019-2024 (c) Tatsuya Yatagawa +Mozilla Public License 2.0, 2019-2026 (c) Tatsuya Yatagawa diff --git a/_torch_dep.py b/_torch_dep.py new file mode 100644 index 0000000..021c4cc --- /dev/null +++ b/_torch_dep.py @@ -0,0 +1,37 @@ +"""scikit-build-core dynamic-metadata provider for the torch runtime requirement. + +torchmcubes links against the C++ ABI of the PyTorch it is compiled with, so the +built wheel must not claim to work with any torch version. This provider pins +the runtime requirement to the minor series of the torch found at build time +(e.g. ``torch==2.14.*``). Patch releases keep the ABI, so they stay allowed; +CUDA/CPU local versions such as ``2.14.0+cu126`` also satisfy the pin. +If torch is not importable at build time the build is aborted with an +explanatory error instead of producing unpinned metadata. +""" + +from __future__ import annotations + +import importlib.metadata + + +def torch_requirement() -> str: + try: + version = importlib.metadata.version("torch") + except importlib.metadata.PackageNotFoundError: + # Fail fast: without torch the build cannot succeed anyway (CMake needs + # its config files), and emitting an unpinned requirement would produce + # wrong metadata. Point the user at the documented install procedure. + raise RuntimeError( + "torchmcubes must be built against an installed PyTorch, but the " + "'torch' package was not found in the build environment. Install " + "torch first and build without isolation, e.g. " + "`pip install --no-build-isolation .` (see README)." + ) from None + major, minor = version.split(".")[:2] + return f"torch=={major}.{minor}.*" + + +class Provider: + @staticmethod + def dynamic_metadata(settings, project): + return {"dependencies": [torch_requirement()]} diff --git a/pyproject.toml b/pyproject.toml index 4a80e71..1e21f63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,25 +1,35 @@ [project] name = "torchmcubes" -version = "0.1.0" description = "torchmcubes: Marching Cubes for PyTorch" readme = "README.md" authors = [ {name = "Tatsuya Yatagawa", email = "tatsy.mail@gmail.com"} ] -license = {file = "LICENSE"} +license = "MPL-2.0" +license-files = ["LICENSE"] classifiers = [ - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', ] requires-python = ">=3.9" -dependencies=["numpy", "torch"] +dynamic = ["version", "dependencies"] + +[[tool.dynamic-metadata]] +provider = "scikit_build_core.metadata.regex" +field = "version" +input = "torchmcubes/__init__.py" + +[[tool.dynamic-metadata]] +provider = { path = ".", module = "_torch_dep:Provider" } [tool.scikit-build] build-dir = "build" -minimum-version = "0.10" +minimum-version = "1.0" +experimental = true ninja.make-fallback = true logging.level = "INFO" build.verbose = true @@ -27,16 +37,15 @@ messages.after-success = "SUCCESS!!" messages.after-failure = "FAILURE!!" [tool.scikit-build.cmake] -version = ">=3.15" +version = ">=3.18" source-dir = "." args = [] [tool.scikit-build.wheel] -license-files = ["LICENSE"] exclude = ["**/.mypy_cache/**", "**/build/**", "**/.vscode/**"] [build-system] -requires = ["scikit-build-core>=0.10", "pybind11>=2.10", "cmake", "ninja"] +requires = ["scikit-build-core>=1.0", "pybind11>=2.10"] build-backend = "scikit_build_core.build" [tool.isort] diff --git a/torchmcubes/__init__.py b/torchmcubes/__init__.py index 6d20b7a..86a9359 100644 --- a/torchmcubes/__init__.py +++ b/torchmcubes/__init__.py @@ -1,37 +1,39 @@ -import os - -os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE" - -from typing import Tuple - -import torch -import torchmcubes_module as mc - - -def marching_cubes(vol: torch.Tensor, thresh: float) -> Tuple[torch.Tensor, torch.Tensor]: - """ - vol: 3D torch tensor - thresh: threshold - """ - - if vol.is_cuda: - return mc.mcubes_cuda(vol, thresh) - else: - return mc.mcubes_cpu(vol, thresh) - - -def grid_interp(vol: torch.Tensor, points: torch.Tensor) -> torch.Tensor: - """ - Interpolate volume data at given points - - Inputs: - vol: 4D torch tensor (C, Nz, Ny, Nx) - points: point locations (Np, 3) - Outputs: - output: interpolated data (Np, C) - """ - - if vol.is_cuda: - return mc.grid_interp_cuda(vol, points) - else: - return mc.grid_interp_cpu(vol, points) +import os + +__version__ = "0.1.0" + +os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE" + +from typing import Tuple + +import torch +import torchmcubes_module as mc + + +def marching_cubes(vol: torch.Tensor, thresh: float) -> Tuple[torch.Tensor, torch.Tensor]: + """ + vol: 3D torch tensor + thresh: threshold + """ + + if vol.is_cuda: + return mc.mcubes_cuda(vol, thresh) + else: + return mc.mcubes_cpu(vol, thresh) + + +def grid_interp(vol: torch.Tensor, points: torch.Tensor) -> torch.Tensor: + """ + Interpolate volume data at given points + + Inputs: + vol: 4D torch tensor (C, Nz, Ny, Nx) + points: point locations (Np, 3) + Outputs: + output: interpolated data (Np, C) + """ + + if vol.is_cuda: + return mc.grid_interp_cuda(vol, points) + else: + return mc.grid_interp_cpu(vol, points)