Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions .github/workflows/continuous.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,82 @@ jobs:
run: |
cd build
ctest --verbose --output-on-failure

####################
# Hybrid solver (nano-mpi)
####################

# The default configuration builds with POLYSOLVE_WITH_MPI=OFF, which leaves
# the hybrid solver out of the build entirely -- so nothing above exercises
# it. This job turns it on. Ranks are threads of the test process here, so
# there is nothing to launch and no MPI to install.

Hybrid:
name: Hybrid solver (${{ matrix.config }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
config: [Debug, Release]
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 10

- name: Dependencies
run: |
sudo apt update
sudo apt -o Acquire::Retries=3 install \
libblas-dev \
libglu1-mesa-dev \
xorg-dev \
ccache \
libsuitesparse-dev
echo 'CACHE_PATH=~/.ccache' >> "$GITHUB_ENV"

- name: Cache build
id: cache-build
uses: actions/cache@v4
with:
path: ${{ env.CACHE_PATH }}
key: hybrid-${{ matrix.config }}-cache-${{ github.sha }}
restore-keys: hybrid-${{ matrix.config }}-cache

- name: Prepare ccache
run: |
ccache --max-size=1.0G
ccache -V && ccache --show-stats && ccache --zero-stats

- name: Configure
run: |
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=${{ matrix.config }} \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DPOLYSOLVE_WITH_MPI=ON

- name: Build
run: cmake --build build -j2 && ccache --show-stats

- name: Tests
run: ctest --test-dir build --output-on-failure

# Ranks really are threads: nothing in the binary links an MPI runtime,
# and the solver still scales.
- name: No MPI runtime is linked
run: |
ldd build/tests/bench_spmd || true
if ldd build/tests/bench_spmd | grep -qiE 'libmpi|libopen-rte|libopen-pal'; then
echo "an MPI runtime got linked in; ranks were supposed to be threads" >&2
exit 1
fi

- name: Hybrid solver scales across rank counts
run: |
for n in 1 2 4; do
NANOMPI_NUM_RANKS=$n ./build/tests/bench_spmd 32 CPUHybrid | tee -a bench.txt
done
# every run must have converged
awk '{ for (i = 1; i <= NF; i++) if ($i ~ /^relres=/) {
split($i, kv, "="); if (kv[2] + 0 > 1e-8) {
print "residual too large: " $0 > "/dev/stderr"; exit 1 } } }' bench.txt
53 changes: 52 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,19 @@ else()
set(POLYSOLVE_ON_APPLE_SILICON OFF)
endif()

if(NOT WIN32)
set(POLYSOLVE_WITH_MPI_DEFAULT ON)
else()
set(POLYSOLVE_WITH_MPI_DEFAULT OFF)
endif()

# Polysolve options
option(POLYSOLVE_WITH_SANITIZERS "Enable sanitizers in compilation targets" OFF)
option(POLYSOLVE_WITH_UNICODE "Use Unicode in logging messages" ON)
option(POLYSOLVE_WITH_CUDA "Enable cuda support" OFF)

# Polysolve options for enabling/disabling optional libraries
option(POLYSOLVE_WITH_MPI "Enable MPI support" ${POLYSOLVE_WITH_MPI_DEFAULT})
option(POLYSOLVE_WITH_ACCELERATE "Enable Apple Accelerate" ${POLYSOLVE_ON_APPLE_SILICON})
option(POLYSOLVE_WITH_CHOLMOD "Enable Cholmod library" ON)
option(POLYSOLVE_WITH_UMFPACK "Enable UmfPack library" ON)
Expand Down Expand Up @@ -169,6 +176,18 @@ if (POLYSOLVE_WITH_CUDA)
set(CMAKE_CUDA_STANDARD_REQUIRED ON)
endif()

################################################################################
# MPI
################################################################################

# MPI here is nano-mpi: ranks are threads of this process, so there is no MPI
# installation to look for and nothing for the user to launch. The recipe brings
# it in and puts a FindMPI shim on the module path so that dependencies calling
# find_package(MPI) -- hypre does -- resolve to it.
if(POLYSOLVE_WITH_MPI)
include(nanompi)
endif()

