From 6bad4415b935f0a9fec5d192f673861e87747144 Mon Sep 17 00:00:00 2001 From: Emily Boudreaux Date: Sat, 13 Jun 2026 07:16:50 -0400 Subject: [PATCH] fix(omp): upped CppAD max num threads to 512 Also added more explicit error handeling to ensure that users know what to do when the thread count exceeds the compiled maximum --- build-config/cppad/meson.build | 2 +- src/include/gridfire/utils/gf_omp.h | 44 ++++++-- src/include/gridfire/utils/macros.h | 23 ++++- utils/wheels/build-wheels-linux_aarch64.sh | 114 +++++++++++++++++++-- utils/wheels/build-wheels-linux_x86_64.sh | 2 +- 5 files changed, 160 insertions(+), 25 deletions(-) diff --git a/build-config/cppad/meson.build b/build-config/cppad/meson.build index 8b2d23f4..585595f4 100644 --- a/build-config/cppad/meson.build +++ b/build-config/cppad/meson.build @@ -2,7 +2,7 @@ cppad_cmake_options = cmake.subproject_options() cppad_cmake_options.add_cmake_defines({ 'cppad_static_lib': 'true', - 'cpp_mas_num_threads': '10', + 'cppad_max_num_threads': '512', 'cppad_debug_and_release': 'false', 'include_doc': 'false', 'CMAKE_POSITION_INDEPENDENT_CODE': true diff --git a/src/include/gridfire/utils/gf_omp.h b/src/include/gridfire/utils/gf_omp.h index 5f00459d..d8b42a1c 100644 --- a/src/include/gridfire/utils/gf_omp.h +++ b/src/include/gridfire/utils/gf_omp.h @@ -7,7 +7,18 @@ #include namespace gridfire::omp { - static bool s_par_mode_initialized = false; + /** + * @brief Namespace containing utilities for initializing and managing parallel execution with OpenMP and CppAD. + * + * @note GF_PAR_INIT should be called at the start of your program regardless of if you run in parallel or serial + * mode. When GridFire has been compiled without openMP support, GF_PAR_INIT will simply log a message that you are not in parallel mode and return. + * However, if in the future you wish to relink against a version which has been compiled with parallel support + * missing GF_PAR_INIT may lead to a silent failure. + * + * @note An end user should only ever need to call the GF_PAR_INIT macro. i.e. never call any of the actual + * functions in this header directly. + */ + inline bool s_par_mode_initialized = false; inline unsigned long get_thread_id() { return static_cast(omp_get_thread_num()); @@ -18,19 +29,30 @@ namespace gridfire::omp { } inline void init_parallel_mode() { - if (s_par_mode_initialized) { - return; // Only initialize once - } + if (s_par_mode_initialized) return; [[maybe_unused]] quill::Logger* logger = fourdst::logging::LogManager::getInstance().getLogger("log"); - LOG_INFO(logger, "Initializing OpenMP parallel mode with {} threads", static_cast(omp_get_max_threads())); - CppAD::thread_alloc::parallel_setup( - static_cast(omp_get_max_threads()), // Max threads - []() -> bool { return in_parallel(); }, // Function to get thread ID - []() -> size_t { return get_thread_id(); } // Function to check parallel state - ); + auto n_threads = static_cast(omp_get_max_threads()); + if (n_threads > CPPAD_MAX_NUM_THREADS) { + LOG_CRITICAL(logger, + "OpenMP reports {} threads but CppAD was built with CPPAD_MAX_NUM_THREADS={}; " + "clamping OpenMP to {} threads.", + n_threads, static_cast(CPPAD_MAX_NUM_THREADS), + static_cast(CPPAD_MAX_NUM_THREADS)); + throw std::runtime_error(std::format( + "OpenMP reports {} threads but CppAD was built with CPPAD_MAX_NUM_THREADS={}; clamping default OpenMP number of threads to {}. Rebuild CppAD with a higher CPPAD_MAX_NUM_THREADS if you need more threads. Alternative, set the environmental variable OMP_NUM_THREADS to a value less than or equal to {} to avoid this error.", + n_threads, static_cast(CPPAD_MAX_NUM_THREADS), + static_cast(CPPAD_MAX_NUM_THREADS), + static_cast(CPPAD_MAX_NUM_THREADS))); + } + + LOG_INFO(logger, "Initializing OpenMP parallel mode with {} threads", n_threads); + CppAD::thread_alloc::parallel_setup( + n_threads, + []() -> bool { return in_parallel(); }, + []() -> size_t { return get_thread_id(); }); CppAD::thread_alloc::hold_memory(true); - CppAD::CheckSimpleVector>(0, 1); + CppAD::parallel_ad(); s_par_mode_initialized = true; } } diff --git a/src/include/gridfire/utils/macros.h b/src/include/gridfire/utils/macros.h index 22add6c8..b9f2a2b6 100644 --- a/src/include/gridfire/utils/macros.h +++ b/src/include/gridfire/utils/macros.h @@ -2,12 +2,27 @@ #if defined(GF_USE_OPENMP) + #include + #include + #include "cppad/configure.hpp" + #define GF_OMP_PRAGMA(x) _Pragma(#x) - #define GF_OMP(omp_args, extra) GF_OMP_PRAGMA(omp omp_args) extra - #define GF_OMP_MAX_THREADS omp_get_max_threads() - #define GF_OMP_THREAD_NUM omp_get_thread_num() + + namespace gridfire::omp { + inline int capped_max_threads() { + return std::min(omp_get_max_threads(), + static_cast(CPPAD_MAX_NUM_THREADS)); + } + } + + #define GF_OMP_NUM_THREADS (gridfire::omp::capped_max_threads()) + #define GF_OMP(omp_args, extra) \ + GF_OMP_PRAGMA(omp omp_args num_threads(GF_OMP_NUM_THREADS)) extra + + #define GF_OMP_MAX_THREADS (gridfire::omp::capped_max_threads()) + #define GF_OMP_THREAD_NUM omp_get_thread_num() #else - #define GF_OMP(_,fallback_args) fallback_args + #define GF_OMP(_, fallback_args) fallback_args #define GF_OMP_MAX_THREADS 1 #define GF_OMP_THREAD_NUM 0 #endif \ No newline at end of file diff --git a/utils/wheels/build-wheels-linux_aarch64.sh b/utils/wheels/build-wheels-linux_aarch64.sh index 98857c4a..82dd5e26 100755 --- a/utils/wheels/build-wheels-linux_aarch64.sh +++ b/utils/wheels/build-wheels-linux_aarch64.sh @@ -31,27 +31,73 @@ do docker run --rm \ "${DOCKER_MOUNTS[@]}" \ "${IMAGE}" \ - /bin/bash -eux -c ' + /bin/bash -uxo pipefail -c ' cd /io/project + # ---------------------------------------------------------------- + # Project identity: package name from pyproject.toml, version from + # meson (the version pip will stamp into the wheel). Used both for + # the skip-if-already-built check and the post-repair checks, so a + # stale wheel from an OLDER project version never causes a skip. + # ---------------------------------------------------------------- + PKG="$(sed -n "s/^name *= *\"\(.*\)\"/\1/p" pyproject.toml | head -n1)" + PKG="${PKG//-/_}" # wheel filename normalization + + BOOT_PY=/opt/python/cp312-cp312/bin/python + "$BOOT_PY" -m pip install --quiet meson + VERSION="$("$BOOT_PY" -c " +import json, subprocess, sys +out = subprocess.check_output( + [sys.executable, \"-m\", \"mesonbuild.mesonmain\", \"introspect\", + \"meson.build\", \"--projectinfo\"]) +print(json.loads(out)[\"version\"]) +" 2>/dev/null || true)" + if [ -z "$VERSION" ]; then + # fallback: literal version in project() + VERSION="$(grep -oE "version *: *.[0-9][0-9a-zA-Z.+-]*" meson.build | head -n1 | grep -oE "[0-9][0-9a-zA-Z.+-]*" || true)" + fi + if [ -z "$VERSION" ]; then + echo "ERROR: could not determine project version; refusing to guess for skip logic" + exit 1 + fi + echo "➤ Building ${PKG} ${VERSION}" + + # Does this project link against the fourdst wheel? Single source of + # truth: the pin in pyproject.toml. FOURDST_PIN="$(grep -oE "fourdst==[0-9][0-9a-zA-Z.]*" pyproject.toml | head -n1 || true)" + # If a local fourdst wheel dir was mounted, let pip (including the + # isolated build env) resolve fourdst from it. if [ -d /io/fourdst-wheels ]; then export PIP_FIND_LINKS=/io/fourdst-wheels fi - for PY in /opt/python/*/bin/python; do + build_one() { + # Runs the full build+repair for one interpreter. Returns nonzero + # on any failure; never exits the whole script (errexit is off in + # the caller around this function). + set -e + local PY="$1" PYTAG="$2" + "$PY" -m pip install --upgrade pip setuptools wheel meson meson-python + # Build into a per-iteration temp dir so we repair exactly the + # wheel we just built. + local BUILD_WHEEL_DIR BUILD_WHEEL_DIR="$(mktemp -d)" - CC=clang CXX=clang++ "$PY" -m pip wheel . \ - --no-deps \ + CC=clang CXX=clang++ "$PY" -m pip wheel . --no-deps \ -w "$BUILD_WHEEL_DIR" -vv + local CURRENT_WHEEL CURRENT_WHEEL="$(find "$BUILD_WHEEL_DIR" -name "*.whl" | head -n1)" if [ -n "$FOURDST_PIN" ]; then + # Install fourdst for THIS interpreter so auditwheel can resolve + # the libraries it must NOT graft. Excluding them keeps fourdst a + # runtime dependency: grafting copies would break cross-package + # pybind11 type compatibility. "$PY" -m pip install --force-reinstall "$FOURDST_PIN" + local FOURDST_LIB_PATH FOURDST_LIB_PATH="$("$PY" -c "import fourdst, os; print(os.pathsep.join(fourdst.get_lib_dirs()))")" LD_LIBRARY_PATH="$FOURDST_LIB_PATH" auditwheel repair \ --exclude "libcomposition.so*" \ @@ -60,18 +106,70 @@ do --exclude "libreflect_cpp.so*" \ -w /io/wheels "$CURRENT_WHEEL" - REPAIRED="$(ls -t /io/wheels/*.whl | head -n1)" + # Post-repair sanity check on the wheel we just produced + local REPAIRED + REPAIRED="$(find /io/wheels -name "${PKG}-${VERSION}-${PYTAG}-*manylinux*.whl" | head -n1)" + if [ -z "$REPAIRED" ]; then + echo "ERROR: repaired wheel for ${PYTAG} not found after auditwheel" + return 1 + fi if unzip -l "$REPAIRED" | grep -E "libcomposition|liblogging|libconst[^a-z]|libreflect_cpp"; then echo "ERROR: repaired wheel contains vendored fourdst libraries" - exit 1 + rm -f "$REPAIRED" # do not leave a poisoned wheel that would be skipped next run + return 1 fi else auditwheel repair -w /io/wheels "$CURRENT_WHEEL" fi rm -rf "$BUILD_WHEEL_DIR" + } + + FAILED_TAGS="" + SKIPPED_TAGS="" + BUILT_TAGS="" + + for PY in /opt/python/*/bin/python; do + # /opt/python/-/bin/python — the directory name + # is exactly the {python tag}-{abi tag} pair used in wheel filenames + PYTAG="$(basename "$(dirname "$(dirname "$PY")")")" + + # ------------------------------------------------------------ + # 1. Skip if a repaired wheel for THIS name+version+interpreter + # already exists (a wheel from an older version will not match + # because VERSION is part of the pattern). + # ------------------------------------------------------------ + if compgen -G "/io/wheels/${PKG}-${VERSION}-${PYTAG}-*manylinux*.whl" > /dev/null; then + echo "➤ ${PYTAG}: wheel for ${PKG} ${VERSION} already present — skipping" + SKIPPED_TAGS="${SKIPPED_TAGS} ${PYTAG}" + continue + fi + + # ------------------------------------------------------------ + # 2. Build; on failure, record and continue with the next python + # ------------------------------------------------------------ + echo "================================================================" + echo "➤ ${PYTAG}: building ${PKG} ${VERSION}" + echo "================================================================" + if ( build_one "$PY" "$PYTAG" ); then + BUILT_TAGS="${BUILT_TAGS} ${PYTAG}" + else + echo "✗ ${PYTAG}: BUILD FAILED — continuing with remaining versions" + FAILED_TAGS="${FAILED_TAGS} ${PYTAG}" + fi done - echo "Linux wheels ready in /io/wheels" + echo "================================================================" + echo "Summary for ${PKG} ${VERSION}:" + echo " built: ${BUILT_TAGS:- none}" + echo " skipped:${SKIPPED_TAGS:- none}" + echo " failed: ${FAILED_TAGS:- none}" + echo "================================================================" + + if [ -n "$FAILED_TAGS" ]; then + echo "✗ Some builds failed:${FAILED_TAGS}" + exit 1 + fi + echo "✅ Linux wheels ready in /io/wheels" ' -done +done \ No newline at end of file diff --git a/utils/wheels/build-wheels-linux_x86_64.sh b/utils/wheels/build-wheels-linux_x86_64.sh index 4ec705c5..c648e207 100755 --- a/utils/wheels/build-wheels-linux_x86_64.sh +++ b/utils/wheels/build-wheels-linux_x86_64.sh @@ -45,7 +45,7 @@ do BUILD_WHEEL_DIR="$(mktemp -d)" CC=clang CXX=clang++ "$PY" -m pip wheel . \ - --no-deps --config-settings=setup-args=-Dunity=on \ + --no-deps \ -w "$BUILD_WHEEL_DIR" -vv CURRENT_WHEEL="$(find "$BUILD_WHEEL_DIR" -name "*.whl" | head -n1)"