diff --git a/.devcontainer/cuda/Dockerfile b/.devcontainer/cuda/Dockerfile new file mode 100644 index 000000000..1882bdacd --- /dev/null +++ b/.devcontainer/cuda/Dockerfile @@ -0,0 +1,76 @@ +# CUDA-enabled development container for the IPC Toolkit. +# +# This mirrors ../Dockerfile (the default Ubuntu dev container) but is based on +# NVIDIA's CUDA "devel" image so nvcc is available. Like the default container +# it does NOT copy the source: the workspace is bind-mounted at runtime by the +# Dev Containers tooling. Compiling CUDA does not require a GPU; running it +# does. On a Linux host with the NVIDIA Container Toolkit you can expose the GPU +# by adding "--gpus=all" to runArgs in devcontainer.json. + +ARG CUDA_IMAGE=nvidia/cuda:12.6.2-devel-ubuntu22.04 +FROM ${CUDA_IMAGE} + +# Set environment variables +ENV DEBIAN_FRONTEND=noninteractive +ENV CCACHE_DIR=/home/devuser/.ccache +ENV CCACHE_MAXSIZE=1G +ENV CXX_STANDARD=17 + +# Install essential packages (nvcc + host gcc ship in the CUDA devel image). +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + git \ + wget \ + curl \ + fish \ + zsh \ + ninja-build \ + ccache \ + rsync \ + python3 \ + python3-pip \ + python3-dev \ + libgmp-dev \ + libssl-dev \ + sudo \ + software-properties-common \ + lsb-release \ + gnupg \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Create a new user with sudo privileges +RUN useradd -m devuser \ + && echo "devuser:password" | chpasswd \ + && usermod -aG sudo devuser \ + && echo "devuser ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers \ + && mkdir -p $CCACHE_DIR \ + && chown devuser:devuser $CCACHE_DIR + +# Set up Python tools +RUN pip3 install --no-cache-dir --upgrade pip setuptools wheel pre-commit + +# Add Kitware APT repository for a recent CMake (project requires >= 3.24) +RUN wget -qO- https://apt.kitware.com/keys/kitware-archive-latest.asc | \ + gpg --dearmor -o /usr/share/keyrings/kitware-archive-keyring.gpg \ + && echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ $(lsb_release -cs) main" | \ + tee /etc/apt/sources.list.d/kitware.list > /dev/null \ + && apt-get update && apt-get install -y --no-install-recommends cmake \ + && rm -rf /var/lib/apt/lists/* \ + && cmake --version && nvcc --version + +# Install LLVM/Clang and clang-format 18 (matches the default dev container so +# pre-commit's clang-format hook behaves identically). +RUN wget -q https://apt.llvm.org/llvm.sh -O /tmp/llvm.sh \ + && chmod +x /tmp/llvm.sh \ + && /tmp/llvm.sh 18 || true +RUN apt-get update \ + && apt-get install -y --no-install-recommends clang-18 clang-tools-18 clang-format-18 \ + && rm -rf /var/lib/apt/lists/* \ + && clang-format-18 --version + +# Set the default user and working directory +USER devuser +WORKDIR /home/devuser/workspace + +CMD ["bash"] diff --git a/.devcontainer/cuda/build-cuda.sh b/.devcontainer/cuda/build-cuda.sh new file mode 100755 index 000000000..82bf40866 --- /dev/null +++ b/.devcontainer/cuda/build-cuda.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# +# Headless CUDA compile check that reuses the CUDA dev-container image. +# +# The dev container (devcontainer.json) is for *interactive* development. This +# script is the *batch* counterpart: it builds the same image and compiles the +# whole project with CUDA enabled, then exits with the build's status. Use it +# from a Mac (compile-only; no GPU) or in CI to keep the CUDA build green. +# +# The source is mounted read-only and copied into the container (minus build +# artifacts and the machine-specific IPCToolkitOptions.cmake) so the build is +# hermetic and never writes into your host working tree. +# +# Usage: +# .devcontainer/cuda/build-cuda.sh # cuda-release, arch 75;80;86;89 +# PRESET=test .devcontainer/cuda/build-cuda.sh # test preset (CUDA + tests) +# CUDA_ARCH="86" .devcontainer/cuda/build-cuda.sh # single architecture +# JOBS=4 .devcontainer/cuda/build-cuda.sh # limit parallelism (memory) +# +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + +IMAGE_NAME="${IMAGE_NAME:-ipc-toolkit-cuda-dev}" +PRESET="${PRESET:-cuda-release}" +CUDA_ARCH="${CUDA_ARCH:-75;80;86;89}" +CUDA_IMAGE="${CUDA_IMAGE:-nvidia/cuda:12.6.2-devel-ubuntu22.04}" +# Heavy TUs (headers textually include implementations under CUDA) can OOM the +# VM at full parallelism; default below nproc. +JOBS="${JOBS:-4}" + +echo ">> Building CUDA dev image '${IMAGE_NAME}'" +docker build \ + -f "${REPO_ROOT}/.devcontainer/cuda/Dockerfile" \ + -t "${IMAGE_NAME}" \ + --build-arg "CUDA_IMAGE=${CUDA_IMAGE}" \ + "${REPO_ROOT}" + +echo ">> Compiling (preset=${PRESET}, arch=${CUDA_ARCH})" +# Run as root so the named cache volumes are writable; the source is mounted +# read-only and copied to a scratch dir inside the container. +docker run --rm --user root \ + -e PRESET="${PRESET}" \ + -e CUDA_ARCH="${CUDA_ARCH}" \ + -e JOBS="${JOBS}" \ + -v "${REPO_ROOT}":/src:ro \ + -v ipc-toolkit-cuda-workspace:/workspace \ + -v ipc-toolkit-cpm-cache:/cpm-cache \ + -v ipc-toolkit-cuda-ccache:/root/.ccache \ + "${IMAGE_NAME}" \ + bash -euo pipefail -c ' + export CPM_SOURCE_CACHE=/cpm-cache CCACHE_DIR=/root/.ccache + mkdir -p /workspace + # /workspace is a persistent named volume: rsync copies only files + # that changed since the last run (the macOS<->VM file-share is slow, + # so minimizing reads matters) and ninja can then build incrementally. + # The excludes also shield the persistent build/ dir from --delete. + echo ">> [1/3] Syncing source into the container (delta copy)..." + time rsync -a --delete \ + --exclude=/build \ + --exclude=/.git \ + --exclude=/.ccache \ + --exclude=/docs \ + --exclude=/notebooks \ + --exclude=/IPCToolkitOptions.cmake \ + /src/ /workspace/ + rm -f /workspace/IPCToolkitOptions.cmake + cd /workspace + echo ">> [2/3] Configuring (preset=${PRESET})..." + cmake --preset="${PRESET}" -G Ninja \ + -DCMAKE_CUDA_ARCHITECTURES="${CUDA_ARCH}" \ + -DSCALABLE_CCD_CUDA_ARCHITECTURES="${CUDA_ARCH}" \ + -DCMAKE_CXX_FLAGS="-Wno-psabi" \ + -DCMAKE_CUDA_FLAGS="-Xcompiler=-Wno-psabi" + echo ">> [3/3] Building (-j ${JOBS})..." + cmake --build --preset="${PRESET}" -j "${JOBS}" + ' + +echo ">> Done. CUDA build succeeded (compile-only; code was not executed)." diff --git a/.devcontainer/cuda/devcontainer.json b/.devcontainer/cuda/devcontainer.json new file mode 100644 index 000000000..b22b5fae9 --- /dev/null +++ b/.devcontainer/cuda/devcontainer.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://raw.githubusercontent.com/devcontainers/spec/main/schemas/devContainer.schema.json", + "name": "IPCToolkit CUDA Development Container", + "build": { + "dockerfile": "Dockerfile", + // Context is the repository root (two levels up from this file) so the + // build sees the whole project, matching the default dev container. + "context": "../.." + }, + // On a Linux host with the NVIDIA Container Toolkit, uncomment the next line + // to pass the GPU into the container so the CUDA code can actually run. + // (Leave it commented on macOS/Windows or GPU-less hosts, where it errors.) + // "runArgs": ["--gpus=all"], + "customizations": { + "vscode": { + "settings": { + "terminal.integrated.shell.linux": "/bin/zsh", + "C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools", + "C_Cpp.default.intelliSenseMode": "gcc-x64", + "C_Cpp.default.compilerPath": "/usr/bin/clang++-18", + "C_Cpp.clang_format_path": "/usr/bin/clang-format-18", + "C_Cpp.clang_format_style": "file", + "cmake.configureOnOpen": false, + "cmake.buildDirectory": "${workspaceFolder}/build", + "python.pythonPath": "/usr/bin/python3" + }, + "extensions": [ + "ms-vscode.cpptools", + "ms-vscode.cmake-tools", + "xaver.clang-format", + "ms-python.python", + "ms-azuretools.vscode-docker", + "eamodio.gitlens", + "twxs.cmake", + "jeff-hykin.better-cpp-syntax", + "vadimcn.vscode-lldb", + "ms-python.vscode-pylance" + ] + } + }, + "postCreateCommand": "pre-commit install", + "remoteUser": "devuser", + "mounts": [ + "source=${localWorkspaceFolder}/.ccache,target=/home/devuser/.ccache,type=bind,consistency=cached" + ], + "forwardPorts": [], + "remoteEnv": { + "CCACHE_DIR": "/home/devuser/.ccache", + "CCACHE_MAXSIZE": "1G" + }, + "workspaceFolder": "/home/devuser/workspace", + "workspaceMount": "source=${localWorkspaceFolder},target=/home/devuser/workspace,type=bind,consistency=cached" +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..5a09fcf55 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +# Shrinks the build context uploaded to the Docker daemon. Both dev containers +# (.devcontainer/Dockerfile and .devcontainer/cuda/Dockerfile) use the repo +# root as their build context but never COPY it in (the workspace is mounted at +# runtime), so this only speeds up context transfer — it does not affect image +# contents. +build/ +docs/_build/ +*.o +*.so +*.a +__pycache__/ +*.pyc +.git/ +.venv/ +venv/ +.DS_Store diff --git a/CLAUDE.md b/CLAUDE.md index ee7ef7500..094cfe013 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,6 +25,37 @@ cmake --build --preset=python # or: pip install . ``` +### CUDA Builds + +CUDA is off by default. The `cuda-release` and `cuda-debug` presets turn it on. +Compiling CUDA needs nvcc but **not** a GPU; only running it needs the GPU. + +On a machine with no NVIDIA GPU or no CUDA toolkit — any Mac, for instance — +use the CUDA dev container to check that the CUDA build still compiles. It +builds an nvcc-equipped image and compiles the whole project inside it, then +exits with the build's status: + +```bash +./.devcontainer/cuda/build-cuda.sh +``` + +`PRESET` (default `cuda-release`), `CUDA_ARCH`, and `JOBS` override the +defaults, e.g. `PRESET=test ./.devcontainer/cuda/build-cuda.sh` to build the +CUDA tests too. The source is mounted read-only and rsynced into a named +volume, so a run never writes into your working tree, and later runs are +incremental. + +**This needs a running Docker daemon.** On macOS that means Docker Desktop or +a colima VM (`colima start`); the failure mode otherwise is a confusing +`/var/run/docker.sock` connection error rather than a clear diagnostic. A VM +also stops when the machine sleeps, so a script that worked an hour ago may +need the VM restarted. + +For interactive work, `.devcontainer/cuda/devcontainer.json` opens the same +image as a VS Code dev container. On a Linux host with the NVIDIA Container +Toolkit, uncomment its `runArgs` to pass the GPU through so the CUDA code can +actually run. + ## Running Tests Tests use Catch2. Test files mirror the source structure: `tests/src/tests/` mirrors `src/ipc/`. diff --git a/CMakeLists.txt b/CMakeLists.txt index ee04d3984..54a6df7d4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -188,6 +188,12 @@ if(IPC_TOOLKIT_WITH_CUDA) # library to be built with -dc as the member functions could be called by # other libraries and executables. set_target_properties(ipc_toolkit PROPERTIES CUDA_SEPARABLE_COMPILATION ON) + + # Device code calls constexpr host functions (std::numeric_limits, + # std::array). PUBLIC so downstream CUDA consumers (e.g. the tests) inherit + # the flag. + target_compile_options(ipc_toolkit PUBLIC + "$<$:--expt-relaxed-constexpr>") endif() # Fill in configuration options @@ -217,6 +223,14 @@ source_group(TREE "${PROJECT_SOURCE_DIR}" FILES ${IPC_TOOLKIT_SOURCES}) include(eigen) target_link_libraries(ipc_toolkit PUBLIC Eigen3::Eigen) +# Logger +# NOTE: Included before the CCD dependencies on purpose. Scalable CCD carries +# its own spdlog recipe guarded on `if(TARGET spdlog::spdlog)`, so whichever +# runs first wins -- and if that is Scalable CCD's, the unpatched spdlog is the +# one CPM registers and cmake/patches/fmt-nvcc-compat.patch never applies. +include(spdlog) +target_link_libraries(ipc_toolkit PUBLIC spdlog::spdlog) + # libigl include(libigl) target_link_libraries(ipc_toolkit PRIVATE igl::core igl::predicates) @@ -233,11 +247,6 @@ target_link_libraries(ipc_toolkit PRIVATE tight_inclusion::tight_inclusion) include(scalable_ccd) target_link_libraries(ipc_toolkit PRIVATE scalable_ccd::scalable_ccd) - -# Logger -include(spdlog) -target_link_libraries(ipc_toolkit PUBLIC spdlog::spdlog) - # TinyAD include(tinyad) # TODO: Make this a private dependency once we stop exposing TinyAD types in the public API. @@ -307,7 +316,10 @@ if(IPC_TOOLKIT_WITH_SIMD) # unit's own flags. ipc/utils/simd.hpp exposes batch types in the public API, # and a consumer compiled without these flags would name a different type # than the one instantiated in the library, failing to link. - target_compile_options(ipc_toolkit PUBLIC ${SIMD_CXX_FLAGS}) + # NOTE: Restricted to C++ sources because these are MSVC-style /arch flags + # that nvcc would otherwise treat as input files, and device code has no use + # for CPU SIMD anyway. + target_compile_options(ipc_toolkit PUBLIC "$<$:${SIMD_CXX_FLAGS}>") # Link against cross-platform xsimd library include(xsimd) diff --git a/cmake/ipc_toolkit/ipc_toolkit_use_colors.cmake b/cmake/ipc_toolkit/ipc_toolkit_use_colors.cmake index 5ee73479e..71dcd9d93 100644 --- a/cmake/ipc_toolkit/ipc_toolkit_use_colors.cmake +++ b/cmake/ipc_toolkit/ipc_toolkit_use_colors.cmake @@ -17,7 +17,9 @@ if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC") # > fatal error C1090: PDB API call failed, error code '23' # To avoid this problem, we force PDB write to be synchronous with /FS. # https://developercommunity.visualstudio.com/content/problem/48897/c1090-pdb-api-call-failed-error-code-23.html - add_compile_options(/FS) + # Exclude CUDA: nvcc would treat a bare /FS as an input file. CMake already + # forwards /FS to the MSVC host compiler for CUDA targets via -Xcompiler. + add_compile_options($<$>:/FS>) else() include(ipc_toolkit_filter_flags) set(IPC_TOOLKIT_GLOBAL_FLAGS diff --git a/cmake/patches/fmt-nvcc-compat.patch b/cmake/patches/fmt-nvcc-compat.patch new file mode 100644 index 000000000..59c40a4c2 --- /dev/null +++ b/cmake/patches/fmt-nvcc-compat.patch @@ -0,0 +1,42 @@ +diff --git a/include/spdlog/fmt/bundled/base.h b/include/spdlog/fmt/bundled/base.h +index 620456b..0740975 100644 +--- a/include/spdlog/fmt/bundled/base.h ++++ b/include/spdlog/fmt/bundled/base.h +@@ -455,7 +455,14 @@ struct is_std_string_like().find_first_of( + const typename T::value_type*> {}; + + // Check if the literal encoding is UTF-8. ++#if defined(__CUDACC__) ++// NVCC's device front-end (EDG) does not honor the host compiler's /utf-8 ++// flag, so the compile-time probe below misfires. The host pass is compiled ++// with /utf-8, so UTF-8 is in fact enabled. ++enum { is_utf8_enabled = 1 }; ++#else + enum { is_utf8_enabled = "\u00A7"[1] == '\xA7' }; ++#endif + enum { use_utf8 = !FMT_WIN32 || is_utf8_enabled }; + + #ifndef FMT_UNICODE +diff --git a/include/spdlog/fmt/bundled/format.h b/include/spdlog/fmt/bundled/format.h +index 4a65300..2ce9069 100644 +--- a/include/spdlog/fmt/bundled/format.h ++++ b/include/spdlog/fmt/bundled/format.h +@@ -3138,8 +3138,18 @@ constexpr auto fractional_part_rounding_thresholds(int index) -> uint32_t { + // It is equal to ceil(2^31 + 2^32/10^(k + 1)). + // These are stored in a string literal because we cannot have static arrays + // in constexpr functions and non-static ones are poorly optimized. ++#if defined(__CUDACC__) ++ // NVCC's device front-end (EDG) rejects char32_t hex escapes whose value has ++ // the high bit set ("character value is out of range"), so use an equivalent ++ // uint32_t array instead of a UTF-32 string literal. ++ constexpr uint32_t thresholds[] = {0x9999999au, 0x828f5c29u, 0x80418938u, ++ 0x80068db9u, 0x8000a7c6u, 0x800010c7u, ++ 0x800001aeu, 0x8000002bu}; ++ return thresholds[index]; ++#else + return U"\x9999999a\x828f5c29\x80418938\x80068db9\x8000a7c6\x800010c7" + U"\x800001ae\x8000002b"[index]; ++#endif + } + + template diff --git a/cmake/recipes/scalable_ccd.cmake b/cmake/recipes/scalable_ccd.cmake index 074b1d355..fa05435e9 100644 --- a/cmake/recipes/scalable_ccd.cmake +++ b/cmake/recipes/scalable_ccd.cmake @@ -8,7 +8,7 @@ message(STATUS "Third-party: creating target 'scalable_ccd::scalable_ccd'") include(CPM) CPMAddPackage( - URI "gh:continuous-collision-detection/scalable-ccd#c80af01cab083b3eeb8dac80312ec9cfe479a5cf" + URI "gh:continuous-collision-detection/scalable-ccd#8f9347c1afc36f2dda17424c15ff5b68087fe8dc" OPTIONS "SCALABLE_CCD_WITH_CUDA ${IPC_TOOLKIT_WITH_CUDA}" ) diff --git a/cmake/recipes/spdlog.cmake b/cmake/recipes/spdlog.cmake index a31020623..d3be8c70c 100644 --- a/cmake/recipes/spdlog.cmake +++ b/cmake/recipes/spdlog.cmake @@ -10,7 +10,14 @@ option(SPDLOG_INSTALL "Generate the install target" ON) set(CMAKE_INSTALL_DEFAULT_COMPONENT_NAME "spdlog") include(CPM) -CPMAddPackage("gh:gabime/spdlog@1.17.0") +# The bundled fmt trips up NVCC's device front-end (EDG): a compile-time /utf-8 +# probe misfires and a char32_t table uses hex escapes with the high bit set. +# Neither is fixable via compiler flags (the front-end never sees /utf-8), so we +# patch the bundled headers to guard both cases behind __CUDACC__. +CPMAddPackage( + URI "gh:gabime/spdlog@1.17.0" + PATCHES "${CMAKE_CURRENT_LIST_DIR}/../patches/fmt-nvcc-compat.patch" +) set_target_properties(spdlog PROPERTIES POSITION_INDEPENDENT_CODE ON) diff --git a/cmake/recipes/tinyad.cmake b/cmake/recipes/tinyad.cmake index 0352b088f..26b0efee7 100644 --- a/cmake/recipes/tinyad.cmake +++ b/cmake/recipes/tinyad.cmake @@ -8,7 +8,7 @@ message(STATUS "Third-party: creating target 'TinyAD::TinyAD'") include(CPM) CPMAddPackage( - URI "gh:zfergus/tinyad#957ede5bcf4b8d924f27230083508c664a9f7a7d" + URI "gh:zfergus/tinyad#5bddce2c83e85d61b30e99359671037ee4a29349" OPTIONS "TINYAD_PARALLEL_BACKEND onetbb" ) diff --git a/src/ipc/utils/simd.hpp b/src/ipc/utils/simd.hpp index a731f960a..3955c9e66 100644 --- a/src/ipc/utils/simd.hpp +++ b/src/ipc/utils/simd.hpp @@ -4,6 +4,9 @@ #include #include +#if defined(IPC_TOOLKIT_WITH_SIMD) && !defined(__CUDACC__) +#include +#endif #include #include @@ -106,9 +109,7 @@ inline auto select_lazy(const Mask& mask, F&& value, Rest&&... rest) } // namespace ipc -#ifdef IPC_TOOLKIT_WITH_SIMD - -#include +#if defined(IPC_TOOLKIT_WITH_SIMD) && !defined(__CUDACC__) namespace Eigen {