################################################################################
# PolySolve Library
################################################################################
Expand Down Expand Up @@ -247,6 +266,21 @@ if(POLYSOLVE_WITH_CUDA)
target_compile_definitions(polysolve_linear PUBLIC POLYSOLVE_WITH_CUDA)
endif()

if(POLYSOLVE_WITH_CUDA AND NOT POLYSOLVE_LARGE_INDEX)
target_compile_definitions(polysolve PUBLIC POLYSOLVE_WITH_CUDSS)
target_compile_definitions(polysolve_linear PUBLIC POLYSOLVE_WITH_CUDSS)
endif()

if(POLYSOLVE_WITH_CUDA AND NOT POLYSOLVE_LARGE_INDEX)
target_compile_definitions(polysolve PUBLIC POLYSOLVE_WITH_GPU_HYBRID)
target_compile_definitions(polysolve_linear PUBLIC POLYSOLVE_WITH_GPU_HYBRID)
endif()

if(POLYSOLVE_WITH_MPI AND NOT POLYSOLVE_LARGE_INDEX)
target_compile_definitions(polysolve PUBLIC POLYSOLVE_WITH_CPU_HYBRID)
target_compile_definitions(polysolve_linear PUBLIC POLYSOLVE_WITH_CPU_HYBRID)
endif()

# Graph partition library Kaminpar does not support windows.
if(POLYSOLVE_WITH_CUDA AND NOT WIN32)
target_compile_definitions(polysolve PUBLIC POLYSOLVE_WITH_MAS)
Expand All @@ -257,6 +291,12 @@ endif()
# Dependencies
################################################################################

# cuDSS
if (POLYSOLVE_WITH_CUDA)
include(cudss)
target_link_libraries(polysolve_linear PRIVATE cudss)
endif()

# CCCL
if(POLYSOLVE_WITH_CUDA)
include(cccl)
Expand All @@ -275,6 +315,17 @@ endif()
# Linear
# ------

if(POLYSOLVE_WITH_MPI)
message(STATUS "MPI enabled for PolySolve.")

# nanompi::nanompi, not MPI::MPI_CXX: the shim's MPI target carries only the
# header path, for the try_compile reason spelled out there.
target_link_libraries(polysolve_linear PUBLIC nanompi::nanompi)

# Define POLYSOLVE_WITH_MPI for your C++ #ifdef checks
target_compile_definitions(polysolve_linear PUBLIC POLYSOLVE_WITH_MPI)
endif()

# Accelerate solver (Include before Eigen)
if(POLYSOLVE_WITH_ACCELERATE)
set(BLA_VENDOR Apple)
Expand Down Expand Up @@ -483,4 +534,4 @@ if(POLYSOLVE_WITH_TESTS)
include("${catch2_SOURCE_DIR}/contrib/Catch.cmake")

add_subdirectory(tests)
endif()
endif()
67 changes: 67 additions & 0 deletions cmake/nanompi-as-mpi/FindMPI.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#
# Inside this build, "MPI" is nano-mpi.
#
# nano-mpi implements MPI with every rank a thread of this process, so there is
# nothing to search for: it is an ordinary CMake target, built from source
# alongside everything else. This module exists so that dependencies which call
# find_package(MPI) -- hypre does, in HYPRE_CMakeUtilities.cmake -- resolve to
# that target instead of to a system Open MPI, which would drag in a launcher
# polysolve has no way to invoke.
#
# It is only on CMAKE_MODULE_PATH when the nanompi recipe has been included;
# without that, CMake's own FindMPI is found as usual.
#

if(NOT TARGET nanompi::nanompi)
message(FATAL_ERROR
"This FindMPI shim is on the module path but nano-mpi is not in the "
"build. Include the nanompi recipe before anything calls "
"find_package(MPI).")
endif()

if("Fortran" IN_LIST MPI_FIND_COMPONENTS)
message(FATAL_ERROR
"MPI Fortran bindings were requested, and nano-mpi has none. It cannot: "
"Fortran SAVE and COMMON storage is per-process by language rule, and "
"nano-mpi's ranks are threads of one process.")
endif()

