From c1e491875e106c7bfdf74f83dc795bcb0a974bd5 Mon Sep 17 00:00:00 2001 From: Marco Barbone Date: Mon, 16 Mar 2026 14:46:04 -0400 Subject: [PATCH] test: add threadsafe execute test and sanitizer CI modes Add threadsafe_execute regression test verifying concurrent execute() calls on the same plan produce correct results. Add sanitizer mode selection via FINUFFT_USE_SANITIZERS=OFF|ON|MEMSAN|TSAN, and extend the sanitizer GitHub workflow to run a focused Linux TSAN job. --- .github/workflows/cmake_sanitizers.yml | 15 +++-- CHANGELOG | 4 ++ CMakeLists.txt | 8 ++- cmake/toolchain.cmake | 14 ++++- test/CMakeLists.txt | 4 ++ test/threadsafe_execute.cpp | 77 ++++++++++++++++++++++++++ 6 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 test/threadsafe_execute.cpp diff --git a/.github/workflows/cmake_sanitizers.yml b/.github/workflows/cmake_sanitizers.yml index de92606a6..e20e8a694 100644 --- a/.github/workflows/cmake_sanitizers.yml +++ b/.github/workflows/cmake_sanitizers.yml @@ -55,8 +55,9 @@ jobs: fail-fast: false matrix: include: - - { os: ubuntu-22.04, toolchain: gcc-13 } - - { os: macos-14, toolchain: llvm } + - { os: ubuntu-22.04, toolchain: gcc-13, sanitizer: ON } + - { os: ubuntu-22.04, toolchain: gcc-13, sanitizer: TSAN } + - { os: macos-14, toolchain: llvm, sanitizer: ON } steps: - name: Show CPU info (Linux) @@ -136,6 +137,12 @@ jobs: for arch in "${arch_flags[@]}"; do rm -rf $build_dir + ctest_args=(--output-on-failure -j) + # Keep the TSAN job focused on the concurrency-sensitive coverage. + if [[ "${{ matrix.sanitizer }}" == "TSAN" ]]; then + ctest_args=(--output-on-failure -R '^(run_testutils|run_threadsafe_execute)$') + fi + cmake -E make_directory "$build_dir" cmake -S . -B "$build_dir" \ -DCMAKE_BUILD_TYPE=$build_type \ @@ -143,8 +150,8 @@ jobs: -DFINUFFT_BUILD_EXAMPLES=ON \ -DFINUFFT_BUILD_TESTS=ON \ -DFINUFFT_USE_DUCC0=ON \ - -DFINUFFT_USE_SANITIZERS=ON + -DFINUFFT_ENABLE_SANITIZERS="${{ matrix.sanitizer }}" cmake --build "$build_dir" --config "$build_type" - ctest --test-dir "$build_dir" --output-on-failure -j + ctest --test-dir "$build_dir" "${ctest_args[@]}" done diff --git a/CHANGELOG b/CHANGELOG index d1cd4a941..057e42f3c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,10 @@ If not stated, FINUFFT is assumed (old cuFINUFFT <=1.3 is listed separately). v2.6.0-dev +* Added `threadsafe_execute` regression test verifying concurrent `execute()` + calls on the same plan produce correct results. Added sanitizer mode selection + via `FINUFFT_USE_SANITIZERS=OFF|ON|MEMSAN|TSAN`, and extended the sanitizer + GitHub workflow to run a focused Linux TSAN job. (Barbone) * SIMD-vectorized bin sort with parallel prefix sum: uint32_t bin counts, ndims dispatch for vectorized coordinate binning, std::exclusive_scan for parallel prefix sum of offsets, restored single-threaded variant as diff --git a/CMakeLists.txt b/CMakeLists.txt index b1607495f..75fb09715 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,13 +26,19 @@ option(FINUFFT_USE_CUDA "Whether to build CUDA accelerated FINUFFT library (libc option(FINUFFT_USE_DUCC0 "Whether to use DUCC0 (instead of FFTW) for CPU FFTs" OFF) option(FINUFFT_USE_IWYU "Set CXX_INCLUDE_WHAT_YOU_USE on target (checker-only)" OFF) option(FINUFFT_USE_OPENMP "Whether to use OpenMP for parallelization. If disabled, the finufft library will be single threaded. This does not affect the choice of FFTW library." ON) -option(FINUFFT_USE_SANITIZERS "Whether to enable sanitizers, only effective for Debug configuration." OFF) +set( + FINUFFT_USE_SANITIZERS + "OFF" + CACHE STRING + "Sanitizer mode for Debug/RelWithDebInfo builds. Supported values: OFF, ON, MEMSAN, TSAN. ON and MEMSAN both select the default address/undefined/bounds bundle." +) # if FINUFFT_USE_DUCC0 is ON, the following options are ignored set(FINUFFT_FFTW_LIBRARIES "DEFAULT" CACHE STRING "Specify a custom FFTW library") set(FINUFFT_FFTW_SUFFIX "DEFAULT" CACHE STRING "Suffix for FFTW libraries (e.g. OpenMP, Threads etc.) defaults to empty string if OpenMP is disabled, else uses OpenMP. Ignored if DUCC0 is used.") # if FINUFFT_USE_CPU is OFF, the following options are ignored set(FINUFFT_ARCH_FLAGS "native" CACHE STRING "Compiler flags for specifying target architecture, defaults to -march=native") # sphinx tag (don't remove): @cmake_opts_end +set_property(CACHE FINUFFT_USE_SANITIZERS PROPERTY STRINGS OFF ON MEMSAN TSAN) cmake_dependent_option(FINUFFT_ENABLE_INSTALL "Disable installation in the case of python builds" ON "NOT FINUFFT_BUILD_PYTHON" OFF) cmake_dependent_option(FINUFFT_STATIC_LINKING "Disable static libraries in the case of python builds" ON "NOT FINUFFT_BUILD_PYTHON" OFF) cmake_dependent_option(FINUFFT_SHARED_LINKING "Shared should be the opposite of static linking" ON "NOT FINUFFT_STATIC_LINKING" OFF) diff --git a/cmake/toolchain.cmake b/cmake/toolchain.cmake index e4f329ede..f8ebb88f1 100644 --- a/cmake/toolchain.cmake +++ b/cmake/toolchain.cmake @@ -91,7 +91,9 @@ endif() # ---- Sanitizers --------------------------------------------------------------- set(FINUFFT_SANITIZER_FLAGS) -if(FINUFFT_USE_SANITIZERS) +string(TOUPPER "${FINUFFT_USE_SANITIZERS}" FINUFFT_USE_SANITIZERS_MODE) +if(FINUFFT_USE_SANITIZERS_MODE STREQUAL "OFF") +elseif(FINUFFT_USE_SANITIZERS_MODE STREQUAL "ON" OR FINUFFT_USE_SANITIZERS_MODE STREQUAL "MEMSAN") set(FINUFFT_SANITIZER_FLAGS -fsanitize=address -fsanitize=undefined @@ -99,6 +101,16 @@ if(FINUFFT_USE_SANITIZERS) /fsanitize=address /RTC1 ) +elseif(FINUFFT_USE_SANITIZERS_MODE STREQUAL "TSAN") + set(FINUFFT_SANITIZER_FLAGS -fsanitize=thread) +else() + message( + FATAL_ERROR + "Unsupported FINUFFT_USE_SANITIZERS value '${FINUFFT_USE_SANITIZERS}'. Use one of: OFF, ON, MEMSAN, TSAN." + ) +endif() + +if(FINUFFT_SANITIZER_FLAGS) filter_supported_compiler_flags(FINUFFT_SANITIZER_FLAGS FINUFFT_SANITIZER_FLAGS) set(FINUFFT_SANITIZER_FLAGS $<$:${FINUFFT_SANITIZER_FLAGS}>) endif() diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 72398685f..e54038ad7 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -56,6 +56,10 @@ target_compile_features(testutils PRIVATE cxx_std_17) finufft_link_test(testutils) add_test(NAME run_testutils COMMAND testutils WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) +add_executable(threadsafe_execute threadsafe_execute.cpp) +finufft_link_test(threadsafe_execute) +add_test(NAME run_threadsafe_execute COMMAND threadsafe_execute WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) + if(NOT FINUFFT_USE_DUCC0 AND FINUFFT_USE_OPENMP) find_package(OpenMP COMPONENTS CXX REQUIRED) add_executable(fftw_lock_test fftw_lock_test.cpp) diff --git a/test/threadsafe_execute.cpp b/test/threadsafe_execute.cpp new file mode 100644 index 000000000..107c4aa84 --- /dev/null +++ b/test/threadsafe_execute.cpp @@ -0,0 +1,77 @@ +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "utils/dirft1d.hpp" +#include "utils/norms.hpp" + +int main() { + constexpr int nthreads = 4; + constexpr int nreps = 16; + constexpr int M = 400; + constexpr int64_t N1 = 2048; + constexpr double tol = 1e-12; + + finufft_opts opts; + finufft_default_opts(&opts); + opts.nthreads = 1; // crucial: parallelism is across concurrent plan executes + opts.debug = 0; + + std::vector x(M); + std::vector> c(M), ref(N1); + for (int j = 0; j < M; ++j) { + double t = static_cast(j) / M; + x[j] = -finufft::common::PI + 2.0 * finufft::common::PI * t; + c[j] = std::complex(0.5 * std::cos(13.0 * t) + 0.25 * std::sin(7.0 * t), + 0.75 * std::sin(11.0 * t) - 0.2 * std::cos(5.0 * t)); + } + + int64_t Ns[3] = {N1, 1, 1}; + finufft_plan plan; + int ier = finufft_makeplan(1, 1, Ns, +1, 1, tol, &plan, &opts); + if (ier != 0) { + std::fprintf(stderr, "finufft_makeplan failed: ier=%d\n", ier); + return ier; + } + ier = finufft_setpts(plan, M, x.data(), nullptr, nullptr, 0, nullptr, nullptr, nullptr); + if (ier != 0) { + std::fprintf(stderr, "finufft_setpts failed: ier=%d\n", ier); + finufft_destroy(plan); + return ier; + } + + dirft1d1(M, x, c, +1, N1, ref); + + std::vector failures(nthreads, 0); + + std::vector workers; + workers.reserve(nthreads); + for (int tid = 0; tid < nthreads; ++tid) { + workers.emplace_back([&, tid]() { + std::vector> out(N1); + for (int rep = 0; rep < nreps; ++rep) { + int local_ier = finufft_execute(plan, c.data(), out.data()); + double relerr = relerrtwonorm(N1, ref.data(), out.data()); + if (local_ier != 0 || relerr > 10.0 * tol) { + failures[tid] = 1; + std::fprintf(stderr, "thread %d rep %d failed: ier=%d relerr=%.3g\n", tid, rep, + local_ier, relerr); + return; + } + } + }); + } + + for (auto &worker : workers) worker.join(); + + finufft_destroy(plan); + return *std::max_element(failures.begin(), failures.end()); +}