diff --git a/.github/workflows/qa-analysis.yml b/.github/workflows/qa-analysis.yml index ba7755a8..94e95de0 100644 --- a/.github/workflows/qa-analysis.yml +++ b/.github/workflows/qa-analysis.yml @@ -257,7 +257,8 @@ jobs: - name: Install package env: - SKBUILD_CMAKE_DEFINE: "monoprop_ENABLE_CXX_UNIT_TESTS=OFF;monoprop_ENABLE_MPI=ON" + SKBUILD_CMAKE_DEFINE: "monoprop_ENABLE_CXX_UNIT_TESTS=OFF" + monoprop_ENABLE_MPI: "ON" run: | uv sync --no-progress --all-extras -v @@ -305,7 +306,8 @@ jobs: - name: Build (generates compile_commands.json) env: - SKBUILD_CMAKE_DEFINE: "CMAKE_CXX_COMPILER=clang++;monoprop_ENABLE_CXX_UNIT_TESTS=OFF;monoprop_ENABLE_MPI=ON" + SKBUILD_CMAKE_DEFINE: "CMAKE_CXX_COMPILER=clang++;monoprop_ENABLE_CXX_UNIT_TESTS=OFF" + monoprop_ENABLE_MPI: "ON" run: | uv sync --no-progress --all-extras -v diff --git a/AGENTS.md b/AGENTS.md index cf2f2fd0..8b191320 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -119,6 +119,7 @@ We also use [`just`](https://github.com/casey/just) for task automation. ```bash uv sync --all-groups --all-extras -v # Build & install (workspace-wide) +monoprop_ENABLE_MPI=ON uv sync --all-extras --reinstall-package monoprop --no-cache -v # MPI-enabled build uv run pytest # Run tests (monoprop's suite + the workspace members' suites) SKBUILD_CMAKE_BUILD_TYPE=AsanUbsan SKBUILD_CMAKE_DEFINE="monoprop_SANITIZER=asan-ubsan" uv sync --group workspace-test --all-extras --reinstall-package monoprop --no-cache -v # Rebuild when changing sanitizer settings. LD_PRELOAD="$(g++ -print-file-name=libasan.so):$(g++ -print-file-name=libstdc++.so.6)" ASAN_OPTIONS=detect_leaks=0 uv run pytest # Python tests against a sanitizer tree diff --git a/README.md b/README.md index 3f03bbbe..6b9d89b6 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ Python bindings (via [uv](https://github.com/astral-sh/uv)): ```bash uv sync --all-extras -v # with MPI: -uv sync --all-extras -v --config-settings=cmake.define.monoprop_ENABLE_MPI=ON +monoprop_ENABLE_MPI=ON uv sync --all-extras -v ``` C++ unit-test build: diff --git a/cpp/tests/README.md b/cpp/tests/README.md index 6fc34490..a08df1f3 100644 --- a/cpp/tests/README.md +++ b/cpp/tests/README.md @@ -30,8 +30,8 @@ uv sync --all-extras -v ctest --test-dir build/editable/Release ``` -For an MPI-enabled tree, rerun `uv sync` with -`--config-settings=cmake.define.monoprop_ENABLE_MPI=ON`. +For an MPI-enabled tree, rerun `uv sync` with `monoprop_ENABLE_MPI=ON` in the +environment. ## Running Tests ```bash diff --git a/docs/content/docs/building.mdx b/docs/content/docs/building.mdx index 87b80b83..23641129 100644 --- a/docs/content/docs/building.mdx +++ b/docs/content/docs/building.mdx @@ -14,7 +14,7 @@ mechanism differs by build: | Build | Enable MPI with | | --- | --- | -| Python bindings and C++ tests (scikit-build / `uv` / `pip`) | `--config-settings=cmake.define.monoprop_ENABLE_MPI=ON` (or export `SKBUILD_CMAKE_ARGS="-Dmonoprop_ENABLE_MPI=ON"`) | +| Python bindings and C++ tests (scikit-build / `uv` / `pip`) | `monoprop_ENABLE_MPI=ON` in the environment | The prebuilt wheels published to PyPI (`pip install monoprop`) are also built without MPI, so a from-source build is required for multi-rank runs. @@ -48,19 +48,59 @@ This produces a single-process build with no MPI dependency. ### With MPI -Pass a `config-settings` override to enable MPI: +Set `monoprop_ENABLE_MPI=ON` in the environment: + +```bash +monoprop_ENABLE_MPI=ON uv sync --all-extras -v +``` + +It works the same with `pip` when installing from a checkout: + +```bash +monoprop_ENABLE_MPI=ON pip install . +``` + +That one variable does two things, matched by the +`[[tool.scikit-build.overrides]]` block in `pyproject.toml`: it sets the +`monoprop_ENABLE_MPI` CMake option, and it adds `mpi4py` to the build +requirements. mpi4py is a build-time input — the bindings need its headers to +hand an MPI communicator across the nanobind boundary — but only for MPI +builds, so it is injected per-build through PEP 517's +`get_requires_for_build_wheel` rather than sitting in `build-system.requires` +where every build would pay for it. + + + Because it is an environment variable, it does not participate in uv's build + cache key. Add `--reinstall-package monoprop --no-cache` when flipping MPI on + or off in an existing environment, or uv may reuse the previous build. + + +#### Driving it with config-settings instead + +A scikit-build-core override can only match environment variables, never +config-settings, so `cmake.define` alone will not pull in mpi4py — the build +would fail at configure time with a message telling you as much. Pass both +settings to use this route: ```bash uv sync --all-extras -v \ - --config-settings=cmake.define.monoprop_ENABLE_MPI=ON + --config-settings=cmake.define.monoprop_ENABLE_MPI=ON \ + --config-settings=build.requires=mpi4py>=4.1.0 ``` -The same override works with `pip` when installing from a checkout: +In a workspace, prefer the per-package form so the settings do not reach the +sibling distributions in `packages/`: ```bash -pip install . --config-settings=cmake.define.monoprop_ENABLE_MPI=ON +uv sync --all-extras -v \ + --config-settings-package="monoprop:cmake.define.monoprop_ENABLE_MPI=ON" \ + --config-settings-package="monoprop:build.requires=mpi4py>=4.1.0" ``` +The cost of this route is that the mpi4py pin is repeated on the command line +instead of living once in `pyproject.toml`; the environment switch is preferred +for that reason. + ### Verify the install ```bash diff --git a/docs/content/docs/testing.mdx b/docs/content/docs/testing.mdx index 51326cff..ebfd6d09 100644 --- a/docs/content/docs/testing.mdx +++ b/docs/content/docs/testing.mdx @@ -41,14 +41,18 @@ just test-mpi-matrix # MPI-marked tests across a rank matrix To do it by hand, build with MPI on, then run under `mpiexec` with `--no-sync` so each rank reuses that build. `--reinstall-package` and `--no-cache` force a -genuine rebuild, since uv does not key its build cache on `SKBUILD_CMAKE_ARGS`: +genuine rebuild, since uv does not key its build cache on the environment: ```bash -SKBUILD_CMAKE_ARGS="-Dmonoprop_ENABLE_MPI=ON" \ +monoprop_ENABLE_MPI=ON \ uv sync --all-extras --reinstall-package monoprop --no-cache mpiexec --allow-run-as-root -n 2 uv run --no-sync python -m pytest tests --with-mpi ``` +`monoprop_ENABLE_MPI` also provisions mpi4py as a build input; see +[Building from source](/building#with-mpi) for why, and for the +config-settings equivalent. + ### C++ unit tests The C++ tests run through CTest. The build tree is produced by `uv sync` (via @@ -65,7 +69,7 @@ With MPI, reuse the MPI-enabled `uv sync` from the Python MPI section above, then run CTest against the same tree: ```bash -SKBUILD_CMAKE_ARGS="-Dmonoprop_ENABLE_MPI=ON" \ +monoprop_ENABLE_MPI=ON \ uv sync --all-extras --reinstall-package monoprop --no-cache ctest --test-dir build/editable/Release --output-on-failure ``` @@ -89,7 +93,7 @@ CTest runs each Boost case as its own process, so an MPI build's `MPI_Init` prob fabric device per case, whether or not the test sends anything. `monoprop_TEST_EXCLUDE_MPI_FABRIC=ON` (default) skips that probe for the single-process `serial` variants only; multi-rank variants keep the full component set, since they exchange real messages. Turn it off by adding -`-Dmonoprop_TEST_EXCLUDE_MPI_FABRIC=OFF` to the `SKBUILD_CMAKE_ARGS` MPI build above. +`SKBUILD_CMAKE_DEFINE="monoprop_TEST_EXCLUDE_MPI_FABRIC=OFF"` to the MPI build above. CTest registers every Boost case individually as a `serial` variant. When the build has MPI enabled and a launcher is found, it also registers the whole suite diff --git a/justfile b/justfile index 9346525c..9609aa0f 100644 --- a/justfile +++ b/justfile @@ -32,7 +32,8 @@ test: # Pass RANKS as either a single integer or a semicolon-separated list (e.g. "1;2;4"). test-mpi RANKS='': - uv sync --all-extras --group workspace-test --reinstall-package monoprop --no-cache --config-settings-package="monoprop:cmake.define.monoprop_ENABLE_MPI=ON" -v + monoprop_ENABLE_MPI=ON \ + uv sync --all-extras --group workspace-test --reinstall-package monoprop --no-cache -v export OMPI_MCA_rmaps_base_oversubscribe="1"; \ ranks="${1:-${monoprop_MPI_TEST_PROCS:-2}}"; \ for r in ${ranks//;/ }; \ @@ -149,8 +150,8 @@ bench-mpi LABEL RANKS *MPIARGS: # Rebuild monoprop with MPI enabled (editable). Run once before `just bench-mpi`. bench-build-mpi: - uv sync --all-extras --group bench --reinstall-package monoprop --no-cache \ - --config-settings-package="monoprop:cmake.define.monoprop_ENABLE_MPI=ON" -v + monoprop_ENABLE_MPI=ON \ + uv sync --all-extras --group bench --reinstall-package monoprop --no-cache -v # Quick sanity run: tiny sizes, skip the slow static benchmarks. bench-smoke: diff --git a/pyproject.toml b/pyproject.toml index f67f7d6d..5e4b8dba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,5 @@ [build-system] requires = [ - "mpi4py>=4.1.0", "nanobind-backend>=1.0.0", "nanobind==3.0.0", "scikit-build-core>=1.0.3,<2", @@ -149,11 +148,22 @@ sdist.exclude = [ ".prettierignore", "cspell.json", ] -sdist.include = ["src/monoprop/_version.py", "src/monoprop/_dispatch.py", ".github/license-header.txt"] +sdist.include = [ + "src/monoprop/_version.py", + "src/monoprop/_dispatch.py", + ".github/license-header.txt", +] build.verbose = true build.tool-args = ["-j2"] -# Default to a single build job for Python bindings; CI and one-off builds can -# still override this via config-settings or environment variables. +# `monoprop_ENABLE_MPI` in the environment is the single switch: it both provisions the +# build input and flips the CMake option. +# NOTE: An override's `if.` clause can only match environment variables, never config-settings, so the +# `--config-settings=cmake.define.monoprop_ENABLE_MPI=ON` route cannot reach this table +# and has to carry `--config-settings=build.requires=mpi4py>=4.1.0` alongside it. +[[tool.scikit-build.overrides]] +if.env.monoprop_ENABLE_MPI = true +build.requires = ["mpi4py>=4.1.0"] +cmake.define.monoprop_ENABLE_MPI = "ON" [tool.setuptools_scm] diff --git a/src/monoprop/bindings/CMakeLists.txt b/src/monoprop/bindings/CMakeLists.txt index 9994563d..257a343e 100644 --- a/src/monoprop/bindings/CMakeLists.txt +++ b/src/monoprop/bindings/CMakeLists.txt @@ -1,6 +1,22 @@ if(monoprop_ENABLE_MPI) include(${PROJECT_SOURCE_DIR}/cmake/custom/FindPythonModule.cmake) - find_python_module(mpi4py REQUIRED) + find_python_module(mpi4py QUIET) + + if(NOT mpi4py_FOUND) + message( + FATAL_ERROR + "MPI is enabled but mpi4py is not importable from ${Python_EXECUTABLE}. " + "mpi4py is a build-time input of the bindings: its headers hand the MPI " + "communicator across the nanobind boundary.\n" + "Enable MPI with the environment switch, which provisions it automatically:\n" + " monoprop_ENABLE_MPI=ON uv sync --all-extras\n" + "If you drive the build with config-settings, request it explicitly too, as " + "config-settings cannot trigger a scikit-build-core override:\n" + " uv sync --all-extras \\\n" + " --config-settings=cmake.define.monoprop_ENABLE_MPI=ON \\\n" + " --config-settings=build.requires=mpi4py>=4.1.0" + ) + endif() # we also need the include directories for mpi4py if(mpi4py_FOUND)