# MPI::MPI_C carries the include path and NOTHING ELSE, deliberately. hypre
# puts it in CMAKE_REQUIRED_LIBRARIES and runs check_c_source_compiles, and
# try_compile() exports the target into a scratch project -- where a link
# interface naming nanompi::nanompi is a hard error, because that target does
# not exist there. Whoever links MPI links nanompi::nanompi explicitly instead;
# the hypre recipe and polysolve's own CMakeLists both do.
#
# The probe hypre runs is for MPI_Comm_f2c, which nano-mpi does not declare
# (it has no Fortran bindings, and cannot), so it correctly comes out false.
foreach(_lang IN ITEMS C CXX)
if(NOT TARGET MPI::MPI_${_lang})
# GLOBAL, so the subdirectories CPM adds later can use it
add_library(MPI::MPI_${_lang} INTERFACE IMPORTED GLOBAL)
set_target_properties(MPI::MPI_${_lang} PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${NANOMPI_INCLUDE_DIR}")
endif()

set(MPI_${_lang}_FOUND TRUE)
set(MPI_${_lang}_COMPILER "${CMAKE_${_lang}_COMPILER}")
set(MPI_${_lang}_LIBRARIES "")
set(MPI_${_lang}_INCLUDE_DIRS "${NANOMPI_INCLUDE_DIR}")
set(MPI_${_lang}_INCLUDE_DIR "${NANOMPI_INCLUDE_DIR}")
set(MPI_${_lang}_INCLUDE_PATH "${NANOMPI_INCLUDE_DIR}")
set(MPI_${_lang}_COMPILE_OPTIONS "")
set(MPI_${_lang}_COMPILE_DEFINITIONS "")
set(MPI_${_lang}_LINK_FLAGS "")
set(MPI_${_lang}_VERSION "3.1")
endforeach()

set(MPI_FOUND TRUE)
set(MPI_VERSION "3.1")

# There is no launcher. nanompiexec exists for scripts that expect one, but it
# only translates -n into an environment variable and execs the program.
set(MPIEXEC_EXECUTABLE "" CACHE FILEPATH "nano-mpi has no launcher: ranks are threads")
set(MPIEXEC_NUMPROC_FLAG "-n")
set(MPIEXEC_MAX_NUMPROCS 1)

message(STATUS "MPI: using nano-mpi (ranks are threads of this process)")
27 changes: 27 additions & 0 deletions cmake/recipes/cudss.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# cuDSS solver

if(TARGET cudss)
return()
endif()

message(STATUS "Third-party: creating target 'cudss'")

set(CUDSS_URL
"https://developer.download.nvidia.com/compute/cudss/redist/libcudss/linux-x86_64/libcudss-linux-x86_64-0.7.1.4_cuda13-archive.tar.xz"
CACHE STRING "cuDSS download URL")
set(CUDSS_URL_SHA256
"84b34ebe7fad40ec10f2aab2957a63b6070bd8ce16e3ada3e6bcac7317256347"
CACHE STRING "cuDSS download URL SHA256 checksum")

include(CPM)
CPMAddPackage(
NAME cudss
URL ${CUDSS_URL}
URL_HASH SHA256=${CUDSS_URL_SHA256}
DOWNLOAD_ONLY ON
)

find_package(cudss CONFIG REQUIRED
PATHS "${cudss_SOURCE_DIR}/lib/cmake/cudss"
NO_DEFAULT_PATH
)
22 changes: 19 additions & 3 deletions cmake/recipes/hypre.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ endif()

message(STATUS "Third-party: creating target 'HYPRE::HYPRE'")

set(HYPRE_ENABLE_MPI OFF CACHE INTERNAL "" FORCE)
set(HYPRE_ENABLE_PRINT_ERRORS ON CACHE INTERNAL "" FORCE)
set(HYPRE_ENABLE_BIGINT OFF CACHE INTERNAL "" FORCE)
set(HYPRE_ENABLE_MIXEDINT OFF CACHE BOOL "" FORCE)
Expand All @@ -22,6 +21,17 @@ else()
set(HYPRE_ENABLE_CUDA OFF CACHE INTERNAL "" FORCE)
endif()

# hypre is built as an ordinary MPI build. What is different is the MPI: with
# POLYSOLVE_WITH_MPI on, the nanompi recipe has already put a FindMPI shim on the
# module path, so hypre's own find_package(MPI REQUIRED) resolves to nano-mpi --
# whose ranks are threads of this process. hypre itself needs no special mode.
if (POLYSOLVE_WITH_MPI)
include(nanompi)
set(HYPRE_ENABLE_MPI ON CACHE INTERNAL "" FORCE)
else()
set(HYPRE_ENABLE_MPI OFF CACHE INTERNAL "" FORCE)
endif()

# HYPRE unconditionally defines an "uninstall" target, which conflicts with other buggy libraries
# as modern cmake requires unique target name. This is a hacky workaround until upstream is fixed.
macro(add_custom_target _target_name)
Expand All @@ -35,7 +45,13 @@ endmacro()
include(CPM)
CPMAddPackage(
NAME hypre
GITHUB_REPOSITORY hypre-space/hypre
GIT_TAG 7e247a231ebdeb44b06c7c9d3b5bee3bac21123f
GITHUB_REPOSITORY danielepanozzo/hypre
GIT_TAG thread-mpi-backend
SOURCE_SUBDIR src
)

# hypre links MPI::MPI_C, which here carries only the header path -- see the
# FindMPI shim for why it cannot carry the library. Supply the library here.
if (POLYSOLVE_WITH_MPI)
target_link_libraries(HYPRE PUBLIC nanompi::nanompi)
endif()
44 changes: 44 additions & 0 deletions cmake/recipes/nanompi.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# nano-mpi -- MPI ranks as threads of this process (https://github.com/danielepanozzo/nano-mpi)
#
# The hybrid solver wants hypre's MPI algorithms, which are the fast ones, but
# polysolve is a library: it cannot ask the application that links it to relaunch
# itself under mpirun. nano-mpi resolves that by making each rank a thread, so
# an ordinary single-threaded caller gets the domain-decomposed solve anyway.

# From here on, find_package(MPI) inside this build resolves to nano-mpi rather
# than to a system Open MPI. hypre calls it, and would otherwise link an MPI
# whose ranks are processes and then need a launcher we cannot provide.
# include() does not open a scope, so this reaches the subdirectories added
# after it -- which is exactly the set that must see it. Done before the
# already-built guard, so a second include() still gets the module path.
list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake/nanompi-as-mpi/")
list(REMOVE_DUPLICATES CMAKE_MODULE_PATH)

if(TARGET nanompi::nanompi)
return()
endif()

message(STATUS "Third-party: creating target 'nanompi::nanompi'")

set(NANOMPI_BUILD_TESTS OFF CACHE INTERNAL "" FORCE)
set(NANOMPI_BUILD_WRAPPERS OFF CACHE INTERNAL "" FORCE)
# hypre exports its own targets and links this one, so it has to be in an
# export set of its own -- and an installed polysolve linking a static hypre
# needs libnanompi at link time regardless.
set(NANOMPI_INSTALL ON CACHE INTERNAL "" FORCE)

include(CPM)
CPMAddPackage(
NAME nanompi
GITHUB_REPOSITORY danielepanozzo/nano-mpi
GIT_TAG v0.1.1
)

# The header path as a plain directory, for the FindMPI shim. It cannot get it
# from the target: an imported MPI::MPI_C must not name a project target in its
# link interface, because try_compile() -- which hypre uses to probe for
# MPI_Comm_f2c -- exports that target into a scratch project where nanompi does
# not exist. So MPI::MPI_C carries includes only, and whoever links MPI also
# links nanompi::nanompi explicitly.
set(NANOMPI_INCLUDE_DIR "${nanompi_SOURCE_DIR}/include"
CACHE PATH "nano-mpi headers" FORCE)
2 changes: 1 addition & 1 deletion cmake/recipes/polyfem-data.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ ExternalProject_Add(
SOURCE_DIR ${POLYFEM_DATA_ROOT}

GIT_REPOSITORY https://github.com/polyfem/polyfem-data
GIT_TAG 9c1bdd5bd02215e80bc1668547e5dbeb5484a527
GIT_TAG 8a2eff19a33ccfd395c9294342e2c5c2babf2626

CONFIGURE_COMMAND ""
BUILD_COMMAND ""
Expand Down
Loading
Loading