From fadf3ca27df33056095034d50b40ee5905825915 Mon Sep 17 00:00:00 2001 From: Steve Bronder Date: Wed, 14 Oct 2026 15:54:42 -0400 Subject: [PATCH 01/10] Add CMake test builds with shared objects and precompiled headers Import and modernize the CMake setup for current vendored dependencies. Use CMake by default in runTests.py, group directory tests into executables, reuse per-source objects and PCH, and cap build parallelism at six jobs. Migrate Jenkins test stages, document the workflow, and ignore CMake build trees. Fix shared test helper linkage, Eigen fixture initialization, and the Bernoulli GLM invalid-label fixture; suppress GCC column-tracking notes. Validation: 21 runner, selection, and CMake build regression checks passed. Previously validated reverse and primitive function groups, generated probability and expression tests, and object reuse across test selections. --- .github/workflows/header_checks.yml | 2 +- .github/workflows/main.yml | 4 +- .gitignore | 6 + CMakeLists.txt | 108 ++++++++ Jenkinsfile | 242 ++++++++++-------- README.md | 98 +++++++ cmake/CMakeLists.txt | 123 +++++++++ cmake/pch.cpp | 1 + runTests.py | 34 ++- test/CMakeLists.txt | 210 +++++++++++++++ test/cmake_build_test.py | 176 +++++++++++++ test/cmake_ci.py | 84 ++++++ test/cmake_ci_test.py | 47 ++++ test/cmake_runner.py | 146 +++++++++++ test/cmake_runner_test.py | 113 ++++++++ .../test_expression_testing_framework.py | 2 + test/prob/CMakeLists.txt | 86 +++++++ test/prob/utility.hpp | 44 ++-- test/unit/CMakeLists.txt | 56 ++++ test/unit/math/prim/fun/eval_test.cpp | 2 +- test/unit/math/prim/fun/value_of_rec_test.cpp | 2 +- test/unit/math/prim/fun/value_of_test.cpp | 2 +- .../math/prim/functor/reduce_sum_util.hpp | 8 +- test/unit/math/rev/core/gradable.hpp | 4 +- test/unit/math/rev/fun/util.hpp | 8 +- .../math/rev/functor/util_algebra_solver.hpp | 2 +- .../prob/bernoulli_logit_glm_lpmf_test.cpp | 5 +- test/unit/math/test_ad.hpp | 4 +- test/unit/util.hpp | 4 +- 29 files changed, 1471 insertions(+), 152 deletions(-) create mode 100644 CMakeLists.txt create mode 100644 cmake/CMakeLists.txt create mode 100644 cmake/pch.cpp create mode 100644 test/CMakeLists.txt create mode 100644 test/cmake_build_test.py create mode 100644 test/cmake_ci.py create mode 100644 test/cmake_ci_test.py create mode 100644 test/cmake_runner.py create mode 100644 test/cmake_runner_test.py create mode 100644 test/prob/CMakeLists.txt create mode 100644 test/unit/CMakeLists.txt diff --git a/.github/workflows/header_checks.yml b/.github/workflows/header_checks.yml index 1fbd51db82c..ebd4684d835 100644 --- a/.github/workflows/header_checks.yml +++ b/.github/workflows/header_checks.yml @@ -68,4 +68,4 @@ jobs: - name: Run header tests run: | echo "STAN_NO_RANGE_CHECKS=true" > make/local - ./runTests.py -j2 ./test/unit/math/prim/err/ + ./runTests.py --backend make -j2 ./test/unit/math/prim/err/ diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ccba1e931b3..a023cecc9c6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -59,7 +59,7 @@ jobs: - name: Run ${{ matrix.config.label }} unit tests shell: pwsh run: | - python runTests.py -j4 ${{ matrix.config.tests }} + python runTests.py --backend make -j4 ${{ matrix.config.tests }} - name: Upload gtest_output xml uses: actions/upload-artifact@v7 @@ -114,7 +114,7 @@ jobs: $MixFunProbTests[(4 * $FifthNumberTests)..($NumberTests - 1)] ` ) - python runTests.py -j2 $MixFunProbTestsArray[(${{ matrix.group }} - 1)] + python runTests.py --backend make -j2 $MixFunProbTestsArray[(${{ matrix.group }} - 1)] - name: Upload gtest_output xml uses: actions/upload-artifact@v7 diff --git a/.gitignore b/.gitignore index c36591f03d8..7ba777a5e88 100644 --- a/.gitignore +++ b/.gitignore @@ -106,3 +106,9 @@ lib/tbb/** # gdb debug history .gdb_history + +# CMake build trees +/build/ +/build-*/ +/.build/ +/.build-*/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000000..a909651b5e1 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,108 @@ +cmake_minimum_required(VERSION 3.16.3) +project(stanmath LANGUAGES C CXX) + +if(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR) + message(FATAL_ERROR "Use an out-of-source build: cmake -S . -B build") +endif() +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED YES) +set(CMAKE_CXX_EXTENSIONS NO) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +option(STAN_BUILD_DOCS "Build the Stan Math library documentation" OFF) +option(STAN_TEST_HEADERS "Create targets that check standalone headers" ON) +option(STAN_TEST_PCH "Reuse precompiled headers when building tests" ON) +option(STAN_BUILD_PROB_TESTS "Generate and build distribution tests" ON) +option(STAN_BUILD_EXPRESSION_TESTS "Build pre-generated expression tests" OFF) +option(STAN_TEST_ROW_VECTORS "Include row vectors in distribution tests" OFF) +option(STAN_PROB_TEST_ALL "Test all distribution autodiff modes" OFF) +option(STAN_NO_RANGE_CHECKS "Disable range checks within the Stan library" OFF) +option(STAN_MPI "Enable MPI support" OFF) +option(STAN_OPENCL "Enable OpenCL support" OFF) +option(STAN_THREADS "Enable multi-threading support" OFF) +set(STAN_OPENCL_DEVICE_ID "0" CACHE STRING "OpenCL device ID") +set(STAN_OPENCL_PLATFORM_ID "0" CACHE STRING "OpenCL platform ID") +set(STAN_INTEGRATED_OPENCL "0" CACHE STRING "Enable integrated OpenCL features") +set(STAN_TEST_FILTER "" CACHE STRING "Regex selecting test source paths relative to the repository") +set(STAN_TEST_NAME_FILTERS "" CACHE STRING "Required substrings in selected test source paths") +include(CTest) +set(STAN_TEST_OPTIMIZATION "1" CACHE STRING "Optimization level for tests and solver libraries") +set_property(CACHE STAN_TEST_OPTIMIZATION PROPERTY STRINGS 0 1 2 3) +if(NOT STAN_TEST_OPTIMIZATION MATCHES "^[0-3]$") + message(FATAL_ERROR "STAN_TEST_OPTIMIZATION must be 0, 1, 2, or 3") +endif() +# Keep solver libraries at the same optimization level as the C++ tests. +if(MSVC) + if(STAN_TEST_OPTIMIZATION STREQUAL "0") + set(STAN_TEST_OPTIMIZATION_FLAGS /Od) + elseif(STAN_TEST_OPTIMIZATION STREQUAL "1") + set(STAN_TEST_OPTIMIZATION_FLAGS /O1) + else() + set(STAN_TEST_OPTIMIZATION_FLAGS /O2) + endif() +else() + set(STAN_TEST_OPTIMIZATION_FLAGS -O${STAN_TEST_OPTIMIZATION}) +endif() + +add_library(stanmath INTERFACE) +add_library(stanmath::stanmath ALIAS stanmath) +target_compile_features(stanmath INTERFACE cxx_std_17) +target_include_directories(stanmath INTERFACE "${PROJECT_SOURCE_DIR}") +target_compile_definitions(stanmath INTERFACE BOOST_DISABLE_ASSERTS TBB_INTERFACE_NEW _REENTRANT) +foreach(feature STAN_NO_RANGE_CHECKS STAN_THREADS STAN_MPI) + if(${feature}) + target_compile_definitions(stanmath INTERFACE ${feature}) + endif() +endforeach() +add_subdirectory(cmake) + +if(STAN_OPENCL) + find_package(OpenCL REQUIRED) + target_include_directories(stanmath SYSTEM INTERFACE "${STAN_OPENCL_ROOT}") + target_link_libraries(stanmath INTERFACE OpenCL::OpenCL) + target_compile_definitions(stanmath INTERFACE + STAN_OPENCL OPENCL_DEVICE_ID=${STAN_OPENCL_DEVICE_ID} + OPENCL_PLATFORM_ID=${STAN_OPENCL_PLATFORM_ID} + CL_HPP_TARGET_OPENCL_VERSION=120 CL_HPP_MINIMUM_OPENCL_VERSION=120 + CL_HPP_ENABLE_EXCEPTIONS INTEGRATED_OPENCL=${STAN_INTEGRATED_OPENCL}) +endif() +if(STAN_MPI) + find_package(MPI REQUIRED COMPONENTS CXX) + if(STAN_USE_SYSTEM_BOOST) + find_package(Boost REQUIRED COMPONENTS mpi serialization) + else() + # Build the MPI libraries against the same headers used by Stan; mixing + # vendored headers with an older system Boost can change serialization ABI. + file(GLOB serialization_sources CONFIGURE_DEPENDS + "${STAN_BOOST_ROOT}/libs/serialization/src/*.cpp") + add_library(stan_boost_serialization STATIC ${serialization_sources}) + add_library(Boost::serialization ALIAS stan_boost_serialization) + target_include_directories(stan_boost_serialization SYSTEM PUBLIC "${STAN_BOOST_ROOT}") + file(GLOB mpi_sources CONFIGURE_DEPENDS "${STAN_BOOST_ROOT}/libs/mpi/src/*.cpp") + add_library(stan_boost_mpi STATIC ${mpi_sources}) + add_library(Boost::mpi ALIAS stan_boost_mpi) + target_compile_definitions(stan_boost_mpi PRIVATE BOOST_MPI_SOURCE=1) + target_compile_definitions(stan_boost_mpi INTERFACE BOOST_MPI_NO_LIB=1) + target_link_libraries(stan_boost_mpi PUBLIC Boost::serialization MPI::MPI_CXX) + endif() + target_link_libraries(stanmath INTERFACE Boost::mpi Boost::serialization MPI::MPI_CXX) + add_library(stan_mpi STATIC stan/math/prim/functor/mpi_cluster_inst.cpp) + target_link_libraries(stan_mpi PRIVATE stanmath) +endif() + +message(STATUS "Stan Math: threads=${STAN_THREADS}, OpenCL=${STAN_OPENCL}, MPI=${STAN_MPI}") +message(STATUS "Test PCH=${STAN_TEST_PCH}, source filter='${STAN_TEST_FILTER}'") +if(BUILD_TESTING) + add_subdirectory(test) +endif() +if(STAN_BUILD_DOCS) + find_package(Doxygen REQUIRED) + add_custom_target(doxygen + COMMAND ${CMAKE_COMMAND} -E make_directory doc/api + COMMAND ${DOXYGEN_EXECUTABLE} doxygen/doxygen.cfg + COMMAND ${CMAKE_COMMAND} -E copy doxygen/pretty_stuff/eigen_navtree_hacks.js + doc/api/html/eigen_navtree_hacks.js + WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" + COMMENT "Generating API documentation with Doxygen" + VERBATIM) +endif() diff --git a/Jenkinsfile b/Jenkinsfile index b194b911beb..ac4ea82d098 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -5,30 +5,61 @@ properties([ string(defaultValue: 'develop', name: 'cmdstan_pr', description: 'PR to test CmdStan upstream against e.g. PR-630'), string(defaultValue: 'develop', name: 'stan_pr', description: 'PR to test Stan upstream against e.g. PR-630'), booleanParam(defaultValue: false, name: 'withRowVector', description: 'Run additional distribution tests on RowVectors (takes 5x as long)'), - booleanParam(defaultValue: false, name: 'disableJumbo', description: 'Disable Jumbo tests. This takes longer and should only be used for debugging if it is believed that the jumbo tests are causing failures.'), - booleanParam(defaultValue: false, name: 'optimizeUnitTests', description: 'Use O=3 for unit tests (takex ~3x as long)'), + booleanParam(defaultValue: false, name: 'optimizeUnitTests', description: 'Use O=3 for unit tests (takes ~3x as long)'), booleanParam(defaultValue: false, name: 'runAllDistributions', description: 'Run all distribution tests, even ones which are unchanged compared to develop'), - booleanParam(defaultValue: false, name: 'run_all', description: 'Pretend all files changes'), + booleanParam(defaultValue: false, name: 'run_all', description: 'Pretend all files changed'), ]) ]) -def image = 'stanorg/ci:v1' +def image = 'stanorg/ci:gpu-cmake3.30.5' def runRemainingStages = false -def CLANG_CXX = 'clang++-7' -def GCC = 'g++' +def CLANG_CXX = 'clang++' +def CLANG_CC = 'clang' def MPICXX = 'mpicxx.openmpi' def mainBranch = env.BRANCH_NAME == 'develop' || env.BRANCH_NAME == 'master' def noOptimize = !(params.optimizeUnitTests || mainBranch) -def jumboFlags = params.disableJumbo ? '' : ' --jumbo --debug' -def runTests(String local, String args) { - writeFile(file: "make/local", text: local) - sh "cat make/local" - sh "make print-compiler-flags" - sh "python3 runTests.py -j\$PARALLEL $args" - junit 'test/**/*.xml' - sh "find test -name *.xml -delete" +// Quote complete arguments, including regexes and compiler flags containing spaces. +def shellQuote(String value) { + return "'" + value.replace("'", "'\"'\"'") + "'" +} + +def runCmakeTests(Map config) { + def buildDir = config.buildDir + def jobs = Math.max(1, Math.min(6, (env.PARALLEL ?: '6').toInteger())) + def flags = [ + '-S', '.', '-B', buildDir, '-DCMAKE_BUILD_TYPE=Release', + '-DSTAN_TEST_HEADERS=OFF', '-DSTAN_BUILD_PROB_TESTS=OFF', + "-DSTAN_TEST_FILTER=${config.filter}", + "-DSTAN_TEST_OPTIMIZATION=${config.optimization ?: '1'}" + ] + (config.flags ?: []) + try { + sh "cmake -E rm -rf ${shellQuote(buildDir + '/test-results')} " + + "${shellQuote(buildDir + '/ctest-results.xml')} ${shellQuote(buildDir + '/Testing')}" + sh "cmake " + flags.collect { shellQuote(it.toString()) }.join(' ') + sh "cmake --build ${shellQuote(buildDir)} --parallel ${jobs}" + sh "cmake -E make_directory ${shellQuote(buildDir + '/test-results')}" + withEnv(["GTEST_OUTPUT=xml:${pwd()}/${buildDir}/test-results/"]) { + sh "ctest --test-dir ${shellQuote(buildDir)} --parallel ${config.testJobs ?: jobs} " + + "--no-tests=error --output-on-failure " + + "--output-junit ${shellQuote(pwd() + '/' + buildDir + '/ctest-results.xml')}" + } + } finally { + junit(testResults: "${buildDir}/test-results/*.xml", allowEmptyResults: true) + archiveArtifacts(artifacts: "${buildDir}/ctest-results.xml,${buildDir}/Testing/Temporary/LastTest.log", + allowEmptyArchive: true) + } +} + +// Var-matrix compatibility still invokes Make directly; expression framework +// self-tests exercise compile and runtime failures through the CMake runner. +def runLegacyFrameworkTests(String compiler) { + writeFile(file: 'make/local', text: "CXX=${compiler}\nO=0\n") + withEnv(['PARALLEL=6', 'MAKEFLAGS=-j1', 'PATH+TBB=./lib/tbb', "CXX=${compiler}"]) { + sh 'python3 test/varmat_compatibility_test.py' + sh 'python3 test/expressions/test_expression_testing_framework.py' + } } catchError { @@ -42,7 +73,7 @@ catchError { runPod(image: image, cpus: 2) { stage('Verify changes') { runRemainingStages = params.run_all || filesChanged( - 'stan', 'make', 'lib', 'test', 'runTests.py', 'runChecks.py', 'makefile', 'Jenkinsfile', '.clang-format') + 'stan', 'make', 'lib', 'test', 'cmake', 'CMakeLists.txt', 'runTests.py', 'runChecks.py', 'makefile', 'Jenkinsfile', '.clang-format') } stage("Clang-format") { @@ -89,7 +120,15 @@ up the autoformatter locally. (Check console output at ${env.BUILD_URL}) parallel( CppLint: { sh "make cpplint" }, Dependencies: { sh "make test-math-dependencies" }, - Documentation: { sh "make doxygen" }, + CMakeSelection: { + sh 'python3 test/cmake_ci_test.py' + sh 'python3 test/cmake_runner_test.py' + sh 'python3 test/cmake_build_test.py' + }, + Documentation: { + sh 'cmake -S . -B build-ci-docs -DBUILD_TESTING=OFF -DSTAN_BUILD_DOCS=ON' + sh 'cmake --build build-ci-docs --parallel 6 --target doxygen' + }, ) } } @@ -99,16 +138,25 @@ up the autoformatter locally. (Check console output at ${env.BUILD_URL}) parallel headers: { runPod(image: image) { stage('Headers check') { - writeFile(file: 'make/local', text: "CXX=$CLANG_CXX -Werror") - sh 'make -j$PARALLEL test-headers' + sh "cmake -S . -B build-ci-headers -DCMAKE_BUILD_TYPE=Release " + + "-DCMAKE_C_COMPILER=${CLANG_CC} -DCMAKE_CXX_COMPILER=${CLANG_CXX} " + + "-DCMAKE_CXX_FLAGS=-Werror -DSTAN_BUILD_PROB_TESTS=OFF " + + '-DSTAN_TEST_FILTER=\'^$\' -DSTAN_TEST_HEADERS=ON' + sh 'cmake --build build-ci-headers --parallel 6 --target test-headers' } } }, unit: { if (env.CHANGE_TARGET) { runPod(image: image) { stage('Run changed unit tests') { - writeFile(file: 'make/local', text: "O=3\nCXXFLAGS+=-march=native -mtune=native") - sh './runTests.py -j$PARALLEL --changed --debug' + def filter = sh(returnStdout: true, + script: 'python3 test/cmake_ci.py changed-unit-filter').trim() + if (filter) { + runCmakeTests(buildDir: 'build-ci-changed', filter: filter, optimization: '3', + flags: ['-DCMAKE_CXX_FLAGS=-march=native -mtune=native']) + } else { + echo 'No priority unit tests selected.' + } } } } @@ -120,49 +168,44 @@ up the autoformatter locally. (Check console output at ${env.BUILD_URL}) revfwd: { runPod(image: image) { stage('Rev/Fwd Unit Tests') { - def local = 'CXXFLAGS+= -fsanitize=address\n' - if (noOptimize) - local += 'O=0\n' - runTests(local, "test/unit/math/rev") - runTests(local, "test/unit/math/fwd") + runCmakeTests(buildDir: 'build-ci-revfwd', filter: '^test/unit/math/(rev|fwd)/', + optimization: noOptimize ? '0' : '3', + flags: ['-DCMAKE_CXX_FLAGS=-fsanitize=address', '-DCMAKE_EXE_LINKER_FLAGS=-fsanitize=address', + '-DCMAKE_SHARED_LINKER_FLAGS=-fsanitize=address']) } } }, mix: { - runPod(image: image, cpus: 8, memory: '192Gi') { + runPod(image: image, cpus: 6, memory: '192Gi') { stage('Mix Unit Tests') { - def local = 'CXXFLAGS+= -fsanitize=address\n' - if (noOptimize) - local += 'O=1\n' - runTests(local, "test/unit/math/mix" + jumboFlags) + runCmakeTests(buildDir: 'build-ci-mix', filter: '^test/unit/math/mix/', + optimization: noOptimize ? '1' : '3', + flags: ['-DCMAKE_CXX_FLAGS=-fsanitize=address', '-DCMAKE_EXE_LINKER_FLAGS=-fsanitize=address', + '-DCMAKE_SHARED_LINKER_FLAGS=-fsanitize=address']) } } }, prim: { runPod(image: image) { stage('Prim Unit Tests') { - def local = 'CXXFLAGS+= -fsanitize=address\n' - if (noOptimize) - local += 'O=0\n' - runTests(local, "test/unit/*_test.cpp") - runTests(local, "test/unit/math/*_test.cpp") - runTests(local, "test/unit/math/prim" + jumboFlags) - runTests(local, "test/unit/math/memory") + runCmakeTests(buildDir: 'build-ci-prim', + filter: '^test/unit/([^/]+_test[.]cpp$|math/([^/]+_test[.]cpp$|prim/|memory/))', + optimization: noOptimize ? '0' : '3', + flags: ['-DCMAKE_CXX_FLAGS=-fsanitize=address', '-DCMAKE_EXE_LINKER_FLAGS=-fsanitize=address', + '-DCMAKE_SHARED_LINKER_FLAGS=-fsanitize=address']) } } }, laplace: { runPod(image: image, memory: '32Gi') { stage('Laplace Unit Tests') { - def local = 'CXXFLAGS+= -march=native -mtune=native\nO=3\n' - - runTests(local, "test/unit/math/laplace/*_test.cpp") - + runCmakeTests(buildDir: 'build-ci-laplace', filter: '^test/unit/math/laplace/', + optimization: '3', flags: ['-DCMAKE_CXX_FLAGS=-march=native -mtune=native']) if (mainBranch) { - // only run ASAN on a selected test to avoid extremely long CI times - sh 'make clean' - local += 'CXXFLAGS+= -fsanitize=address' - runTests(local, "test/unit/math/laplace/laplace_marginal_lpdf_moto_test.cpp") + runCmakeTests(buildDir: 'build-ci-laplace-asan', optimization: '3', + filter: '^test/unit/math/laplace/laplace_marginal_lpdf_moto_test[.]cpp$', + flags: ['-DCMAKE_CXX_FLAGS=-march=native -mtune=native -fsanitize=address', + '-DCMAKE_EXE_LINKER_FLAGS=-fsanitize=address']) } } } @@ -170,89 +213,81 @@ up the autoformatter locally. (Check console output at ${env.BUILD_URL}) gpu: { runPod(image: image, memory: '32Gi', gpus: 1) { stage('OpenCL GPU tests') { - def local = """CXX=$CLANG_CXX -Werror -STAN_OPENCL=true -OPENCL_PLATFORM_ID_GPU=0 -OPENCL_DEVICE_ID=0 -LDFLAGS_OPENCL=-L/usr/local/cuda/targets/x86_64-linux/lib -""" - if (noOptimize) - local += 'O=1\n' - runTests(local, "test/unit/math/opencl") // TODO(bward): try to enable - runTests(local, "test/unit/multiple_translation_units_test.cpp") + runCmakeTests(buildDir: 'build-ci-opencl', + filter: '^test/unit/(math/opencl/|multiple_translation_units_test[.]cpp$)', + optimization: noOptimize ? '1' : '3', + flags: ["-DCMAKE_C_COMPILER=${CLANG_CC}", "-DCMAKE_CXX_COMPILER=${CLANG_CXX}", + '-DCMAKE_CXX_FLAGS=-Werror', '-DSTAN_OPENCL=ON', + '-DSTAN_OPENCL_PLATFORM_ID=0', '-DSTAN_OPENCL_DEVICE_ID=0', + '-DOpenCL_LIBRARY=/usr/local/cuda/targets/x86_64-linux/lib/libOpenCL.so']) } } }, mpi: { runPod(image: image) { stage('MPI Tests') { - def local = "CXX=$MPICXX\nCXX_TYPE=gcc\nSTAN_MPI=true\n" - runTests(local, "test/unit/math/prim/functor") - runTests(local, "test/unit/math/rev/functor") + runCmakeTests(buildDir: 'build-ci-mpi', + filter: '^test/unit/math/(prim|rev)/functor/', testJobs: 1, + flags: ["-DCMAKE_CXX_COMPILER=${MPICXX}", '-DSTAN_MPI=ON']) } } }, expr: { runPod(image: image, memory: '64Gi') { stage('Expressions test') { - def local = "CXX=$CLANG_CXX -Werror\nO=0\n" - writeFile(file: 'make/local', text: local) - sh "python ./test/code_generator_test.py" - sh "python ./test/signature_parser_test.py" - sh "python ./test/statement_types_test.py" - sh "python ./test/varmat_compatibility_summary_test.py" - sh "python ./test/varmat_compatibility_test.py" - withEnv(['PATH+TBB=./lib/tbb']) { - sh "python ./test/expressions/test_expression_testing_framework.py" - runTests(local, 'test/expressions') - sh "make clean-all" - runTests(local + "STAN_THREADS=true\n", 'test/expressions --only-functions reduce_sum map_rect') + sh 'python3 test/code_generator_test.py' + sh 'python3 test/signature_parser_test.py' + sh 'python3 test/statement_types_test.py' + sh 'python3 test/varmat_compatibility_summary_test.py' + runLegacyFrameworkTests(CLANG_CXX) + sh 'python3 test/cmake_ci.py generate-expressions build-ci-expressions' + runCmakeTests(buildDir: 'build-ci-expressions', filter: '^test/expressions/', optimization: '0', + flags: ["-DCMAKE_C_COMPILER=${CLANG_CC}", "-DCMAKE_CXX_COMPILER=${CLANG_CXX}", + '-DCMAKE_CXX_FLAGS=-Werror', '-DSTAN_BUILD_EXPRESSION_TESTS=ON']) + sh 'python3 test/cmake_ci.py generate-expressions build-ci-expressions-threads --functions reduce_sum map_rect' + withEnv(['STAN_NUM_THREADS=4']) { + runCmakeTests(buildDir: 'build-ci-expressions-threads', filter: '^test/expressions/', + optimization: '0', testJobs: 1, + flags: ["-DCMAKE_C_COMPILER=${CLANG_CC}", "-DCMAKE_CXX_COMPILER=${CLANG_CXX}", + '-DCMAKE_CXX_FLAGS=-Werror', '-DSTAN_BUILD_EXPRESSION_TESTS=ON', '-DSTAN_THREADS=ON']) } } } }, thread: { - runPod(image: image, cpus: 8, memory: '128Gi') { + runPod(image: image, cpus: 6, memory: '128Gi') { stage('Threading tests') { - def local = "CXX=$CLANG_CXX -Werror\nSTAN_THREADS=true\n" - withEnv(['STAN_NUM_THREADS=4', 'PARALLEL=8']) { // runTests currently only runs 1 test at a time post-build - if (mainBranch) { - runTests(local, "test/unit") - } else { - runTests(local, "test/unit -f thread") - runTests(local, "test/unit -f map_rect") - runTests(local, "test/unit -f reduce_sum") - } + withEnv(['STAN_NUM_THREADS=4']) { + runCmakeTests(buildDir: 'build-ci-threads', testJobs: 1, + filter: mainBranch ? '^test/unit/' : '^test/unit/.*(thread|map_rect|reduce_sum)', + flags: ["-DCMAKE_C_COMPILER=${CLANG_CC}", "-DCMAKE_CXX_COMPILER=${CLANG_CXX}", + '-DCMAKE_CXX_FLAGS=-Werror', '-DSTAN_THREADS=ON']) } } } }, dist: { - def cores_per_test = 6 - def parallel_tests = 10 - runPod(image: image, cpus: cores_per_test*parallel_tests, memory: '128Gi') { - stage ('Distribution tests') { - def local = "CXX=$CLANG_CXX\nO=0\nN_TESTS=100\n" - if (params.withRowVector || mainBranch) { - local += "CXXFLAGS+= -DSTAN_TEST_ROW_VECTORS -DSTAN_PROB_TEST_ALL\n" - } - writeFile(file: 'make/local', text: local) - - def cmd = 'python3 test/prob/getDependencies.py' - if (params.runAllDistributions || mainBranch) { - cmd += ' --pretend-all' + runPod(image: image, cpus: 6, memory: '128Gi') { + stage('Distribution tests') { + def command = 'python3 test/cmake_ci.py changed-distribution-filter' + if (params.runAllDistributions || params.run_all || mainBranch) { + command += ' --all' + } + def filter = sh(returnStdout: true, script: command).trim() + if (filter) { + def flags = ["-DCMAKE_C_COMPILER=${CLANG_CC}", "-DCMAKE_CXX_COMPILER=${CLANG_CXX}", + '-DSTAN_BUILD_PROB_TESTS=ON', '-DSTAN_PROB_TEST_BATCH_SIZE=100'] + if (params.withRowVector || mainBranch) { + flags += ['-DSTAN_TEST_ROW_VECTORS=ON', '-DSTAN_PROB_TEST_ALL=ON'] + } + runCmakeTests(buildDir: 'build-ci-distributions', filter: filter, + optimization: '0', flags: flags) + } else { + echo 'No distribution tests affected by this change.' + } } - sh """ - $cmd > changed-tests - if [ -s changed-tests ] ; then - ./runTests.py -j${cores_per_test*parallel_tests} --make-only `cat changed-tests` - unset PARALLEL - parallel --halt now,fail=1 -r -j$parallel_tests ./runTests.py --test-only -j$cores_per_test {} < changed-tests - fi - """ } } - } } if (env.CHANGE_TARGET) { @@ -271,7 +306,8 @@ LDFLAGS_OPENCL=-L/usr/local/cuda/targets/x86_64-linux/lib runPod(image: image, cpus: 2) { stage('Upload doxygen') { def branch = 'gh-pages' - sh 'make doxygen' + sh 'cmake -S . -B build-ci-docs -DBUILD_TESTING=OFF -DSTAN_BUILD_DOCS=ON' + sh 'cmake --build build-ci-docs --parallel 6 --target doxygen' dir('gh-pages') { def scm = scmGit( branches: [[name: "refs/heads/$branch"]], diff --git a/README.md b/README.md index 600f5712ad0..11b4c899842 100644 --- a/README.md +++ b/README.md @@ -26,3 +26,101 @@ licensed under the Apache 2.0 license. This dependency implies an additional restriction as compared to the new BSD license alone. The Apache 2.0 license is incompatible with GPL-2 licensed code if distributed as a unitary binary. You may refer to the Licensing page on the [Stan wiki](https://github.com/stan-dev/stan/wiki/Stan-Licensing). + +Building tests with CMake +------------------------- + +`runTests.py` uses CMake by default. A directory builds and runs one executable +containing all its tests; a source file builds and runs its individual executable: + +```sh +./runTests.py -j6 ./test/unit/math/prim/fun +./runTests.py -j6 test/unit/math/prim/fun/sum_test.cpp +./runTests.py -j6 --build-dir .build -f sum test/unit/math/prim/fun +./runTests.py -j6 --cmake-arg=-DSTAN_THREADS=ON test/unit/math/rev/core +``` + +The first command selects `unit_math_prim_fun_all_tests`. `-f` filters the sources +compiled into the directory executable, and repeated filters must all match. +`--make-only` builds without running; `--test-only` runs the executable already +built with the same selection. `--changed` selects individual changed tests. +`--run-all` continues after failures and still returns a failure status. +`--jumbo-test` is accepted for compatibility; CMake directory builds already +combine the tests without generating amalgamated source files. +Build jobs are capped at six. XML results are written under +`/test-results`, and `build`, `.build`, and their suffixed variants +are ignored. CMake options go in `--cmake-arg`; `make/local` is used only by +`--backend make`. The existing Windows/ARM GitHub jobs explicitly retain Make. + +The CMake test setup is adapted from `feature/cmake-tests` at +`5d304f35b23d355de9cc5958cbdc0df2abedb45f`. It uses the vendored dependency +versions in `make/libraries` and needs no downloads. Configure and build out +of source, with at most six compile jobs: + +```sh +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build --parallel 6 +ctest --test-dir build --parallel 6 --output-on-failure +``` + +Each test source has its own executable and CTest entry. Recursive build +targets such as `unit_math_rev_core_subtests` and `prob_math_bernoulli_subtests` +build a directory's tests. Select the matching CTest entries with `-R`, for +example `ctest --test-dir build -R '^test_unit_math_rev_core_' --output-on-failure`. +The optional `*_all_tests` targets link a directory's sources together; these +are excluded from the default build and CTest to avoid repeated execution. +Each test source compiles once into a shared object target, reused by its +individual executable and all containing directory executables. Switching +between a file, directory, and ancestor directory does not recompile that +test source. Tests use the repository root as their working directory. + +Set `STAN_TEST_FILTER` to a regex matching repository-relative test source +paths to configure a smaller suite. For example: + +```sh +cmake -S . -B build-core -DCMAKE_BUILD_TYPE=Release \ + -DSTAN_BUILD_PROB_TESTS=OFF -DSTAN_TEST_HEADERS=OFF \ + -DSTAN_TEST_FILTER='test/unit/math/(prim|fwd|rev|mix)/core/' +cmake --build build-core --parallel 6 +ctest --test-dir build-core --parallel 6 --output-on-failure +``` + +`STAN_TEST_PCH` defaults to `ON`. Unit tests reuse a PCH for their autodiff +layer, or the AD helpers when the source already includes `test_ad.hpp`. +Grouped executables retain each source's PCH choice through shared objects. +Sources that change Stan or Eigen configuration macros skip PCH individually; +other sources in the same directory continue using their PCH. Umbrella and +multiple-translation-unit checks also compile without PCH. +Distribution tests reuse their fixture headers, without generated typedefs +in the PCH. Producers and consumers share compile flags and definitions; +GCC treats an invalid PCH as an error. Tests and vendored SUNDIALS share +`STAN_TEST_OPTIMIZATION` (default `1`, matching Make builds with `O=1`). Use +`-DSTAN_TEST_PCH=OFF` to check ordinary compilation. Standalone header +targets (`test-headers`, or a target +such as `stan_math_prim_fun_sum-test`) never use a PCH. + +Distribution sources are generated during the build under `build/test/prob`, +using the existing `test/prob/generate_tests.cpp`. `STAN_PROB_TEST_BATCH_SIZE` +defaults to 100 cases per source. Each fixture's generation runs once before +its consumers and reruns when the fixture or generator changes. + +`STAN_THREADS`, `STAN_OPENCL`, `STAN_MPI`, and `STAN_NO_RANGE_CHECKS` retain +their usual meanings. OpenCL and MPI tests are omitted when those features +are disabled. System dependencies can be selected with +`STAN_USE_SYSTEM_{BOOST,EIGEN,TBB,SUNDIALS}`; individual vendored roots can be +overridden with `STAN_{BOOST,EIGEN,TBB,SUNDIALS,OPENCL,BENCHMARK}_ROOT`. +The vendored TBB build currently supports GCC and Clang on Linux/macOS; +Windows builds require an installed TBB. MPI builds its Boost libraries from +the vendored sources, or uses installed libraries with `STAN_USE_SYSTEM_BOOST`. Set `BUILD_TESTING=OFF` to use the `stanmath` +interface target without configuring tests. + +Jenkins uses the CMake/CTest pipeline with separate build directories per +configuration and no more than six compile jobs per build. ASan, GPU, MPI, +threading, and expression test configurations retain their coverage. +`STAN_TEST_ROW_VECTORS` and `STAN_PROB_TEST_ALL` enable the additional +distribution cases requested by Jenkins. Expression sources are prepared with +`python3 test/cmake_ci.py generate-expressions build-expressions`, then enabled +with `STAN_BUILD_EXPRESSION_TESTS=ON`. The runner also generates expression +sources when passed `test/expressions`, honoring `-e` and `--only-functions`. +Expression framework self-tests use the CMake runner. Var-matrix compatibility +compilation and linting retain Make. diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt new file mode 100644 index 00000000000..e1dac5a04d7 --- /dev/null +++ b/cmake/CMakeLists.txt @@ -0,0 +1,123 @@ +# Share dependency versions with Make; cache paths may be overridden. +file(STRINGS "${PROJECT_SOURCE_DIR}/make/libraries" library_locations + REGEX "^(BOOST|EIGEN|OPENCL|TBB|SUNDIALS|BENCHMARK) ") +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS + "${PROJECT_SOURCE_DIR}/make/libraries") +foreach(line IN LISTS library_locations) + if(line MATCHES "^([A-Z]+) .*lib/([^ ]+)") + set(dependency "${CMAKE_MATCH_1}") + set(vendored_root "${PROJECT_SOURCE_DIR}/lib/${CMAKE_MATCH_2}") + set(STAN_${dependency}_ROOT "" CACHE PATH "Override ${dependency} source directory (empty: vendored)") + if(NOT STAN_${dependency}_ROOT) + set(STAN_${dependency}_ROOT "${vendored_root}") + endif() + set(STAN_${dependency}_ROOT "${STAN_${dependency}_ROOT}" PARENT_SCOPE) + endif() +endforeach() +option(STAN_USE_SYSTEM_TBB "Use an installed TBB library" OFF) +option(STAN_USE_SYSTEM_EIGEN "Use an installed Eigen library" OFF) +option(STAN_USE_SYSTEM_BOOST "Use installed Boost headers" OFF) +option(STAN_USE_SYSTEM_SUNDIALS "Use installed SUNDIALS libraries" OFF) +find_package(Threads REQUIRED) +target_link_libraries(stanmath INTERFACE Threads::Threads) + +if(STAN_USE_SYSTEM_EIGEN) + find_package(Eigen3 REQUIRED) + target_link_libraries(stanmath INTERFACE Eigen3::Eigen) +else() + target_include_directories(stanmath SYSTEM INTERFACE "${STAN_EIGEN_ROOT}") +endif() +if(STAN_USE_SYSTEM_BOOST) + find_package(Boost REQUIRED) + target_link_libraries(stanmath INTERFACE Boost::boost) +else() + target_include_directories(stanmath SYSTEM INTERFACE "${STAN_BOOST_ROOT}") +endif() + +if(STAN_USE_SYSTEM_TBB) + find_package(TBB REQUIRED COMPONENTS tbb) +else() + # Vendored TBB predates oneTBB's CMake build. One nested Make job keeps + # dependency compilation within the outer build's parallelism limit. + include(ExternalProject) + find_program(STAN_TBB_MAKE NAMES gmake make) + if(NOT STAN_TBB_MAKE) + message(FATAL_ERROR "Building vendored TBB requires GNU Make") + endif() + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + set(tbb_compiler clang) + elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + set(tbb_compiler gcc) + else() + message(FATAL_ERROR "Use STAN_USE_SYSTEM_TBB with this compiler") + endif() + if(APPLE) + set(tbb_library "${CMAKE_CURRENT_BINARY_DIR}/tbb/libtbb.dylib") + elseif(WIN32) + message(FATAL_ERROR "Use STAN_USE_SYSTEM_TBB on Windows") + else() + set(tbb_library "${CMAKE_CURRENT_BINARY_DIR}/tbb/libtbb.so.2") + endif() + ExternalProject_Add(stan_tbb_build + SOURCE_DIR "${STAN_TBB_ROOT}" + BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/tbb" + DOWNLOAD_COMMAND "" + CONFIGURE_COMMAND "" + BUILD_COMMAND ${STAN_TBB_MAKE} -j1 -r -f "${STAN_TBB_ROOT}/build/Makefile.tbb" + "tbb_root=${STAN_TBB_ROOT}" compiler=${tbb_compiler} cfg=release stdver=c++17 + "CXX=${CMAKE_CXX_COMPILER}" "CC=${CMAKE_C_COMPILER}" + "CXXFLAGS=-Wno-deprecated-declarations -Wno-deprecated-copy" + BUILD_BYPRODUCTS "${tbb_library}" + BUILD_ALWAYS TRUE + INSTALL_COMMAND "") + add_library(TBB::tbb SHARED IMPORTED GLOBAL) + set_target_properties(TBB::tbb PROPERTIES + IMPORTED_LOCATION "${tbb_library}" + INTERFACE_INCLUDE_DIRECTORIES "${STAN_TBB_ROOT}/include") + add_dependencies(TBB::tbb stan_tbb_build) +endif() +target_link_libraries(stanmath INTERFACE TBB::tbb) + +if(STAN_USE_SYSTEM_SUNDIALS) + find_package(SUNDIALS REQUIRED COMPONENTS cvodes idas kinsol nvecserial) + target_link_libraries(stanmath INTERFACE + SUNDIALS::cvodes SUNDIALS::idas SUNDIALS::kinsol SUNDIALS::nvecserial) +else() + # The pruned vendored distribution has no upstream CMakeLists.txt. + # Match make/libraries, including Stan's printf override and common modules. + file(GLOB sundials_sources CONFIGURE_DEPENDS + "${STAN_SUNDIALS_ROOT}/src/cvodes/*.c" + "${STAN_SUNDIALS_ROOT}/src/idas/*.c" + "${STAN_SUNDIALS_ROOT}/src/kinsol/*.c" + "${STAN_SUNDIALS_ROOT}/src/sundials/*.c" + "${STAN_SUNDIALS_ROOT}/src/sunmatrix/band/*.c" + "${STAN_SUNDIALS_ROOT}/src/sunmatrix/dense/*.c" + "${STAN_SUNDIALS_ROOT}/src/sunlinsol/band/*.c" + "${STAN_SUNDIALS_ROOT}/src/sunlinsol/dense/*.c" + "${STAN_SUNDIALS_ROOT}/src/sunnonlinsol/newton/*.c" + "${STAN_SUNDIALS_ROOT}/src/sunnonlinsol/fixedpoint/*.c" + "${STAN_SUNDIALS_ROOT}/src/nvector/serial/nvector_serial.c") + list(FILTER sundials_sources EXCLUDE REGEX "/f[^/]*\\.c$|/sundials_profiler\\.c$") + add_library(stan_sundials STATIC ${sundials_sources}) + if(BUILD_TESTING) + target_compile_options(stan_sundials PRIVATE ${STAN_TEST_OPTIMIZATION_FLAGS}) + endif() + target_include_directories(stan_sundials SYSTEM PUBLIC "${STAN_SUNDIALS_ROOT}/include") + target_include_directories(stan_sundials PRIVATE "${STAN_SUNDIALS_ROOT}/src/sundials") + target_compile_definitions(stan_sundials PRIVATE NO_FPRINTF_OUTPUT) + if(MSVC) + target_compile_options(stan_sundials PRIVATE + "/FI${STAN_SUNDIALS_ROOT}/include/stan_sundials_printf_override.hpp") + else() + target_compile_options(stan_sundials PRIVATE + -include "${STAN_SUNDIALS_ROOT}/include/stan_sundials_printf_override.hpp") + target_link_libraries(stan_sundials PUBLIC m) + endif() + target_link_libraries(stanmath INTERFACE stan_sundials) +endif() +if(BUILD_TESTING) + set(BUILD_GMOCK OFF CACHE BOOL "Build Google Mock") + set(INSTALL_GTEST OFF CACHE BOOL "Install Google Test") + add_subdirectory("${STAN_BENCHMARK_ROOT}/googletest" + "${CMAKE_CURRENT_BINARY_DIR}/googletest" EXCLUDE_FROM_ALL) +endif() diff --git a/cmake/pch.cpp b/cmake/pch.cpp new file mode 100644 index 00000000000..1b2de39da12 --- /dev/null +++ b/cmake/pch.cpp @@ -0,0 +1 @@ +// Empty translation unit for shared PCH providers and standalone header checks. diff --git a/runTests.py b/runTests.py index ddf65fe83c1..d1747235a0a 100755 --- a/runTests.py +++ b/runTests.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ -Replacement for runtest target in Makefile. +Build and run Stan Math tests with CMake (or the legacy Make backend). Call script with '-h' as an option to see a helpful message. """ @@ -85,7 +85,7 @@ def processCLIArgs(): # Now define all the rules of the command line args and opts parser.add_argument( - "-j", metavar="N", type=int, default=1, help="number of cores for make to use" + "-j", metavar="N", type=int, default=1, help="parallel build jobs (maximum 6)" ) parser.add_argument( @@ -147,8 +147,20 @@ def processCLIArgs(): tests_help_msg += "Example: 'test/unit', 'test/prob', and/or\n" tests_help_msg += " 'test/unit/math/prim/fun/abs_test.cpp'" parser.add_argument("tests", nargs="*", type=str, help=tests_help_msg) + parser.add_argument("--backend", choices=("cmake", "make"), default="cmake", + help="build backend (default: cmake)") + parser.add_argument("--build-dir", default="build", help="CMake build directory") + parser.add_argument("--cmake-arg", action="append", default=[], + help="extra configure argument, e.g. --cmake-arg=-DSTAN_THREADS=ON") + parser.add_argument("--config", default="Release", help="CMake build configuration") # And parse the command line against those rules - return parser.parse_args() + inputs = parser.parse_args() + if inputs.j < 1: + parser.error("-j must be positive") + inputs.j = min(inputs.j, 6) + if inputs.make_only and inputs.test_only: + parser.error("--make-only and --test-only cannot be combined") + return inputs def stopErr(msg, returncode): @@ -397,8 +409,7 @@ def checkToolchainPathWindows(): ) -def main(): - inputs = processCLIArgs() +def runMake(inputs): checkToolchainPathWindows() try: @@ -458,5 +469,18 @@ def main(): pass +def main(): + inputs = processCLIArgs() + if inputs.backend == "make": + runMake(inputs) + return + sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "test")) + from cmake_runner import run + try: + sys.exit(run(inputs)) + except (ValueError, OSError, subprocess.CalledProcessError) as error: + stopErr(str(error), 1) + + if __name__ == "__main__": main() diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 00000000000..93fbb6fe09a --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,210 @@ +# A single set of flags and dependencies keeps PCH producers and consumers +# compatible. Do not enable IPO or add target-specific optimization flags. +# Stan's vendored gtest_main initializes the MPI cluster and includes Stan +# headers even when MPI is disabled. +target_link_libraries(gtest_main PRIVATE stanmath) +if(STAN_MPI) + target_link_libraries(gtest_main PRIVATE stan_mpi) +endif() +add_library(stan_test_options INTERFACE) +target_link_libraries(stan_test_options INTERFACE stanmath gtest_main) +if(STAN_MPI) + target_link_libraries(stan_test_options INTERFACE stan_mpi) +endif() +if(MSVC) + target_compile_options(stan_test_options INTERFACE ${STAN_TEST_OPTIMIZATION_FLAGS} /W3) +else() + target_compile_options(stan_test_options INTERFACE + ${STAN_TEST_OPTIMIZATION_FLAGS} -Wall -Wno-deprecated-declarations -Wno-sign-compare) + if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + target_compile_options(stan_test_options INTERFACE + -Werror=invalid-pch -Wno-misleading-indentation) + endif() +endif() + +function(stan_add_pch target) + if(STAN_TEST_PCH) + add_library(${target} OBJECT EXCLUDE_FROM_ALL "${PROJECT_SOURCE_DIR}/cmake/pch.cpp") + set_target_properties(${target} PROPERTIES POSITION_INDEPENDENT_CODE FALSE) + target_link_libraries(${target} PRIVATE stan_test_options) + target_precompile_headers(${target} PRIVATE ${ARGN}) + endif() +endfunction() + +function(stan_configure_test target pch) + set_target_properties(${target} PROPERTIES POSITION_INDEPENDENT_CODE FALSE) + target_link_libraries(${target} PRIVATE stan_test_options) + if(STAN_TEST_PCH AND pch) + if(TARGET ${pch}) + target_precompile_headers(${target} REUSE_FROM ${pch}) + endif() + endif() +endfunction() + +function(stan_add_test_object target source pch) + add_library(${target} OBJECT EXCLUDE_FROM_ALL "${source}") + stan_configure_test(${target} "${pch}") + get_source_file_property(generator_target "${source}" STAN_GENERATOR_TARGET) + if(generator_target) + # Every consuming executable needs generation before object compilation. + add_dependencies(${target} ${generator_target}) + endif() +endfunction() + +function(stan_add_test_binary target) + # Xcode needs a real source alongside object-library references. + add_executable(${target} "${PROJECT_SOURCE_DIR}/cmake/pch.cpp" ${ARGN}) + stan_configure_test(${target} "") + set_property(TARGET ${target} PROPERTY STAN_TEST_OBJECTS "${ARGN}") +endfunction() + +# Directory and per-file executables share objects with per-source PCH choices. +function(add_gtest_grouped_test test_directory folder_test_name target_pch) + set(sources ${ARGN}) + if(NOT sources) + file(GLOB_RECURSE sources CONFIGURE_DEPENDS "${test_directory}/*test.cpp") + endif() + set(directory_keys "") + foreach(src IN LISTS sources) + file(RELATIVE_PATH relative_source "${test_directory}" "${src}") + set(repo_relative "test/${folder_test_name}/${relative_source}") + if(folder_test_name STREQUAL "unit_math") + set(repo_relative "test/unit/math/${relative_source}") + elseif(folder_test_name STREQUAL "prob_math") + set(repo_relative "test/prob/${relative_source}") + elseif(folder_test_name STREQUAL "expressions") + set(repo_relative "test/expressions/${relative_source}") + endif() + if(STAN_TEST_FILTER AND NOT repo_relative MATCHES "${STAN_TEST_FILTER}") + continue() + endif() + set(matches_name_filters TRUE) + foreach(filter IN LISTS STAN_TEST_NAME_FILTERS) + string(FIND "${repo_relative}" "${filter}" match_index) + if(match_index EQUAL -1) + set(matches_name_filters FALSE) + endif() + endforeach() + if(NOT matches_name_filters) + continue() + endif() + if(NOT STAN_OPENCL AND repo_relative MATCHES "/opencl/") + continue() + endif() + if(NOT STAN_MPI AND repo_relative MATCHES "/[^/]*mpi[^/]*test\\.cpp$") + continue() + endif() + string(REPLACE "/" "_" name "${repo_relative}") + string(REGEX REPLACE "\\.cpp$" "" name "${name}") + set(pch "${target_pch}") + if(folder_test_name STREQUAL "unit_math") + string(REGEX MATCH "^[^/]+" layer "${relative_source}") + if(TARGET unit_${layer}_pch) + set(pch unit_${layer}_pch) + endif() + # Precompile the AD helpers only for sources that already include them; + # their global aliases must not leak into unrelated tests. + file(STRINGS "${src}" ad_include REGEX "^#include ") + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${src}") + if(ad_include AND TARGET unit_ad_pch) + set(pch unit_ad_pch) + endif() + file(STRINGS "${src}" configuration_macros + REGEX "^#[ \t]*(define|undef)[ \t]+(STAN_|EIGEN_)") + if(configuration_macros) + # Such a source must set its macros before including Stan/Eigen. + set_source_files_properties("${src}" PROPERTIES SKIP_PRECOMPILE_HEADERS ON) + set(pch "") + endif() + endif() + stan_add_test_object(${name}_objects "${src}" "${pch}") + set(object "$") + stan_add_test_binary(${name} "${object}") + if(STAN_MPI) + set(mpi_ranks 1) + if(repo_relative MATCHES "mpi_") + set(mpi_ranks 2) + endif() + add_test(NAME ${name} COMMAND ${MPIEXEC_EXECUTABLE} ${MPIEXEC_NUMPROC_FLAG} ${mpi_ranks} + ${MPIEXEC_PREFLAGS} $ ${MPIEXEC_POSTFLAGS}) + else() + add_test(NAME ${name} COMMAND ${name}) + endif() + get_filename_component(relative_dir "${relative_source}" DIRECTORY) + string(REPLACE "/" ";" labels "${relative_dir}") + set_tests_properties(${name} PROPERTIES + WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" + LABELS "${folder_test_name};${labels}") + + set(key "${relative_dir}") + while(TRUE) + if(key STREQUAL "") + set(key ROOT) + endif() + string(REPLACE "/" "_" key "${key}") + list(APPEND directory_keys "${key}") + list(APPEND subtests_${key} "${name}") + list(APPEND objects_${key} "${object}") + if(key STREQUAL "ROOT") + break() + endif() + # The next ancestor comes from the path, not the underscore target name. + get_filename_component(relative_dir "${relative_dir}" DIRECTORY) + set(key "${relative_dir}") + endwhile() + endforeach() + list(REMOVE_DUPLICATES directory_keys) + foreach(key IN LISTS directory_keys) + if(key STREQUAL "ROOT") + set(prefix "${folder_test_name}") + else() + set(prefix "${folder_test_name}_${key}") + endif() + add_custom_target(${prefix}_subtests DEPENDS ${subtests_${key}}) + stan_add_test_binary(${prefix}_all_tests ${objects_${key}}) + set_target_properties(${prefix}_all_tests PROPERTIES EXCLUDE_FROM_ALL TRUE) + endforeach() +endfunction() + +add_subdirectory(unit) +if(STAN_BUILD_PROB_TESTS) + add_subdirectory(prob) +endif() +if(STAN_BUILD_EXPRESSION_TESTS) + set(STAN_EXPRESSION_TEST_DIR "${PROJECT_BINARY_DIR}/expression-sources" + CACHE PATH "Directory containing generated expression tests") + file(GLOB expression_sources CONFIGURE_DEPENDS "${STAN_EXPRESSION_TEST_DIR}/*test.cpp") + if(NOT expression_sources) + message(FATAL_ERROR "Generate expression tests before configuring STAN_BUILD_EXPRESSION_TESTS") + endif() + # The expression helper has no include guard; precompile its heavy guarded + # dependencies so each generated source can still include the helper once. + stan_add_pch(expression_pch ) + add_gtest_grouped_test("${STAN_EXPRESSION_TEST_DIR}" expressions expression_pch ${expression_sources}) +endif() + +if(STAN_TEST_HEADERS) + # A PCH would conceal missing includes. Force each header into an otherwise + # empty translation unit, and do not inherit the tests' precompiled headers. + file(GLOB_RECURSE headers CONFIGURE_DEPENDS "${PROJECT_SOURCE_DIR}/stan/*.hpp") + add_custom_target(test-headers) + foreach(header IN LISTS headers) + file(RELATIVE_PATH relative_header "${PROJECT_SOURCE_DIR}" "${header}") + if(NOT STAN_OPENCL AND relative_header MATCHES "/opencl/") + continue() + endif() + if(NOT STAN_MPI AND relative_header MATCHES "/mpi_[^/]+\\.hpp$") + continue() + endif() + string(REPLACE "/" "_" name "${relative_header}") + string(REGEX REPLACE "\\.hpp$" "-test" name "${name}") + add_library(${name} OBJECT EXCLUDE_FROM_ALL "${PROJECT_SOURCE_DIR}/cmake/pch.cpp") + target_link_libraries(${name} PRIVATE stanmath) + if(MSVC) + target_compile_options(${name} PRIVATE "/FI${header}") + else() + target_compile_options(${name} PRIVATE -O0 -include "${header}" -Wunused-local-typedefs) + endif() + add_dependencies(test-headers ${name}) + endforeach() +endif() diff --git a/test/cmake_build_test.py b/test/cmake_build_test.py new file mode 100644 index 00000000000..e6d9a0ed688 --- /dev/null +++ b/test/cmake_build_test.py @@ -0,0 +1,176 @@ +"""Exercise the production CMake test helpers with small compile/link fixtures.""" + +import json +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parent.parent + + +@unittest.skipUnless(shutil.which("cmake"), "CMake is required") +class CMakeBuildTest(unittest.TestCase): + def check_build(self, pch_enabled): + with tempfile.TemporaryDirectory(prefix="stan-cmake-build-") as temporary: + source = Path(temporary) / "source" + build = Path(temporary) / "build" + commands = Path(temporary) / "compile.jsonl" + + def write(path, text): + destination = source / path + destination.parent.mkdir(parents=True, exist_ok=True) + destination.write_text(text) + + def run(*args): + result = subprocess.run(args, text=True, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + self.assertEqual(result.returncode, 0, result.stdout) + + write("launcher.py", """import json, subprocess, sys +with open(sys.argv[1], 'a') as log: + log.write(json.dumps(sys.argv[2:]) + '\\n') +sys.exit(subprocess.call(sys.argv[2:])) +""") + write("CMakeLists.txt", f""" +cmake_minimum_required(VERSION 3.16.3) +project(stan_cmake_fixture LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 17) +set(STAN_TEST_PCH {'ON' if pch_enabled else 'OFF'}) +set(STAN_TEST_OPTIMIZATION_FLAGS -O1) +add_library(stanmath INTERFACE) +target_include_directories(stanmath INTERFACE "${{CMAKE_CURRENT_SOURCE_DIR}}") +add_library(gtest_main STATIC main.cpp) +enable_testing() +include("{(ROOT / 'test/CMakeLists.txt').as_posix()}") +""") + write("cmake/pch.cpp", "// Empty executable/PCH source.\n") + write("gtest/gtest.h", """#pragma once + +#include + +using test_callback = bool (*)(); +inline std::vector& test_callbacks() { + static std::vector callbacks; + return callbacks; +} +struct test_registration { + explicit test_registration(test_callback callback) { + test_callbacks().push_back(callback); + } +}; +""") + write("main.cpp", """#include +int main() { + for (auto callback : test_callbacks()) { + if (!callback()) return 1; + } + return 0; +} +""") + write("stan/math/prim.hpp", """#pragma once + +#ifdef STAN_PCH_TEST_SETTING +constexpr int configured_value = STAN_PCH_TEST_SETTING; +#else +constexpr int configured_value = 1; +#endif +""") + for layer in ("fwd", "rev", "mix"): + write(f"stan/math/{layer}.hpp", "#include \n") + write("stan/math/mix.hpp", "#include \nusing mix_only_alias = double;\n") + write("test/unit/math/test_ad.hpp", """#pragma once + +#include +#include + +using test_alias = double; +""") + write("unit/math/prim/fun/plain_test.cpp", """#include +#include + +using mix_only_alias = int; +using test_alias = int; +bool plain_test() { return configured_value == 1; } +test_registration plain_registration(plain_test); +""") + write("unit/math/prim/fun/ad_test.cpp", """#include +bool ad_test() { return configured_value == 1 && sizeof(test_alias) == sizeof(double); } +test_registration ad_registration(ad_test); +""") + write("unit/math/prim/fun/configuration_test.cpp", """#define STAN_PCH_TEST_SETTING 2 + +#include +#include + +static_assert(configured_value == 2, "Configuration must precede Stan headers"); +bool configuration_test() { return configured_value == 2; } +test_registration configuration_registration(configuration_test); +""") + write("generated_template.cpp", """#include +#include + +bool generated_test() { return configured_value == 1; } +test_registration generated_registration(generated_test); +""") + write("unit/CMakeLists.txt", """ +foreach(layer prim fwd rev mix) + stan_add_pch(unit_${layer}_pch ) +endforeach() +stan_add_pch(unit_ad_pch ) +add_gtest_grouped_test("${CMAKE_CURRENT_SOURCE_DIR}/math" unit_math unit_mix_pch) +set(generated "${PROJECT_BINARY_DIR}/generated/generated_test.cpp") +add_custom_command(OUTPUT "${generated}" + COMMAND "${CMAKE_COMMAND}" -E make_directory "${PROJECT_BINARY_DIR}/generated" + COMMAND "${CMAKE_COMMAND}" -E copy "${PROJECT_SOURCE_DIR}/generated_template.cpp" "${generated}" + DEPENDS "${PROJECT_SOURCE_DIR}/generated_template.cpp") +add_custom_target(generate_fixture DEPENDS "${generated}") +set_source_files_properties("${generated}" PROPERTIES STAN_GENERATOR_TARGET generate_fixture) +add_gtest_grouped_test("${PROJECT_BINARY_DIR}/generated" expressions unit_prim_pch "${generated}") +""") + launcher = f"{sys.executable};{source / 'launcher.py'};{commands}" + run("cmake", "-S", str(source), "-B", str(build), + "-DCMAKE_CXX_COMPILER_LAUNCHER=" + launcher) + run("cmake", "--build", str(build), "--parallel", "1", "--target", + "unit_math_prim_fun_all_tests", "expressions_all_tests") + run(str(build / "unit/unit_math_prim_fun_all_tests")) + run(str(build / "unit/expressions_all_tests")) + + compiled = [json.loads(line) for line in commands.read_text().splitlines()] + fixtures = {name: source / f"unit/math/prim/fun/{name}_test.cpp" + for name in ("plain", "ad", "configuration")} + fixtures["generated"] = build / "generated/generated_test.cpp" + expected_pch = {"plain": "unit_prim_pch", "ad": "unit_ad_pch", + "configuration": None, "generated": "unit_prim_pch"} + for name, path in fixtures.items(): + invocations = [args for args in compiled if str(path) in args] + self.assertEqual(len(invocations), 1, (name, invocations)) + pch = expected_pch[name] if pch_enabled else None + pch_arguments = [arg for arg in invocations[0] if "cmake_pch" in arg] + if pch: + self.assertTrue(any(pch in arg for arg in pch_arguments), invocations[0]) + else: + self.assertFalse(pch_arguments, invocations[0]) + + # Switching executables must not compile any test source again. + run("cmake", "--build", str(build), "--parallel", "1", "--target", + "test_unit_math_prim_fun_plain_test", "unit_math_all_tests", + "test_expressions_generated_test") + run(str(build / "unit/test_unit_math_prim_fun_plain_test")) + run(str(build / "unit/unit_math_all_tests")) + run(str(build / "unit/test_expressions_generated_test")) + after = [json.loads(line) for line in commands.read_text().splitlines()] + for name, path in fixtures.items(): + self.assertEqual(sum(str(path) in args for args in after), 1, name) + + def test_pch_profiles_opt_out_generation_and_object_reuse(self): + self.check_build(True) + + def test_without_pch_preserves_object_reuse(self): + self.check_build(False) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/cmake_ci.py b/test/cmake_ci.py new file mode 100644 index 00000000000..ac45effe600 --- /dev/null +++ b/test/cmake_ci.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Select CI tests and generate expression sources for CMake builds.""" + +import argparse +from pathlib import Path +import re +import subprocess +import sys + + +def changed_files(): + return subprocess.run( + ["git", "diff", "--name-only", "--diff-filter=d", "origin/develop...HEAD"], + check=True, text=True, capture_output=True, + ).stdout.splitlines() + + +def source_filter(paths): + return "^(" + "|".join(re.escape(path) for path in sorted(set(paths))) + ")$" if paths else "" + + +def changed_units(): + # Preserve the existing runner's priority-test selection and size limit. + sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + from runTests import findChangedTests + + return source_filter([path + ".cpp" for path in findChangedTests(False)]) + + +def changed_distributions(files, run_all=False): + # Every distribution fixture includes the Stan umbrella headers. Library, + # build-system, and shared test-header changes can affect all distributions. + if run_all or any( + path.startswith(("stan/", "lib/", "make/", "cmake/")) + or path in ("CMakeLists.txt", "Jenkinsfile", "makefile") + or (path.startswith("test/") and path.endswith(".hpp") + and not re.fullmatch(r"test/prob/[^/]+/[^/]+_test\.hpp", path)) + or (path.startswith("test/prob/") and not path.endswith("_test.hpp")) + for path in files + ): + return "^test/prob/" + fixtures = [path for path in files + if re.fullmatch(r"test/prob/[^/]+/[^/]+_test\.hpp", path)] + return ("^(" + "|".join(re.escape(path[:-len("_test.hpp")]) + for path in sorted(fixtures)) + ")_[0-9]+_generated_" + if fixtures else "") + + +def generate_expressions(build_dir, functions, batches): + import generate_expression_tests + + output = Path(build_dir).resolve() / "expression-sources" + output.mkdir(parents=True, exist_ok=True) + for source in output.glob("*test.cpp"): + source.unlink() + generate_expression_tests.src_folder = str(output) + "/" + generate_expression_tests.main(functions, batches) + if not list(output.glob("*test.cpp")): + raise RuntimeError("Expression generator produced no tests") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + commands = parser.add_subparsers(dest="command", required=True) + commands.add_parser("changed-unit-filter") + distributions = commands.add_parser("changed-distribution-filter") + distributions.add_argument("--all", action="store_true") + expressions = commands.add_parser("generate-expressions") + expressions.add_argument("build_dir") + expressions.add_argument("--functions", nargs="*", default=[]) + expressions.add_argument("--batches", type=int, default=6) + args = parser.parse_args() + if args.command == "changed-unit-filter": + print(changed_units()) + elif args.command == "changed-distribution-filter": + print(changed_distributions([] if args.all else changed_files(), args.all)) + else: + if args.batches < 1: + parser.error("--batches must be positive") + generate_expressions(args.build_dir, args.functions, args.batches) + + +if __name__ == "__main__": + main() diff --git a/test/cmake_ci_test.py b/test/cmake_ci_test.py new file mode 100644 index 00000000000..471a732b459 --- /dev/null +++ b/test/cmake_ci_test.py @@ -0,0 +1,47 @@ +import re +from pathlib import Path +import sys +import unittest +from unittest.mock import patch + +import cmake_ci +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + + +class CmakeCISelectionTest(unittest.TestCase): + def test_unit_only_changes_skip_distributions(self): + self.assertEqual(cmake_ci.changed_distributions( + ["test/unit/math/prim/fun/sum_test.cpp"]), "") + + def test_fixture_change_selects_all_modes_and_batches(self): + selected = cmake_ci.changed_distributions( + ["test/prob/bernoulli/bernoulli_test.hpp"]) + for mode in ("v", "ffv", "vv", "fd", "fv", "ffd"): + for batch in ("00000", "00010"): + self.assertRegex( + f"test/prob/bernoulli/bernoulli_{batch}_generated_{mode}_test.cpp", selected) + self.assertIsNone(re.search(selected, + "test/prob/bernoulli/bernoulli_cdf_00000_generated_v_test.cpp")) + + def test_shared_dependencies_select_all_distributions(self): + for dependency in ("stan/math/prim/fun/sum.hpp", "lib/boost_1.87.0/boost/config.hpp", + "test/prob/utility.hpp", "test/unit/math/expect_near_rel.hpp", + "test/prob/generate_tests.cpp", "cmake/CMakeLists.txt", "Jenkinsfile"): + with self.subTest(dependency=dependency): + selected = cmake_ci.changed_distributions([dependency]) + self.assertRegex("test/prob/bernoulli/bernoulli_00000_generated_v_test.cpp", selected) + self.assertRegex("test/prob/normal/normal_00000_generated_ffv_test.cpp", selected) + + def test_force_all_with_no_changed_files(self): + self.assertTrue(cmake_ci.changed_distributions([], run_all=True)) + + def test_changed_units_preserve_runner_selection(self): + with patch("runTests.findChangedTests", return_value=[ + "test/unit/math/prim/core/get_num_threads_test"]): + selected = cmake_ci.changed_units() + self.assertRegex("test/unit/math/prim/core/get_num_threads_test.cpp", selected) + self.assertIsNone(re.search(selected, "test/unit/math/prim/core/set_num_threads_test.cpp")) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/cmake_runner.py b/test/cmake_runner.py new file mode 100644 index 00000000000..7ebffb49cbf --- /dev/null +++ b/test/cmake_runner.py @@ -0,0 +1,146 @@ +"""CMake backend for runTests.py; directories select one grouped executable.""" + +import glob +import os +from pathlib import Path +import re +import shlex +import subprocess + +ROOT = Path(__file__).resolve().parent.parent + + +def select_paths(arguments): + paths = set() + for argument in arguments: + matches = glob.glob(str(ROOT / argument)) + # Distribution sources are generated in the build tree, not the source tree. + if not matches and argument.replace("\\", "/").lstrip("./").startswith(("test/prob/", "test/expressions/")) and argument.endswith("_test.cpp"): + matches = [str(ROOT / argument)] + if not matches: + raise ValueError("No matching tests: " + argument) + for match in matches: + path = Path(match).resolve().relative_to(ROOT).as_posix() + if not (path == "test/unit" or path.startswith("test/unit/") + or path == "test/prob" or path.startswith("test/prob/") + or path == "test/expressions" or path.startswith("test/expressions/")): + raise ValueError("Unsupported test path: " + path) + if not (ROOT / path).is_dir() and not path.endswith("_test.cpp"): + raise ValueError("Expected a test directory or *_test.cpp: " + path) + paths.add(path) + # A parent directory already covers its descendants. + return [path for path in sorted(paths) + if not any(path.startswith(parent + "/") for parent in paths + if (ROOT / parent).is_dir())] + + +def target_for(path): + if path.endswith(".cpp"): + target = path[:-4].replace("/", "_") + else: + prefix = path.replace("test/unit/math", "unit_math", 1) + prefix = prefix.replace("test/unit", "unit", 1) + prefix = prefix.replace("test/prob", "prob_math", 1) + prefix = prefix.replace("test/expressions", "expressions", 1) + target = prefix.replace("/", "_") + "_all_tests" + folder = "test/unit" if path.startswith("test/unit") else ( + "test/prob" if path.startswith("test/prob") else "test") + return target, folder + + +def selection_filter(paths, filters): + parts = [] + for path in paths: + if path.endswith(".cpp"): + if all(value in path for value in filters): + parts.append(re.escape(path) + "$") + else: + parts.append(re.escape(path) + "/") + if not parts: + raise ValueError("No matching tests found") + return "^(" + "|".join(parts) + ")" + + +def command(args): + print(" ".join(shlex.quote(str(arg)) for arg in args), flush=True) + subprocess.run(args, cwd=ROOT, check=True) + + +def read_cache(build_dir): + cache = {} + cache_file = build_dir / "CMakeCache.txt" + if cache_file.exists(): + for line in cache_file.read_text().splitlines(): + if not line.startswith(("//", "#")) and "=" in line and ":" in line.split("=", 1)[0]: + key, value = line.split("=", 1) + cache[key.split(":", 1)[0]] = value + return cache + + +def run(inputs): + arguments = inputs.tests + if inputs.changed: + from runTests import findChangedTests + arguments = [re.sub(r"\.exe$", "", path) + ".cpp" for path in findChangedTests(inputs.debug)] + if not arguments: + print("No changed tests were found!") + return 0 + paths = select_paths(arguments) + if not paths: + raise ValueError("Specify a test directory or source file") + if inputs.f: + paths = [path for path in paths if not path.endswith(".cpp") + or all(value in path for value in inputs.f)] + if not paths: + raise ValueError("No matching tests found") + expressions = any(path.startswith("test/expressions") for path in paths) + if inputs.only_functions and not expressions: + raise ValueError("--only-functions requires test/expressions") + build_dir = Path(inputs.build_dir).resolve() + if not inputs.test_only: + if expressions: + from cmake_ci import generate_expressions + generate_expressions(build_dir, inputs.only_functions, + inputs.e if inputs.e > 0 else inputs.j) + configure = ["cmake", "-S", str(ROOT), "-B", str(build_dir), + "-DCMAKE_BUILD_TYPE=" + inputs.config, "-DBUILD_TESTING=ON", + "-DSTAN_TEST_HEADERS=OFF", + "-DSTAN_BUILD_PROB_TESTS=" + ("ON" if any(path.startswith("test/prob") for path in paths) else "OFF"), + "-DSTAN_BUILD_EXPRESSION_TESTS=" + ("ON" if expressions else "OFF"), + # Keep ordinary directory/file targets configured so switching + # selections preserves their generated build metadata. + "-DSTAN_TEST_FILTER=" + (selection_filter(paths, inputs.f) if inputs.f else ""), + "-DSTAN_TEST_NAME_FILTERS=" + ";".join(inputs.f)] + if expressions: + configure.append("-DSTAN_EXPRESSION_TEST_DIR=" + str(build_dir / "expression-sources")) + command(configure + inputs.cmake_arg) + command(["cmake", "--build", str(build_dir), "--config", inputs.config, + "--parallel", str(inputs.j), "--target"] + [target_for(path)[0] for path in paths]) + if inputs.make_only: + return 0 + cache = read_cache(build_dir) + results = build_dir / "test-results" + results.mkdir(parents=True, exist_ok=True) + failed = False + for path in paths: + target, folder = target_for(path) + executable = build_dir / folder / target + if cache.get("CMAKE_CONFIGURATION_TYPES"): + executable = executable.parent / inputs.config / executable.name + if cache.get("CMAKE_SYSTEM_NAME") == "Windows" or os.name == "nt": + executable = executable.with_suffix(".exe") + args = [str(executable), "--gtest_output=xml:" + str(results / (target + ".xml"))] + if cache.get("STAN_MPI") in ("ON", "TRUE", "1"): + # Grouped MPI suites can contain collective tests requiring two ranks. + ranks = "2" if not path.endswith(".cpp") or "mpi_" in path else "1" + args = [cache.get("MPIEXEC_EXECUTABLE", "mpirun"), + cache.get("MPIEXEC_NUMPROC_FLAG", "-np"), ranks] + [ + flag for flag in cache.get("MPIEXEC_PREFLAGS", "").split(";") if flag] + args + [ + flag for flag in cache.get("MPIEXEC_POSTFLAGS", "").split(";") if flag] + try: + command(args) + except subprocess.CalledProcessError: + if not inputs.run_all: + raise + failed = True + return int(failed) diff --git a/test/cmake_runner_test.py b/test/cmake_runner_test.py new file mode 100644 index 00000000000..6e869bec401 --- /dev/null +++ b/test/cmake_runner_test.py @@ -0,0 +1,113 @@ +"""Regression checks for directory grouping and runner failure handling.""" + +from pathlib import Path +import subprocess +import sys +import tempfile +from types import SimpleNamespace +import unittest +from unittest.mock import patch + +import cmake_runner + +sys.path.insert(0, str(cmake_runner.ROOT)) +import runTests + + +class CMakeRunnerTest(unittest.TestCase): + def inputs(self, build_dir, **overrides): + values = dict(tests=["./test/unit/math/prim/fun"], changed=False, + debug=False, f=[], only_functions=[], build_dir=build_dir, + test_only=False, make_only=False, config="Release", e=-1, + j=6, cmake_arg=[], run_all=False) + values.update(overrides) + return SimpleNamespace(**values) + + def test_directory_builds_and_runs_one_group(self): + with tempfile.TemporaryDirectory() as build_dir, patch.object(cmake_runner, "command") as command: + self.assertEqual(cmake_runner.run(self.inputs(build_dir)), 0) + configure, build, execute = [call.args[0] for call in command.call_args_list] + self.assertIn("-DSTAN_TEST_FILTER=", configure) + self.assertEqual(build[build.index("--target") + 1:], ["unit_math_prim_fun_all_tests"]) + self.assertEqual(Path(execute[0]).name, "unit_math_prim_fun_all_tests") + self.assertIn("test-results/unit_math_prim_fun_all_tests.xml", execute[1]) + + def test_file_builds_individual_executable(self): + self.assertEqual(cmake_runner.target_for("test/unit/math/prim/fun/sum_test.cpp"), + ("test_unit_math_prim_fun_sum_test", "test/unit")) + + def test_other_directory_targets(self): + for path, target in [("test/unit", "unit_all_tests"), + ("test/unit/math", "unit_math_all_tests"), + ("test/prob/bernoulli", "prob_math_bernoulli_all_tests"), + ("test/expressions", "expressions_all_tests")]: + self.assertEqual(cmake_runner.target_for(path)[0], target) + + def test_overlap_runs_only_parent(self): + self.assertEqual(cmake_runner.select_paths([ + "test/unit/math/prim/fun/sum_test.cpp", "./test/unit/math/prim/fun", + "test/unit/math/prim", "test/unit/math/prim"]), ["test/unit/math/prim"]) + + def test_filters_do_not_turn_directory_into_individual_targets(self): + with tempfile.TemporaryDirectory() as build_dir, patch.object(cmake_runner, "command") as command: + cmake_runner.run(self.inputs(build_dir, f=["sum", "prim"], make_only=True)) + self.assertIn("-DSTAN_TEST_NAME_FILTERS=sum;prim", command.call_args_list[0].args[0]) + self.assertEqual(command.call_args_list[1].args[0][-1], "unit_math_prim_fun_all_tests") + self.assertEqual(command.call_count, 2) + + def test_test_only_skips_build(self): + with tempfile.TemporaryDirectory() as build_dir, patch.object(cmake_runner, "command") as command: + cmake_runner.run(self.inputs(build_dir, test_only=True)) + self.assertEqual(command.call_count, 1) + self.assertEqual(Path(command.call_args.args[0][0]).name, "unit_math_prim_fun_all_tests") + + def test_run_all_preserves_failure_and_continues(self): + with tempfile.TemporaryDirectory() as build_dir, patch.object(cmake_runner, "command") as command: + command.side_effect = [subprocess.CalledProcessError(1, "failed-test"), None] + result = cmake_runner.run(self.inputs(build_dir, tests=[ + "test/unit/math/prim/fun/sum_test.cpp", + "test/unit/math/prim/fun/abs_test.cpp"], test_only=True, run_all=True)) + self.assertEqual(result, 1) + self.assertEqual(command.call_count, 2) + + def test_build_failure_stops_before_execution(self): + with tempfile.TemporaryDirectory() as build_dir, patch.object(cmake_runner, "command") as command: + command.side_effect = [None, subprocess.CalledProcessError(2, "build")] + with self.assertRaises(subprocess.CalledProcessError): + cmake_runner.run(self.inputs(build_dir)) + self.assertEqual(command.call_count, 2) + + def test_cli_caps_jobs(self): + with patch.object(sys, "argv", ["runTests.py", "-j20", "test/unit"]): + self.assertEqual(runTests.processCLIArgs().j, 6) + + def test_cli_rejects_invalid_job_count(self): + with patch.object(sys, "argv", ["runTests.py", "-j0", "test/unit"]), patch.object(sys, "stderr"): + with self.assertRaises(SystemExit) as error: + runTests.processCLIArgs() + self.assertEqual(error.exception.code, 2) + + def test_legacy_backend_dispatch(self): + with patch.object(sys, "argv", ["runTests.py", "--backend", "make", "test/unit"]), patch.object(runTests, "runMake") as run: + runTests.main() + self.assertEqual(run.call_args.args[0].tests, ["test/unit"]) + + def test_invalid_path_rejected(self): + with self.assertRaises(ValueError): + cmake_runner.select_paths(["stan/math"]) + + def test_filtered_file_selection_cannot_succeed_without_tests(self): + with self.assertRaises(ValueError): + cmake_runner.run(self.inputs("build", tests=[ + "test/unit/math/prim/fun/sum_test.cpp"], f=["cholesky"], test_only=True)) + + def test_multi_configuration_executable_path(self): + with tempfile.TemporaryDirectory() as build_dir, patch.object(cmake_runner, "command") as command: + Path(build_dir, "CMakeCache.txt").write_text("CMAKE_CONFIGURATION_TYPES:STRING=Debug;Release\n") + cmake_runner.run(self.inputs(build_dir, test_only=True, config="Debug")) + self.assertEqual(Path(command.call_args.args[0][0]), + Path(build_dir, "test/unit/Debug/unit_math_prim_fun_all_tests")) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/expressions/test_expression_testing_framework.py b/test/expressions/test_expression_testing_framework.py index 487ee8b9bfa..a9f4d418e07 100644 --- a/test/expressions/test_expression_testing_framework.py +++ b/test/expressions/test_expression_testing_framework.py @@ -16,6 +16,8 @@ class TestExpressionTestingFramework(unittest.TestCase): def runCommand(self, command): + command = command + ("--build-dir", "build-expression-framework", + "--cmake-arg=-DSTAN_TEST_OPTIMIZATION=0") p1 = subprocess.Popen(command, stdout = subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) out, err = p1.communicate() return p1.returncode, out, err diff --git a/test/prob/CMakeLists.txt b/test/prob/CMakeLists.txt new file mode 100644 index 00000000000..eb90dbcae9c --- /dev/null +++ b/test/prob/CMakeLists.txt @@ -0,0 +1,86 @@ +# Use the existing Make test generator without importing the branch's C++ +# generator/fixture changes or its large generated typedef headers. +set(STAN_PROB_TEST_BATCH_SIZE 100 CACHE STRING "Distribution test cases per generated source") +if(NOT STAN_PROB_TEST_BATCH_SIZE MATCHES "^[1-9][0-9]*$") + message(FATAL_ERROR "STAN_PROB_TEST_BATCH_SIZE must be a positive integer") +endif() +add_executable(generate_tests generate_tests.cpp) +foreach(feature STAN_TEST_ROW_VECTORS STAN_PROB_TEST_ALL) + if(${feature}) + target_compile_definitions(generate_tests PRIVATE ${feature}) + endif() +endforeach() +if(STAN_USE_SYSTEM_BOOST) + target_link_libraries(generate_tests PRIVATE Boost::boost) +else() + target_include_directories(generate_tests SYSTEM PRIVATE "${STAN_BOOST_ROOT}") +endif() +stan_add_pch(prob_pch + + ) + +file(GLOB_RECURSE fixtures CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/*_test.hpp") +set(generated_sources "") +add_custom_target(run_generate_tests) +foreach(fixture IN LISTS fixtures) + file(RELATIVE_PATH relative_fixture "${PROJECT_SOURCE_DIR}" "${fixture}") + string(REGEX REPLACE "_test\\.hpp$" "" base "${relative_fixture}") + configure_file("${fixture}" "${PROJECT_BINARY_DIR}/${relative_fixture}" COPYONLY) + file(STRINGS "${fixture}" arguments REGEX "^// Arguments:") + string(REGEX REPLACE "^// Arguments: *" "" arguments "${arguments}") + string(REGEX REPLACE ", *" ";" arguments "${arguments}") + # These are the argument combinations used by generate_tests.cpp with its + # selected row-vector setting. Non-reverse modes omit all-constant combinations. + set(vector_types 3) + if(STAN_TEST_ROW_VECTORS) + set(vector_types 4) + endif() + set(combinations 1) + set(constants 1) + foreach(argument IN LISTS arguments) + if(argument STREQUAL "Ints") + math(EXPR combinations "${combinations} * ${vector_types}") + math(EXPR constants "${constants} * ${vector_types}") + elseif(argument STREQUAL "Doubles") + math(EXPR combinations "${combinations} * 2 * ${vector_types}") + math(EXPR constants "${constants} * ${vector_types}") + else() + message(FATAL_ERROR "Unsupported distribution argument '${argument}' in ${fixture}") + endif() + endforeach() + set(outputs "") + set(modes v ffv vv) + if(STAN_PROB_TEST_ALL) + list(APPEND modes fd fv ffd) + endif() + foreach(mode IN LISTS modes) + set(count ${combinations}) + if(NOT mode STREQUAL "v") + math(EXPR count "${count} - ${constants}") + endif() + # The existing generator deliberately emits one extra (possibly empty) + # batch when the case count is a multiple of the batch size. + math(EXPR last_batch "${count} / ${STAN_PROB_TEST_BATCH_SIZE}") + foreach(batch RANGE 0 ${last_batch}) + set(padded "00000${batch}") + string(LENGTH "${padded}" length) + math(EXPR start "${length} - 5") + string(SUBSTRING "${padded}" ${start} 5 padded) + list(APPEND outputs "${PROJECT_BINARY_DIR}/${base}_${padded}_generated_${mode}_test.cpp") + endforeach() + endforeach() + add_custom_command(OUTPUT ${outputs} + COMMAND generate_tests "${relative_fixture}" ${STAN_PROB_TEST_BATCH_SIZE} + DEPENDS generate_tests "${fixture}" "${PROJECT_BINARY_DIR}/${relative_fixture}" + WORKING_DIRECTORY "${PROJECT_BINARY_DIR}" + COMMENT "Generating distribution tests for ${relative_fixture}" + VERBATIM) + string(REPLACE "/" "_" generation_target "generate_${base}") + add_custom_target(${generation_target} DEPENDS ${outputs}) + add_dependencies(run_generate_tests ${generation_target}) + set_source_files_properties(${outputs} PROPERTIES STAN_GENERATOR_TARGET ${generation_target}) + list(APPEND generated_sources ${outputs}) +endforeach() +if(generated_sources) + add_gtest_grouped_test("${CMAKE_CURRENT_BINARY_DIR}" prob_math prob_pch ${generated_sources}) +endif() diff --git a/test/prob/utility.hpp b/test/prob/utility.hpp index 0a6a7ee0857..2c4f5469eb8 100644 --- a/test/prob/utility.hpp +++ b/test/prob/utility.hpp @@ -29,7 +29,7 @@ struct is_empty { // ------------------------------------------------------------ namespace std { -std::ostream& operator<<(std::ostream& os, const vector& param) { +inline std::ostream& operator<<(std::ostream& os, const vector& param) { os << "("; for (size_t n = 0; n < param.size(); n++) { os << param[n]; @@ -40,7 +40,7 @@ std::ostream& operator<<(std::ostream& os, const vector& param) { return os; } -std::ostream& operator<<(std::ostream& os, const vector& param) { +inline std::ostream& operator<<(std::ostream& os, const vector& param) { os << "("; for (size_t n = 0; n < param.size(); n++) { os << param[n]; @@ -64,7 +64,7 @@ inline T get_param(const vector& params, const size_t n) { } template <> -empty get_param(const vector& /*params*/, const size_t /*n*/) { +inline empty get_param(const vector& /*params*/, const size_t /*n*/) { return empty(); } @@ -133,7 +133,7 @@ inline T get_params(const vector>& parameters, const size_t p) { // handle empty template <> -empty get_params(const vector>& /*parameters*/, +inline empty get_params(const vector>& /*parameters*/, const size_t /*p*/) { return empty(); } @@ -175,7 +175,7 @@ inline fvar get_params>(const vector>& parameters, return param; } template <> -fvar> get_params>>( +inline fvar> get_params>>( const vector>& parameters, const size_t p) { fvar> param(0); if (p < parameters[0].size()) { @@ -185,7 +185,7 @@ fvar> get_params>>( return param; } template <> -fvar> get_params>>( +inline fvar> get_params>>( const vector>& parameters, const size_t p) { fvar> param(0); if (p < parameters[0].size()) { @@ -195,7 +195,7 @@ fvar> get_params>>( return param; } template <> -int get_params(const vector>& parameters, const size_t p) { +inline int get_params(const vector>& parameters, const size_t p) { int param(0); if (p < parameters[0].size()) param = (int)parameters[0][p]; @@ -212,7 +212,7 @@ inline vector get_params>( return param; } template <> -vector get_params>( +inline vector get_params>( const vector>& parameters, const size_t p) { vector param(parameters.size()); for (size_t n = 0; n < parameters.size(); n++) @@ -230,7 +230,7 @@ inline vector get_params>( return param; } template <> -vector> get_params>>( +inline vector> get_params>>( const vector>& parameters, const size_t p) { vector> param(parameters.size()); for (size_t n = 0; n < parameters.size(); n++) @@ -241,7 +241,7 @@ vector> get_params>>( return param; } template <> -vector> get_params>>( +inline vector> get_params>>( const vector>& parameters, const size_t p) { vector> param(parameters.size()); for (size_t n = 0; n < parameters.size(); n++) @@ -252,7 +252,7 @@ vector> get_params>>( return param; } template <> -vector>> get_params>>>( +inline vector>> get_params>>>( const vector>& parameters, const size_t p) { vector>> param(parameters.size()); for (size_t n = 0; n < parameters.size(); n++) @@ -263,7 +263,7 @@ vector>> get_params>>>( return param; } template <> -vector>> get_params>>>( +inline vector>> get_params>>>( const vector>& parameters, const size_t p) { vector>> param(parameters.size()); for (size_t n = 0; n < parameters.size(); n++) @@ -300,7 +300,7 @@ inline T get_params(const vector>& parameters, // handle empty template <> -empty get_params(const vector>& /*parameters*/, +inline empty get_params(const vector>& /*parameters*/, const size_t /*n*/, const size_t /*p*/) { return empty(); } @@ -342,7 +342,7 @@ inline fvar get_params>(const vector>& parameters, return param; } template <> -fvar> get_params>>( +inline fvar> get_params>>( const vector>& parameters, const size_t n, const size_t p) { fvar> param(0); if (p < parameters[0].size()) { @@ -352,7 +352,7 @@ fvar> get_params>>( return param; } template <> -fvar> get_params>>( +inline fvar> get_params>>( const vector>& parameters, const size_t n, const size_t p) { fvar> param(0); if (p < parameters[0].size()) { @@ -362,7 +362,7 @@ fvar> get_params>>( return param; } template <> -int get_params(const vector>& parameters, const size_t n, +inline int get_params(const vector>& parameters, const size_t n, const size_t p) { int param(0); if (p < parameters[0].size()) @@ -381,7 +381,7 @@ inline vector get_params>( return param; } template <> -vector get_params>( +inline vector get_params>( const vector>& parameters, const size_t /*n*/, const size_t p) { vector param(parameters.size()); @@ -401,7 +401,7 @@ inline vector get_params>( return param; } template <> -vector> get_params>>( +inline vector> get_params>>( const vector>& parameters, const size_t /*n*/, const size_t p) { vector> param(parameters.size()); @@ -413,7 +413,7 @@ vector> get_params>>( return param; } template <> -vector> get_params>>( +inline vector> get_params>>( const vector>& parameters, const size_t /*n*/, const size_t p) { vector> param(parameters.size()); @@ -425,7 +425,7 @@ vector> get_params>>( return param; } template <> -vector>> get_params>>>( +inline vector>> get_params>>>( const vector>& parameters, const size_t /*n*/, const size_t p) { vector>> param(parameters.size()); @@ -437,7 +437,7 @@ vector>> get_params>>>( return param; } template <> -vector>> get_params>>>( +inline vector>> get_params>>>( const vector>& parameters, const size_t /*n*/, const size_t p) { vector>> param(parameters.size()); @@ -541,7 +541,7 @@ typename scalar_type::type select_var_param( } template <> -empty select_var_param(const vector>& /*parameters*/, +inline empty select_var_param(const vector>& /*parameters*/, const size_t /*n*/, const size_t /*p*/) { return empty(); } diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt new file mode 100644 index 00000000000..899e1482886 --- /dev/null +++ b/test/unit/CMakeLists.txt @@ -0,0 +1,56 @@ +# Keep each autodiff layer's PCH small. The AD helper header includes mix.hpp +# and test_ad_matvar.hpp, so those need no separate entries in its PCH. +foreach(layer prim fwd rev mix) + stan_add_pch(unit_${layer}_pch ) +endforeach() +stan_add_pch(unit_ad_pch ) +add_gtest_grouped_test("${CMAKE_CURRENT_SOURCE_DIR}/math" unit_math unit_mix_pch) + +# Umbrella and multiple-translation-unit checks must compile without a PCH. +foreach(source math_c11_test.cpp math_include_test.cpp multiple_translation_units_test.cpp) + if(STAN_TEST_FILTER AND NOT "test/unit/${source}" MATCHES "${STAN_TEST_FILTER}") + continue() + endif() + set(matches_name_filters TRUE) + foreach(filter IN LISTS STAN_TEST_NAME_FILTERS) + string(FIND "test/unit/${source}" "${filter}" match_index) + if(match_index EQUAL -1) + set(matches_name_filters FALSE) + endif() + endforeach() + if(NOT matches_name_filters) + continue() + endif() + get_filename_component(name "${source}" NAME_WE) + set_source_files_properties("${source}" PROPERTIES SKIP_PRECOMPILE_HEADERS ON) + stan_add_test_object(test_unit_${name}_objects "${source}" "") + stan_add_test_binary(test_unit_${name} "$") + add_test(NAME test_unit_${name} COMMAND test_unit_${name}) + set_tests_properties(test_unit_${name} PROPERTIES LABELS unit + WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}") +endforeach() +if(TARGET test_unit_multiple_translation_units_test) + add_library(stan_multiple_translation_units SHARED + multiple_translation_units1.cpp multiple_translation_units2.cpp) + target_link_libraries(stan_multiple_translation_units PRIVATE stanmath) + target_link_libraries(test_unit_multiple_translation_units_test PRIVATE stan_multiple_translation_units) +endif() + +# The public runner also accepts test/unit, including the umbrella checks. +set(unit_objects "") +if(TARGET unit_math_all_tests) + get_target_property(unit_objects unit_math_all_tests STAN_TEST_OBJECTS) +endif() +foreach(source math_c11_test.cpp math_include_test.cpp multiple_translation_units_test.cpp) + get_filename_component(name "${source}" NAME_WE) + if(TARGET test_unit_${name}) + list(APPEND unit_objects "$") + endif() +endforeach() +if(unit_objects) + stan_add_test_binary(unit_all_tests ${unit_objects}) + set_target_properties(unit_all_tests PROPERTIES EXCLUDE_FROM_ALL TRUE) + if(TARGET stan_multiple_translation_units) + target_link_libraries(unit_all_tests PRIVATE stan_multiple_translation_units) + endif() +endif() diff --git a/test/unit/math/prim/fun/eval_test.cpp b/test/unit/math/prim/fun/eval_test.cpp index 148cc2f1e12..5b6605bd7f3 100644 --- a/test/unit/math/prim/fun/eval_test.cpp +++ b/test/unit/math/prim/fun/eval_test.cpp @@ -196,7 +196,7 @@ TEST(MathFunctions, eval_return_type_expression) { TEST(MathFunctions, eval_return_type_short_circuit_static_sized_matrix) { Eigen::Matrix a; - const Eigen::Matrix b; + const Eigen::Matrix b{}; EXPECT_TRUE((std::is_same&>::value)); EXPECT_TRUE((std::is_same a; - const Eigen::Matrix b; + const Eigen::Matrix b{}; EXPECT_TRUE((std::is_same&>::value)); EXPECT_TRUE((std::is_same a; - const Eigen::Matrix b; + const Eigen::Matrix b{}; EXPECT_TRUE((std::is_same&>::value)); EXPECT_TRUE((std::is_same(std::vector(2, 10.0), 1, get_new_msg(), args...); }; -auto reduce_sum_static_sum_lpdf = [](auto&& data, auto&&... args) { +inline auto reduce_sum_static_sum_lpdf = [](auto&& data, auto&&... args) { return stan::math::reduce_sum_static(data, 1, get_new_msg(), args...); }; -auto reduce_sum_int_sum_lpdf = [](auto&&... args) { +inline auto reduce_sum_int_sum_lpdf = [](auto&&... args) { return stan::math::reduce_sum(std::vector(2, 10.0), 1, get_new_msg(), args...); }; -auto reduce_sum_sum_lpdf = [](auto&& data, auto&&... args) { +inline auto reduce_sum_sum_lpdf = [](auto&& data, auto&&... args) { return stan::math::reduce_sum(data, 1, get_new_msg(), args...); }; diff --git a/test/unit/math/rev/core/gradable.hpp b/test/unit/math/rev/core/gradable.hpp index a4c198d1ed6..c7185ae988c 100644 --- a/test/unit/math/rev/core/gradable.hpp +++ b/test/unit/math/rev/core/gradable.hpp @@ -25,7 +25,7 @@ struct gradable { double adj() { return f_.adj(); } }; -gradable setup_quad_form() { +inline gradable setup_quad_form() { using Eigen::Dynamic; using Eigen::Matrix; using stan::math::quad_form; @@ -52,7 +52,7 @@ gradable setup_quad_form() { return gradable(x, f, g_expected); } -gradable setup_simple() { +inline gradable setup_simple() { stan::math::var a = 3; stan::math::var b = 7; std::vector x; diff --git a/test/unit/math/rev/fun/util.hpp b/test/unit/math/rev/fun/util.hpp index 7fdca697ef7..22d51a4aaf3 100644 --- a/test/unit/math/rev/fun/util.hpp +++ b/test/unit/math/rev/fun/util.hpp @@ -26,7 +26,7 @@ struct third_order_mixed { } }; -Eigen::Matrix third_order_mixed_hess( +inline Eigen::Matrix third_order_mixed_hess( const Eigen::Matrix& inp_vec) { Eigen::Matrix hess; @@ -46,7 +46,7 @@ Eigen::Matrix third_order_mixed_hess( return hess; } -Eigen::Matrix norm_hess( +inline Eigen::Matrix norm_hess( const Eigen::Matrix& inp_vec) { using Eigen::Dynamic; using Eigen::Matrix; @@ -62,7 +62,7 @@ Eigen::Matrix norm_hess( return hess; } -std::vector> +inline std::vector> third_order_mixed_grad_hess( const Eigen::Matrix& inp_vec) { using Eigen::Dynamic; @@ -91,7 +91,7 @@ third_order_mixed_grad_hess( return grad_hess_ret; } -std::vector> +inline std::vector> norm_grad_hess(const Eigen::Matrix& inp_vec) { using Eigen::Dynamic; using Eigen::Matrix; diff --git a/test/unit/math/rev/functor/util_algebra_solver.hpp b/test/unit/math/rev/functor/util_algebra_solver.hpp index 6035ca4d0d7..0ac3865dc77 100644 --- a/test/unit/math/rev/functor/util_algebra_solver.hpp +++ b/test/unit/math/rev/functor/util_algebra_solver.hpp @@ -518,7 +518,7 @@ inline void max_num_steps_test(Eigen::Matrix& y, std::domain_error); } -Eigen::Matrix variadic_eq_impl_test( +inline Eigen::Matrix variadic_eq_impl_test( const Eigen::Matrix A, const stan::math::var& y_1, const stan::math::var& y_2, const stan::math::var& y_3, const int i, bool is_newton = false, diff --git a/test/unit/math/rev/prob/bernoulli_logit_glm_lpmf_test.cpp b/test/unit/math/rev/prob/bernoulli_logit_glm_lpmf_test.cpp index cb8c2a16395..9a89fc2e1ed 100644 --- a/test/unit/math/rev/prob/bernoulli_logit_glm_lpmf_test.cpp +++ b/test/unit/math/rev/prob/bernoulli_logit_glm_lpmf_test.cpp @@ -573,6 +573,8 @@ TYPED_TEST(ProbDistributionsBernoulliLogitGLM, for (int n = 0; n < N; n++) { yw2[n] = -(Eigen::Matrix::Random(1, 1)[0] % 2); } + // Random draws can all be zero, so guarantee an out-of-domain label. + yw2[N - 1] = -1; Eigen::Matrix yw3(N, 1); for (int n = 0; n < N; n++) { yw3[n] = 42 + (Eigen::Matrix::Random(1, 1)[0] % 2); @@ -600,7 +602,8 @@ TYPED_TEST(ProbDistributionsBernoulliLogitGLM, EXPECT_THROW(stan::math::bernoulli_logit_glm_lpmf(yw1, x, alpha, beta), std::invalid_argument); EXPECT_THROW(stan::math::bernoulli_logit_glm_lpmf(yw2, x, alpha, beta), - std::domain_error); + std::domain_error) + << "Negative-label fixture: " << yw2.transpose(); EXPECT_THROW(stan::math::bernoulli_logit_glm_lpmf(yw3, x, alpha, beta), std::domain_error); EXPECT_THROW(stan::math::bernoulli_logit_glm_lpmf(y, xw1, alpha, beta), diff --git a/test/unit/math/test_ad.hpp b/test/unit/math/test_ad.hpp index 6336f1b03d0..78c8aecf15a 100644 --- a/test/unit/math/test_ad.hpp +++ b/test/unit/math/test_ad.hpp @@ -59,7 +59,7 @@ auto eval(const std::complex& x) { * @param[in] x value * @return value */ -auto eval(const stan::math::var& x) { return x; } +inline auto eval(const stan::math::var& x) { return x; } /** * Evaluates all matrix expression templates, which is a no-op for @@ -974,7 +974,7 @@ inline void expect_ad_vvv(const ad_tolerances& tols, const F& f, int x1, int x2, * * @return non-zero arguments */ -const std::vector& common_nonzero_args() { +inline const std::vector& common_nonzero_args() { static const std::vector common_nz_args{ -1.3, 0.49, diff --git a/test/unit/util.hpp b/test/unit/util.hpp index 6d13989d579..a0343c1a111 100644 --- a/test/unit/util.hpp +++ b/test/unit/util.hpp @@ -167,7 +167,7 @@ * @param s string to match count occurrences * @return number of found occurrences of target in s */ -int count_matches(const std::string& target, const std::string& s) { +inline int count_matches(const std::string& target, const std::string& s) { if (target.size() == 0) return -1; // error int count = 0; @@ -212,7 +212,7 @@ int count_matches(const std::string& target, const std::string& s) { namespace stan { namespace test { -auto make_sparse_matrix_random(int rows, int cols) { +inline auto make_sparse_matrix_random(int rows, int cols) { using eigen_triplet = Eigen::Triplet; boost::mt19937 gen; boost::random::uniform_real_distribution dist(0.0, 1.0); From 53da0821642ea1ff95c4b24641e63b83fb4798e6 Mon Sep 17 00:00:00 2001 From: Steve Bronder Date: Wed, 14 Oct 2026 17:50:28 -0400 Subject: [PATCH 02/10] Simplify CMake testing and reuse Jenkins build directories --- .github/workflows/header_checks.yml | 14 +- .github/workflows/main.yml | 18 +- CMakeLists.txt | 33 +- Jenkinsfile | 101 +-- README.md | 553 ++++++++++++---- cmake/CMakeLists.txt | 27 +- cmake/MakeLocal.cmake | 47 ++ cmake/make_local.py | 120 ++++ cmake/stanc.cmake | 26 + runTests.py | 353 +---------- test/CMakeLists.txt | 18 +- test/cmake.py | 596 ++++++++++++++++++ test/cmake_build_test.py | 176 ------ test/cmake_ci.py | 84 --- test/cmake_ci_test.py | 47 -- test/cmake_runner.py | 146 ----- test/cmake_runner_test.py | 113 ---- .../test_expression_testing_framework.py | 2 +- test/sig_utils.py | 26 +- test/unit/CMakeLists.txt | 12 +- test/varmat_compatibility.py | 36 +- test/varmat_compatibility_test.py | 8 + 22 files changed, 1431 insertions(+), 1125 deletions(-) create mode 100644 cmake/MakeLocal.cmake create mode 100644 cmake/make_local.py create mode 100644 cmake/stanc.cmake create mode 100644 test/cmake.py delete mode 100644 test/cmake_build_test.py delete mode 100644 test/cmake_ci.py delete mode 100644 test/cmake_ci_test.py delete mode 100644 test/cmake_runner.py delete mode 100644 test/cmake_runner_test.py diff --git a/.github/workflows/header_checks.yml b/.github/workflows/header_checks.yml index ebd4684d835..1f3b3b91a30 100644 --- a/.github/workflows/header_checks.yml +++ b/.github/workflows/header_checks.yml @@ -39,13 +39,14 @@ jobs: Get-Command mingw32-make | Select-Object -ExpandProperty Definition shell: powershell - - name: Build Math libs - shell: powershell - run: mingw32-make -f make/standalone math-libs -j2 + - name: Install Ninja + run: python -m pip install ninja - name: Run header tests shell: powershell - run: make -j2 test-headers + run: | + cmake -S . -B build -G Ninja -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ -DSTAN_BUILD_PROB_TESTS=OFF + cmake --build build --parallel 2 --target test-headers opencl: name: OpenCL @@ -57,7 +58,8 @@ jobs: - name: Run header tests run: | echo "STAN_OPENCL=true" > make/local - make -j2 test-headers + cmake -S . -B build -DSTAN_OPENCL=ON -DSTAN_BUILD_PROB_TESTS=OFF + cmake --build build --parallel 2 --target test-headers no_range_checks: name: NoRange runs-on: ubuntu-latest @@ -68,4 +70,4 @@ jobs: - name: Run header tests run: | echo "STAN_NO_RANGE_CHECKS=true" > make/local - ./runTests.py --backend make -j2 ./test/unit/math/prim/err/ + ./runTests.py -j2 ./test/unit/math/prim/err/ diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a023cecc9c6..8f34d0c9cd2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -48,25 +48,24 @@ jobs: with: r-version: 'release' - - name: Build Math libs + - name: Configure test tools shell: pwsh run: | - echo "$(pwd)/lib/tbb" | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8 + python -m pip install ninja Add-Content make\local "CPPFLAGS=-w -Wno-psabi -Wno-misleading-indentation`n" Add-Content make\local "STAN_THREADS=true`n" - make -f make/standalone math-libs -j2 - name: Run ${{ matrix.config.label }} unit tests shell: pwsh run: | - python runTests.py --backend make -j4 ${{ matrix.config.tests }} + python runTests.py --cmake-arg=-GNinja --cmake-arg=-DCMAKE_C_COMPILER=gcc --cmake-arg=-DCMAKE_CXX_COMPILER=g++ -j4 ${{ matrix.config.tests }} - name: Upload gtest_output xml uses: actions/upload-artifact@v7 if: failure() with: name: gtest_outputs_xml - path: '**/*_test.xml' + path: 'build/test-results/*.xml' mix-fun: name: mix fun and prob tests ${{ matrix.group }} (${{ matrix.os }}) @@ -91,13 +90,12 @@ jobs: r-version: 'release' - - name: Build Math libs + - name: Configure test tools shell: pwsh run: | - echo "$(pwd)/lib/tbb" | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8 + python -m pip install ninja Add-Content make\local "CPPFLAGS=-w -Wno-psabi -Wno-misleading-indentation`n" Add-Content make\local "STAN_THREADS=true`n" - make -f make/standalone math-libs -j2 - name: Run mix/fun unit tests shell: pwsh @@ -114,11 +112,11 @@ jobs: $MixFunProbTests[(4 * $FifthNumberTests)..($NumberTests - 1)] ` ) - python runTests.py --backend make -j2 $MixFunProbTestsArray[(${{ matrix.group }} - 1)] + python runTests.py --cmake-arg=-GNinja --cmake-arg=-DCMAKE_C_COMPILER=gcc --cmake-arg=-DCMAKE_CXX_COMPILER=g++ -j2 $MixFunProbTestsArray[(${{ matrix.group }} - 1)] - name: Upload gtest_output xml uses: actions/upload-artifact@v7 if: failure() with: name: gtest_outputs_xml - path: '**/*_test.xml' + path: 'build/test-results/*.xml' diff --git a/CMakeLists.txt b/CMakeLists.txt index a909651b5e1..7d40a29f45d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,7 @@ cmake_minimum_required(VERSION 3.16.3) +include(cmake/MakeLocal.cmake) project(stanmath LANGUAGES C CXX) +stan_apply_make_local_flags() if(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR) message(FATAL_ERROR "Use an out-of-source build: cmake -S . -B build") @@ -20,14 +22,15 @@ option(STAN_NO_RANGE_CHECKS "Disable range checks within the Stan library" OFF) option(STAN_MPI "Enable MPI support" OFF) option(STAN_OPENCL "Enable OpenCL support" OFF) option(STAN_THREADS "Enable multi-threading support" OFF) -set(STAN_OPENCL_DEVICE_ID "0" CACHE STRING "OpenCL device ID") -set(STAN_OPENCL_PLATFORM_ID "0" CACHE STRING "OpenCL platform ID") -set(STAN_INTEGRATED_OPENCL "0" CACHE STRING "Enable integrated OpenCL features") -set(STAN_TEST_FILTER "" CACHE STRING "Regex selecting test source paths relative to the repository") +stan_cache_default(STAN_OPENCL_DEVICE_ID "0" STRING "OpenCL device ID") +stan_cache_default(STAN_OPENCL_PLATFORM_ID "0" STRING "OpenCL platform ID") +stan_cache_default(STAN_INTEGRATED_OPENCL "0" STRING "Enable integrated OpenCL features") set(STAN_TEST_NAME_FILTERS "" CACHE STRING "Required substrings in selected test source paths") include(CTest) -set(STAN_TEST_OPTIMIZATION "1" CACHE STRING "Optimization level for tests and solver libraries") -set_property(CACHE STAN_TEST_OPTIMIZATION PROPERTY STRINGS 0 1 2 3) +stan_cache_default(STAN_TEST_OPTIMIZATION "1" STRING "Optimization level for tests and solver libraries") +if(DEFINED CACHE{STAN_TEST_OPTIMIZATION}) + set_property(CACHE STAN_TEST_OPTIMIZATION PROPERTY STRINGS 0 1 2 3) +endif() if(NOT STAN_TEST_OPTIMIZATION MATCHES "^[0-3]$") message(FATAL_ERROR "STAN_TEST_OPTIMIZATION must be 0, 1, 2, or 3") endif() @@ -91,7 +94,8 @@ if(STAN_MPI) endif() message(STATUS "Stan Math: threads=${STAN_THREADS}, OpenCL=${STAN_OPENCL}, MPI=${STAN_MPI}") -message(STATUS "Test PCH=${STAN_TEST_PCH}, source filter='${STAN_TEST_FILTER}'") +set(STAN_EFFECTIVE_MPI "${STAN_MPI}" CACHE INTERNAL "MPI setting used by test executables" FORCE) +message(STATUS "Test PCH=${STAN_TEST_PCH}") if(BUILD_TESTING) add_subdirectory(test) endif() @@ -106,3 +110,18 @@ if(STAN_BUILD_DOCS) COMMENT "Generating API documentation with Doxygen" VERBATIM) endif() + +set(STAN_COMPILE_TEST_SOURCE "" CACHE FILEPATH "Source file for compile-only compatibility checks") +if(STAN_COMPILE_TEST_SOURCE) + add_library(stan_compile_test_pch OBJECT EXCLUDE_FROM_ALL cmake/pch.cpp) + add_library(stan_compile_test OBJECT EXCLUDE_FROM_ALL "${STAN_COMPILE_TEST_SOURCE}") + foreach(target stan_compile_test_pch stan_compile_test) + target_link_libraries(${target} PRIVATE stanmath) + target_include_directories(${target} SYSTEM PRIVATE + "${STAN_BENCHMARK_ROOT}/googletest/googletest/include") + target_compile_options(${target} PRIVATE ${STAN_TEST_OPTIMIZATION_FLAGS}) + set_target_properties(${target} PROPERTIES POSITION_INDEPENDENT_CODE FALSE) + endforeach() + target_precompile_headers(stan_compile_test_pch PRIVATE ) + target_precompile_headers(stan_compile_test REUSE_FROM stan_compile_test_pch) +endif() diff --git a/Jenkinsfile b/Jenkinsfile index ac4ea82d098..0535fa2baf4 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -25,38 +25,55 @@ def shellQuote(String value) { return "'" + value.replace("'", "'\"'\"'") + "'" } +def configureCmake(List arguments) { + def flags = ['-S', '.', '-B', 'build'] + arguments + if (fileExists('build/CMakeCache.txt')) { + flags += ['--fresh'] + } + sh 'cmake ' + flags.collect { shellQuote(it.toString()) }.join(' ') +} + def runCmakeTests(Map config) { - def buildDir = config.buildDir - def jobs = Math.max(1, Math.min(6, (env.PARALLEL ?: '6').toInteger())) + def buildDir = 'build' + def reportDir = "test-results/${config.name}" + def jobs = Math.max(1, (env.PARALLEL ?: '1').toInteger()) def flags = [ - '-S', '.', '-B', buildDir, '-DCMAKE_BUILD_TYPE=Release', + '-DCMAKE_BUILD_TYPE=Release', '-DSTAN_TEST_HEADERS=OFF', '-DSTAN_BUILD_PROB_TESTS=OFF', - "-DSTAN_TEST_FILTER=${config.filter}", "-DSTAN_TEST_OPTIMIZATION=${config.optimization ?: '1'}" ] + (config.flags ?: []) try { sh "cmake -E rm -rf ${shellQuote(buildDir + '/test-results')} " + - "${shellQuote(buildDir + '/ctest-results.xml')} ${shellQuote(buildDir + '/Testing')}" - sh "cmake " + flags.collect { shellQuote(it.toString()) }.join(' ') - sh "cmake --build ${shellQuote(buildDir)} --parallel ${jobs}" + "${shellQuote(buildDir + '/ctest-results.xml')} ${shellQuote(buildDir + '/Testing')} " + + shellQuote(reportDir) + configureCmake(flags) + sh "python3 test/cmake.py build-tests ${shellQuote(buildDir)} " + + "--source-regex ${shellQuote(config.filter)} --jobs ${jobs}" sh "cmake -E make_directory ${shellQuote(buildDir + '/test-results')}" withEnv(["GTEST_OUTPUT=xml:${pwd()}/${buildDir}/test-results/"]) { - sh "ctest --test-dir ${shellQuote(buildDir)} --parallel ${config.testJobs ?: jobs} " + + sh "ctest --test-dir ${shellQuote(buildDir)} -L ${shellQuote(config.filter)} " + + "--parallel ${config.testJobs ?: jobs} " + "--no-tests=error --output-on-failure " + "--output-junit ${shellQuote(pwd() + '/' + buildDir + '/ctest-results.xml')}" } } finally { junit(testResults: "${buildDir}/test-results/*.xml", allowEmptyResults: true) - archiveArtifacts(artifacts: "${buildDir}/ctest-results.xml,${buildDir}/Testing/Temporary/LastTest.log", - allowEmptyArchive: true) + sh "cmake -E make_directory ${shellQuote(reportDir)}" + if (fileExists(buildDir + '/test-results')) { + sh "cmake -E copy_directory ${shellQuote(buildDir + '/test-results')} ${shellQuote(reportDir + '/gtest')}" + } + for (file in ['ctest-results.xml', 'Testing/Temporary/LastTest.log']) { + if (fileExists(buildDir + '/' + file)) { + sh "cmake -E copy ${shellQuote(buildDir + '/' + file)} ${shellQuote(reportDir)}" + } + } + archiveArtifacts(artifacts: "${reportDir}/**", allowEmptyArchive: true) } } -// Var-matrix compatibility still invokes Make directly; expression framework -// self-tests exercise compile and runtime failures through the CMake runner. -def runLegacyFrameworkTests(String compiler) { +def runFrameworkTests(String compiler) { writeFile(file: 'make/local', text: "CXX=${compiler}\nO=0\n") - withEnv(['PARALLEL=6', 'MAKEFLAGS=-j1', 'PATH+TBB=./lib/tbb', "CXX=${compiler}"]) { + withEnv(["CXX=${compiler}"]) { sh 'python3 test/varmat_compatibility_test.py' sh 'python3 test/expressions/test_expression_testing_framework.py' } @@ -121,13 +138,11 @@ up the autoformatter locally. (Check console output at ${env.BUILD_URL}) CppLint: { sh "make cpplint" }, Dependencies: { sh "make test-math-dependencies" }, CMakeSelection: { - sh 'python3 test/cmake_ci_test.py' - sh 'python3 test/cmake_runner_test.py' - sh 'python3 test/cmake_build_test.py' + sh 'python3 test/cmake.py self-test' }, Documentation: { - sh 'cmake -S . -B build-ci-docs -DBUILD_TESTING=OFF -DSTAN_BUILD_DOCS=ON' - sh 'cmake --build build-ci-docs --parallel 6 --target doxygen' + configureCmake(['-DBUILD_TESTING=OFF', '-DSTAN_BUILD_DOCS=ON']) + sh "cmake --build build --parallel ${env.PARALLEL ?: '1'} --target doxygen" }, ) } @@ -138,11 +153,11 @@ up the autoformatter locally. (Check console output at ${env.BUILD_URL}) parallel headers: { runPod(image: image) { stage('Headers check') { - sh "cmake -S . -B build-ci-headers -DCMAKE_BUILD_TYPE=Release " + - "-DCMAKE_C_COMPILER=${CLANG_CC} -DCMAKE_CXX_COMPILER=${CLANG_CXX} " + - "-DCMAKE_CXX_FLAGS=-Werror -DSTAN_BUILD_PROB_TESTS=OFF " + - '-DSTAN_TEST_FILTER=\'^$\' -DSTAN_TEST_HEADERS=ON' - sh 'cmake --build build-ci-headers --parallel 6 --target test-headers' + configureCmake(['-DCMAKE_BUILD_TYPE=Release', + "-DCMAKE_C_COMPILER=${CLANG_CC}", "-DCMAKE_CXX_COMPILER=${CLANG_CXX}", + '-DCMAKE_CXX_FLAGS=-Werror', '-DSTAN_BUILD_PROB_TESTS=OFF', + '-DSTAN_TEST_HEADERS=ON']) + sh "cmake --build build --parallel ${env.PARALLEL ?: '1'} --target test-headers" } } }, unit: { @@ -150,9 +165,9 @@ up the autoformatter locally. (Check console output at ${env.BUILD_URL}) runPod(image: image) { stage('Run changed unit tests') { def filter = sh(returnStdout: true, - script: 'python3 test/cmake_ci.py changed-unit-filter').trim() + script: 'python3 test/cmake.py changed-unit-filter').trim() if (filter) { - runCmakeTests(buildDir: 'build-ci-changed', filter: filter, optimization: '3', + runCmakeTests(name: 'changed', filter: filter, optimization: '3', flags: ['-DCMAKE_CXX_FLAGS=-march=native -mtune=native']) } else { echo 'No priority unit tests selected.' @@ -168,7 +183,7 @@ up the autoformatter locally. (Check console output at ${env.BUILD_URL}) revfwd: { runPod(image: image) { stage('Rev/Fwd Unit Tests') { - runCmakeTests(buildDir: 'build-ci-revfwd', filter: '^test/unit/math/(rev|fwd)/', + runCmakeTests(name: 'revfwd', filter: '^test/unit/math/(rev|fwd)/', optimization: noOptimize ? '0' : '3', flags: ['-DCMAKE_CXX_FLAGS=-fsanitize=address', '-DCMAKE_EXE_LINKER_FLAGS=-fsanitize=address', '-DCMAKE_SHARED_LINKER_FLAGS=-fsanitize=address']) @@ -178,7 +193,7 @@ up the autoformatter locally. (Check console output at ${env.BUILD_URL}) mix: { runPod(image: image, cpus: 6, memory: '192Gi') { stage('Mix Unit Tests') { - runCmakeTests(buildDir: 'build-ci-mix', filter: '^test/unit/math/mix/', + runCmakeTests(name: 'mix', filter: '^test/unit/math/mix/', optimization: noOptimize ? '1' : '3', flags: ['-DCMAKE_CXX_FLAGS=-fsanitize=address', '-DCMAKE_EXE_LINKER_FLAGS=-fsanitize=address', '-DCMAKE_SHARED_LINKER_FLAGS=-fsanitize=address']) @@ -188,7 +203,7 @@ up the autoformatter locally. (Check console output at ${env.BUILD_URL}) prim: { runPod(image: image) { stage('Prim Unit Tests') { - runCmakeTests(buildDir: 'build-ci-prim', + runCmakeTests(name: 'prim', filter: '^test/unit/([^/]+_test[.]cpp$|math/([^/]+_test[.]cpp$|prim/|memory/))', optimization: noOptimize ? '0' : '3', flags: ['-DCMAKE_CXX_FLAGS=-fsanitize=address', '-DCMAKE_EXE_LINKER_FLAGS=-fsanitize=address', @@ -199,10 +214,10 @@ up the autoformatter locally. (Check console output at ${env.BUILD_URL}) laplace: { runPod(image: image, memory: '32Gi') { stage('Laplace Unit Tests') { - runCmakeTests(buildDir: 'build-ci-laplace', filter: '^test/unit/math/laplace/', + runCmakeTests(name: 'laplace', filter: '^test/unit/math/laplace/', optimization: '3', flags: ['-DCMAKE_CXX_FLAGS=-march=native -mtune=native']) if (mainBranch) { - runCmakeTests(buildDir: 'build-ci-laplace-asan', optimization: '3', + runCmakeTests(name: 'laplace-asan', optimization: '3', filter: '^test/unit/math/laplace/laplace_marginal_lpdf_moto_test[.]cpp$', flags: ['-DCMAKE_CXX_FLAGS=-march=native -mtune=native -fsanitize=address', '-DCMAKE_EXE_LINKER_FLAGS=-fsanitize=address']) @@ -213,7 +228,7 @@ up the autoformatter locally. (Check console output at ${env.BUILD_URL}) gpu: { runPod(image: image, memory: '32Gi', gpus: 1) { stage('OpenCL GPU tests') { - runCmakeTests(buildDir: 'build-ci-opencl', + runCmakeTests(name: 'opencl', filter: '^test/unit/(math/opencl/|multiple_translation_units_test[.]cpp$)', optimization: noOptimize ? '1' : '3', flags: ["-DCMAKE_C_COMPILER=${CLANG_CC}", "-DCMAKE_CXX_COMPILER=${CLANG_CXX}", @@ -226,7 +241,7 @@ up the autoformatter locally. (Check console output at ${env.BUILD_URL}) mpi: { runPod(image: image) { stage('MPI Tests') { - runCmakeTests(buildDir: 'build-ci-mpi', + runCmakeTests(name: 'mpi', filter: '^test/unit/math/(prim|rev)/functor/', testJobs: 1, flags: ["-DCMAKE_CXX_COMPILER=${MPICXX}", '-DSTAN_MPI=ON']) } @@ -239,14 +254,14 @@ up the autoformatter locally. (Check console output at ${env.BUILD_URL}) sh 'python3 test/signature_parser_test.py' sh 'python3 test/statement_types_test.py' sh 'python3 test/varmat_compatibility_summary_test.py' - runLegacyFrameworkTests(CLANG_CXX) - sh 'python3 test/cmake_ci.py generate-expressions build-ci-expressions' - runCmakeTests(buildDir: 'build-ci-expressions', filter: '^test/expressions/', optimization: '0', + runFrameworkTests(CLANG_CXX) + sh 'python3 test/cmake.py generate-expressions build' + runCmakeTests(name: 'expressions', filter: '^test/expressions/', optimization: '0', flags: ["-DCMAKE_C_COMPILER=${CLANG_CC}", "-DCMAKE_CXX_COMPILER=${CLANG_CXX}", '-DCMAKE_CXX_FLAGS=-Werror', '-DSTAN_BUILD_EXPRESSION_TESTS=ON']) - sh 'python3 test/cmake_ci.py generate-expressions build-ci-expressions-threads --functions reduce_sum map_rect' + sh 'python3 test/cmake.py generate-expressions build --functions reduce_sum map_rect' withEnv(['STAN_NUM_THREADS=4']) { - runCmakeTests(buildDir: 'build-ci-expressions-threads', filter: '^test/expressions/', + runCmakeTests(name: 'expressions-threads', filter: '^test/expressions/', optimization: '0', testJobs: 1, flags: ["-DCMAKE_C_COMPILER=${CLANG_CC}", "-DCMAKE_CXX_COMPILER=${CLANG_CXX}", '-DCMAKE_CXX_FLAGS=-Werror', '-DSTAN_BUILD_EXPRESSION_TESTS=ON', '-DSTAN_THREADS=ON']) @@ -258,7 +273,7 @@ up the autoformatter locally. (Check console output at ${env.BUILD_URL}) runPod(image: image, cpus: 6, memory: '128Gi') { stage('Threading tests') { withEnv(['STAN_NUM_THREADS=4']) { - runCmakeTests(buildDir: 'build-ci-threads', testJobs: 1, + runCmakeTests(name: 'threads', testJobs: 1, filter: mainBranch ? '^test/unit/' : '^test/unit/.*(thread|map_rect|reduce_sum)', flags: ["-DCMAKE_C_COMPILER=${CLANG_CC}", "-DCMAKE_CXX_COMPILER=${CLANG_CXX}", '-DCMAKE_CXX_FLAGS=-Werror', '-DSTAN_THREADS=ON']) @@ -269,7 +284,7 @@ up the autoformatter locally. (Check console output at ${env.BUILD_URL}) dist: { runPod(image: image, cpus: 6, memory: '128Gi') { stage('Distribution tests') { - def command = 'python3 test/cmake_ci.py changed-distribution-filter' + def command = 'python3 test/cmake.py changed-distribution-filter' if (params.runAllDistributions || params.run_all || mainBranch) { command += ' --all' } @@ -280,7 +295,7 @@ up the autoformatter locally. (Check console output at ${env.BUILD_URL}) if (params.withRowVector || mainBranch) { flags += ['-DSTAN_TEST_ROW_VECTORS=ON', '-DSTAN_PROB_TEST_ALL=ON'] } - runCmakeTests(buildDir: 'build-ci-distributions', filter: filter, + runCmakeTests(name: 'distributions', filter: filter, optimization: '0', flags: flags) } else { echo 'No distribution tests affected by this change.' @@ -306,8 +321,8 @@ up the autoformatter locally. (Check console output at ${env.BUILD_URL}) runPod(image: image, cpus: 2) { stage('Upload doxygen') { def branch = 'gh-pages' - sh 'cmake -S . -B build-ci-docs -DBUILD_TESTING=OFF -DSTAN_BUILD_DOCS=ON' - sh 'cmake --build build-ci-docs --parallel 6 --target doxygen' + configureCmake(['-DBUILD_TESTING=OFF', '-DSTAN_BUILD_DOCS=ON']) + sh "cmake --build build --parallel ${env.PARALLEL ?: '1'} --target doxygen" dir('gh-pages') { def scm = scmGit( branches: [[name: "refs/heads/$branch"]], diff --git a/README.md b/README.md index 11b4c899842..a90c1c6e0d5 100644 --- a/README.md +++ b/README.md @@ -1,126 +1,453 @@ -
- -The Stan Math Library is a C++, reverse-mode automatic -differentiation library designed to be usable, extensive and -extensible, efficient, scalable, stable, portable, and redistributable -in order to facilitate the construction and utilization of algorithms -that utilize derivatives. - - -Documentation, Installation, and Examples --------------- - -All of Stan math's documentation is hosted on our website below. Please do not -reference articles in the wiki as they are outdated and not maintained. - -[mc-stan.org/math](https://mc-stan.org/math/) - - -Licensing ---------- -The Stan Math Library is licensed under the [new BSD -license](https://github.com/stan-dev/math/blob/develop/LICENSE%2Emd). - -The Stan Math Library depends on the Intel TBB library which is -licensed under the Apache 2.0 license. This dependency implies an -additional restriction as compared to the new BSD license alone. The -Apache 2.0 license is incompatible with GPL-2 licensed code if -distributed as a unitary binary. You may refer to the Licensing page on the [Stan wiki](https://github.com/stan-dev/stan/wiki/Stan-Licensing). +
+ +The **Stan Math Library** is a C++ library for reverse-mode automatic +differentiation. Use Stan Math to build algorithms requiring derivatives. + + +Documentation, Installation, and Examples +-------------- + +Read the Stan Math documentation at mc-stan.org. +The wiki articles are outdated. Stan Math developers do not maintain wiki articles. + +[mc-stan.org/math](https://mc-stan.org/math/) + + +Licensing +--------- +The Stan Math Library is licensed under the [new BSD +license](https://github.com/stan-dev/math/blob/develop/LICENSE%2Emd). + +Stan Math depends on Intel TBB. TBB uses the Apache 2.0 license. +The Apache 2.0 license adds restrictions to the BSD license terms. +The Apache 2.0 license is incompatible with GPL-2 licensed code in a combined binary. See the [Licensing page](https://github.com/stan-dev/stan/wiki/Stan-Licensing) for details. Building tests with CMake ------------------------- -`runTests.py` uses CMake by default. A directory builds and runs one executable -containing all its tests; a source file builds and runs its individual executable: +Stan Math has recently used to using cmake as its primary build system. +CMake creates build rules for a selected build tool while CTest is CMake's test runner. + +`runTests.py` is the main way to compile the tests for Stan Math and it now uses cmake on the backend. +`runTests.py` configures CMake, builds the selected executables, and runs the tests. + +```sh +# Builds a single executable for the whole `fun` directory +./runTests.py test/unit/math/prim/fun +# Builds a single executable for the `sum_test.cpp` file +./runTests.py test/unit/math/prim/fun/sum_test.cpp +``` + +Some useful options for `runTests.py` include: + +- `--fresh` resets cached CMake settings before configuration. + +CMake saves persistent build settings in a cache. +The default cache file is `build/CMakeCache.txt`. +Cached settings can override later changes to `make/local`. +`--fresh` resets the CMake configuration before building. +`--fresh` passes CMake's native `--fresh` option during configuration. +CMake removes `CMakeCache.txt` and the top-level `CMakeFiles` directory. +CMake then configures the build with the current arguments and `make/local` settings. +Use `--fresh` when cached settings no longer match the intended build. +Pass required custom settings again because `--fresh` removes cached overrides. +`--fresh` requires CMake 3.24 or newer. +`--test-only` runs an existing executable without configuration or compilation. +`--fresh` cannot be combined with `--test-only`. + +```sh +# Reset cached settings, then build and run the sum tests. +./runTests.py --fresh test/unit/math/prim/fun/sum_test.cpp +``` + +- `--build-dir PATH` selects the build directory. The default is `build`. +The build directory contains generated build rules, compiled files, and test results. ```sh -./runTests.py -j6 ./test/unit/math/prim/fun -./runTests.py -j6 test/unit/math/prim/fun/sum_test.cpp -./runTests.py -j6 --build-dir .build -f sum test/unit/math/prim/fun -./runTests.py -j6 --cmake-arg=-DSTAN_THREADS=ON test/unit/math/rev/core +# Store the build in `.build`. +./runTests.py --build-dir .build test/unit/math/prim/fun/sum_test.cpp ``` -The first command selects `unit_math_prim_fun_all_tests`. `-f` filters the sources -compiled into the directory executable, and repeated filters must all match. -`--make-only` builds without running; `--test-only` runs the executable already -built with the same selection. `--changed` selects individual changed tests. -`--run-all` continues after failures and still returns a failure status. -`--jumbo-test` is accepted for compatibility; CMake directory builds already -combine the tests without generating amalgamated source files. -Build jobs are capped at six. XML results are written under -`/test-results`, and `build`, `.build`, and their suffixed variants -are ignored. CMake options go in `--cmake-arg`; `make/local` is used only by -`--backend make`. The existing Windows/ARM GitHub jobs explicitly retain Make. +- `--config NAME` selects the build configuration such as `Debug` or `Release`. Setting a configuration changes the default behavior of the build system such as compiler flags and debug information. +The default is `Release`. + +```sh +# Use the `Release` configuration. +./runTests.py --config Release test/unit/math/prim/fun/sum_test.cpp +``` + +- `-j N` sets the number of parallel build jobs. The default is 1. + +```sh +# Build with four jobs. +./runTests.py -j4 test/unit/math/prim/fun +``` + +- `-f TEXT` selects source files whose paths contain `TEXT`. +A directory executable includes only source files matching the filter. +Repeat `-f` to require all filters to match. + +```sh +# Build and run primitive function tests with `sum` in the source paths. +./runTests.py -f sum test/unit/math/prim/fun +``` + +- `--make-only` builds the tests without running the test executables. + +```sh +# Build the executable for `sum_test.cpp`. +./runTests.py --make-only test/unit/math/prim/fun/sum_test.cpp +``` + +- `--test-only` runs an existing test executable. +`--test-only` skips configuration and compilation. +`--test-only` does not rebuild tests after source changes. + +```sh +# Build the primitive function test executable in `.build` +./runTests.py --make-only --build-dir .build test/unit/math/prim/fun +# run the executable. +./runTests.py --test-only --build-dir .build test/unit/math/prim/fun +``` -The CMake test setup is adapted from `feature/cmake-tests` at -`5d304f35b23d355de9cc5958cbdc0df2abedb45f`. It uses the vendored dependency -versions in `make/libraries` and needs no downloads. Configure and build out -of source, with at most six compile jobs: +- `--changed` selects tests for committed changes on your branch. +`--changed` compares your branch with `origin/develop`. +`--changed` selects changed test files and tests for changed Stan headers. +Each selected file builds and runs as a separate executable: + +```sh +./runTests.py --changed +``` + +- `--run-all` continues to the next executable if a test executable fails. +`runTests.py --run-all` still returns a failure status if any tests fail. +Build failures stop `runTests.py`. + +```sh +# Run the `abs` tests even if the `sum` tests fail. +./runTests.py --run-all test/unit/math/prim/fun/sum_test.cpp test/unit/math/prim/fun/abs_test.cpp +``` + +`` means the directory selected by `--build-dir`. +XML results are written under `/test-results` by default. +`make/local` is an optional file for local compiler and build settings. +The CMake test setup reads `make/local`. +All CMake generators use the imported settings, including Ninja and Unix Makefiles. +The Windows and ARM GitHub jobs use CMake. + +`--cmake-arg=ARGUMENT` passes one argument to CMake configuration. +Repeat `--cmake-arg` to pass multiple arguments. +The CMake argument `-DNAME=VALUE` sets a named cache setting. + +```sh +# Build reverse-mode core tests with threading enabled. +./runTests.py --cmake-arg=-DSTAN_THREADS=ON test/unit/math/rev/core +``` + +The CMake argument `-G NAME` selects a generator. +The combined form `-GNAME` selects the generator with one argument. + +```sh +# Build with Ninja and the settings from `make/local`. +./runTests.py --build-dir build-ninja --cmake-arg=-GNinja test/unit/math/prim/fun/sum_test.cpp +``` + +CMake imports the following Make settings: + +| Make setting | CMake setting | +| --- | --- | +| `CC`, `CXX` | C and C++ compilers | +| `O` | Optimization level from 0 to 3, mapped to `STAN_TEST_OPTIMIZATION` | +| `STAN_THREADS` | Enable threading support | +| `STAN_OPENCL` | Enable OpenCL GPU support | +| `STAN_MPI` | Enable MPI support for communication between processes | +| `STAN_NO_RANGE_CHECKS` | Disable range checks | +| `STAN_TEST_ROW_VECTORS` | Include row vectors in distribution tests | +| `STAN_PROB_TEST_ALL` | Enable all distribution test modes | +| `OPENCL_DEVICE_ID` | Select an OpenCL device with `STAN_OPENCL_DEVICE_ID` | +| `OPENCL_PLATFORM_ID` | Select an OpenCL platform with `STAN_OPENCL_PLATFORM_ID` | +| `INTEGRATED_OPENCL` | Enable integrated GPU settings with `STAN_INTEGRATED_OPENCL` | +| `BOOST`, `EIGEN`, `TBB`, `SUNDIALS`, `OPENCL`, `BENCHMARK` | Dependency source directories | +| `CXXFLAGS`, `CXXFLAGS_OPTIM`, `CXXFLAGS_WARNINGS` | Additional C++ compiler flags | +| `CFLAGS` | Additional C compiler flags | +| `CPPFLAGS`, `CPPFLAGS_OPTIM` | Additional C and C++ preprocessor flags | +| `LDFLAGS`, `LDFLAGS_OPTIM` | Additional linker flags | +| `LDLIBS` | Additional link libraries | + +CMake options passed with `-D` take priority over imported defaults. +Existing CMake cache values also take priority over imported defaults. +Use a new build directory after changing cached settings or compilers. +Use `--fresh` to discard cached settings in the selected build directory. +Flags from `make/local` are added to the CMake compiler and linker flags. +Changes to `make/local` and included Make files trigger CMake configuration. +Make settings outside the table are not imported. + +`CXX = clang++` selects the Clang C++ compiler. +`O = 0` disables compiler optimization. +`STAN_THREADS = true` enables threading support. +`CXXFLAGS += -g` adds compiler debug information. +Example: Set compiler settings in `make/local`. + +```make +CXX = clang++ +O = 0 +STAN_THREADS = true +CXXFLAGS += -g +``` + +Use `STAN_USE_MAKE_LOCAL=OFF` to ignore `make/local`: + +```sh +./runTests.py --cmake-arg=-DSTAN_USE_MAKE_LOCAL=OFF test/unit/math/prim/fun/sum_test.cpp +``` + +### Common CMake commands + +Run the commands from the repository root. +CMake configuration creates build rules and saves settings in `{BUILD_DIRECTORY}/CMakeCache.txt`. +Configuration does not build the test executables. +`cmake --build` compiles and links the test executables. +CTest runs test executables already built by CMake. +`runTests.py` configures, builds, and runs the selected tests automatically. + +#### Configure a build + +`-S PATH` selects the source directory. Use `.` for the repository root. +`-B PATH` selects the build directory. CMake creates the directory if needed. +`-DNAME=VALUE` sets a named cache setting. +`CMAKE_BUILD_TYPE` selects the configuration for Ninja and Unix Makefiles. +`Release` enables release compiler settings. +Direct CMake commands also read `make/local`. +Example: Configure a release build in `build`. ```sh cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -cmake --build build --parallel 6 -ctest --test-dir build --parallel 6 --output-on-failure -``` - -Each test source has its own executable and CTest entry. Recursive build -targets such as `unit_math_rev_core_subtests` and `prob_math_bernoulli_subtests` -build a directory's tests. Select the matching CTest entries with `-R`, for -example `ctest --test-dir build -R '^test_unit_math_rev_core_' --output-on-failure`. -The optional `*_all_tests` targets link a directory's sources together; these -are excluded from the default build and CTest to avoid repeated execution. -Each test source compiles once into a shared object target, reused by its -individual executable and all containing directory executables. Switching -between a file, directory, and ancestor directory does not recompile that -test source. Tests use the repository root as their working directory. - -Set `STAN_TEST_FILTER` to a regex matching repository-relative test source -paths to configure a smaller suite. For example: - -```sh -cmake -S . -B build-core -DCMAKE_BUILD_TYPE=Release \ - -DSTAN_BUILD_PROB_TESTS=OFF -DSTAN_TEST_HEADERS=OFF \ - -DSTAN_TEST_FILTER='test/unit/math/(prim|fwd|rev|mix)/core/' -cmake --build build-core --parallel 6 -ctest --test-dir build-core --parallel 6 --output-on-failure -``` - -`STAN_TEST_PCH` defaults to `ON`. Unit tests reuse a PCH for their autodiff -layer, or the AD helpers when the source already includes `test_ad.hpp`. -Grouped executables retain each source's PCH choice through shared objects. -Sources that change Stan or Eigen configuration macros skip PCH individually; -other sources in the same directory continue using their PCH. Umbrella and -multiple-translation-unit checks also compile without PCH. -Distribution tests reuse their fixture headers, without generated typedefs -in the PCH. Producers and consumers share compile flags and definitions; -GCC treats an invalid PCH as an error. Tests and vendored SUNDIALS share -`STAN_TEST_OPTIMIZATION` (default `1`, matching Make builds with `O=1`). Use -`-DSTAN_TEST_PCH=OFF` to check ordinary compilation. Standalone header -targets (`test-headers`, or a target -such as `stan_math_prim_fun_sum-test`) never use a PCH. - -Distribution sources are generated during the build under `build/test/prob`, -using the existing `test/prob/generate_tests.cpp`. `STAN_PROB_TEST_BATCH_SIZE` -defaults to 100 cases per source. Each fixture's generation runs once before -its consumers and reruns when the fixture or generator changes. - -`STAN_THREADS`, `STAN_OPENCL`, `STAN_MPI`, and `STAN_NO_RANGE_CHECKS` retain -their usual meanings. OpenCL and MPI tests are omitted when those features -are disabled. System dependencies can be selected with -`STAN_USE_SYSTEM_{BOOST,EIGEN,TBB,SUNDIALS}`; individual vendored roots can be -overridden with `STAN_{BOOST,EIGEN,TBB,SUNDIALS,OPENCL,BENCHMARK}_ROOT`. -The vendored TBB build currently supports GCC and Clang on Linux/macOS; -Windows builds require an installed TBB. MPI builds its Boost libraries from -the vendored sources, or uses installed libraries with `STAN_USE_SYSTEM_BOOST`. Set `BUILD_TESTING=OFF` to use the `stanmath` -interface target without configuring tests. - -Jenkins uses the CMake/CTest pipeline with separate build directories per -configuration and no more than six compile jobs per build. ASan, GPU, MPI, -threading, and expression test configurations retain their coverage. +``` + +The native CMake option `--fresh` resets the configuration cache. +`--fresh` requires CMake 3.24 or newer. +Example: Reset cached settings and configure a release build. + +```sh +cmake --fresh -S . -B build -DCMAKE_BUILD_TYPE=Release +``` + +#### List settings and descriptions + +`-N` reads the cache without configuring the project. +`-L` lists cache variables. +`H` in `-LH` or `-LAH` prints the description above each value. +`A` in `-LAH` includes advanced variables. +Advanced variables contain settings changed less often during normal builds. +Cache listings omit temporary variables and uncached `make/local` defaults. +Print the current cache values and option descriptions: + +```sh +cmake -N -LH build +``` + +Add `A` to include advanced cache settings: + +```sh +cmake -N -LAH build +``` + +To inspect project defaults, configure an empty build directory. +Disable `make/local` imports to exclude local overrides. +The `build-defaults` directory must not contain an existing CMake cache: + +```sh +cmake -S . -B build-defaults -DSTAN_USE_MAKE_LOCAL=OFF -LAH +``` + +Project option definitions contain the default values and descriptions. +Read [CMakeLists.txt](CMakeLists.txt) for the main options. +Read [cmake/CMakeLists.txt](cmake/CMakeLists.txt) for dependency options. +Read [cmake/MakeLocal.cmake](cmake/MakeLocal.cmake) for local settings options. +Read [test/prob/CMakeLists.txt](test/prob/CMakeLists.txt) for distribution options. + +`--help-variable-list` prints CMake's built-in variable names. +`--help-variable NAME` prints the definition of a built-in variable. +CMake's built-in help does not define Stan-specific options. +Use the cache descriptions or the project CMake files for Stan-specific options. +Example: List built-in variables and read the definition of `CMAKE_BUILD_TYPE`. + +```sh +cmake --help-variable-list +cmake --help-variable CMAKE_BUILD_TYPE +``` + +#### Change settings or select a generator + +Use `-DNAME=VALUE` to change a cached setting. +Example: Enable threading in `build`. + +```sh +cmake -S . -B build -DSTAN_THREADS=ON +``` + +Use `-U` to remove a named cache override. +Configuration then uses the `make/local` value or the project default. +Example: Remove the cached threading setting. + +```sh +cmake -S . -B build -U STAN_THREADS +``` + +Run `cmake --help` to list the available generators. +Use `-G` to select Ninja or Unix Makefiles. +Use a separate build directory when changing generators or compilers: + +```sh +cmake --help +cmake -S . -B build-ninja -G Ninja -DCMAKE_BUILD_TYPE=Release +cmake -S . -B build-make -G 'Unix Makefiles' -DCMAKE_BUILD_TYPE=Release +``` + +#### List targets and build tests + +A build target names an executable, library, or build task. +`--build PATH` builds targets from the selected build directory. +`--target NAME` selects a build target. +The `help` target lists available targets for Ninja and Unix Makefiles. +`test_unit_math_prim_fun_sum_test` builds the executable for `sum_test.cpp`. +`--parallel N` sets the number of parallel build jobs. +`--verbose` prints compiler and linker commands. +List build targets for Ninja or Unix Makefiles: + +```sh +cmake --build build --target help +``` + +Build one test executable with four parallel jobs: + +```sh +cmake --build build --parallel 4 --target test_unit_math_prim_fun_sum_test +``` + +Add `--verbose` to print compiler and linker commands: + +```sh +cmake --build build --parallel 4 --verbose --target test_unit_math_prim_fun_sum_test +``` + +#### List and run tests + +CTest does not build missing executables. +Build the selected test targets before running CTest. +The CTest option `--test-dir PATH` selects the build directory containing test registrations. +A regular expression is a pattern used to match text. +The CTest option `-N` lists registered tests without running the tests. +`-R PATTERN` selects test names matching a regular expression. +`^` matches the start of a name. `$` matches the end of a name. +`--output-on-failure` prints output from failing tests. +The CTest option `--parallel N` sets the number of parallel test jobs. +List registered tests without running the test executables: + +```sh +ctest --test-dir build -N +``` + +Run one registered test and print output on failure: + +```sh +ctest --test-dir build -R '^test_unit_math_prim_fun_sum_test$' --output-on-failure +``` + +Run all registered tests with four parallel jobs: + +```sh +ctest --test-dir build --parallel 4 --output-on-failure +``` + +See the [CMake command reference](https://cmake.org/cmake/help/latest/manual/cmake.1.html) +and [CTest command reference](https://cmake.org/cmake/help/latest/manual/ctest.1.html) +for more commands. + +Each test source has a separate executable and CTest entry. +Targets including `unit_math_rev_core_subtests` and `prob_math_bernoulli_subtests` +build tests in the selected directory and the selected directory's subdirectories. +Use `-R` to select matching CTest entries: + +```sh +ctest --test-dir build -R '^test_unit_math_rev_core_' --output-on-failure +``` + +The optional `*_all_tests` targets combine a directory's tests into one executable. +The default build and CTest exclude `*_all_tests` targets to prevent duplicate test runs. +Each test source compiles into one object file. +Individual test executables and directory executables reuse the compiled object files. +Switching between individual and directory executables does not recompile the test source. +The working directory for tests is the repository root. + +CMake creates targets for all enabled tests. +Select a directory target to build only the required test executables. +The `unit_math_prim_core_subtests` target builds primitive core test executables. +The CTest label `test/unit/math/prim/core/` selects primitive core tests. +The CTest option `-L PATTERN` selects labels matching a regular expression. + +```sh +cmake --build build --parallel 4 --target unit_math_prim_core_subtests +ctest --test-dir build -L '^test/unit/math/prim/core/' --output-on-failure +``` + +Precompiled headers (PCH) are enabled by default with `STAN_TEST_PCH=ON`. +Automatic differentiation (AD) computes derivatives through the Stan Math type system. +Unit tests reuse a PCH for the automatic differentiation layer under test. +Sources including `test_ad.hpp` use the AD helper PCH instead. +The PCH choice for each source also applies to directory executables. +Sources changing Stan or Eigen configuration macros skip PCH. +Sources without configuration macro changes continue to use PCH. +Umbrella header checks and tests with multiple translation units compile without PCH. + +Distribution tests precompile distribution fixture headers. +The distribution test PCH excludes generated typedefs. +PCH builds and test builds use identical compiler flags and definitions. +GCC reports an invalid PCH as an error. +Use `-DSTAN_TEST_PCH=OFF` to compile tests without PCH. +Standalone header targets never use PCH. +Examples include `test-headers` and `stan_math_prim_fun_sum-test`. + +Tests and the included SUNDIALS library use `STAN_TEST_OPTIMIZATION`. +`STAN_TEST_OPTIMIZATION` defaults to `1`. Optimization level `1` matches Make builds with `O=1`. + +The build generates distribution test sources under `/test/prob`. +The distribution test generator is `test/prob/generate_tests.cpp`. +`STAN_PROB_TEST_BATCH_SIZE` defaults to 100 cases per source. +The build generates distribution test sources once per fixture before compilation. +The build regenerates distribution test sources when a fixture or the generator changes. + +`STAN_THREADS`, `STAN_OPENCL`, `STAN_MPI`, and `STAN_NO_RANGE_CHECKS` work as before. +The build excludes OpenCL tests when `STAN_OPENCL=OFF`. +The build excludes MPI tests when `STAN_MPI=OFF`. +Use `STAN_USE_SYSTEM_{BOOST,EIGEN,TBB,SUNDIALS}` to select installed dependencies. +Use `STAN_{BOOST,EIGEN,TBB,SUNDIALS,OPENCL,BENCHMARK}_ROOT` to set dependency paths. +The included TBB source supports GCC and Clang builds on Linux and macOS. +MinGW and Rtools builds on Windows use the included TBB source. +MSVC builds require an installed TBB. +MPI builds use the included Boost sources. +Set `STAN_USE_SYSTEM_BOOST=ON` to use installed Boost libraries. +Set `BUILD_TESTING=OFF` to use the `stanmath` interface target without configuring tests. + +Jenkins uses CMake and CTest. Each worker reuses the `build` directory. +Jenkins passes `--fresh` when a configuration cache already exists. +Test reports are saved under `test-results` with a directory for each test stage. +`PARALLEL` sets the number of build jobs. +The pipeline includes ASan, GPU, MPI, threading, and expression tests. `STAN_TEST_ROW_VECTORS` and `STAN_PROB_TEST_ALL` enable the additional -distribution cases requested by Jenkins. Expression sources are prepared with -`python3 test/cmake_ci.py generate-expressions build-expressions`, then enabled -with `STAN_BUILD_EXPRESSION_TESTS=ON`. The runner also generates expression -sources when passed `test/expressions`, honoring `-e` and `--only-functions`. -Expression framework self-tests use the CMake runner. Var-matrix compatibility -compilation and linting retain Make. +distribution cases requested by Jenkins. +`test/cmake.py generate-expressions PATH` creates expression test sources in +`PATH/expression-sources`. +`STAN_BUILD_EXPRESSION_TESTS=ON` enables generated expression tests. +`runTests.py` also generates expression sources when you select `test/expressions`. +The runner option `-e N` sets the number of expression source files. +The runner option `--only-functions` selects functions for expression tests. +Example: Generate expression test sources in `build-expressions/expression-sources`. + +```sh +python3 test/cmake.py generate-expressions build-expressions +``` + +Expression framework self-tests use the CMake runner. +Var-matrix compatibility checks compile through CMake. +The Makefiles remain available for upstream packages. diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt index e1dac5a04d7..413564e59f9 100644 --- a/cmake/CMakeLists.txt +++ b/cmake/CMakeLists.txt @@ -7,7 +7,7 @@ foreach(line IN LISTS library_locations) if(line MATCHES "^([A-Z]+) .*lib/([^ ]+)") set(dependency "${CMAKE_MATCH_1}") set(vendored_root "${PROJECT_SOURCE_DIR}/lib/${CMAKE_MATCH_2}") - set(STAN_${dependency}_ROOT "" CACHE PATH "Override ${dependency} source directory (empty: vendored)") + stan_cache_default(STAN_${dependency}_ROOT "" PATH "Override ${dependency} source directory (empty: vendored)") if(NOT STAN_${dependency}_ROOT) set(STAN_${dependency}_ROOT "${vendored_root}") endif() @@ -40,11 +40,19 @@ else() # Vendored TBB predates oneTBB's CMake build. One nested Make job keeps # dependency compilation within the outer build's parallelism limit. include(ExternalProject) - find_program(STAN_TBB_MAKE NAMES gmake make) + find_program(STAN_TBB_MAKE NAMES gmake make mingw32-make) if(NOT STAN_TBB_MAKE) message(FATAL_ERROR "Building vendored TBB requires GNU Make") endif() - if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + set(tbb_extra_args "") + set(tbb_extra_cxx_flags "") + if(WIN32 AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + set(tbb_compiler gcc) + if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(ARM64|arm64|aarch64)$") + list(APPEND tbb_extra_args WINARM64=true) + set(tbb_extra_cxx_flags "-DTBB_USE_GCC_BUILTINS") + endif() + elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang") set(tbb_compiler clang) elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") set(tbb_compiler gcc) @@ -54,7 +62,7 @@ else() if(APPLE) set(tbb_library "${CMAKE_CURRENT_BINARY_DIR}/tbb/libtbb.dylib") elseif(WIN32) - message(FATAL_ERROR "Use STAN_USE_SYSTEM_TBB on Windows") + set(tbb_library "${CMAKE_CURRENT_BINARY_DIR}/tbb/tbb.dll") else() set(tbb_library "${CMAKE_CURRENT_BINARY_DIR}/tbb/libtbb.so.2") endif() @@ -65,8 +73,9 @@ else() CONFIGURE_COMMAND "" BUILD_COMMAND ${STAN_TBB_MAKE} -j1 -r -f "${STAN_TBB_ROOT}/build/Makefile.tbb" "tbb_root=${STAN_TBB_ROOT}" compiler=${tbb_compiler} cfg=release stdver=c++17 - "CXX=${CMAKE_CXX_COMPILER}" "CC=${CMAKE_C_COMPILER}" - "CXXFLAGS=-Wno-deprecated-declarations -Wno-deprecated-copy" + "CPLUS=${CMAKE_CXX_COMPILER}" "CONLY=${CMAKE_C_COMPILER}" + "CXXFLAGS=-Wno-deprecated-declarations -Wno-deprecated-copy ${STAN_MAKE_LOCAL_CPP_FLAGS} ${STAN_MAKE_LOCAL_CXX_FLAGS} ${tbb_extra_cxx_flags}" + ${tbb_extra_args} BUILD_BYPRODUCTS "${tbb_library}" BUILD_ALWAYS TRUE INSTALL_COMMAND "") @@ -74,6 +83,9 @@ else() set_target_properties(TBB::tbb PROPERTIES IMPORTED_LOCATION "${tbb_library}" INTERFACE_INCLUDE_DIRECTORIES "${STAN_TBB_ROOT}/include") + if(WIN32) + set_property(TARGET TBB::tbb PROPERTY IMPORTED_IMPLIB "${tbb_library}") + endif() add_dependencies(TBB::tbb stan_tbb_build) endif() target_link_libraries(stanmath INTERFACE TBB::tbb) @@ -120,4 +132,7 @@ if(BUILD_TESTING) set(INSTALL_GTEST OFF CACHE BOOL "Install Google Test") add_subdirectory("${STAN_BENCHMARK_ROOT}/googletest" "${CMAKE_CURRENT_BINARY_DIR}/googletest" EXCLUDE_FROM_ALL) + if(WIN32) + target_compile_definitions(gtest PUBLIC GTEST_HAS_PTHREAD=0) + endif() endif() diff --git a/cmake/MakeLocal.cmake b/cmake/MakeLocal.cmake new file mode 100644 index 00000000000..3190411533f --- /dev/null +++ b/cmake/MakeLocal.cmake @@ -0,0 +1,47 @@ +option(STAN_USE_MAKE_LOCAL "Read supported settings from make/local" ON) +set(STAN_MAKE_LOCAL "${CMAKE_SOURCE_DIR}/make/local" CACHE FILEPATH "Local Make settings file") + +macro(stan_cache_default name value type description) + if(NOT DEFINED ${name}) + set(${name} "${value}" CACHE ${type} "${description}") + endif() +endmacro() + +if(STAN_USE_MAKE_LOCAL) + file(GLOB local_settings CONFIGURE_DEPENDS "${STAN_MAKE_LOCAL}") + if(local_settings) + find_package(Python3 REQUIRED COMPONENTS Interpreter) + find_program(STAN_SETTINGS_MAKE NAMES gmake make mingw32-make) + if(NOT STAN_SETTINGS_MAKE) + message(FATAL_ERROR "Reading make/local requires GNU Make. Use STAN_USE_MAKE_LOCAL=OFF to ignore make/local.") + endif() + execute_process( + COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_LIST_DIR}/make_local.py" + --make "${STAN_SETTINGS_MAKE}" --source "${CMAKE_SOURCE_DIR}" + --local "${STAN_MAKE_LOCAL}" --output "${CMAKE_BINARY_DIR}/make-local.cmake" + RESULT_VARIABLE settings_status + ERROR_VARIABLE settings_error) + if(NOT settings_status EQUAL 0) + message(FATAL_ERROR "${settings_error}") + endif() + include("${CMAKE_BINARY_DIR}/make-local.cmake") + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS + "${CMAKE_CURRENT_LIST_DIR}/make_local.py") + message(STATUS "Loaded Make settings from ${STAN_MAKE_LOCAL}") + endif() +endif() + +function(stan_apply_make_local_flags) + foreach(language C CXX) + separate_arguments(flags NATIVE_COMMAND "${STAN_MAKE_LOCAL_${language}_FLAGS}") + foreach(flag IN LISTS flags) + add_compile_options("$<$:${flag}>") + endforeach() + endforeach() + separate_arguments(flags NATIVE_COMMAND "${STAN_MAKE_LOCAL_CPP_FLAGS}") + add_compile_options(${flags}) + separate_arguments(flags NATIVE_COMMAND "${STAN_MAKE_LOCAL_LINK_FLAGS}") + add_link_options(${flags}) + separate_arguments(libraries NATIVE_COMMAND "${STAN_MAKE_LOCAL_LIBRARIES_FLAGS}") + link_libraries(${libraries}) +endfunction() diff --git a/cmake/make_local.py b/cmake/make_local.py new file mode 100644 index 00000000000..2af6bde2b54 --- /dev/null +++ b/cmake/make_local.py @@ -0,0 +1,120 @@ +"""Evaluate supported make/local settings and export CMake defaults.""" + +import argparse +import os +from pathlib import Path +import re +import shlex +import subprocess + + +SETTINGS = { + "CC": "CMAKE_C_COMPILER", + "CXX": "CMAKE_CXX_COMPILER", + "O": "STAN_TEST_OPTIMIZATION", + "OPENCL_DEVICE_ID": "STAN_OPENCL_DEVICE_ID", + "OPENCL_PLATFORM_ID": "STAN_OPENCL_PLATFORM_ID", + **{name: name for name in ( + "STAN_THREADS", "STAN_OPENCL", "STAN_MPI", "STAN_NO_RANGE_CHECKS", + "STAN_TEST_ROW_VECTORS", "STAN_PROB_TEST_ALL")}, + **{name: "STAN_" + name + "_ROOT" for name in ( + "BOOST", "EIGEN", "TBB", "SUNDIALS", "OPENCL", "BENCHMARK")}, + "INTEGRATED_OPENCL": "STAN_INTEGRATED_OPENCL", +} +FLAGS = { + "CXX": ("CXXFLAGS", "CXXFLAGS_OPTIM", "CXXFLAGS_WARNINGS"), + "C": ("CFLAGS",), + "CPP": ("CPPFLAGS", "CPPFLAGS_OPTIM"), + "LINK": ("LDFLAGS", "LDFLAGS_OPTIM"), + "LIBRARIES": ("LDLIBS",), +} +FEATURES = {name for name in SETTINGS if name.startswith("STAN_")} + + +def bracket(value): + delimiter = "=" + while "]" + delimiter + "]" in value: + delimiter += "=" + return "[" + delimiter + "[" + value + "]" + delimiter + "]" + + +def read_local(make, source, local): + names = list(SETTINGS) + [name for group in FLAGS.values() for name in group] + escaped_local = str(local).replace("\\", "/").replace(" ", "\\ ").replace("#", "\\#").replace("$", "$$") + makefile = "include " + escaped_local + "\n" + for name in names: + makefile += (f"$(info __STAN_CMAKE_BEGIN_{name}__)\n" + f"$(info $(if $(filter file override,$(origin {name})),$({name})))\n" + f"$(info __STAN_CMAKE_END_{name}__)\n") + makefile += ("$(info __STAN_CMAKE_BEGIN_FILES__)\n$(info $(MAKEFILE_LIST))\n" + "$(info __STAN_CMAKE_END_FILES__)\n" + ".PHONY: stan-cmake-settings\nstan-cmake-settings:\n") + environment = dict(os.environ) + for name in ("MAKEFLAGS", "MFLAGS", "GNUMAKEFLAGS", "MAKELEVEL"): + environment.pop(name, None) + result = subprocess.run([make, "--no-print-directory", "-s", "-f", "-", + "stan-cmake-settings"], cwd=source, env=environment, + input=makefile, text=True, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + if result.returncode: + raise ValueError(f"Cannot evaluate {local}:\n{result.stderr}") + values = {} + for name in names + ["FILES"]: + match = re.search(f"__STAN_CMAKE_BEGIN_{name}__\n(.*?)\n__STAN_CMAKE_END_{name}__", + result.stdout, re.DOTALL) + if not match: + raise ValueError(f"Missing GNU Make output for {name} in {local}") + values[name] = match.group(1).strip() + return values + + +def cmake_settings(values, source): + output = [] + for name, target in SETTINGS.items(): + value = values[name] + if name in FEATURES or name == "INTEGRATED_OPENCL": + value = ("ON" if value else "OFF") if name in FEATURES else ("1" if value else "0") + elif not value: + continue + elif target.endswith("_ROOT"): + path = Path(value) + value = str((source / path).resolve()) + elif name in ("CC", "CXX"): + value = ";".join(shlex.split(value)) + output.append(f"if(NOT DEFINED {target})\n set({target} {bracket(value)})\nendif()\n") + for group, names in FLAGS.items(): + value = " ".join(values[name] for name in names if values[name]) + output.append(f"set(STAN_MAKE_LOCAL_{group}_FLAGS {bracket(value)})\n") + files = [] + words = values["FILES"].split() + index = 0 + while index < len(words): + for end in range(index + 1, len(words) + 1): + path = source / " ".join(words[index:end]) + if path.is_file(): + files.append(str(path.resolve())) + index = end - 1 + break + index += 1 + output.append("set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS\n") + output.extend(" " + bracket(path) + "\n" for path in files) + output.append(")\n") + return "".join(output) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--make", required=True) + parser.add_argument("--source", type=Path, required=True) + parser.add_argument("--local", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + try: + values = read_local(args.make, args.source, args.local) + args.output.write_text(cmake_settings(values, args.source)) + except ValueError as error: + parser.exit(1, str(error) + "\n") + + +if __name__ == "__main__": + main() diff --git a/cmake/stanc.cmake b/cmake/stanc.cmake new file mode 100644 index 00000000000..09583e4fda8 --- /dev/null +++ b/cmake/stanc.cmake @@ -0,0 +1,26 @@ +if(NOT EXISTS "${STAN_STANC_FILE}") + if(CMAKE_HOST_WIN32) + set(platform windows) + elseif(CMAKE_HOST_APPLE) + set(platform mac) + else() + set(platform linux) + endif() + find_program(curl_program NAMES curl) + if(NOT curl_program) + message(FATAL_ERROR "Downloading the expression test compiler requires curl") + endif() + execute_process(COMMAND "${curl_program}" -L --fail --retry 5 --retry-delay 10 + "https://github.com/stan-dev/stanc3/releases/download/nightly/${platform}-stanc" + -o "${STAN_STANC_FILE}" RESULT_VARIABLE status) + if(NOT status EQUAL 0) + file(REMOVE "${STAN_STANC_FILE}") + message(FATAL_ERROR "Downloading the expression test compiler failed") + endif() + if(NOT CMAKE_HOST_WIN32) + execute_process(COMMAND chmod +x "${STAN_STANC_FILE}" RESULT_VARIABLE status) + if(NOT status EQUAL 0) + message(FATAL_ERROR "Setting expression test compiler permissions failed") + endif() + endif() +endif() diff --git a/runTests.py b/runTests.py index d1747235a0a..4436e345b8a 100755 --- a/runTests.py +++ b/runTests.py @@ -1,75 +1,17 @@ #!/usr/bin/env python3 +"""Build and run Stan Math tests with CMake.""" -""" -Build and run Stan Math tests with CMake (or the legacy Make backend). - -Call script with '-h' as an option to see a helpful message. -""" - -from __future__ import print_function - -import glob import os -import os.path -import platform +from pathlib import Path import re import subprocess import sys import time from argparse import ArgumentParser, RawTextHelpFormatter -winsfx = ".exe" +maxChangedTests = 20 testsfx = "_test.cpp" -allowed_paths_with_jumbo = [ - "test/unit/math/prim/", - "test/unit/math/prim/constraint", - "test/unit/math/", - "test/unit/math/rev/", - "test/unit/math/rev/constraint", - "test/unit/math/fwd/", - "test/unit/math/fwd/constraint", - "test/unit/math/mix/", - "test/unit/math/mix/fun/", - "test/unit/math/opencl/", - "test/unit/", -] - -jumbo_folders = [ - "test/unit/math/prim/constraint", - "test/unit/math/prim/core", - "test/unit/math/prim/err", - "test/unit/math/prim/fun", - "test/unit/math/prim/functor", - "test/unit/math/prim/meta", - "test/unit/math/prim/prob", - "test/unit/math/rev/constraint", - "test/unit/math/rev/core", - "test/unit/math/rev/err", - "test/unit/math/rev/fun", - "test/unit/math/rev/functor", - "test/unit/math/rev/meta", - "test/unit/math/rev/prob", - "test/unit/math/fwd/constraint", - "test/unit/math/fwd/core", - "test/unit/math/fwd/fun", - "test/unit/math/fwd/functor", - "test/unit/math/fwd/meta", - "test/unit/math/fwd/prob", - "test/unit/math/mix/core", - "test/unit/math/mix/fun", - "test/unit/math/mix/functor", - "test/unit/math/mix/meta", - "test/unit/math/mix/prob", - "test/unit/math/opencl/device_functions", - "test/unit/math/opencl/kernel_generator", - "test/unit/math/opencl/prim", - "test/unit/math/opencl/prim/constraint", - "test/unit/math/opencl/rev", - "test/unit/math/opencl/rev/constraint", -] - - def processCLIArgs(): """ Define and process the command line interface to the runTests.py script. @@ -85,7 +27,7 @@ def processCLIArgs(): # Now define all the rules of the command line args and opts parser.add_argument( - "-j", metavar="N", type=int, default=1, help="parallel build jobs (maximum 6)" + "-j", metavar="N", type=int, default=1, help="parallel build jobs (default: 1)" ) parser.add_argument( @@ -140,16 +82,16 @@ def processCLIArgs(): "--jumbo-test", dest="do_jumbo", action="store_true", - help="Build/run jumbo tests.", + help="accepted for compatibility, directory tests already use one executable", ) tests_help_msg = "The path(s) to the test case(s) to run.\n" tests_help_msg += "Example: 'test/unit', 'test/prob', and/or\n" tests_help_msg += " 'test/unit/math/prim/fun/abs_test.cpp'" parser.add_argument("tests", nargs="*", type=str, help=tests_help_msg) - parser.add_argument("--backend", choices=("cmake", "make"), default="cmake", - help="build backend (default: cmake)") parser.add_argument("--build-dir", default="build", help="CMake build directory") + parser.add_argument("--fresh", action="store_true", + help="reset the CMake cache before configuring (requires CMake 3.24 or newer)") parser.add_argument("--cmake-arg", action="append", default=[], help="extra configure argument, e.g. --cmake-arg=-DSTAN_THREADS=ON") parser.add_argument("--config", default="Release", help="CMake build configuration") @@ -157,9 +99,10 @@ def processCLIArgs(): inputs = parser.parse_args() if inputs.j < 1: parser.error("-j must be positive") - inputs.j = min(inputs.j, 6) if inputs.make_only and inputs.test_only: parser.error("--make-only and --test-only cannot be combined") + if inputs.fresh and inputs.test_only: + parser.error("--fresh cannot be combined with --test-only") return inputs @@ -170,176 +113,10 @@ def stopErr(msg, returncode): sys.exit(returncode) -def isWin(): - return platform.system().lower().startswith( - "windows" - ) or os.name.lower().startswith("windows") - - -batchSize = 20 if isWin() else 200 -jumboSize = 5 if isWin() else 12 -maxChangedTests = 20 - - -def mungeName(name): - """Set up the makefile target name""" - if name.endswith(testsfx): - name = name.replace(testsfx, "_test") - - if isWin(): - name += winsfx - name = name.replace("\\", "/") - return name - - -def doCommand(command, exit_on_failure=True): - """Run command as a shell command and report/exit on errors.""" - print("------------------------------------------------------------") - print("%s" % command) - p1 = subprocess.Popen(command, shell=True) - p1.wait() - if exit_on_failure and (not (p1.returncode is None) and not (p1.returncode == 0)): - stopErr("%s failed" % command, p1.returncode) - - -def generateTests(j): - """Generate all tests and pass along the j parameter to make.""" - doCommand("make -j%d generate-tests -s" % (j or 1)) - - -def divide_chunks(l, n): - # looping till length l - for i in range(0, len(l), n): - yield l[i : i + n] - -include_pattern = re.compile(r'^\s*#include\s+<.*>$') - -def generateJumboTests(paths, debug=False): - jumbo_files_to_create = [] - jumbo_files = [] - for p in paths: - if not p.endswith(testsfx) and not p.endswith("/"): - p = p + "/" - if p in allowed_paths_with_jumbo: - jumbo_files_to_create.extend([x for x in jumbo_folders if x.startswith(p)]) - else: - stopErr("The --jumbo flag is only allowed with top level folders.", 10) - for jf in jumbo_files_to_create: - tests_in_subfolder = sorted(os.path.join(jf, x) for x in os.listdir(jf) if x.endswith(testsfx)) - for i, tests in enumerate(divide_chunks(tests_in_subfolder, jumboSize)): - - includes = [] - jumbo_contents = [] - for t in tests: - with open(t, "r") as test_file: - contents_raw = test_file.read() - - test_contents = [] - for l in contents_raw.splitlines(): - if include_pattern.fullmatch(l.strip()): - includes.append(l.strip()) - continue - test_contents.append(l) - - jumbo_contents.append('\n'.join(test_contents)) - - jumbo_file_path = jf + "_" + str(i) + testsfx - jumbo_files.append(jumbo_file_path) - if debug: - print("Generating jumbo test file '{}' with tests: {}".format( jumbo_file_path, ', '.join(tests))) - with open(jumbo_file_path, "w") as jumbo_file: - jumbo_file.write('\n'.join(list(dict.fromkeys(includes)))) - jumbo_file.write('\n') - jumbo_file.write('\n'.join(jumbo_contents)) - return jumbo_files - - -def cleanupJumboTests(paths): - for f in paths: - if os.path.exists(f): - os.remove(f) - - -def makeTest(name, j): - """Run the make command for a given single test.""" - doCommand("make -j%d %s" % (j or 1, name)) - - -def commandExists(command): - p = subprocess.Popen( - command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - p.wait() - return p.returncode != 127 - - -def runTest(name, run_all=False, mpi=False, j=1): - executable = mungeName(name).replace("/", os.sep) - xml = mungeName(name).replace(winsfx, "") - command = '%s --gtest_output="xml:%s.xml"' % (executable, xml) - if mpi: - if not commandExists("mpirun"): - stopErr( - "Error: need to have mpi (and mpirun) installed to run mpi tests", - -1, - ) - if "mpi_" in name: - j = j > 2 and j or 2 - else: - j = 1 - command = "mpirun -np {} {}".format(j, command) - doCommand(command, not run_all) - - -def test_files_in_folder(folder): - """Returns a list of test files (*_test.cpp) in the folder and all - its subfolders recursively. The folder can be written with - wildcards as with the Unix find command. - """ - files = [] - for f in glob.glob(folder): - if os.path.isdir(f): - files.extend(test_files_in_folder(f + os.sep + "**")) - else: - if f.endswith(testsfx): - files.append(f) - return files - - -def findTests(base_path, filter_names, do_jumbo=False): - tests = [] - for path in base_path: - if (not os.path.isdir(path)) and path.endswith("_test"): - tests.append(path + ".cpp") - else: - tests.extend(test_files_in_folder(path)) - tests = map(mungeName, tests) - tests = [ - test - for test in tests - if all(filter_name in test for filter_name in filter_names) - ] - if do_jumbo: - filtered_jumbo_tests = [] - for t in tests: - add = True - for k in jumbo_folders: - k = k + "/" - if t.startswith(k): - add = False - break - if add: - filtered_jumbo_tests.append(t) - tests = filtered_jumbo_tests - return tests - - def findChangedTests(debug): - import subprocess - changed_files = subprocess.run( ["git", "diff", "--name-only", "--diff-filter=d", "origin/develop...HEAD"], - text=True, capture_output=True + text=True, capture_output=True, check=True, cwd=Path(__file__).resolve().parent ).stdout.splitlines() if debug: print("Changed files:", changed_files) @@ -364,123 +141,19 @@ def findChangedTests(debug): if len(changed_tests) > maxChangedTests: stopErr("Number of changed tests excluded maximum, not running priority tests", 0) - return list(map(mungeName, changed_tests)) - - -def batched(tests): - return [tests[i : i + batchSize] for i in range(0, len(tests), batchSize)] - - -def handleExpressionTests(tests, only_functions, n_test_files): - expression_tests = False - for n, i in list(enumerate(tests))[::-1]: - if "test/expressions" in i or "test\\expressions" in i: - del tests[n] - expression_tests = True - if expression_tests: - HERE = os.path.dirname(os.path.realpath(__file__)) - sys.path.append(os.path.join(HERE, "test")) - sys.path.append(os.path.join(HERE, "test/expressions")) - import generate_expression_tests - - generate_expression_tests.main(only_functions, n_test_files) - for i in range(n_test_files): - tests.append("test/expressions/tests%d_test.cpp" % i) - elif only_functions: - stopErr( - "--only-functions can only be specified if running expression tests (test/expressions)", - -1, - ) - - -def checkToolchainPathWindows(): - if isWin(): - p1 = subprocess.Popen( - "where.exe make", - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True, - ) - out, err = p1.communicate() - if re.search(r" |\(|\)", out): - stopErr( - "The RTools toolchain is installed in a path with spaces or bracket. Please reinstall to a valid path.", - -1, - ) - - -def runMake(inputs): - checkToolchainPathWindows() - - try: - with open("make/local") as f: - stan_mpi = "STAN_MPI" in f.read() - except IOError: - stan_mpi = False - - jumboFiles = [] - - if inputs.changed: - tests = findChangedTests(inputs.debug) - else: - if not inputs.test_only: - # pass 0: generate all auto-generated tests - if any("test/prob" in arg for arg in inputs.tests): - generateTests(inputs.j) - - if inputs.do_jumbo: - jumboFiles = generateJumboTests(inputs.tests) - if inputs.e == -1: - if inputs.j == 1: - num_expr_test_files = 1 - else: - num_expr_test_files = inputs.j * 4 - else: - num_expr_test_files = inputs.e - handleExpressionTests(inputs.tests, inputs.only_functions, num_expr_test_files) - - tests = findTests(inputs.tests, inputs.f, inputs.do_jumbo) - - if not tests: - if inputs.changed: - stopErr("No changed tests were found!", 0) - stopErr( - "No matching tests found. Check the path passed on the command line", -1 - ) - - try: - # pass 1: make test executables - if not inputs.test_only: - for batch in batched(tests): - if inputs.debug: - print("Test batch: ", batch) - makeTest(" ".join(batch), inputs.j) - if not inputs.make_only: - # pass 2: run test targets - for t in tests: - if inputs.debug: - print("run single test: %s" % t) - runTest(t, inputs.run_all, mpi=stan_mpi, j=inputs.j) - except BaseException as e: - print(e, file=sys.stderr) - sys.exit(1) - finally: - cleanupJumboTests(jumboFiles) - pass + return sorted(changed_tests) def main(): inputs = processCLIArgs() - if inputs.backend == "make": - runMake(inputs) - return sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "test")) - from cmake_runner import run + from cmake import run try: sys.exit(run(inputs)) except (ValueError, OSError, subprocess.CalledProcessError) as error: stopErr(str(error), 1) + if __name__ == "__main__": main() diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 93fbb6fe09a..5da5d2cb840 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -56,6 +56,12 @@ function(stan_add_test_binary target) add_executable(${target} "${PROJECT_SOURCE_DIR}/cmake/pch.cpp" ${ARGN}) stan_configure_test(${target} "") set_property(TARGET ${target} PROPERTY STAN_TEST_OBJECTS "${ARGN}") + if(WIN32) + add_custom_command(TARGET ${target} POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_if_different + "$" "$" + VERBATIM) + endif() endfunction() # Directory and per-file executables share objects with per-source PCH choices. @@ -75,9 +81,6 @@ function(add_gtest_grouped_test test_directory folder_test_name target_pch) elseif(folder_test_name STREQUAL "expressions") set(repo_relative "test/expressions/${relative_source}") endif() - if(STAN_TEST_FILTER AND NOT repo_relative MATCHES "${STAN_TEST_FILTER}") - continue() - endif() set(matches_name_filters TRUE) foreach(filter IN LISTS STAN_TEST_NAME_FILTERS) string(FIND "${repo_relative}" "${filter}" match_index) @@ -85,9 +88,6 @@ function(add_gtest_grouped_test test_directory folder_test_name target_pch) set(matches_name_filters FALSE) endif() endforeach() - if(NOT matches_name_filters) - continue() - endif() if(NOT STAN_OPENCL AND repo_relative MATCHES "/opencl/") continue() endif() @@ -134,7 +134,11 @@ function(add_gtest_grouped_test test_directory folder_test_name target_pch) string(REPLACE "/" ";" labels "${relative_dir}") set_tests_properties(${name} PROPERTIES WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" - LABELS "${folder_test_name};${labels}") + LABELS "${folder_test_name};${labels};${repo_relative}") + + if(NOT matches_name_filters) + continue() + endif() set(key "${relative_dir}") while(TRUE) diff --git a/test/cmake.py b/test/cmake.py new file mode 100644 index 00000000000..5b727a5eaed --- /dev/null +++ b/test/cmake.py @@ -0,0 +1,596 @@ +#!/usr/bin/env python3 +"""CMake test runner, CI commands, and regression checks. + +Run regression checks with: python3 test/cmake.py self-test +""" + +import argparse +import glob +import json +import os +from pathlib import Path +import re +import shlex +import shutil +import subprocess +import sys +import tempfile +from types import SimpleNamespace +import unittest +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parent.parent +MODULE = sys.modules[__name__] + +def select_paths(arguments): + paths = set() + for argument in arguments: + matches = glob.glob(str(ROOT / argument)) + # Distribution sources are generated in the build tree, not the source tree. + if not matches and argument.replace("\\", "/").lstrip("./").startswith(("test/prob/", "test/expressions/")) and argument.endswith("_test.cpp"): + matches = [str(ROOT / argument)] + if not matches: + raise ValueError("No matching tests: " + argument) + for match in matches: + path = Path(match).resolve().relative_to(ROOT).as_posix() + if not (path == "test/unit" or path.startswith("test/unit/") + or path == "test/prob" or path.startswith("test/prob/") + or path == "test/expressions" or path.startswith("test/expressions/")): + raise ValueError("Unsupported test path: " + path) + if not (ROOT / path).is_dir() and not path.endswith("_test.cpp"): + raise ValueError("Expected a test directory or *_test.cpp: " + path) + paths.add(path) + # A parent directory already covers its descendants. + return [path for path in sorted(paths) + if not any(path.startswith(parent + "/") for parent in paths + if (ROOT / parent).is_dir())] + + +def target_for(path): + if path.endswith(".cpp"): + target = path[:-4].replace("/", "_") + else: + prefix = path.replace("test/unit/math", "unit_math", 1) + prefix = prefix.replace("test/unit", "unit", 1) + prefix = prefix.replace("test/prob", "prob_math", 1) + prefix = prefix.replace("test/expressions", "expressions", 1) + target = prefix.replace("/", "_") + "_all_tests" + folder = "test/unit" if path.startswith("test/unit") else ( + "test/prob" if path.startswith("test/prob") else "test") + return target, folder + + +def command(args): + print(" ".join(shlex.quote(str(arg)) for arg in args), flush=True) + subprocess.run(args, cwd=ROOT, check=True) + + +def read_cache(build_dir): + cache = {} + cache_file = build_dir / "CMakeCache.txt" + if cache_file.exists(): + for line in cache_file.read_text().splitlines(): + if not line.startswith(("//", "#")) and "=" in line and ":" in line.split("=", 1)[0]: + key, value = line.split("=", 1) + cache[key.split(":", 1)[0]] = value + return cache + + +def run(inputs): + arguments = inputs.tests + if inputs.changed: + from runTests import findChangedTests + arguments = findChangedTests(inputs.debug) + if not arguments: + print("No changed tests were found!") + return 0 + paths = select_paths(arguments) + if not paths: + raise ValueError("Specify a test directory or source file") + if inputs.f: + paths = [path for path in paths if not path.endswith(".cpp") + or all(value in path for value in inputs.f)] + if not paths: + raise ValueError("No matching tests found") + expressions = any(path.startswith("test/expressions") for path in paths) + if inputs.only_functions and not expressions: + raise ValueError("--only-functions requires test/expressions") + build_dir = Path(inputs.build_dir).resolve() + if not inputs.test_only: + if expressions: + generate_expressions(build_dir, inputs.only_functions, + inputs.e if inputs.e > 0 else inputs.j) + configure = ["cmake", "-S", str(ROOT), "-B", str(build_dir), + "-DCMAKE_BUILD_TYPE=" + inputs.config, "-DBUILD_TESTING=ON", + "-DSTAN_TEST_HEADERS=OFF", + "-DSTAN_BUILD_PROB_TESTS=" + ("ON" if any(path.startswith("test/prob") for path in paths) else "OFF"), + "-DSTAN_BUILD_EXPRESSION_TESTS=" + ("ON" if expressions else "OFF"), + "-DSTAN_TEST_NAME_FILTERS=" + ";".join(inputs.f)] + if expressions: + configure.append("-DSTAN_EXPRESSION_TEST_DIR=" + str(build_dir / "expression-sources")) + if inputs.fresh: + configure.append("--fresh") + command(configure + inputs.cmake_arg) + command(["cmake", "--build", str(build_dir), "--config", inputs.config, + "--parallel", str(inputs.j), "--target"] + [target_for(path)[0] for path in paths]) + if inputs.make_only: + return 0 + cache = read_cache(build_dir) + results = build_dir / "test-results" + results.mkdir(parents=True, exist_ok=True) + failed = False + for path in paths: + target, folder = target_for(path) + executable = build_dir / folder / target + if cache.get("CMAKE_CONFIGURATION_TYPES"): + executable = executable.parent / inputs.config / executable.name + if cache.get("CMAKE_SYSTEM_NAME") == "Windows" or os.name == "nt": + executable = executable.with_suffix(".exe") + args = [str(executable), "--gtest_output=xml:" + str(results / (target + ".xml"))] + if cache.get("STAN_EFFECTIVE_MPI", cache.get("STAN_MPI")) in ("ON", "TRUE", "1"): + # Grouped MPI suites can contain collective tests requiring two ranks. + ranks = "2" if not path.endswith(".cpp") or "mpi_" in path else "1" + args = [cache.get("MPIEXEC_EXECUTABLE", "mpirun"), + cache.get("MPIEXEC_NUMPROC_FLAG", "-np"), ranks] + [ + flag for flag in cache.get("MPIEXEC_PREFLAGS", "").split(";") if flag] + args + [ + flag for flag in cache.get("MPIEXEC_POSTFLAGS", "").split(";") if flag] + try: + command(args) + except subprocess.CalledProcessError: + if not inputs.run_all: + raise + failed = True + return int(failed) + + +def changed_files(): + return subprocess.run( + ["git", "diff", "--name-only", "--diff-filter=d", "origin/develop...HEAD"], + check=True, text=True, capture_output=True, + ).stdout.splitlines() + + +def source_filter(paths): + return "^(" + "|".join(re.escape(path) for path in sorted(set(paths))) + ")$" if paths else "" + + +def changed_units(): + # Preserve the existing runner's priority-test selection and size limit. + sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + from runTests import findChangedTests + + return source_filter(findChangedTests(False)) + + +def build_tests(build_dir, source_pattern, jobs): + inventory = subprocess.run(["ctest", "--test-dir", str(build_dir), "--show-only=json-v1"], + check=True, text=True, capture_output=True) + selected = [] + pattern = re.compile(source_pattern) + for test in json.loads(inventory.stdout)["tests"]: + labels = next((entry["value"] for entry in test.get("properties", []) + if entry["name"] == "LABELS"), []) + if any(pattern.search(label) for label in labels): + selected.append(test["name"]) + if not selected: + raise ValueError("No registered tests match: " + source_pattern) + batch_size = 50 if sys.platform == "win32" else 500 + for offset in range(0, len(selected), batch_size): + subprocess.run(["cmake", "--build", str(build_dir), "--parallel", str(jobs), + "--target"] + selected[offset:offset + batch_size], check=True) + + +def changed_distributions(files, run_all=False): + # Every distribution fixture includes the Stan umbrella headers. Library, + # build-system, and shared test-header changes can affect all distributions. + if run_all or any( + path.startswith(("stan/", "lib/", "make/", "cmake/")) + or path in ("CMakeLists.txt", "Jenkinsfile", "makefile") + or (path.startswith("test/") and path.endswith(".hpp") + and not re.fullmatch(r"test/prob/[^/]+/[^/]+_test\.hpp", path)) + or (path.startswith("test/prob/") and not path.endswith("_test.hpp")) + for path in files + ): + return "^test/prob/" + fixtures = [path for path in files + if re.fullmatch(r"test/prob/[^/]+/[^/]+_test\.hpp", path)] + return ("^(" + "|".join(re.escape(path[:-len("_test.hpp")]) + for path in sorted(fixtures)) + ")_[0-9]+_generated_" + if fixtures else "") + + +def generate_expressions(build_dir, functions, batches): + import generate_expression_tests + + output = Path(build_dir).resolve() / "expression-sources" + output.mkdir(parents=True, exist_ok=True) + for source in output.glob("*test.cpp"): + source.unlink() + generate_expression_tests.src_folder = str(output) + "/" + generate_expression_tests.main(functions, batches) + if not list(output.glob("*test.cpp")): + raise RuntimeError("Expression generator produced no tests") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + commands = parser.add_subparsers(dest="command", required=True) + commands.add_parser("self-test") + commands.add_parser("changed-unit-filter") + distributions = commands.add_parser("changed-distribution-filter") + distributions.add_argument("--all", action="store_true") + expressions = commands.add_parser("generate-expressions") + expressions.add_argument("build_dir") + expressions.add_argument("--functions", nargs="*", default=[]) + expressions.add_argument("--batches", type=int, default=6) + build = commands.add_parser("build-tests") + build.add_argument("build_dir") + build.add_argument("--source-regex", required=True) + build.add_argument("--jobs", type=int, default=1) + args = parser.parse_args() + if args.command == "self-test": + global runTests + sys.path.insert(0, str(ROOT)) + import runTests + unittest.main(argv=[sys.argv[0]]) + elif args.command == "changed-unit-filter": + print(changed_units()) + elif args.command == "changed-distribution-filter": + print(changed_distributions([] if args.all else changed_files(), args.all)) + elif args.command == "build-tests": + if args.jobs < 1: + parser.error("--jobs must be positive") + try: + build_tests(args.build_dir, args.source_regex, args.jobs) + except ValueError as error: + parser.error(str(error)) + else: + if args.batches < 1: + parser.error("--batches must be positive") + generate_expressions(args.build_dir, args.functions, args.batches) + + +class CMakeRunnerTest(unittest.TestCase): + def inputs(self, build_dir, **overrides): + values = dict(tests=["./test/unit/math/prim/fun"], changed=False, + debug=False, f=[], only_functions=[], build_dir=build_dir, + test_only=False, make_only=False, config="Release", e=-1, + j=6, cmake_arg=[], run_all=False, fresh=False) + values.update(overrides) + return SimpleNamespace(**values) + + def test_directory_builds_and_runs_one_group(self): + with tempfile.TemporaryDirectory() as build_dir, patch.object(MODULE, "command") as command: + self.assertEqual(run(self.inputs(build_dir)), 0) + configure, build, execute = [call.args[0] for call in command.call_args_list] + self.assertEqual(build[build.index("--target") + 1:], ["unit_math_prim_fun_all_tests"]) + self.assertEqual(Path(execute[0]).name, "unit_math_prim_fun_all_tests") + self.assertIn("test-results/unit_math_prim_fun_all_tests.xml", execute[1]) + self.assertNotIn("--fresh", configure) + + + @unittest.skipUnless(shutil.which("cmake"), "CMake is required") + def test_fresh_resets_cached_settings_and_preserves_build_files(self): + version = subprocess.run(["cmake", "--version"], check=True, + text=True, stdout=subprocess.PIPE).stdout + match = re.search(r"cmake version (\d+)\.(\d+)", version) + if tuple(map(int, match.groups())) < (3, 24): + self.skipTest("CMake --fresh requires CMake 3.24") + with tempfile.TemporaryDirectory() as temporary: + source = Path(temporary) / "source" + source.mkdir() + fixture = source / "test/unit/math/prim/fun/sum_test.cpp" + fixture.parent.mkdir(parents=True) + fixture.touch() + (source / "CMakeLists.txt").write_text(""" +cmake_minimum_required(VERSION 3.24) +project(fresh_fixture NONE) +option(STAN_THREADS "Thread support" OFF) +add_custom_target(test_unit_math_prim_fun_sum_test + COMMAND "${CMAKE_COMMAND}" -E touch "${CMAKE_BINARY_DIR}/built-marker") +""") + build = Path(temporary) / "build" + subprocess.run(["cmake", "-S", str(source), "-B", str(build), + "-DSTAN_THREADS=ON"], check=True, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + (build / "keep-marker").touch() + (build / "CMakeFiles/remove-marker").touch() + with patch.object(MODULE, "ROOT", source): + run(self.inputs(str(build), make_only=True, + tests=["test/unit/math/prim/fun/sum_test.cpp"])) + self.assertEqual(read_cache(build)["STAN_THREADS"], "ON") + with patch.object(sys, "argv", ["runTests.py", "--fresh", "--make-only", + "--build-dir", str(build), "test/unit/math/prim/fun/sum_test.cpp"]): + run(runTests.processCLIArgs()) + cache = read_cache(build) + self.assertEqual(cache["STAN_THREADS"], "OFF") + self.assertTrue((build / "keep-marker").exists()) + self.assertTrue((build / "built-marker").exists()) + self.assertFalse((build / "CMakeFiles/remove-marker").exists()) + + + def test_overlap_runs_only_parent(self): + self.assertEqual(select_paths([ + "test/unit/math/prim/fun/sum_test.cpp", "./test/unit/math/prim/fun", + "test/unit/math/prim", "test/unit/math/prim"]), ["test/unit/math/prim"]) + + def test_filters_do_not_turn_directory_into_individual_targets(self): + with tempfile.TemporaryDirectory() as build_dir, patch.object(MODULE, "command") as command: + run(self.inputs(build_dir, f=["sum", "prim"], make_only=True)) + self.assertIn("-DSTAN_TEST_NAME_FILTERS=sum;prim", command.call_args_list[0].args[0]) + self.assertEqual(command.call_args_list[1].args[0][-1], "unit_math_prim_fun_all_tests") + self.assertEqual(command.call_count, 2) + + def test_test_only_skips_build(self): + with tempfile.TemporaryDirectory() as build_dir, patch.object(MODULE, "command") as command: + run(self.inputs(build_dir, test_only=True)) + self.assertEqual(command.call_count, 1) + self.assertEqual(Path(command.call_args.args[0][0]).name, "unit_math_prim_fun_all_tests") + + def test_run_all_preserves_failure_and_continues(self): + with tempfile.TemporaryDirectory() as build_dir, patch.object(MODULE, "command") as command: + command.side_effect = [subprocess.CalledProcessError(1, "failed-test"), None] + result = run(self.inputs(build_dir, tests=[ + "test/unit/math/prim/fun/sum_test.cpp", + "test/unit/math/prim/fun/abs_test.cpp"], test_only=True, run_all=True)) + self.assertEqual(result, 1) + self.assertEqual(command.call_count, 2) + + def test_build_failure_stops_before_execution(self): + with tempfile.TemporaryDirectory() as build_dir, patch.object(MODULE, "command") as command: + command.side_effect = [None, subprocess.CalledProcessError(2, "build")] + with self.assertRaises(subprocess.CalledProcessError): + run(self.inputs(build_dir)) + self.assertEqual(command.call_count, 2) + + + def test_cli_jobs_reach_build_command(self): + for flags, jobs in (([], 1), (["-j20"], 20)): + with self.subTest(jobs=jobs), tempfile.TemporaryDirectory() as directory, patch.object( + sys, "argv", ["runTests.py", "--make-only", "--build-dir", directory] + flags + [ + "test/unit/math/prim/fun"]), patch.object(MODULE, "command") as command: + inputs = runTests.processCLIArgs() + self.assertFalse(inputs.fresh) + run(inputs) + build = command.call_args.args[0] + self.assertEqual(build[build.index("--parallel") + 1], str(jobs)) + + def test_cli_rejects_invalid_options(self): + for flags in (["-j0"], ["--fresh", "--test-only"], ["--backend", "make"]): + with self.subTest(flags=flags), patch.object(sys, "argv", ["runTests.py"] + flags + [ + "test/unit"]), patch.object(sys, "stderr"): + with self.assertRaises(SystemExit) as error: + runTests.processCLIArgs() + self.assertEqual(error.exception.code, 2) + + def test_invalid_path_rejected(self): + with self.assertRaises(ValueError): + select_paths(["stan/math"]) + + def test_filtered_file_selection_cannot_succeed_without_tests(self): + with self.assertRaises(ValueError): + run(self.inputs("build", tests=[ + "test/unit/math/prim/fun/sum_test.cpp"], f=["cholesky"], test_only=True)) + + def test_multi_configuration_executable_path(self): + with tempfile.TemporaryDirectory() as build_dir, patch.object(MODULE, "command") as command: + Path(build_dir, "CMakeCache.txt").write_text("CMAKE_CONFIGURATION_TYPES:STRING=Debug;Release\n") + run(self.inputs(build_dir, test_only=True, config="Debug")) + self.assertEqual(Path(command.call_args.args[0][0]), + Path(build_dir, "test/unit/Debug/unit_math_prim_fun_all_tests")) + + def test_effective_mpi_setting_controls_launch(self): + with tempfile.TemporaryDirectory() as build_dir, patch.object(MODULE, "command") as command: + Path(build_dir, "CMakeCache.txt").write_text( + "STAN_EFFECTIVE_MPI:INTERNAL=ON\nMPIEXEC_EXECUTABLE:FILEPATH=mpirun\n") + run(self.inputs(build_dir, test_only=True)) + self.assertEqual(command.call_args.args[0][:3], ["mpirun", "-np", "2"]) + + +class CmakeCISelectionTest(unittest.TestCase): + def test_unit_only_changes_skip_distributions(self): + self.assertEqual(changed_distributions( + ["test/unit/math/prim/fun/sum_test.cpp"]), "") + + def test_fixture_change_selects_all_modes_and_batches(self): + selected = changed_distributions( + ["test/prob/bernoulli/bernoulli_test.hpp"]) + for mode in ("v", "ffv", "vv", "fd", "fv", "ffd"): + for batch in ("00000", "00010"): + self.assertRegex( + f"test/prob/bernoulli/bernoulli_{batch}_generated_{mode}_test.cpp", selected) + self.assertIsNone(re.search(selected, + "test/prob/bernoulli/bernoulli_cdf_00000_generated_v_test.cpp")) + + def test_shared_dependencies_select_all_distributions(self): + for dependency in ("stan/math/prim/fun/sum.hpp", "lib/boost_1.87.0/boost/config.hpp", + "test/prob/utility.hpp", "test/unit/math/expect_near_rel.hpp", + "test/prob/generate_tests.cpp", "cmake/CMakeLists.txt", "Jenkinsfile"): + with self.subTest(dependency=dependency): + selected = changed_distributions([dependency]) + self.assertRegex("test/prob/bernoulli/bernoulli_00000_generated_v_test.cpp", selected) + self.assertRegex("test/prob/normal/normal_00000_generated_ffv_test.cpp", selected) + + + def test_build_selection_uses_registered_labels(self): + inventory = {"tests": [ + {"name": "test_unit_math_prim_fun_sum_test", "properties": [ + {"name": "LABELS", "value": ["prim", "test/unit/math/prim/fun/sum_test.cpp"]}]}, + {"name": "test_unit_math_rev_core_grad_test", "properties": [ + {"name": "LABELS", "value": ["rev", "test/unit/math/rev/core/grad_test.cpp"]}]}, + ]} + with patch.object(subprocess, "run") as command: + command.return_value = subprocess.CompletedProcess([], 0, json.dumps(inventory)) + build_tests("build", "^test/unit/math/prim/", 12) + self.assertEqual(command.call_args.args[0], [ + "cmake", "--build", "build", "--parallel", "12", "--target", + "test_unit_math_prim_fun_sum_test"]) + + def test_empty_build_selection_fails(self): + with patch.object(subprocess, "run") as command: + command.return_value = subprocess.CompletedProcess([], 0, '{"tests": []}') + with self.assertRaisesRegex(ValueError, "No registered tests match"): + build_tests("build", "^test/unit/", 1) + + +class CMakeBuildTest(unittest.TestCase): + def check_build(self, pch_enabled): + with tempfile.TemporaryDirectory(prefix="stan-cmake-build-") as temporary: + source = Path(temporary) / "source" + build = Path(temporary) / "build" + commands = Path(temporary) / "compile.jsonl" + + def write(path, text): + destination = source / path + destination.parent.mkdir(parents=True, exist_ok=True) + destination.write_text(text) + + def run(*args): + result = subprocess.run(args, text=True, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + self.assertEqual(result.returncode, 0, result.stdout) + + write("launcher.py", """import json, subprocess, sys +with open(sys.argv[1], 'a') as log: + log.write(json.dumps(sys.argv[2:]) + '\\n') +sys.exit(subprocess.call(sys.argv[2:])) +""") + write("CMakeLists.txt", f""" +cmake_minimum_required(VERSION 3.16.3) +project(stan_cmake_fixture LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 17) +set(STAN_TEST_PCH {'ON' if pch_enabled else 'OFF'}) +set(STAN_TEST_OPTIMIZATION_FLAGS -O1) +add_library(stanmath INTERFACE) +target_include_directories(stanmath INTERFACE "${{CMAKE_CURRENT_SOURCE_DIR}}") +add_library(gtest_main STATIC main.cpp) +enable_testing() +include("{(ROOT / 'test/CMakeLists.txt').as_posix()}") +""") + write("cmake/pch.cpp", "// Empty executable/PCH source.\n") + write("gtest/gtest.h", """#pragma once + +#include + +using test_callback = bool (*)(); +inline std::vector& test_callbacks() { + static std::vector callbacks; + return callbacks; +} +struct test_registration { + explicit test_registration(test_callback callback) { + test_callbacks().push_back(callback); + } +}; +""") + write("main.cpp", """#include +int main() { + for (auto callback : test_callbacks()) { + if (!callback()) return 1; + } + return 0; +} +""") + write("stan/math/prim.hpp", """#pragma once + +#ifdef STAN_PCH_TEST_SETTING +constexpr int configured_value = STAN_PCH_TEST_SETTING; +#else +constexpr int configured_value = 1; +#endif +""") + for layer in ("fwd", "rev", "mix"): + write(f"stan/math/{layer}.hpp", "#include \n") + write("stan/math/mix.hpp", "#include \nusing mix_only_alias = double;\n") + write("test/unit/math/test_ad.hpp", """#pragma once + +#include +#include + +using test_alias = double; +""") + write("unit/math/prim/fun/plain_test.cpp", """#include +#include + +using mix_only_alias = int; +using test_alias = int; +bool plain_test() { return configured_value == 1; } +test_registration plain_registration(plain_test); +""") + write("unit/math/prim/fun/ad_test.cpp", """#include +bool ad_test() { return configured_value == 1 && sizeof(test_alias) == sizeof(double); } +test_registration ad_registration(ad_test); +""") + write("unit/math/prim/fun/configuration_test.cpp", """#define STAN_PCH_TEST_SETTING 2 + +#include +#include + +static_assert(configured_value == 2, "Configuration must precede Stan headers"); +bool configuration_test() { return configured_value == 2; } +test_registration configuration_registration(configuration_test); +""") + write("generated_template.cpp", """#include +#include + +bool generated_test() { return configured_value == 1; } +test_registration generated_registration(generated_test); +""") + write("unit/CMakeLists.txt", """ +foreach(layer prim fwd rev mix) + stan_add_pch(unit_${layer}_pch ) +endforeach() +stan_add_pch(unit_ad_pch ) +add_gtest_grouped_test("${CMAKE_CURRENT_SOURCE_DIR}/math" unit_math unit_mix_pch) +set(generated "${PROJECT_BINARY_DIR}/generated/generated_test.cpp") +add_custom_command(OUTPUT "${generated}" + COMMAND "${CMAKE_COMMAND}" -E make_directory "${PROJECT_BINARY_DIR}/generated" + COMMAND "${CMAKE_COMMAND}" -E copy "${PROJECT_SOURCE_DIR}/generated_template.cpp" "${generated}" + DEPENDS "${PROJECT_SOURCE_DIR}/generated_template.cpp") +add_custom_target(generate_fixture DEPENDS "${generated}") +set_source_files_properties("${generated}" PROPERTIES STAN_GENERATOR_TARGET generate_fixture) +add_gtest_grouped_test("${PROJECT_BINARY_DIR}/generated" expressions unit_prim_pch "${generated}") +""") + launcher = f"{sys.executable};{source / 'launcher.py'};{commands}" + run("cmake", "-S", str(source), "-B", str(build), + "-DCMAKE_CXX_COMPILER_LAUNCHER=" + launcher) + run("cmake", "--build", str(build), "--parallel", "1", "--target", + "unit_math_prim_fun_all_tests", "expressions_all_tests") + run(str(build / "unit/unit_math_prim_fun_all_tests")) + run(str(build / "unit/expressions_all_tests")) + + compiled = [json.loads(line) for line in commands.read_text().splitlines()] + fixtures = {name: source / f"unit/math/prim/fun/{name}_test.cpp" + for name in ("plain", "ad", "configuration")} + fixtures["generated"] = build / "generated/generated_test.cpp" + expected_pch = {"plain": "unit_prim_pch", "ad": "unit_ad_pch", + "configuration": None, "generated": "unit_prim_pch"} + for name, path in fixtures.items(): + invocations = [args for args in compiled if str(path) in args] + self.assertEqual(len(invocations), 1, (name, invocations)) + pch = expected_pch[name] if pch_enabled else None + pch_arguments = [arg for arg in invocations[0] if "cmake_pch" in arg] + if pch: + self.assertTrue(any(pch in arg for arg in pch_arguments), invocations[0]) + else: + self.assertFalse(pch_arguments, invocations[0]) + + # Switching executables must not compile any test source again. + run("cmake", "--build", str(build), "--parallel", "1", "--target", + "test_unit_math_prim_fun_plain_test", "unit_math_all_tests", + "test_expressions_generated_test") + run(str(build / "unit/test_unit_math_prim_fun_plain_test")) + run(str(build / "unit/unit_math_all_tests")) + run(str(build / "unit/test_expressions_generated_test")) + after = [json.loads(line) for line in commands.read_text().splitlines()] + for name, path in fixtures.items(): + self.assertEqual(sum(str(path) in args for args in after), 1, name) + + def test_pch_profiles_generation_and_object_reuse(self): + for pch_enabled in (True, False): + with self.subTest(pch_enabled=pch_enabled): + self.check_build(pch_enabled) + + +if __name__ == "__main__": + main() diff --git a/test/cmake_build_test.py b/test/cmake_build_test.py deleted file mode 100644 index e6d9a0ed688..00000000000 --- a/test/cmake_build_test.py +++ /dev/null @@ -1,176 +0,0 @@ -"""Exercise the production CMake test helpers with small compile/link fixtures.""" - -import json -from pathlib import Path -import shutil -import subprocess -import sys -import tempfile -import unittest - -ROOT = Path(__file__).resolve().parent.parent - - -@unittest.skipUnless(shutil.which("cmake"), "CMake is required") -class CMakeBuildTest(unittest.TestCase): - def check_build(self, pch_enabled): - with tempfile.TemporaryDirectory(prefix="stan-cmake-build-") as temporary: - source = Path(temporary) / "source" - build = Path(temporary) / "build" - commands = Path(temporary) / "compile.jsonl" - - def write(path, text): - destination = source / path - destination.parent.mkdir(parents=True, exist_ok=True) - destination.write_text(text) - - def run(*args): - result = subprocess.run(args, text=True, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) - self.assertEqual(result.returncode, 0, result.stdout) - - write("launcher.py", """import json, subprocess, sys -with open(sys.argv[1], 'a') as log: - log.write(json.dumps(sys.argv[2:]) + '\\n') -sys.exit(subprocess.call(sys.argv[2:])) -""") - write("CMakeLists.txt", f""" -cmake_minimum_required(VERSION 3.16.3) -project(stan_cmake_fixture LANGUAGES CXX) -set(CMAKE_CXX_STANDARD 17) -set(STAN_TEST_PCH {'ON' if pch_enabled else 'OFF'}) -set(STAN_TEST_OPTIMIZATION_FLAGS -O1) -add_library(stanmath INTERFACE) -target_include_directories(stanmath INTERFACE "${{CMAKE_CURRENT_SOURCE_DIR}}") -add_library(gtest_main STATIC main.cpp) -enable_testing() -include("{(ROOT / 'test/CMakeLists.txt').as_posix()}") -""") - write("cmake/pch.cpp", "// Empty executable/PCH source.\n") - write("gtest/gtest.h", """#pragma once - -#include - -using test_callback = bool (*)(); -inline std::vector& test_callbacks() { - static std::vector callbacks; - return callbacks; -} -struct test_registration { - explicit test_registration(test_callback callback) { - test_callbacks().push_back(callback); - } -}; -""") - write("main.cpp", """#include -int main() { - for (auto callback : test_callbacks()) { - if (!callback()) return 1; - } - return 0; -} -""") - write("stan/math/prim.hpp", """#pragma once - -#ifdef STAN_PCH_TEST_SETTING -constexpr int configured_value = STAN_PCH_TEST_SETTING; -#else -constexpr int configured_value = 1; -#endif -""") - for layer in ("fwd", "rev", "mix"): - write(f"stan/math/{layer}.hpp", "#include \n") - write("stan/math/mix.hpp", "#include \nusing mix_only_alias = double;\n") - write("test/unit/math/test_ad.hpp", """#pragma once - -#include -#include - -using test_alias = double; -""") - write("unit/math/prim/fun/plain_test.cpp", """#include -#include - -using mix_only_alias = int; -using test_alias = int; -bool plain_test() { return configured_value == 1; } -test_registration plain_registration(plain_test); -""") - write("unit/math/prim/fun/ad_test.cpp", """#include -bool ad_test() { return configured_value == 1 && sizeof(test_alias) == sizeof(double); } -test_registration ad_registration(ad_test); -""") - write("unit/math/prim/fun/configuration_test.cpp", """#define STAN_PCH_TEST_SETTING 2 - -#include -#include - -static_assert(configured_value == 2, "Configuration must precede Stan headers"); -bool configuration_test() { return configured_value == 2; } -test_registration configuration_registration(configuration_test); -""") - write("generated_template.cpp", """#include -#include - -bool generated_test() { return configured_value == 1; } -test_registration generated_registration(generated_test); -""") - write("unit/CMakeLists.txt", """ -foreach(layer prim fwd rev mix) - stan_add_pch(unit_${layer}_pch ) -endforeach() -stan_add_pch(unit_ad_pch ) -add_gtest_grouped_test("${CMAKE_CURRENT_SOURCE_DIR}/math" unit_math unit_mix_pch) -set(generated "${PROJECT_BINARY_DIR}/generated/generated_test.cpp") -add_custom_command(OUTPUT "${generated}" - COMMAND "${CMAKE_COMMAND}" -E make_directory "${PROJECT_BINARY_DIR}/generated" - COMMAND "${CMAKE_COMMAND}" -E copy "${PROJECT_SOURCE_DIR}/generated_template.cpp" "${generated}" - DEPENDS "${PROJECT_SOURCE_DIR}/generated_template.cpp") -add_custom_target(generate_fixture DEPENDS "${generated}") -set_source_files_properties("${generated}" PROPERTIES STAN_GENERATOR_TARGET generate_fixture) -add_gtest_grouped_test("${PROJECT_BINARY_DIR}/generated" expressions unit_prim_pch "${generated}") -""") - launcher = f"{sys.executable};{source / 'launcher.py'};{commands}" - run("cmake", "-S", str(source), "-B", str(build), - "-DCMAKE_CXX_COMPILER_LAUNCHER=" + launcher) - run("cmake", "--build", str(build), "--parallel", "1", "--target", - "unit_math_prim_fun_all_tests", "expressions_all_tests") - run(str(build / "unit/unit_math_prim_fun_all_tests")) - run(str(build / "unit/expressions_all_tests")) - - compiled = [json.loads(line) for line in commands.read_text().splitlines()] - fixtures = {name: source / f"unit/math/prim/fun/{name}_test.cpp" - for name in ("plain", "ad", "configuration")} - fixtures["generated"] = build / "generated/generated_test.cpp" - expected_pch = {"plain": "unit_prim_pch", "ad": "unit_ad_pch", - "configuration": None, "generated": "unit_prim_pch"} - for name, path in fixtures.items(): - invocations = [args for args in compiled if str(path) in args] - self.assertEqual(len(invocations), 1, (name, invocations)) - pch = expected_pch[name] if pch_enabled else None - pch_arguments = [arg for arg in invocations[0] if "cmake_pch" in arg] - if pch: - self.assertTrue(any(pch in arg for arg in pch_arguments), invocations[0]) - else: - self.assertFalse(pch_arguments, invocations[0]) - - # Switching executables must not compile any test source again. - run("cmake", "--build", str(build), "--parallel", "1", "--target", - "test_unit_math_prim_fun_plain_test", "unit_math_all_tests", - "test_expressions_generated_test") - run(str(build / "unit/test_unit_math_prim_fun_plain_test")) - run(str(build / "unit/unit_math_all_tests")) - run(str(build / "unit/test_expressions_generated_test")) - after = [json.loads(line) for line in commands.read_text().splitlines()] - for name, path in fixtures.items(): - self.assertEqual(sum(str(path) in args for args in after), 1, name) - - def test_pch_profiles_opt_out_generation_and_object_reuse(self): - self.check_build(True) - - def test_without_pch_preserves_object_reuse(self): - self.check_build(False) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/cmake_ci.py b/test/cmake_ci.py deleted file mode 100644 index ac45effe600..00000000000 --- a/test/cmake_ci.py +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env python3 -"""Select CI tests and generate expression sources for CMake builds.""" - -import argparse -from pathlib import Path -import re -import subprocess -import sys - - -def changed_files(): - return subprocess.run( - ["git", "diff", "--name-only", "--diff-filter=d", "origin/develop...HEAD"], - check=True, text=True, capture_output=True, - ).stdout.splitlines() - - -def source_filter(paths): - return "^(" + "|".join(re.escape(path) for path in sorted(set(paths))) + ")$" if paths else "" - - -def changed_units(): - # Preserve the existing runner's priority-test selection and size limit. - sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) - from runTests import findChangedTests - - return source_filter([path + ".cpp" for path in findChangedTests(False)]) - - -def changed_distributions(files, run_all=False): - # Every distribution fixture includes the Stan umbrella headers. Library, - # build-system, and shared test-header changes can affect all distributions. - if run_all or any( - path.startswith(("stan/", "lib/", "make/", "cmake/")) - or path in ("CMakeLists.txt", "Jenkinsfile", "makefile") - or (path.startswith("test/") and path.endswith(".hpp") - and not re.fullmatch(r"test/prob/[^/]+/[^/]+_test\.hpp", path)) - or (path.startswith("test/prob/") and not path.endswith("_test.hpp")) - for path in files - ): - return "^test/prob/" - fixtures = [path for path in files - if re.fullmatch(r"test/prob/[^/]+/[^/]+_test\.hpp", path)] - return ("^(" + "|".join(re.escape(path[:-len("_test.hpp")]) - for path in sorted(fixtures)) + ")_[0-9]+_generated_" - if fixtures else "") - - -def generate_expressions(build_dir, functions, batches): - import generate_expression_tests - - output = Path(build_dir).resolve() / "expression-sources" - output.mkdir(parents=True, exist_ok=True) - for source in output.glob("*test.cpp"): - source.unlink() - generate_expression_tests.src_folder = str(output) + "/" - generate_expression_tests.main(functions, batches) - if not list(output.glob("*test.cpp")): - raise RuntimeError("Expression generator produced no tests") - - -def main(): - parser = argparse.ArgumentParser(description=__doc__) - commands = parser.add_subparsers(dest="command", required=True) - commands.add_parser("changed-unit-filter") - distributions = commands.add_parser("changed-distribution-filter") - distributions.add_argument("--all", action="store_true") - expressions = commands.add_parser("generate-expressions") - expressions.add_argument("build_dir") - expressions.add_argument("--functions", nargs="*", default=[]) - expressions.add_argument("--batches", type=int, default=6) - args = parser.parse_args() - if args.command == "changed-unit-filter": - print(changed_units()) - elif args.command == "changed-distribution-filter": - print(changed_distributions([] if args.all else changed_files(), args.all)) - else: - if args.batches < 1: - parser.error("--batches must be positive") - generate_expressions(args.build_dir, args.functions, args.batches) - - -if __name__ == "__main__": - main() diff --git a/test/cmake_ci_test.py b/test/cmake_ci_test.py deleted file mode 100644 index 471a732b459..00000000000 --- a/test/cmake_ci_test.py +++ /dev/null @@ -1,47 +0,0 @@ -import re -from pathlib import Path -import sys -import unittest -from unittest.mock import patch - -import cmake_ci -sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) - - -class CmakeCISelectionTest(unittest.TestCase): - def test_unit_only_changes_skip_distributions(self): - self.assertEqual(cmake_ci.changed_distributions( - ["test/unit/math/prim/fun/sum_test.cpp"]), "") - - def test_fixture_change_selects_all_modes_and_batches(self): - selected = cmake_ci.changed_distributions( - ["test/prob/bernoulli/bernoulli_test.hpp"]) - for mode in ("v", "ffv", "vv", "fd", "fv", "ffd"): - for batch in ("00000", "00010"): - self.assertRegex( - f"test/prob/bernoulli/bernoulli_{batch}_generated_{mode}_test.cpp", selected) - self.assertIsNone(re.search(selected, - "test/prob/bernoulli/bernoulli_cdf_00000_generated_v_test.cpp")) - - def test_shared_dependencies_select_all_distributions(self): - for dependency in ("stan/math/prim/fun/sum.hpp", "lib/boost_1.87.0/boost/config.hpp", - "test/prob/utility.hpp", "test/unit/math/expect_near_rel.hpp", - "test/prob/generate_tests.cpp", "cmake/CMakeLists.txt", "Jenkinsfile"): - with self.subTest(dependency=dependency): - selected = cmake_ci.changed_distributions([dependency]) - self.assertRegex("test/prob/bernoulli/bernoulli_00000_generated_v_test.cpp", selected) - self.assertRegex("test/prob/normal/normal_00000_generated_ffv_test.cpp", selected) - - def test_force_all_with_no_changed_files(self): - self.assertTrue(cmake_ci.changed_distributions([], run_all=True)) - - def test_changed_units_preserve_runner_selection(self): - with patch("runTests.findChangedTests", return_value=[ - "test/unit/math/prim/core/get_num_threads_test"]): - selected = cmake_ci.changed_units() - self.assertRegex("test/unit/math/prim/core/get_num_threads_test.cpp", selected) - self.assertIsNone(re.search(selected, "test/unit/math/prim/core/set_num_threads_test.cpp")) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/cmake_runner.py b/test/cmake_runner.py deleted file mode 100644 index 7ebffb49cbf..00000000000 --- a/test/cmake_runner.py +++ /dev/null @@ -1,146 +0,0 @@ -"""CMake backend for runTests.py; directories select one grouped executable.""" - -import glob -import os -from pathlib import Path -import re -import shlex -import subprocess - -ROOT = Path(__file__).resolve().parent.parent - - -def select_paths(arguments): - paths = set() - for argument in arguments: - matches = glob.glob(str(ROOT / argument)) - # Distribution sources are generated in the build tree, not the source tree. - if not matches and argument.replace("\\", "/").lstrip("./").startswith(("test/prob/", "test/expressions/")) and argument.endswith("_test.cpp"): - matches = [str(ROOT / argument)] - if not matches: - raise ValueError("No matching tests: " + argument) - for match in matches: - path = Path(match).resolve().relative_to(ROOT).as_posix() - if not (path == "test/unit" or path.startswith("test/unit/") - or path == "test/prob" or path.startswith("test/prob/") - or path == "test/expressions" or path.startswith("test/expressions/")): - raise ValueError("Unsupported test path: " + path) - if not (ROOT / path).is_dir() and not path.endswith("_test.cpp"): - raise ValueError("Expected a test directory or *_test.cpp: " + path) - paths.add(path) - # A parent directory already covers its descendants. - return [path for path in sorted(paths) - if not any(path.startswith(parent + "/") for parent in paths - if (ROOT / parent).is_dir())] - - -def target_for(path): - if path.endswith(".cpp"): - target = path[:-4].replace("/", "_") - else: - prefix = path.replace("test/unit/math", "unit_math", 1) - prefix = prefix.replace("test/unit", "unit", 1) - prefix = prefix.replace("test/prob", "prob_math", 1) - prefix = prefix.replace("test/expressions", "expressions", 1) - target = prefix.replace("/", "_") + "_all_tests" - folder = "test/unit" if path.startswith("test/unit") else ( - "test/prob" if path.startswith("test/prob") else "test") - return target, folder - - -def selection_filter(paths, filters): - parts = [] - for path in paths: - if path.endswith(".cpp"): - if all(value in path for value in filters): - parts.append(re.escape(path) + "$") - else: - parts.append(re.escape(path) + "/") - if not parts: - raise ValueError("No matching tests found") - return "^(" + "|".join(parts) + ")" - - -def command(args): - print(" ".join(shlex.quote(str(arg)) for arg in args), flush=True) - subprocess.run(args, cwd=ROOT, check=True) - - -def read_cache(build_dir): - cache = {} - cache_file = build_dir / "CMakeCache.txt" - if cache_file.exists(): - for line in cache_file.read_text().splitlines(): - if not line.startswith(("//", "#")) and "=" in line and ":" in line.split("=", 1)[0]: - key, value = line.split("=", 1) - cache[key.split(":", 1)[0]] = value - return cache - - -def run(inputs): - arguments = inputs.tests - if inputs.changed: - from runTests import findChangedTests - arguments = [re.sub(r"\.exe$", "", path) + ".cpp" for path in findChangedTests(inputs.debug)] - if not arguments: - print("No changed tests were found!") - return 0 - paths = select_paths(arguments) - if not paths: - raise ValueError("Specify a test directory or source file") - if inputs.f: - paths = [path for path in paths if not path.endswith(".cpp") - or all(value in path for value in inputs.f)] - if not paths: - raise ValueError("No matching tests found") - expressions = any(path.startswith("test/expressions") for path in paths) - if inputs.only_functions and not expressions: - raise ValueError("--only-functions requires test/expressions") - build_dir = Path(inputs.build_dir).resolve() - if not inputs.test_only: - if expressions: - from cmake_ci import generate_expressions - generate_expressions(build_dir, inputs.only_functions, - inputs.e if inputs.e > 0 else inputs.j) - configure = ["cmake", "-S", str(ROOT), "-B", str(build_dir), - "-DCMAKE_BUILD_TYPE=" + inputs.config, "-DBUILD_TESTING=ON", - "-DSTAN_TEST_HEADERS=OFF", - "-DSTAN_BUILD_PROB_TESTS=" + ("ON" if any(path.startswith("test/prob") for path in paths) else "OFF"), - "-DSTAN_BUILD_EXPRESSION_TESTS=" + ("ON" if expressions else "OFF"), - # Keep ordinary directory/file targets configured so switching - # selections preserves their generated build metadata. - "-DSTAN_TEST_FILTER=" + (selection_filter(paths, inputs.f) if inputs.f else ""), - "-DSTAN_TEST_NAME_FILTERS=" + ";".join(inputs.f)] - if expressions: - configure.append("-DSTAN_EXPRESSION_TEST_DIR=" + str(build_dir / "expression-sources")) - command(configure + inputs.cmake_arg) - command(["cmake", "--build", str(build_dir), "--config", inputs.config, - "--parallel", str(inputs.j), "--target"] + [target_for(path)[0] for path in paths]) - if inputs.make_only: - return 0 - cache = read_cache(build_dir) - results = build_dir / "test-results" - results.mkdir(parents=True, exist_ok=True) - failed = False - for path in paths: - target, folder = target_for(path) - executable = build_dir / folder / target - if cache.get("CMAKE_CONFIGURATION_TYPES"): - executable = executable.parent / inputs.config / executable.name - if cache.get("CMAKE_SYSTEM_NAME") == "Windows" or os.name == "nt": - executable = executable.with_suffix(".exe") - args = [str(executable), "--gtest_output=xml:" + str(results / (target + ".xml"))] - if cache.get("STAN_MPI") in ("ON", "TRUE", "1"): - # Grouped MPI suites can contain collective tests requiring two ranks. - ranks = "2" if not path.endswith(".cpp") or "mpi_" in path else "1" - args = [cache.get("MPIEXEC_EXECUTABLE", "mpirun"), - cache.get("MPIEXEC_NUMPROC_FLAG", "-np"), ranks] + [ - flag for flag in cache.get("MPIEXEC_PREFLAGS", "").split(";") if flag] + args + [ - flag for flag in cache.get("MPIEXEC_POSTFLAGS", "").split(";") if flag] - try: - command(args) - except subprocess.CalledProcessError: - if not inputs.run_all: - raise - failed = True - return int(failed) diff --git a/test/cmake_runner_test.py b/test/cmake_runner_test.py deleted file mode 100644 index 6e869bec401..00000000000 --- a/test/cmake_runner_test.py +++ /dev/null @@ -1,113 +0,0 @@ -"""Regression checks for directory grouping and runner failure handling.""" - -from pathlib import Path -import subprocess -import sys -import tempfile -from types import SimpleNamespace -import unittest -from unittest.mock import patch - -import cmake_runner - -sys.path.insert(0, str(cmake_runner.ROOT)) -import runTests - - -class CMakeRunnerTest(unittest.TestCase): - def inputs(self, build_dir, **overrides): - values = dict(tests=["./test/unit/math/prim/fun"], changed=False, - debug=False, f=[], only_functions=[], build_dir=build_dir, - test_only=False, make_only=False, config="Release", e=-1, - j=6, cmake_arg=[], run_all=False) - values.update(overrides) - return SimpleNamespace(**values) - - def test_directory_builds_and_runs_one_group(self): - with tempfile.TemporaryDirectory() as build_dir, patch.object(cmake_runner, "command") as command: - self.assertEqual(cmake_runner.run(self.inputs(build_dir)), 0) - configure, build, execute = [call.args[0] for call in command.call_args_list] - self.assertIn("-DSTAN_TEST_FILTER=", configure) - self.assertEqual(build[build.index("--target") + 1:], ["unit_math_prim_fun_all_tests"]) - self.assertEqual(Path(execute[0]).name, "unit_math_prim_fun_all_tests") - self.assertIn("test-results/unit_math_prim_fun_all_tests.xml", execute[1]) - - def test_file_builds_individual_executable(self): - self.assertEqual(cmake_runner.target_for("test/unit/math/prim/fun/sum_test.cpp"), - ("test_unit_math_prim_fun_sum_test", "test/unit")) - - def test_other_directory_targets(self): - for path, target in [("test/unit", "unit_all_tests"), - ("test/unit/math", "unit_math_all_tests"), - ("test/prob/bernoulli", "prob_math_bernoulli_all_tests"), - ("test/expressions", "expressions_all_tests")]: - self.assertEqual(cmake_runner.target_for(path)[0], target) - - def test_overlap_runs_only_parent(self): - self.assertEqual(cmake_runner.select_paths([ - "test/unit/math/prim/fun/sum_test.cpp", "./test/unit/math/prim/fun", - "test/unit/math/prim", "test/unit/math/prim"]), ["test/unit/math/prim"]) - - def test_filters_do_not_turn_directory_into_individual_targets(self): - with tempfile.TemporaryDirectory() as build_dir, patch.object(cmake_runner, "command") as command: - cmake_runner.run(self.inputs(build_dir, f=["sum", "prim"], make_only=True)) - self.assertIn("-DSTAN_TEST_NAME_FILTERS=sum;prim", command.call_args_list[0].args[0]) - self.assertEqual(command.call_args_list[1].args[0][-1], "unit_math_prim_fun_all_tests") - self.assertEqual(command.call_count, 2) - - def test_test_only_skips_build(self): - with tempfile.TemporaryDirectory() as build_dir, patch.object(cmake_runner, "command") as command: - cmake_runner.run(self.inputs(build_dir, test_only=True)) - self.assertEqual(command.call_count, 1) - self.assertEqual(Path(command.call_args.args[0][0]).name, "unit_math_prim_fun_all_tests") - - def test_run_all_preserves_failure_and_continues(self): - with tempfile.TemporaryDirectory() as build_dir, patch.object(cmake_runner, "command") as command: - command.side_effect = [subprocess.CalledProcessError(1, "failed-test"), None] - result = cmake_runner.run(self.inputs(build_dir, tests=[ - "test/unit/math/prim/fun/sum_test.cpp", - "test/unit/math/prim/fun/abs_test.cpp"], test_only=True, run_all=True)) - self.assertEqual(result, 1) - self.assertEqual(command.call_count, 2) - - def test_build_failure_stops_before_execution(self): - with tempfile.TemporaryDirectory() as build_dir, patch.object(cmake_runner, "command") as command: - command.side_effect = [None, subprocess.CalledProcessError(2, "build")] - with self.assertRaises(subprocess.CalledProcessError): - cmake_runner.run(self.inputs(build_dir)) - self.assertEqual(command.call_count, 2) - - def test_cli_caps_jobs(self): - with patch.object(sys, "argv", ["runTests.py", "-j20", "test/unit"]): - self.assertEqual(runTests.processCLIArgs().j, 6) - - def test_cli_rejects_invalid_job_count(self): - with patch.object(sys, "argv", ["runTests.py", "-j0", "test/unit"]), patch.object(sys, "stderr"): - with self.assertRaises(SystemExit) as error: - runTests.processCLIArgs() - self.assertEqual(error.exception.code, 2) - - def test_legacy_backend_dispatch(self): - with patch.object(sys, "argv", ["runTests.py", "--backend", "make", "test/unit"]), patch.object(runTests, "runMake") as run: - runTests.main() - self.assertEqual(run.call_args.args[0].tests, ["test/unit"]) - - def test_invalid_path_rejected(self): - with self.assertRaises(ValueError): - cmake_runner.select_paths(["stan/math"]) - - def test_filtered_file_selection_cannot_succeed_without_tests(self): - with self.assertRaises(ValueError): - cmake_runner.run(self.inputs("build", tests=[ - "test/unit/math/prim/fun/sum_test.cpp"], f=["cholesky"], test_only=True)) - - def test_multi_configuration_executable_path(self): - with tempfile.TemporaryDirectory() as build_dir, patch.object(cmake_runner, "command") as command: - Path(build_dir, "CMakeCache.txt").write_text("CMAKE_CONFIGURATION_TYPES:STRING=Debug;Release\n") - cmake_runner.run(self.inputs(build_dir, test_only=True, config="Debug")) - self.assertEqual(Path(command.call_args.args[0][0]), - Path(build_dir, "test/unit/Debug/unit_math_prim_fun_all_tests")) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/expressions/test_expression_testing_framework.py b/test/expressions/test_expression_testing_framework.py index a9f4d418e07..05370e703b7 100644 --- a/test/expressions/test_expression_testing_framework.py +++ b/test/expressions/test_expression_testing_framework.py @@ -16,7 +16,7 @@ class TestExpressionTestingFramework(unittest.TestCase): def runCommand(self, command): - command = command + ("--build-dir", "build-expression-framework", + command = command + ("--build-dir", "build", "--fresh", "--cmake-arg=-DSTAN_TEST_OPTIMIZATION=0") p1 = subprocess.Popen(command, stdout = subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) out, err = p1.communicate() diff --git a/test/sig_utils.py b/test/sig_utils.py index 69d91b2cf13..03e9cce3c67 100644 --- a/test/sig_utils.py +++ b/test/sig_utils.py @@ -1,6 +1,7 @@ import itertools import numbers import os +from pathlib import Path import re import subprocess import sys @@ -276,25 +277,20 @@ def get_signatures(): Retrieves function signatures from stanc3 :return: list of signatures """ - if os.name == "nt": - stanc3 = ".\\test\\expressions\\stanc.exe" - else: - stanc3 = "./test/expressions/stanc" - p = subprocess.Popen((make, stanc3.replace("\\", "/"))) + root = Path(__file__).resolve().parent.parent + stanc3 = root / "test/expressions" / ("stanc.exe" if os.name == "nt" else "stanc") + p = subprocess.Popen(("cmake", "-DSTAN_STANC_FILE=" + str(stanc3), + "-P", str(root / "cmake/stanc.cmake"))) if p.wait() != 0: sys.stderr.write("Error in making stanc3!") sys.exit(-1) - p = subprocess.Popen( - (stanc3 + " --dump-stan-math-signatures"), - stdout=subprocess.PIPE, - universal_newlines=True, - shell=True, - ) - res = parse_signature_file(p.stdout) - if p.wait() != 0: - sys.stderr.write("Error in getting signatures from stanc3!\n") - sys.exit(-1) + with subprocess.Popen([str(stanc3), "--dump-stan-math-signatures"], + stdout=subprocess.PIPE, universal_newlines=True) as p: + res = parse_signature_file(p.stdout) + if p.wait() != 0: + sys.stderr.write("Error in getting signatures from stanc3!\n") + sys.exit(-1) return res + internal_signatures diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index 899e1482886..0c7dc5d5d28 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -8,9 +8,6 @@ add_gtest_grouped_test("${CMAKE_CURRENT_SOURCE_DIR}/math" unit_math unit_mix_pch # Umbrella and multiple-translation-unit checks must compile without a PCH. foreach(source math_c11_test.cpp math_include_test.cpp multiple_translation_units_test.cpp) - if(STAN_TEST_FILTER AND NOT "test/unit/${source}" MATCHES "${STAN_TEST_FILTER}") - continue() - endif() set(matches_name_filters TRUE) foreach(filter IN LISTS STAN_TEST_NAME_FILTERS) string(FIND "test/unit/${source}" "${filter}" match_index) @@ -18,15 +15,13 @@ foreach(source math_c11_test.cpp math_include_test.cpp multiple_translation_unit set(matches_name_filters FALSE) endif() endforeach() - if(NOT matches_name_filters) - continue() - endif() + set_source_files_properties("${source}" PROPERTIES STAN_INCLUDE_IN_GROUP ${matches_name_filters}) get_filename_component(name "${source}" NAME_WE) set_source_files_properties("${source}" PROPERTIES SKIP_PRECOMPILE_HEADERS ON) stan_add_test_object(test_unit_${name}_objects "${source}" "") stan_add_test_binary(test_unit_${name} "$") add_test(NAME test_unit_${name} COMMAND test_unit_${name}) - set_tests_properties(test_unit_${name} PROPERTIES LABELS unit + set_tests_properties(test_unit_${name} PROPERTIES LABELS "unit;test/unit/${source}" WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}") endforeach() if(TARGET test_unit_multiple_translation_units_test) @@ -43,7 +38,8 @@ if(TARGET unit_math_all_tests) endif() foreach(source math_c11_test.cpp math_include_test.cpp multiple_translation_units_test.cpp) get_filename_component(name "${source}" NAME_WE) - if(TARGET test_unit_${name}) + get_source_file_property(include_in_group "${source}" STAN_INCLUDE_IN_GROUP) + if(TARGET test_unit_${name} AND include_in_group) list(APPEND unit_objects "$") endif() endforeach() diff --git a/test/varmat_compatibility.py b/test/varmat_compatibility.py index cfd4100430e..bfb8a5d1d59 100755 --- a/test/varmat_compatibility.py +++ b/test/varmat_compatibility.py @@ -4,6 +4,8 @@ import json from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser import os +from pathlib import Path +import shutil import queue as Queue import subprocess import re @@ -11,7 +13,7 @@ import tempfile import threading -from sig_utils import make, handle_function_list, get_signatures +from sig_utils import handle_function_list, get_signatures from signature_parser import SignatureParser from code_generator import CodeGenerator @@ -19,6 +21,25 @@ TEST_FOLDER = os.path.abspath(os.path.join(HERE, "..", "test")) sys.path.append(TEST_FOLDER) WORKING_FOLDER = "test/varmat-compatibility" +_compile_worker = threading.local() + + +def compile_signature(cpp_path): + if not hasattr(_compile_worker, "directory"): + _compile_worker.directory = tempfile.TemporaryDirectory(prefix="cmake_", dir=WORKING_FOLDER) + directory = Path(_compile_worker.directory.name).resolve() + _compile_worker.source = directory / "check.cpp" + _compile_worker.build = directory / "build" + _compile_worker.source.write_text("\n") + successful, stdout, stderr = run_command([ + "cmake", "-S", str(Path(HERE).parent), "-B", str(_compile_worker.build), + "-DBUILD_TESTING=OFF", "-DSTAN_TEST_OPTIMIZATION=0", + "-DSTAN_COMPILE_TEST_SOURCE=" + str(_compile_worker.source)]) + if not successful: + raise RuntimeError(stderr.decode("utf-8") + stdout.decode("utf-8")) + shutil.copyfile(cpp_path, _compile_worker.source) + return run_command(["cmake", "--build", str(_compile_worker.build), + "--parallel", "1", "--target", "stan_compile_test"]) TEST_TEMPLATE = """ static void {test_name}() {{ @@ -61,7 +82,7 @@ def build_signature(prefix, cpp_code, debug): stdout_path = cpp_path.replace(".cpp", ".stdout") stderr_path = cpp_path.replace(".cpp", ".stderr") - successful, stdout, stderr = run_command([make, object_path]) + successful, stdout, stderr = compile_signature(cpp_path) if successful or not debug: try: @@ -175,6 +196,7 @@ def main(functions_or_sigs, results_file, cores, debug): os.mkdir(WORKING_FOLDER) work_queue_original_length = work_queue.qsize() + compilation_errors = [] # Test if each cpp file builds and update the output file # This part is done in parallel @@ -189,7 +211,13 @@ def worker(): prefix = re.sub('[^0-9a-zA-Z]+', '_', signature.strip()) # Test the signature - successful = build_signature(prefix, cpp_code, debug) + try: + successful = build_signature(prefix, cpp_code, debug) + except Exception as error: + with output_lock: + compilation_errors.append(error) + work_queue.task_done() + continue # Acquire a lock to do I/O with output_lock: @@ -208,6 +236,8 @@ def worker(): threading.Thread(target = worker).start() work_queue.join() + if compilation_errors: + raise RuntimeError("CMake compilation setup failed") from compilation_errors[0] with open(results_file, "w") as f: json.dump({ "compatible_signatures" : list(compatible_signatures), diff --git a/test/varmat_compatibility_test.py b/test/varmat_compatibility_test.py index e14c46ee48a..028b10e200e 100644 --- a/test/varmat_compatibility_test.py +++ b/test/varmat_compatibility_test.py @@ -3,9 +3,17 @@ import tempfile import unittest import os +from unittest.mock import patch class VarmatCompatibilityMainTest(unittest.TestCase): + def test_configuration_failure_propagates(self): + with tempfile.TemporaryDirectory() as directory, patch.object( + varmat_compatibility, "get_signatures", return_value=["sum(matrix) => real\n"]), patch.object( + varmat_compatibility, "build_signature", side_effect=RuntimeError("configuration failed")): + with self.assertRaisesRegex(RuntimeError, "CMake compilation setup failed"): + varmat_compatibility.main(["sum"], os.path.join(directory, "results.json"), 1, False) + def test_sum_works(self): f = tempfile.NamedTemporaryFile("w", suffix="_test.json", delete=False) f.close() From 48ab6217cee894e121c9bc5bef7dee17217c6c05 Mon Sep 17 00:00:00 2001 From: Stan Jenkins Date: Wed, 14 Oct 2026 17:56:12 -0400 Subject: [PATCH 03/10] [Jenkins] auto-formatting by clang-format version 10.0.0-4ubuntu1 --- test/prob/utility.hpp | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/test/prob/utility.hpp b/test/prob/utility.hpp index 2c4f5469eb8..035416e959e 100644 --- a/test/prob/utility.hpp +++ b/test/prob/utility.hpp @@ -64,7 +64,8 @@ inline T get_param(const vector& params, const size_t n) { } template <> -inline empty get_param(const vector& /*params*/, const size_t /*n*/) { +inline empty get_param(const vector& /*params*/, + const size_t /*n*/) { return empty(); } @@ -134,7 +135,7 @@ inline T get_params(const vector>& parameters, const size_t p) { // handle empty template <> inline empty get_params(const vector>& /*parameters*/, - const size_t /*p*/) { + const size_t /*p*/) { return empty(); } // handle scalars @@ -195,7 +196,8 @@ inline fvar> get_params>>( return param; } template <> -inline int get_params(const vector>& parameters, const size_t p) { +inline int get_params(const vector>& parameters, + const size_t p) { int param(0); if (p < parameters[0].size()) param = (int)parameters[0][p]; @@ -301,7 +303,7 @@ inline T get_params(const vector>& parameters, // handle empty template <> inline empty get_params(const vector>& /*parameters*/, - const size_t /*n*/, const size_t /*p*/) { + const size_t /*n*/, const size_t /*p*/) { return empty(); } // handle scalars @@ -362,8 +364,8 @@ inline fvar> get_params>>( return param; } template <> -inline int get_params(const vector>& parameters, const size_t n, - const size_t p) { +inline int get_params(const vector>& parameters, + const size_t n, const size_t p) { int param(0); if (p < parameters[0].size()) param = (int)parameters[n][p]; @@ -541,8 +543,9 @@ typename scalar_type::type select_var_param( } template <> -inline empty select_var_param(const vector>& /*parameters*/, - const size_t /*n*/, const size_t /*p*/) { +inline empty select_var_param( + const vector>& /*parameters*/, const size_t /*n*/, + const size_t /*p*/) { return empty(); } From c550c5b198ac59935c9bb943b286d9c7c030c851 Mon Sep 17 00:00:00 2001 From: Steve Bronder Date: Thu, 15 Oct 2026 15:15:07 -0400 Subject: [PATCH 04/10] Fix CMake CI and consolidate test runner commands Keep grouped thread-pool tests independent of singleton initialization and restore thread-count environments. Match Windows UCRT and ARM definitions across TBB and consumers, disable broken Clang 9/10 PCH use, and validate varmat build setup before classifying signatures. Scope the old GoogleTest policy minimum and allow OpenCL header checks without a loader while retaining executable link requirements. Move CMake runner commands into runTests.py and update Jenkins and documentation. --- CMakeLists.txt | 40 +- Jenkinsfile | 12 +- README.md | 7 +- cmake/CMakeLists.txt | 39 +- runTests.py | 288 ++++++-- stan/math/prim/core/init_threadpool_tbb.hpp | 32 +- test/CMakeLists.txt | 4 +- test/cmake.py | 654 ++---------------- .../math/prim/core/get_num_threads_test.cpp | 24 + .../core/init_threadpool_tbb_late_test.cpp | 30 +- .../prim/core/init_threadpool_tbb_test.cpp | 37 +- .../math/prim/core/set_num_threads_test.cpp | 26 +- test/unit/math/prim/functor/utils_threads.hpp | 26 +- .../functor/map_rect_concurrent_prim_test.cpp | 1 + .../rev/functor/map_rect_concurrent_test.cpp | 1 + .../map_rect_concurrent_threads_test.cpp | 1 + test/varmat_compatibility.py | 9 +- test/varmat_compatibility_test.py | 13 + 18 files changed, 550 insertions(+), 694 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7d40a29f45d..4f37c83f532 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,16 @@ set(CMAKE_POSITION_INDEPENDENT_CODE ON) option(STAN_BUILD_DOCS "Build the Stan Math library documentation" OFF) option(STAN_TEST_HEADERS "Create targets that check standalone headers" ON) option(STAN_TEST_PCH "Reuse precompiled headers when building tests" ON) +set(STAN_EFFECTIVE_TEST_PCH ${STAN_TEST_PCH}) +set(stan_pch_reason "") +if(STAN_TEST_PCH AND CMAKE_CXX_COMPILER_ID STREQUAL "Clang" + AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "9" + AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS "11") + # Clang 9/10 cannot deserialize Eigen's __builtin_bit_cast expressions. + # Fixed in LLVM 95d7ccb70b9cbd53f1f137c0b2411852c42c122b (Clang 11). + set(STAN_EFFECTIVE_TEST_PCH OFF) + set(stan_pch_reason " (Clang 9/10 PCH bit_cast defect; fixed in Clang 11)") +endif() option(STAN_BUILD_PROB_TESTS "Generate and build distribution tests" ON) option(STAN_BUILD_EXPRESSION_TESTS "Build pre-generated expression tests" OFF) option(STAN_TEST_ROW_VECTORS "Include row vectors in distribution tests" OFF) @@ -60,9 +70,21 @@ endforeach() add_subdirectory(cmake) if(STAN_OPENCL) - find_package(OpenCL REQUIRED) + # Configuration and object-only header checks need headers, not a loader. + # Respect explicit FindOpenCL overrides while defaulting to vendored headers. + if(NOT DEFINED OpenCL_INCLUDE_DIR) + set(OpenCL_INCLUDE_DIR "${STAN_OPENCL_ROOT}") + endif() + find_package(OpenCL QUIET) target_include_directories(stanmath SYSTEM INTERFACE "${STAN_OPENCL_ROOT}") - target_link_libraries(stanmath INTERFACE OpenCL::OpenCL) + if(TARGET OpenCL::OpenCL) + target_link_libraries(stanmath INTERFACE OpenCL::OpenCL) + elseif(APPLE) + target_link_libraries(stanmath INTERFACE "-framework OpenCL") + else() + # Keep the ordinary platform link requirement even without a found loader. + target_link_libraries(stanmath INTERFACE OpenCL) + endif() target_compile_definitions(stanmath INTERFACE STAN_OPENCL OPENCL_DEVICE_ID=${STAN_OPENCL_DEVICE_ID} OPENCL_PLATFORM_ID=${STAN_OPENCL_PLATFORM_ID} @@ -95,7 +117,7 @@ endif() message(STATUS "Stan Math: threads=${STAN_THREADS}, OpenCL=${STAN_OPENCL}, MPI=${STAN_MPI}") set(STAN_EFFECTIVE_MPI "${STAN_MPI}" CACHE INTERNAL "MPI setting used by test executables" FORCE) -message(STATUS "Test PCH=${STAN_TEST_PCH}") +message(STATUS "Test PCH: requested=${STAN_TEST_PCH}, effective=${STAN_EFFECTIVE_TEST_PCH}${stan_pch_reason}") if(BUILD_TESTING) add_subdirectory(test) endif() @@ -113,15 +135,19 @@ endif() set(STAN_COMPILE_TEST_SOURCE "" CACHE FILEPATH "Source file for compile-only compatibility checks") if(STAN_COMPILE_TEST_SOURCE) - add_library(stan_compile_test_pch OBJECT EXCLUDE_FROM_ALL cmake/pch.cpp) add_library(stan_compile_test OBJECT EXCLUDE_FROM_ALL "${STAN_COMPILE_TEST_SOURCE}") - foreach(target stan_compile_test_pch stan_compile_test) + set(compile_test_targets stan_compile_test) + if(STAN_EFFECTIVE_TEST_PCH) + add_library(stan_compile_test_pch OBJECT EXCLUDE_FROM_ALL cmake/pch.cpp) + list(APPEND compile_test_targets stan_compile_test_pch) + target_precompile_headers(stan_compile_test_pch PRIVATE ) + target_precompile_headers(stan_compile_test REUSE_FROM stan_compile_test_pch) + endif() + foreach(target IN LISTS compile_test_targets) target_link_libraries(${target} PRIVATE stanmath) target_include_directories(${target} SYSTEM PRIVATE "${STAN_BENCHMARK_ROOT}/googletest/googletest/include") target_compile_options(${target} PRIVATE ${STAN_TEST_OPTIMIZATION_FLAGS}) set_target_properties(${target} PROPERTIES POSITION_INDEPENDENT_CODE FALSE) endforeach() - target_precompile_headers(stan_compile_test_pch PRIVATE ) - target_precompile_headers(stan_compile_test REUSE_FROM stan_compile_test_pch) endif() diff --git a/Jenkinsfile b/Jenkinsfile index 0535fa2baf4..d62d56107c7 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -47,7 +47,7 @@ def runCmakeTests(Map config) { "${shellQuote(buildDir + '/ctest-results.xml')} ${shellQuote(buildDir + '/Testing')} " + shellQuote(reportDir) configureCmake(flags) - sh "python3 test/cmake.py build-tests ${shellQuote(buildDir)} " + + sh "python3 runTests.py build-tests ${shellQuote(buildDir)} " + "--source-regex ${shellQuote(config.filter)} --jobs ${jobs}" sh "cmake -E make_directory ${shellQuote(buildDir + '/test-results')}" withEnv(["GTEST_OUTPUT=xml:${pwd()}/${buildDir}/test-results/"]) { @@ -138,7 +138,7 @@ up the autoformatter locally. (Check console output at ${env.BUILD_URL}) CppLint: { sh "make cpplint" }, Dependencies: { sh "make test-math-dependencies" }, CMakeSelection: { - sh 'python3 test/cmake.py self-test' + sh 'python3 test/cmake.py' }, Documentation: { configureCmake(['-DBUILD_TESTING=OFF', '-DSTAN_BUILD_DOCS=ON']) @@ -165,7 +165,7 @@ up the autoformatter locally. (Check console output at ${env.BUILD_URL}) runPod(image: image) { stage('Run changed unit tests') { def filter = sh(returnStdout: true, - script: 'python3 test/cmake.py changed-unit-filter').trim() + script: 'python3 runTests.py changed-unit-filter').trim() if (filter) { runCmakeTests(name: 'changed', filter: filter, optimization: '3', flags: ['-DCMAKE_CXX_FLAGS=-march=native -mtune=native']) @@ -255,11 +255,11 @@ up the autoformatter locally. (Check console output at ${env.BUILD_URL}) sh 'python3 test/statement_types_test.py' sh 'python3 test/varmat_compatibility_summary_test.py' runFrameworkTests(CLANG_CXX) - sh 'python3 test/cmake.py generate-expressions build' + sh 'python3 runTests.py generate-expressions build' runCmakeTests(name: 'expressions', filter: '^test/expressions/', optimization: '0', flags: ["-DCMAKE_C_COMPILER=${CLANG_CC}", "-DCMAKE_CXX_COMPILER=${CLANG_CXX}", '-DCMAKE_CXX_FLAGS=-Werror', '-DSTAN_BUILD_EXPRESSION_TESTS=ON']) - sh 'python3 test/cmake.py generate-expressions build --functions reduce_sum map_rect' + sh 'python3 runTests.py generate-expressions build --functions reduce_sum map_rect' withEnv(['STAN_NUM_THREADS=4']) { runCmakeTests(name: 'expressions-threads', filter: '^test/expressions/', optimization: '0', testJobs: 1, @@ -284,7 +284,7 @@ up the autoformatter locally. (Check console output at ${env.BUILD_URL}) dist: { runPod(image: image, cpus: 6, memory: '128Gi') { stage('Distribution tests') { - def command = 'python3 test/cmake.py changed-distribution-filter' + def command = 'python3 runTests.py changed-distribution-filter' if (params.runAllDistributions || params.run_all || mainBranch) { command += ' --all' } diff --git a/README.md b/README.md index a90c1c6e0d5..517715d2352 100644 --- a/README.md +++ b/README.md @@ -392,6 +392,9 @@ ctest --test-dir build -L '^test/unit/math/prim/core/' --output-on-failure ``` Precompiled headers (PCH) are enabled by default with `STAN_TEST_PCH=ON`. +Upstream Clang 9 and 10 automatically compile without PCH because of a compiler +defect fixed in Clang 11. Configuration reports the requested and effective +settings; the same setting applies to var-matrix compatibility checks. Automatic differentiation (AD) computes derivatives through the Stan Math type system. Unit tests reuse a PCH for the automatic differentiation layer under test. Sources including `test_ad.hpp` use the AD helper PCH instead. @@ -436,7 +439,7 @@ Test reports are saved under `test-results` with a directory for each test stage The pipeline includes ASan, GPU, MPI, threading, and expression tests. `STAN_TEST_ROW_VECTORS` and `STAN_PROB_TEST_ALL` enable the additional distribution cases requested by Jenkins. -`test/cmake.py generate-expressions PATH` creates expression test sources in +`runTests.py generate-expressions PATH` creates expression test sources in `PATH/expression-sources`. `STAN_BUILD_EXPRESSION_TESTS=ON` enables generated expression tests. `runTests.py` also generates expression sources when you select `test/expressions`. @@ -445,7 +448,7 @@ The runner option `--only-functions` selects functions for expression tests. Example: Generate expression test sources in `build-expressions/expression-sources`. ```sh -python3 test/cmake.py generate-expressions build-expressions +python3 runTests.py generate-expressions build-expressions ``` Expression framework self-tests use the CMake runner. diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt index 413564e59f9..12a3e927446 100644 --- a/cmake/CMakeLists.txt +++ b/cmake/CMakeLists.txt @@ -34,6 +34,23 @@ else() target_include_directories(stanmath SYSTEM INTERFACE "${STAN_BOOST_ROOT}") endif() +if(WIN32) + # Match Make's windows.h macro probe, without requiring an executable link. + function(stan_detect_ucrt) + include(CheckCXXSourceCompiles) + set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) + check_cxx_source_compiles("#include + #ifndef _UCRT + #error Not UCRT + #endif + int main() { return 0; }" STAN_WINDOWS_UCRT) + endfunction() + stan_detect_ucrt() + if(STAN_WINDOWS_UCRT) + target_compile_definitions(stanmath INTERFACE _UCRT) + endif() +endif() + if(STAN_USE_SYSTEM_TBB) find_package(TBB REQUIRED COMPONENTS tbb) else() @@ -46,11 +63,15 @@ else() endif() set(tbb_extra_args "") set(tbb_extra_cxx_flags "") + if(STAN_WINDOWS_UCRT) + set(tbb_extra_cxx_flags "-D_UCRT") + endif() if(WIN32 AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") set(tbb_compiler gcc) if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(ARM64|arm64|aarch64)$") list(APPEND tbb_extra_args WINARM64=true) - set(tbb_extra_cxx_flags "-DTBB_USE_GCC_BUILTINS") + string(APPEND tbb_extra_cxx_flags " -DTBB_USE_GCC_BUILTINS") + set(tbb_interface_definitions TBB_USE_GCC_BUILTINS) endif() elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang") set(tbb_compiler clang) @@ -82,7 +103,8 @@ else() add_library(TBB::tbb SHARED IMPORTED GLOBAL) set_target_properties(TBB::tbb PROPERTIES IMPORTED_LOCATION "${tbb_library}" - INTERFACE_INCLUDE_DIRECTORIES "${STAN_TBB_ROOT}/include") + INTERFACE_INCLUDE_DIRECTORIES "${STAN_TBB_ROOT}/include" + INTERFACE_COMPILE_DEFINITIONS "${tbb_interface_definitions}") if(WIN32) set_property(TARGET TBB::tbb PROPERTY IMPORTED_IMPLIB "${tbb_library}") endif() @@ -130,8 +152,17 @@ endif() if(BUILD_TESTING) set(BUILD_GMOCK OFF CACHE BOOL "Build Google Mock") set(INSTALL_GTEST OFF CACHE BOOL "Install Google Test") - add_subdirectory("${STAN_BENCHMARK_ROOT}/googletest" - "${CMAKE_CURRENT_BINARY_DIR}/googletest" EXCLUDE_FROM_ALL) + function(stan_add_googletest) + # CMake 4 removed policies older than 3.5. Limit the compatibility setting + # to this old dependency, preserving a higher caller-provided minimum. + if(NOT DEFINED CMAKE_POLICY_VERSION_MINIMUM + OR CMAKE_POLICY_VERSION_MINIMUM VERSION_LESS "3.5") + set(CMAKE_POLICY_VERSION_MINIMUM 3.5) + endif() + add_subdirectory("${STAN_BENCHMARK_ROOT}/googletest" + "${CMAKE_CURRENT_BINARY_DIR}/googletest" EXCLUDE_FROM_ALL) + endfunction() + stan_add_googletest() if(WIN32) target_compile_definitions(gtest PUBLIC GTEST_HAS_PTHREAD=0) endif() diff --git a/runTests.py b/runTests.py index 4436e345b8a..1351c4493ba 100755 --- a/runTests.py +++ b/runTests.py @@ -1,18 +1,17 @@ #!/usr/bin/env python3 """Build and run Stan Math tests with CMake.""" +import glob +import json import os from pathlib import Path import re +import shlex import subprocess import sys -import time from argparse import ArgumentParser, RawTextHelpFormatter -maxChangedTests = 20 -testsfx = "_test.cpp" - -def processCLIArgs(): +def processCLIArgs(arguments): """ Define and process the command line interface to the runTests.py script. """ @@ -25,7 +24,26 @@ def processCLIArgs(): formatter_class=RawTextHelpFormatter, ) - # Now define all the rules of the command line args and opts + parser.set_defaults(command=None) + if arguments and arguments[0] in ("changed-unit-filter", "changed-distribution-filter", + "generate-expressions", "build-tests"): + commands = parser.add_subparsers(dest="command", required=True) + commands.add_parser("changed-unit-filter") + distributions = commands.add_parser("changed-distribution-filter") + distributions.add_argument("--all", action="store_true") + expressions = commands.add_parser("generate-expressions") + expressions.add_argument("build_dir") + expressions.add_argument("--functions", nargs="*", default=[]) + expressions.add_argument("--batches", type=int, default=6) + build = commands.add_parser("build-tests") + build.add_argument("build_dir") + build.add_argument("--source-regex", required=True) + build.add_argument("--jobs", type=int, default=1) + inputs = parser.parse_args(arguments) + if getattr(inputs, "jobs", 1) < 1 or getattr(inputs, "batches", 1) < 1: + parser.error("job and batch counts must be positive") + return inputs + parser.add_argument( "-j", metavar="N", type=int, default=1, help="parallel build jobs (default: 1)" ) @@ -96,7 +114,7 @@ def processCLIArgs(): help="extra configure argument, e.g. --cmake-arg=-DSTAN_THREADS=ON") parser.add_argument("--config", default="Release", help="CMake build configuration") # And parse the command line against those rules - inputs = parser.parse_args() + inputs = parser.parse_args(arguments) if inputs.j < 1: parser.error("-j must be positive") if inputs.make_only and inputs.test_only: @@ -106,54 +124,234 @@ def processCLIArgs(): return inputs -def stopErr(msg, returncode): - """Report an error message to stderr and exit with a given code.""" - sys.stderr.write("%s\n" % msg) - sys.stderr.write("exit now (%s)\n" % time.strftime("%x %X %Z")) - sys.exit(returncode) +def findChangedTests(files, existing_tests, limit=20): + """Select priority tests from supplied changes and existing test paths.""" + dependencies = {"prim": ("prim", "rev", "mix", "fwd"), + "rev": ("rev", "mix"), "fwd": ("fwd", "mix")} + selected = set() + for path in files: + if "opencl" in path: + continue + if path.endswith("_test.cpp"): + selected.add(path) + elif path.startswith("stan/") and path.endswith(".hpp"): + candidate = path.replace("stan/", "test/unit/", 1)[:-4] + "_test.cpp" + layer = candidate.split("/")[3] + for replacement in dependencies.get(layer, (layer,)): + test = candidate.replace("/" + layer + "/", "/" + replacement + "/", 1) + if test in existing_tests: + selected.add(test) + return sorted(selected) if len(selected) <= limit else [] -def findChangedTests(debug): - changed_files = subprocess.run( - ["git", "diff", "--name-only", "--diff-filter=d", "origin/develop...HEAD"], - text=True, capture_output=True, check=True, cwd=Path(__file__).resolve().parent - ).stdout.splitlines() - if debug: - print("Changed files:", changed_files) +def select_paths(matches, directories): + """Validate expanded paths and remove selections covered by a directory.""" + paths = set() + for argument, expanded in matches.items(): + if not expanded: + raise ValueError("No matching tests: " + argument) + for path in expanded: + if not any(path == base or path.startswith(base + "/") + for base in ("test/unit", "test/prob", "test/expressions")): + raise ValueError("Unsupported test path: " + path) + if path not in directories and not path.endswith("_test.cpp"): + raise ValueError("Expected a test directory or *_test.cpp: " + path) + paths.add(path) + return [path for path in sorted(paths) + if not any(path.startswith(parent + "/") for parent in directories)] - # changes in prim should also test other non-prim signatures - test_deps = {"prim/": ["prim/", "rev/", "mix/", "fwd/"], "rev/": ["rev/", "mix/"], "fwd/": ["fwd/", "mix/"]} - changed_tests = set() - for f in changed_files: - if 'stan/' in f and f.endswith('.hpp') and 'opencl' not in f: # changed fn - maybe_test = f.replace('stan/', 'test/unit/').replace('.hpp', testsfx) - for (path, replacements) in test_deps.items(): - if path in maybe_test: - for rep in replacements: - maybe_test = maybe_test.replace(path, rep) - if os.path.exists(maybe_test): - changed_tests.add(maybe_test) +def target_for(path): + if path.endswith(".cpp"): + target = path[:-4].replace("/", "_") + else: + prefix = path.replace("test/unit/math", "unit_math", 1) + prefix = prefix.replace("test/unit", "unit", 1) + prefix = prefix.replace("test/prob", "prob_math", 1) + prefix = prefix.replace("test/expressions", "expressions", 1) + target = prefix.replace("/", "_") + "_all_tests" + folder = "test/unit" if path.startswith("test/unit") else ( + "test/prob" if path.startswith("test/prob") else "test") + return target, folder - if f.endswith(testsfx) and 'opencl' not in f: - changed_tests.add(f) - if len(changed_tests) > maxChangedTests: - stopErr("Number of changed tests excluded maximum, not running priority tests", 0) +def build_commands(inputs, paths, root, build_dir): + """Plan configuration and builds for the CMake targets selected by the user.""" + if inputs.test_only: + return [] + expressions = any(path.startswith("test/expressions") for path in paths) + configure = ["cmake", "-S", str(root), "-B", str(build_dir), + "-DCMAKE_BUILD_TYPE=" + inputs.config, "-DBUILD_TESTING=ON", + "-DSTAN_TEST_HEADERS=OFF", + "-DSTAN_BUILD_PROB_TESTS=" + ("ON" if any(path.startswith("test/prob") for path in paths) else "OFF"), + "-DSTAN_BUILD_EXPRESSION_TESTS=" + ("ON" if expressions else "OFF"), + "-DSTAN_TEST_NAME_FILTERS=" + ";".join(inputs.f)] + if expressions: + configure.append("-DSTAN_EXPRESSION_TEST_DIR=" + str(build_dir / "expression-sources")) + if inputs.fresh: + configure.append("--fresh") + build = ["cmake", "--build", str(build_dir), "--config", inputs.config, + "--parallel", str(inputs.j), "--target"] + [target_for(path)[0] for path in paths] + return [configure + inputs.cmake_arg, build] - return sorted(changed_tests) +def parse_cache(text): + cache = {} + for line in text.splitlines(): + if not line.startswith(("//", "#")) and "=" in line and ":" in line.split("=", 1)[0]: + key, value = line.split("=", 1) + cache[key.split(":", 1)[0]] = value + return cache -def main(): - inputs = processCLIArgs() - sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "test")) - from cmake import run - try: - sys.exit(run(inputs)) - except (ValueError, OSError, subprocess.CalledProcessError) as error: - stopErr(str(error), 1) +def test_command(path, build_dir, config, cache, windows): + """Plan an executable invocation using supplied CMake settings.""" + target, folder = target_for(path) + executable = build_dir / folder / target + if cache.get("CMAKE_CONFIGURATION_TYPES"): + executable = executable.parent / config / executable.name + if cache.get("CMAKE_SYSTEM_NAME") == "Windows" or windows: + executable = executable.with_suffix(".exe") + args = [str(executable), "--gtest_output=xml:" + str(build_dir / "test-results" / (target + ".xml"))] + if cache.get("STAN_EFFECTIVE_MPI", cache.get("STAN_MPI")) in ("ON", "TRUE", "1"): + ranks = "2" if not path.endswith(".cpp") or "mpi_" in path else "1" + args = [cache.get("MPIEXEC_EXECUTABLE", "mpirun"), + cache.get("MPIEXEC_NUMPROC_FLAG", "-np"), ranks] + [ + flag for flag in cache.get("MPIEXEC_PREFLAGS", "").split(";") if flag] + args + [ + flag for flag in cache.get("MPIEXEC_POSTFLAGS", "").split(";") if flag] + return args + + +def source_filter(paths): + return "^(" + "|".join(re.escape(path) for path in sorted(set(paths))) + ")$" if paths else "" + + +def build_tests(inventory, source_pattern, build_dir, jobs, batch_size): + """Plan builds using the source labels registered by CMake.""" + pattern = re.compile(source_pattern) + selected = [test["name"] for test in inventory["tests"] + if any(pattern.search(label) for entry in test.get("properties", []) + if entry["name"] == "LABELS" for label in entry["value"])] + if not selected: + raise ValueError("No registered tests match: " + source_pattern) + return [["cmake", "--build", str(build_dir), "--parallel", str(jobs), "--target"] + + selected[offset:offset + batch_size] for offset in range(0, len(selected), batch_size)] + + +def changed_distributions(files, run_all=False): + # Every distribution fixture includes the Stan umbrella headers. Library, + # build-system, and shared test-header changes can affect all distributions. + if run_all or any( + path.startswith(("stan/", "lib/", "make/", "cmake/")) + or path in ("CMakeLists.txt", "Jenkinsfile", "makefile") + or (path.startswith("test/") and path.endswith(".hpp") + and not re.fullmatch(r"test/prob/[^/]+/[^/]+_test\.hpp", path)) + or (path.startswith("test/prob/") and not path.endswith("_test.hpp")) + for path in files + ): + return "^test/prob/" + fixtures = [path for path in files + if re.fullmatch(r"test/prob/[^/]+/[^/]+_test\.hpp", path)] + return ("^(" + "|".join(re.escape(path[:-len("_test.hpp")]) + for path in sorted(fixtures)) + ")_[0-9]+_generated_" + if fixtures else "") + + +def main(arguments, root, working_dir, windows, error_stream): + """Perform command-line I/O. Helpers above only transform explicit inputs.""" + inputs = processCLIArgs(arguments) + try: + files = [] + if getattr(inputs, "changed", False) or inputs.command == "changed-unit-filter" or ( + inputs.command == "changed-distribution-filter" and not inputs.all): + files = subprocess.run(["git", "diff", "--name-only", "--diff-filter=d", + "origin/develop...HEAD"], cwd=root, check=True, + text=True, capture_output=True).stdout.splitlines() + if getattr(inputs, "debug", False): + print("Changed files:", files) + if inputs.command == "changed-distribution-filter": + print(changed_distributions(files, inputs.all)) + return 0 + if getattr(inputs, "changed", False) or inputs.command == "changed-unit-filter": + existing = {path.relative_to(root).as_posix() + for path in (root / "test/unit").rglob("*_test.cpp")} + paths = findChangedTests(files, existing) + if inputs.command == "changed-unit-filter": + print(source_filter(paths)) + return 0 + if not paths: + print("No changed tests were found!") + return 0 + elif inputs.command is None: + matches = {} + for argument in inputs.tests: + expanded = glob.glob(str(root / argument)) + if not expanded and argument.replace("\\", "/").lstrip("./").startswith( + ("test/prob/", "test/expressions/")) and argument.endswith("_test.cpp"): + expanded = [str(root / argument)] + matches[argument] = [Path(path).resolve().relative_to(root).as_posix() + for path in expanded] + directories = {path for expanded in matches.values() for path in expanded + if (root / path).is_dir()} + paths = select_paths(matches, directories) + build_dir = (working_dir / inputs.build_dir).resolve() + if inputs.command == "build-tests": + inventory = subprocess.run(["ctest", "--test-dir", str(build_dir), "--show-only=json-v1"], + cwd=root, check=True, text=True, capture_output=True) + commands = build_tests(json.loads(inventory.stdout), inputs.source_regex, + build_dir, inputs.jobs, 50 if windows else 500) + else: + generate = inputs.command == "generate-expressions" + if not generate: + paths = [path for path in paths if not path.endswith(".cpp") + or all(value in path for value in inputs.f)] + if not paths: + raise ValueError("No matching tests found") + expressions = any(path.startswith("test/expressions") for path in paths) + if inputs.only_functions and not expressions: + raise ValueError("--only-functions requires test/expressions") + generate = expressions and not inputs.test_only + if generate: + # Keep the legacy generator's output setting inside the I/O boundary. + sys.path.insert(0, str(root / "test")) + import generate_expression_tests + output = build_dir / "expression-sources" + output.mkdir(parents=True, exist_ok=True) + for source in output.glob("*test.cpp"): + source.unlink() + functions = inputs.functions if inputs.command else inputs.only_functions + batches = inputs.batches if inputs.command else (inputs.e if inputs.e > 0 else inputs.j) + generate_expression_tests.src_folder = str(output) + "/" + generate_expression_tests.main(functions, batches) + if not list(output.glob("*test.cpp")): + raise RuntimeError("Expression generator produced no tests") + if inputs.command == "generate-expressions": + return 0 + commands = build_commands(inputs, paths, root, build_dir) + for command in commands: + print(" ".join(shlex.quote(str(arg)) for arg in command), flush=True) + subprocess.run(command, cwd=root, check=True) + if inputs.command == "build-tests" or inputs.make_only: + return 0 + cache_file = build_dir / "CMakeCache.txt" + cache = parse_cache(cache_file.read_text() if cache_file.exists() else "") + (build_dir / "test-results").mkdir(parents=True, exist_ok=True) + failed = False + for path in paths: + command = test_command(path, build_dir, inputs.config, cache, windows) + print(" ".join(shlex.quote(arg) for arg in command), flush=True) + try: + subprocess.run(command, cwd=root, check=True) + except subprocess.CalledProcessError: + if not inputs.run_all: + raise + failed = True + return int(failed) + except (ValueError, OSError, RuntimeError, subprocess.CalledProcessError) as error: + print(error, file=error_stream) + return 1 if __name__ == "__main__": - main() + sys.exit(main(sys.argv[1:], Path(__file__).resolve().parent, + Path.cwd(), os.name == "nt", sys.stderr)) diff --git a/stan/math/prim/core/init_threadpool_tbb.hpp b/stan/math/prim/core/init_threadpool_tbb.hpp index ca96dc3a8b2..99e4f361a56 100644 --- a/stan/math/prim/core/init_threadpool_tbb.hpp +++ b/stan/math/prim/core/init_threadpool_tbb.hpp @@ -68,6 +68,25 @@ inline int get_num_threads() { #endif } +#ifdef TBB_INTERFACE_NEW +/** Own the concurrency control before the arena, so the arena dies first. */ +struct threadpool_tbb_state { + tbb::global_control control; + tbb::task_arena arena; + + explicit threadpool_tbb_state(int num_threads) + : control(tbb::global_control::max_allowed_parallelism, num_threads), + arena(num_threads, 1) { + arena.initialize(); + } +}; + +/** Construct independently owned thread-pool state for an explicit count. */ +inline threadpool_tbb_state make_threadpool_tbb(int num_threads) { + return threadpool_tbb_state(num_threads); +} +#endif + } // namespace internal #ifdef TBB_INTERFACE_NEW @@ -83,13 +102,12 @@ inline int get_num_threads() { * of get_num_threads. The TBB scheduler will be activated by calling * this function. * - * The function returns a reference to the static - * tbb::global_control instance. + * The first call fixes the concurrency configuration for the process. * * @param n_threads The maximum number of threads available to the tbb. If not * set will search for the environment variable `STAN_NUM_THREADS`. A value of * -1 will assume all detectable threads are available for the process. - * @return reference to the static tbb::global_control + * @return reference to the static tbb::task_arena * @throws std::runtime_error if n_threads (defaults to zero) is not provided * and the value of STAN_NUM_THREADS environment variable is invalid. */ @@ -108,12 +126,10 @@ inline tbb::task_arena& init_threadpool_tbb(int n_threads = 0) { "' but it must be positive or -1"); } #endif - static tbb::global_control tbb_gc( - tbb::global_control::max_allowed_parallelism, tbb_max_threads); - static tbb::task_arena tbb_arena(tbb_max_threads, 1); - tbb_arena.initialize(); + static auto state = internal::make_threadpool_tbb(tbb_max_threads); + state.arena.initialize(); - return tbb_arena; + return state.arena; } #else /** diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 5da5d2cb840..d471f0cf489 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -23,7 +23,7 @@ else() endif() function(stan_add_pch target) - if(STAN_TEST_PCH) + if(STAN_EFFECTIVE_TEST_PCH) add_library(${target} OBJECT EXCLUDE_FROM_ALL "${PROJECT_SOURCE_DIR}/cmake/pch.cpp") set_target_properties(${target} PROPERTIES POSITION_INDEPENDENT_CODE FALSE) target_link_libraries(${target} PRIVATE stan_test_options) @@ -34,7 +34,7 @@ endfunction() function(stan_configure_test target pch) set_target_properties(${target} PROPERTIES POSITION_INDEPENDENT_CODE FALSE) target_link_libraries(${target} PRIVATE stan_test_options) - if(STAN_TEST_PCH AND pch) + if(STAN_EFFECTIVE_TEST_PCH AND pch) if(TARGET ${pch}) target_precompile_headers(${target} REUSE_FROM ${pch}) endif() diff --git a/test/cmake.py b/test/cmake.py index 5b727a5eaed..3b260eae764 100644 --- a/test/cmake.py +++ b/test/cmake.py @@ -1,596 +1,90 @@ -#!/usr/bin/env python3 -"""CMake test runner, CI commands, and regression checks. +"""Check CMake command planning. Run with python3 test/cmake.py.""" -Run regression checks with: python3 test/cmake.py self-test -""" - -import argparse -import glob -import json -import os from pathlib import Path import re -import shlex -import shutil -import subprocess import sys -import tempfile -from types import SimpleNamespace import unittest from unittest.mock import patch -ROOT = Path(__file__).resolve().parent.parent -MODULE = sys.modules[__name__] - -def select_paths(arguments): - paths = set() - for argument in arguments: - matches = glob.glob(str(ROOT / argument)) - # Distribution sources are generated in the build tree, not the source tree. - if not matches and argument.replace("\\", "/").lstrip("./").startswith(("test/prob/", "test/expressions/")) and argument.endswith("_test.cpp"): - matches = [str(ROOT / argument)] - if not matches: - raise ValueError("No matching tests: " + argument) - for match in matches: - path = Path(match).resolve().relative_to(ROOT).as_posix() - if not (path == "test/unit" or path.startswith("test/unit/") - or path == "test/prob" or path.startswith("test/prob/") - or path == "test/expressions" or path.startswith("test/expressions/")): - raise ValueError("Unsupported test path: " + path) - if not (ROOT / path).is_dir() and not path.endswith("_test.cpp"): - raise ValueError("Expected a test directory or *_test.cpp: " + path) - paths.add(path) - # A parent directory already covers its descendants. - return [path for path in sorted(paths) - if not any(path.startswith(parent + "/") for parent in paths - if (ROOT / parent).is_dir())] - - -def target_for(path): - if path.endswith(".cpp"): - target = path[:-4].replace("/", "_") - else: - prefix = path.replace("test/unit/math", "unit_math", 1) - prefix = prefix.replace("test/unit", "unit", 1) - prefix = prefix.replace("test/prob", "prob_math", 1) - prefix = prefix.replace("test/expressions", "expressions", 1) - target = prefix.replace("/", "_") + "_all_tests" - folder = "test/unit" if path.startswith("test/unit") else ( - "test/prob" if path.startswith("test/prob") else "test") - return target, folder - - -def command(args): - print(" ".join(shlex.quote(str(arg)) for arg in args), flush=True) - subprocess.run(args, cwd=ROOT, check=True) - - -def read_cache(build_dir): - cache = {} - cache_file = build_dir / "CMakeCache.txt" - if cache_file.exists(): - for line in cache_file.read_text().splitlines(): - if not line.startswith(("//", "#")) and "=" in line and ":" in line.split("=", 1)[0]: - key, value = line.split("=", 1) - cache[key.split(":", 1)[0]] = value - return cache - - -def run(inputs): - arguments = inputs.tests - if inputs.changed: - from runTests import findChangedTests - arguments = findChangedTests(inputs.debug) - if not arguments: - print("No changed tests were found!") - return 0 - paths = select_paths(arguments) - if not paths: - raise ValueError("Specify a test directory or source file") - if inputs.f: - paths = [path for path in paths if not path.endswith(".cpp") - or all(value in path for value in inputs.f)] - if not paths: - raise ValueError("No matching tests found") - expressions = any(path.startswith("test/expressions") for path in paths) - if inputs.only_functions and not expressions: - raise ValueError("--only-functions requires test/expressions") - build_dir = Path(inputs.build_dir).resolve() - if not inputs.test_only: - if expressions: - generate_expressions(build_dir, inputs.only_functions, - inputs.e if inputs.e > 0 else inputs.j) - configure = ["cmake", "-S", str(ROOT), "-B", str(build_dir), - "-DCMAKE_BUILD_TYPE=" + inputs.config, "-DBUILD_TESTING=ON", - "-DSTAN_TEST_HEADERS=OFF", - "-DSTAN_BUILD_PROB_TESTS=" + ("ON" if any(path.startswith("test/prob") for path in paths) else "OFF"), - "-DSTAN_BUILD_EXPRESSION_TESTS=" + ("ON" if expressions else "OFF"), - "-DSTAN_TEST_NAME_FILTERS=" + ";".join(inputs.f)] - if expressions: - configure.append("-DSTAN_EXPRESSION_TEST_DIR=" + str(build_dir / "expression-sources")) - if inputs.fresh: - configure.append("--fresh") - command(configure + inputs.cmake_arg) - command(["cmake", "--build", str(build_dir), "--config", inputs.config, - "--parallel", str(inputs.j), "--target"] + [target_for(path)[0] for path in paths]) - if inputs.make_only: - return 0 - cache = read_cache(build_dir) - results = build_dir / "test-results" - results.mkdir(parents=True, exist_ok=True) - failed = False - for path in paths: - target, folder = target_for(path) - executable = build_dir / folder / target - if cache.get("CMAKE_CONFIGURATION_TYPES"): - executable = executable.parent / inputs.config / executable.name - if cache.get("CMAKE_SYSTEM_NAME") == "Windows" or os.name == "nt": - executable = executable.with_suffix(".exe") - args = [str(executable), "--gtest_output=xml:" + str(results / (target + ".xml"))] - if cache.get("STAN_EFFECTIVE_MPI", cache.get("STAN_MPI")) in ("ON", "TRUE", "1"): - # Grouped MPI suites can contain collective tests requiring two ranks. - ranks = "2" if not path.endswith(".cpp") or "mpi_" in path else "1" - args = [cache.get("MPIEXEC_EXECUTABLE", "mpirun"), - cache.get("MPIEXEC_NUMPROC_FLAG", "-np"), ranks] + [ - flag for flag in cache.get("MPIEXEC_PREFLAGS", "").split(";") if flag] + args + [ - flag for flag in cache.get("MPIEXEC_POSTFLAGS", "").split(";") if flag] - try: - command(args) - except subprocess.CalledProcessError: - if not inputs.run_all: - raise - failed = True - return int(failed) - - -def changed_files(): - return subprocess.run( - ["git", "diff", "--name-only", "--diff-filter=d", "origin/develop...HEAD"], - check=True, text=True, capture_output=True, - ).stdout.splitlines() - - -def source_filter(paths): - return "^(" + "|".join(re.escape(path) for path in sorted(set(paths))) + ")$" if paths else "" - - -def changed_units(): - # Preserve the existing runner's priority-test selection and size limit. - sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) - from runTests import findChangedTests - - return source_filter(findChangedTests(False)) - - -def build_tests(build_dir, source_pattern, jobs): - inventory = subprocess.run(["ctest", "--test-dir", str(build_dir), "--show-only=json-v1"], - check=True, text=True, capture_output=True) - selected = [] - pattern = re.compile(source_pattern) - for test in json.loads(inventory.stdout)["tests"]: - labels = next((entry["value"] for entry in test.get("properties", []) - if entry["name"] == "LABELS"), []) - if any(pattern.search(label) for label in labels): - selected.append(test["name"]) - if not selected: - raise ValueError("No registered tests match: " + source_pattern) - batch_size = 50 if sys.platform == "win32" else 500 - for offset in range(0, len(selected), batch_size): - subprocess.run(["cmake", "--build", str(build_dir), "--parallel", str(jobs), - "--target"] + selected[offset:offset + batch_size], check=True) - - -def changed_distributions(files, run_all=False): - # Every distribution fixture includes the Stan umbrella headers. Library, - # build-system, and shared test-header changes can affect all distributions. - if run_all or any( - path.startswith(("stan/", "lib/", "make/", "cmake/")) - or path in ("CMakeLists.txt", "Jenkinsfile", "makefile") - or (path.startswith("test/") and path.endswith(".hpp") - and not re.fullmatch(r"test/prob/[^/]+/[^/]+_test\.hpp", path)) - or (path.startswith("test/prob/") and not path.endswith("_test.hpp")) - for path in files - ): - return "^test/prob/" - fixtures = [path for path in files - if re.fullmatch(r"test/prob/[^/]+/[^/]+_test\.hpp", path)] - return ("^(" + "|".join(re.escape(path[:-len("_test.hpp")]) - for path in sorted(fixtures)) + ")_[0-9]+_generated_" - if fixtures else "") - - -def generate_expressions(build_dir, functions, batches): - import generate_expression_tests - - output = Path(build_dir).resolve() / "expression-sources" - output.mkdir(parents=True, exist_ok=True) - for source in output.glob("*test.cpp"): - source.unlink() - generate_expression_tests.src_folder = str(output) + "/" - generate_expression_tests.main(functions, batches) - if not list(output.glob("*test.cpp")): - raise RuntimeError("Expression generator produced no tests") - - -def main(): - parser = argparse.ArgumentParser(description=__doc__) - commands = parser.add_subparsers(dest="command", required=True) - commands.add_parser("self-test") - commands.add_parser("changed-unit-filter") - distributions = commands.add_parser("changed-distribution-filter") - distributions.add_argument("--all", action="store_true") - expressions = commands.add_parser("generate-expressions") - expressions.add_argument("build_dir") - expressions.add_argument("--functions", nargs="*", default=[]) - expressions.add_argument("--batches", type=int, default=6) - build = commands.add_parser("build-tests") - build.add_argument("build_dir") - build.add_argument("--source-regex", required=True) - build.add_argument("--jobs", type=int, default=1) - args = parser.parse_args() - if args.command == "self-test": - global runTests - sys.path.insert(0, str(ROOT)) - import runTests - unittest.main(argv=[sys.argv[0]]) - elif args.command == "changed-unit-filter": - print(changed_units()) - elif args.command == "changed-distribution-filter": - print(changed_distributions([] if args.all else changed_files(), args.all)) - elif args.command == "build-tests": - if args.jobs < 1: - parser.error("--jobs must be positive") - try: - build_tests(args.build_dir, args.source_regex, args.jobs) - except ValueError as error: - parser.error(str(error)) - else: - if args.batches < 1: - parser.error("--batches must be positive") - generate_expressions(args.build_dir, args.functions, args.batches) - - -class CMakeRunnerTest(unittest.TestCase): - def inputs(self, build_dir, **overrides): - values = dict(tests=["./test/unit/math/prim/fun"], changed=False, - debug=False, f=[], only_functions=[], build_dir=build_dir, - test_only=False, make_only=False, config="Release", e=-1, - j=6, cmake_arg=[], run_all=False, fresh=False) - values.update(overrides) - return SimpleNamespace(**values) - - def test_directory_builds_and_runs_one_group(self): - with tempfile.TemporaryDirectory() as build_dir, patch.object(MODULE, "command") as command: - self.assertEqual(run(self.inputs(build_dir)), 0) - configure, build, execute = [call.args[0] for call in command.call_args_list] - self.assertEqual(build[build.index("--target") + 1:], ["unit_math_prim_fun_all_tests"]) - self.assertEqual(Path(execute[0]).name, "unit_math_prim_fun_all_tests") - self.assertIn("test-results/unit_math_prim_fun_all_tests.xml", execute[1]) - self.assertNotIn("--fresh", configure) - - - @unittest.skipUnless(shutil.which("cmake"), "CMake is required") - def test_fresh_resets_cached_settings_and_preserves_build_files(self): - version = subprocess.run(["cmake", "--version"], check=True, - text=True, stdout=subprocess.PIPE).stdout - match = re.search(r"cmake version (\d+)\.(\d+)", version) - if tuple(map(int, match.groups())) < (3, 24): - self.skipTest("CMake --fresh requires CMake 3.24") - with tempfile.TemporaryDirectory() as temporary: - source = Path(temporary) / "source" - source.mkdir() - fixture = source / "test/unit/math/prim/fun/sum_test.cpp" - fixture.parent.mkdir(parents=True) - fixture.touch() - (source / "CMakeLists.txt").write_text(""" -cmake_minimum_required(VERSION 3.24) -project(fresh_fixture NONE) -option(STAN_THREADS "Thread support" OFF) -add_custom_target(test_unit_math_prim_fun_sum_test - COMMAND "${CMAKE_COMMAND}" -E touch "${CMAKE_BINARY_DIR}/built-marker") -""") - build = Path(temporary) / "build" - subprocess.run(["cmake", "-S", str(source), "-B", str(build), - "-DSTAN_THREADS=ON"], check=True, - stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - (build / "keep-marker").touch() - (build / "CMakeFiles/remove-marker").touch() - with patch.object(MODULE, "ROOT", source): - run(self.inputs(str(build), make_only=True, - tests=["test/unit/math/prim/fun/sum_test.cpp"])) - self.assertEqual(read_cache(build)["STAN_THREADS"], "ON") - with patch.object(sys, "argv", ["runTests.py", "--fresh", "--make-only", - "--build-dir", str(build), "test/unit/math/prim/fun/sum_test.cpp"]): - run(runTests.processCLIArgs()) - cache = read_cache(build) - self.assertEqual(cache["STAN_THREADS"], "OFF") - self.assertTrue((build / "keep-marker").exists()) - self.assertTrue((build / "built-marker").exists()) - self.assertFalse((build / "CMakeFiles/remove-marker").exists()) - - - def test_overlap_runs_only_parent(self): - self.assertEqual(select_paths([ - "test/unit/math/prim/fun/sum_test.cpp", "./test/unit/math/prim/fun", - "test/unit/math/prim", "test/unit/math/prim"]), ["test/unit/math/prim"]) - - def test_filters_do_not_turn_directory_into_individual_targets(self): - with tempfile.TemporaryDirectory() as build_dir, patch.object(MODULE, "command") as command: - run(self.inputs(build_dir, f=["sum", "prim"], make_only=True)) - self.assertIn("-DSTAN_TEST_NAME_FILTERS=sum;prim", command.call_args_list[0].args[0]) - self.assertEqual(command.call_args_list[1].args[0][-1], "unit_math_prim_fun_all_tests") - self.assertEqual(command.call_count, 2) - - def test_test_only_skips_build(self): - with tempfile.TemporaryDirectory() as build_dir, patch.object(MODULE, "command") as command: - run(self.inputs(build_dir, test_only=True)) - self.assertEqual(command.call_count, 1) - self.assertEqual(Path(command.call_args.args[0][0]).name, "unit_math_prim_fun_all_tests") - - def test_run_all_preserves_failure_and_continues(self): - with tempfile.TemporaryDirectory() as build_dir, patch.object(MODULE, "command") as command: - command.side_effect = [subprocess.CalledProcessError(1, "failed-test"), None] - result = run(self.inputs(build_dir, tests=[ - "test/unit/math/prim/fun/sum_test.cpp", - "test/unit/math/prim/fun/abs_test.cpp"], test_only=True, run_all=True)) - self.assertEqual(result, 1) - self.assertEqual(command.call_count, 2) - - def test_build_failure_stops_before_execution(self): - with tempfile.TemporaryDirectory() as build_dir, patch.object(MODULE, "command") as command: - command.side_effect = [None, subprocess.CalledProcessError(2, "build")] - with self.assertRaises(subprocess.CalledProcessError): - run(self.inputs(build_dir)) - self.assertEqual(command.call_count, 2) - - - def test_cli_jobs_reach_build_command(self): - for flags, jobs in (([], 1), (["-j20"], 20)): - with self.subTest(jobs=jobs), tempfile.TemporaryDirectory() as directory, patch.object( - sys, "argv", ["runTests.py", "--make-only", "--build-dir", directory] + flags + [ - "test/unit/math/prim/fun"]), patch.object(MODULE, "command") as command: - inputs = runTests.processCLIArgs() - self.assertFalse(inputs.fresh) - run(inputs) - build = command.call_args.args[0] - self.assertEqual(build[build.index("--parallel") + 1], str(jobs)) - - def test_cli_rejects_invalid_options(self): - for flags in (["-j0"], ["--fresh", "--test-only"], ["--backend", "make"]): - with self.subTest(flags=flags), patch.object(sys, "argv", ["runTests.py"] + flags + [ - "test/unit"]), patch.object(sys, "stderr"): +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) +import runTests as runner + + +def inputs(*flags): + return runner.processCLIArgs(["test/unit/math/prim/fun", *flags]) + + +class CMakeTest(unittest.TestCase): + def test_directory_build_with_fresh_filters_and_jobs(self): + configure, build = runner.build_commands(inputs("--fresh", "-j20", "-f", "sum"), + ["test/unit/math/prim/fun"], Path("source"), Path("build")) + self.assertIn("--fresh", configure) + self.assertIn("-DSTAN_TEST_NAME_FILTERS=sum", configure) + self.assertEqual(build[build.index("--parallel") + 1], "20") + self.assertEqual(build[build.index("--target") + 1:], ["unit_math_prim_fun_all_tests"]) + execute = runner.test_command("test/unit/math/prim/fun", Path("build"), "Release", {}, False) + self.assertEqual(Path(execute[0]).name, "unit_math_prim_fun_all_tests") + + def test_build_only_and_test_only(self): + build = runner.build_commands(inputs("--make-only"), ["test/unit/math/prim/fun"], + Path("source"), Path("build")) + self.assertEqual(len(build), 2) + self.assertNotIn("--fresh", build[0]) + self.assertEqual(runner.build_commands(inputs("--test-only"), ["test/unit/math/prim/fun"], + Path("source"), Path("build")), []) + + def test_cmake_cache_controls_configuration_and_mpi_launch(self): + cache = runner.parse_cache("// comment\nCMAKE_CONFIGURATION_TYPES:STRING=Debug;Release\n" + "STAN_EFFECTIVE_MPI:INTERNAL=ON\nMPIEXEC_PREFLAGS:STRING=--oversubscribe\n") + command = runner.test_command("test/unit/math/prim/fun", Path("build"), "Debug", cache, True) + self.assertEqual(command[:4], ["mpirun", "-np", "2", "--oversubscribe"]) + self.assertEqual(Path(command[4]), Path("build/test/unit/Debug/unit_math_prim_fun_all_tests.exe")) + + def test_cli_reads_explicit_arguments(self): + self.assertEqual(inputs().j, 1) + self.assertEqual(inputs("-j20").j, 20) + parsed = runner.processCLIArgs(["build-tests", "build", "--source-regex", "^test/unit/", "--jobs", "4"]) + self.assertEqual((parsed.command, parsed.jobs), ("build-tests", 4)) + for flags in (("-j0",), ("--fresh", "--test-only"), ("--make-only", "--test-only")): + with self.subTest(flags=flags), patch.object(sys, "stderr"): with self.assertRaises(SystemExit) as error: - runTests.processCLIArgs() + inputs(*flags) self.assertEqual(error.exception.code, 2) - def test_invalid_path_rejected(self): + def test_selection_removes_overlap_and_rejects_invalid_paths(self): + directory = "test/unit/math/prim/fun" + matches = {directory: [directory], "sum": [directory + "/sum_test.cpp"]} + self.assertEqual(runner.select_paths(matches, {directory}), [directory]) + self.assertEqual(runner.target_for(directory + "/sum_test.cpp")[0], + "test_unit_math_prim_fun_sum_test") with self.assertRaises(ValueError): - select_paths(["stan/math"]) - - def test_filtered_file_selection_cannot_succeed_without_tests(self): + runner.select_paths({"stan/math": ["stan/math"]}, {"stan/math"}) with self.assertRaises(ValueError): - run(self.inputs("build", tests=[ - "test/unit/math/prim/fun/sum_test.cpp"], f=["cholesky"], test_only=True)) - - def test_multi_configuration_executable_path(self): - with tempfile.TemporaryDirectory() as build_dir, patch.object(MODULE, "command") as command: - Path(build_dir, "CMakeCache.txt").write_text("CMAKE_CONFIGURATION_TYPES:STRING=Debug;Release\n") - run(self.inputs(build_dir, test_only=True, config="Debug")) - self.assertEqual(Path(command.call_args.args[0][0]), - Path(build_dir, "test/unit/Debug/unit_math_prim_fun_all_tests")) - - def test_effective_mpi_setting_controls_launch(self): - with tempfile.TemporaryDirectory() as build_dir, patch.object(MODULE, "command") as command: - Path(build_dir, "CMakeCache.txt").write_text( - "STAN_EFFECTIVE_MPI:INTERNAL=ON\nMPIEXEC_EXECUTABLE:FILEPATH=mpirun\n") - run(self.inputs(build_dir, test_only=True)) - self.assertEqual(command.call_args.args[0][:3], ["mpirun", "-np", "2"]) - - -class CmakeCISelectionTest(unittest.TestCase): - def test_unit_only_changes_skip_distributions(self): - self.assertEqual(changed_distributions( - ["test/unit/math/prim/fun/sum_test.cpp"]), "") - - def test_fixture_change_selects_all_modes_and_batches(self): - selected = changed_distributions( - ["test/prob/bernoulli/bernoulli_test.hpp"]) + runner.select_paths({"missing": []}, set()) + + def test_changed_headers_select_existing_dependent_tests(self): + existing = {"test/unit/math/" + layer + "/fun/sum_test.cpp" for layer in ("prim", "rev", "fwd", "mix")} + selected = runner.findChangedTests(["stan/math/prim/fun/sum.hpp"], existing) + self.assertEqual(set(selected), existing) + self.assertEqual(runner.findChangedTests(["stan/math/prim/fun/sum.hpp"], existing, limit=1), []) + self.assertRegex(selected[0], runner.source_filter(selected)) + + def test_ci_build_reuses_registered_cmake_labels(self): + inventory = {"tests": [{"name": "sum", "properties": [{"name": "LABELS", "value": + ["prim", "test/unit/math/prim/fun/sum_test.cpp"]}]}, {"name": "grad"}]} + self.assertEqual(runner.build_tests(inventory, "^test/unit/math/prim/", "build", 4, 500), + [["cmake", "--build", "build", "--parallel", "4", "--target", "sum"]]) + with self.assertRaisesRegex(ValueError, "No registered tests match"): + runner.build_tests({"tests": []}, "^test/unit/", "build", 1, 500) + + def test_distribution_selection(self): + self.assertEqual(runner.changed_distributions(["test/unit/math/prim/fun/sum_test.cpp"]), "") + self.assertEqual(runner.changed_distributions(["stan/math/prim/fun/sum.hpp"]), "^test/prob/") + selected = runner.changed_distributions(["test/prob/bernoulli/bernoulli_test.hpp"]) for mode in ("v", "ffv", "vv", "fd", "fv", "ffd"): - for batch in ("00000", "00010"): - self.assertRegex( - f"test/prob/bernoulli/bernoulli_{batch}_generated_{mode}_test.cpp", selected) - self.assertIsNone(re.search(selected, - "test/prob/bernoulli/bernoulli_cdf_00000_generated_v_test.cpp")) - - def test_shared_dependencies_select_all_distributions(self): - for dependency in ("stan/math/prim/fun/sum.hpp", "lib/boost_1.87.0/boost/config.hpp", - "test/prob/utility.hpp", "test/unit/math/expect_near_rel.hpp", - "test/prob/generate_tests.cpp", "cmake/CMakeLists.txt", "Jenkinsfile"): - with self.subTest(dependency=dependency): - selected = changed_distributions([dependency]) - self.assertRegex("test/prob/bernoulli/bernoulli_00000_generated_v_test.cpp", selected) - self.assertRegex("test/prob/normal/normal_00000_generated_ffv_test.cpp", selected) - - - def test_build_selection_uses_registered_labels(self): - inventory = {"tests": [ - {"name": "test_unit_math_prim_fun_sum_test", "properties": [ - {"name": "LABELS", "value": ["prim", "test/unit/math/prim/fun/sum_test.cpp"]}]}, - {"name": "test_unit_math_rev_core_grad_test", "properties": [ - {"name": "LABELS", "value": ["rev", "test/unit/math/rev/core/grad_test.cpp"]}]}, - ]} - with patch.object(subprocess, "run") as command: - command.return_value = subprocess.CompletedProcess([], 0, json.dumps(inventory)) - build_tests("build", "^test/unit/math/prim/", 12) - self.assertEqual(command.call_args.args[0], [ - "cmake", "--build", "build", "--parallel", "12", "--target", - "test_unit_math_prim_fun_sum_test"]) - - def test_empty_build_selection_fails(self): - with patch.object(subprocess, "run") as command: - command.return_value = subprocess.CompletedProcess([], 0, '{"tests": []}') - with self.assertRaisesRegex(ValueError, "No registered tests match"): - build_tests("build", "^test/unit/", 1) - - -class CMakeBuildTest(unittest.TestCase): - def check_build(self, pch_enabled): - with tempfile.TemporaryDirectory(prefix="stan-cmake-build-") as temporary: - source = Path(temporary) / "source" - build = Path(temporary) / "build" - commands = Path(temporary) / "compile.jsonl" - - def write(path, text): - destination = source / path - destination.parent.mkdir(parents=True, exist_ok=True) - destination.write_text(text) - - def run(*args): - result = subprocess.run(args, text=True, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) - self.assertEqual(result.returncode, 0, result.stdout) - - write("launcher.py", """import json, subprocess, sys -with open(sys.argv[1], 'a') as log: - log.write(json.dumps(sys.argv[2:]) + '\\n') -sys.exit(subprocess.call(sys.argv[2:])) -""") - write("CMakeLists.txt", f""" -cmake_minimum_required(VERSION 3.16.3) -project(stan_cmake_fixture LANGUAGES CXX) -set(CMAKE_CXX_STANDARD 17) -set(STAN_TEST_PCH {'ON' if pch_enabled else 'OFF'}) -set(STAN_TEST_OPTIMIZATION_FLAGS -O1) -add_library(stanmath INTERFACE) -target_include_directories(stanmath INTERFACE "${{CMAKE_CURRENT_SOURCE_DIR}}") -add_library(gtest_main STATIC main.cpp) -enable_testing() -include("{(ROOT / 'test/CMakeLists.txt').as_posix()}") -""") - write("cmake/pch.cpp", "// Empty executable/PCH source.\n") - write("gtest/gtest.h", """#pragma once - -#include - -using test_callback = bool (*)(); -inline std::vector& test_callbacks() { - static std::vector callbacks; - return callbacks; -} -struct test_registration { - explicit test_registration(test_callback callback) { - test_callbacks().push_back(callback); - } -}; -""") - write("main.cpp", """#include -int main() { - for (auto callback : test_callbacks()) { - if (!callback()) return 1; - } - return 0; -} -""") - write("stan/math/prim.hpp", """#pragma once - -#ifdef STAN_PCH_TEST_SETTING -constexpr int configured_value = STAN_PCH_TEST_SETTING; -#else -constexpr int configured_value = 1; -#endif -""") - for layer in ("fwd", "rev", "mix"): - write(f"stan/math/{layer}.hpp", "#include \n") - write("stan/math/mix.hpp", "#include \nusing mix_only_alias = double;\n") - write("test/unit/math/test_ad.hpp", """#pragma once - -#include -#include - -using test_alias = double; -""") - write("unit/math/prim/fun/plain_test.cpp", """#include -#include - -using mix_only_alias = int; -using test_alias = int; -bool plain_test() { return configured_value == 1; } -test_registration plain_registration(plain_test); -""") - write("unit/math/prim/fun/ad_test.cpp", """#include -bool ad_test() { return configured_value == 1 && sizeof(test_alias) == sizeof(double); } -test_registration ad_registration(ad_test); -""") - write("unit/math/prim/fun/configuration_test.cpp", """#define STAN_PCH_TEST_SETTING 2 - -#include -#include - -static_assert(configured_value == 2, "Configuration must precede Stan headers"); -bool configuration_test() { return configured_value == 2; } -test_registration configuration_registration(configuration_test); -""") - write("generated_template.cpp", """#include -#include - -bool generated_test() { return configured_value == 1; } -test_registration generated_registration(generated_test); -""") - write("unit/CMakeLists.txt", """ -foreach(layer prim fwd rev mix) - stan_add_pch(unit_${layer}_pch ) -endforeach() -stan_add_pch(unit_ad_pch ) -add_gtest_grouped_test("${CMAKE_CURRENT_SOURCE_DIR}/math" unit_math unit_mix_pch) -set(generated "${PROJECT_BINARY_DIR}/generated/generated_test.cpp") -add_custom_command(OUTPUT "${generated}" - COMMAND "${CMAKE_COMMAND}" -E make_directory "${PROJECT_BINARY_DIR}/generated" - COMMAND "${CMAKE_COMMAND}" -E copy "${PROJECT_SOURCE_DIR}/generated_template.cpp" "${generated}" - DEPENDS "${PROJECT_SOURCE_DIR}/generated_template.cpp") -add_custom_target(generate_fixture DEPENDS "${generated}") -set_source_files_properties("${generated}" PROPERTIES STAN_GENERATOR_TARGET generate_fixture) -add_gtest_grouped_test("${PROJECT_BINARY_DIR}/generated" expressions unit_prim_pch "${generated}") -""") - launcher = f"{sys.executable};{source / 'launcher.py'};{commands}" - run("cmake", "-S", str(source), "-B", str(build), - "-DCMAKE_CXX_COMPILER_LAUNCHER=" + launcher) - run("cmake", "--build", str(build), "--parallel", "1", "--target", - "unit_math_prim_fun_all_tests", "expressions_all_tests") - run(str(build / "unit/unit_math_prim_fun_all_tests")) - run(str(build / "unit/expressions_all_tests")) - - compiled = [json.loads(line) for line in commands.read_text().splitlines()] - fixtures = {name: source / f"unit/math/prim/fun/{name}_test.cpp" - for name in ("plain", "ad", "configuration")} - fixtures["generated"] = build / "generated/generated_test.cpp" - expected_pch = {"plain": "unit_prim_pch", "ad": "unit_ad_pch", - "configuration": None, "generated": "unit_prim_pch"} - for name, path in fixtures.items(): - invocations = [args for args in compiled if str(path) in args] - self.assertEqual(len(invocations), 1, (name, invocations)) - pch = expected_pch[name] if pch_enabled else None - pch_arguments = [arg for arg in invocations[0] if "cmake_pch" in arg] - if pch: - self.assertTrue(any(pch in arg for arg in pch_arguments), invocations[0]) - else: - self.assertFalse(pch_arguments, invocations[0]) - - # Switching executables must not compile any test source again. - run("cmake", "--build", str(build), "--parallel", "1", "--target", - "test_unit_math_prim_fun_plain_test", "unit_math_all_tests", - "test_expressions_generated_test") - run(str(build / "unit/test_unit_math_prim_fun_plain_test")) - run(str(build / "unit/unit_math_all_tests")) - run(str(build / "unit/test_expressions_generated_test")) - after = [json.loads(line) for line in commands.read_text().splitlines()] - for name, path in fixtures.items(): - self.assertEqual(sum(str(path) in args for args in after), 1, name) - - def test_pch_profiles_generation_and_object_reuse(self): - for pch_enabled in (True, False): - with self.subTest(pch_enabled=pch_enabled): - self.check_build(pch_enabled) + self.assertRegex(f"test/prob/bernoulli/bernoulli_00000_generated_{mode}_test.cpp", selected) + self.assertIsNone(re.search(selected, "test/prob/bernoulli/bernoulli_cdf_00000_generated_v_test.cpp")) if __name__ == "__main__": - main() + unittest.main() diff --git a/test/unit/math/prim/core/get_num_threads_test.cpp b/test/unit/math/prim/core/get_num_threads_test.cpp index 5b86e308c89..c65e2d05a49 100644 --- a/test/unit/math/prim/core/get_num_threads_test.cpp +++ b/test/unit/math/prim/core/get_num_threads_test.cpp @@ -5,6 +5,7 @@ #ifdef STAN_THREADS TEST(get_num_threads, correct_values_stan_threads) { + scoped_stan_num_threads restore_environment; set_n_threads("10"); EXPECT_EQ(stan::math::internal::get_num_threads(), 10); @@ -17,6 +18,7 @@ TEST(get_num_threads, correct_values_stan_threads) { } TEST(get_num_threads, incorrect_values) { + scoped_stan_num_threads restore_environment; set_n_threads("abc"); EXPECT_THROW_MSG(stan::math::internal::get_num_threads(), std::invalid_argument, "positive number or -1"); @@ -35,6 +37,7 @@ TEST(get_num_threads, incorrect_values) { } #else TEST(get_num_threads, correct_values_no_stan_threads) { + scoped_stan_num_threads restore_environment; set_n_threads("10"); EXPECT_EQ(stan::math::internal::get_num_threads(), 1); @@ -45,3 +48,24 @@ TEST(get_num_threads, correct_values_no_stan_threads) { EXPECT_EQ(stan::math::internal::get_num_threads(), 1); } #endif + +TEST(get_num_threads, restores_environment) { + scoped_stan_num_threads restore_environment; + set_n_threads("7"); + { + scoped_stan_num_threads restore_previous; + set_n_threads("4"); + } + EXPECT_STREQ("7", std::getenv("STAN_NUM_THREADS")); + +#ifdef _WIN32 + _putenv_s("STAN_NUM_THREADS", ""); +#else + unsetenv("STAN_NUM_THREADS"); +#endif + { + scoped_stan_num_threads restore_absence; + set_n_threads("2"); + } + EXPECT_EQ(nullptr, std::getenv("STAN_NUM_THREADS")); +} diff --git a/test/unit/math/prim/core/init_threadpool_tbb_late_test.cpp b/test/unit/math/prim/core/init_threadpool_tbb_late_test.cpp index 4cefd6b96c0..f9fc73e924d 100644 --- a/test/unit/math/prim/core/init_threadpool_tbb_late_test.cpp +++ b/test/unit/math/prim/core/init_threadpool_tbb_late_test.cpp @@ -7,26 +7,19 @@ #ifdef TBB_INTERFACE_NEW -#include - TEST(intel_tbb_new_late_init, check_status) { - const int num_threads = tbb::global_control::max_allowed_parallelism; - - if (num_threads > 1) { - set_n_threads(num_threads - 1); - tbb::task_arena& tbb_arena = stan::math::init_threadpool_tbb(); - tbb_arena.execute([&]() { - EXPECT_TRUE(tbb_arena.is_active()); - - // STAN_NUM_THREADS is not being honored if we have first - // initialized the TBB scheduler outside of init_threadpool_tbb -#ifdef STAN_THREADS - EXPECT_EQ(num_threads, tbb::this_task_arena::max_concurrency()); -#else - EXPECT_EQ(num_threads, 1); -#endif + const int num_threads = 4; + tbb::task_arena existing_arena(num_threads, 1); + existing_arena.execute([num_threads]() { + EXPECT_EQ(num_threads, tbb::this_task_arena::max_concurrency()); + auto state = stan::math::internal::make_threadpool_tbb(num_threads - 1); + EXPECT_TRUE(state.arena.is_active()); + EXPECT_EQ(num_threads - 1, state.arena.max_concurrency()); + state.arena.execute([num_threads]() { + EXPECT_EQ(num_threads - 1, tbb::this_task_arena::max_concurrency()); }); - } + EXPECT_EQ(num_threads, tbb::this_task_arena::max_concurrency()); + }); } #else @@ -34,6 +27,7 @@ TEST(intel_tbb_new_late_init, check_status) { #include TEST(intel_tbb_late_init, check_status) { + scoped_stan_num_threads restore_environment; const int num_threads = tbb::task_scheduler_init::default_num_threads(); tbb::task_scheduler_init tbb_scheduler; diff --git a/test/unit/math/prim/core/init_threadpool_tbb_test.cpp b/test/unit/math/prim/core/init_threadpool_tbb_test.cpp index b20adbf2e2e..b9522099cc8 100644 --- a/test/unit/math/prim/core/init_threadpool_tbb_test.cpp +++ b/test/unit/math/prim/core/init_threadpool_tbb_test.cpp @@ -5,19 +5,34 @@ #include TEST(intel_tbb_new_init, check_status) { + scoped_stan_num_threads restore_environment; set_n_threads(-1); - auto& tbb_init = stan::math::init_threadpool_tbb(); - EXPECT_TRUE(tbb_init.is_active()); - -#ifdef STAN_THREADS - EXPECT_EQ(std::thread::hardware_concurrency(), tbb_init.max_concurrency()); -#else - EXPECT_EQ(1, tbb_init.max_concurrency()); + auto& arena = stan::math::init_threadpool_tbb(); + ASSERT_TRUE(arena.is_active()); + const int original_threads = arena.max_concurrency(); +#ifndef STAN_THREADS + EXPECT_EQ(1, original_threads); #endif - auto& tbb_reinit = stan::math::init_threadpool_tbb(); - EXPECT_TRUE(tbb_init.is_active()); + // Other tests may have initialized the singleton before this test. + set_n_threads(original_threads + 1); + auto& reinit = stan::math::init_threadpool_tbb(); + EXPECT_EQ(&arena, &reinit); + EXPECT_TRUE(reinit.is_active()); + EXPECT_EQ(original_threads, reinit.max_concurrency()); + auto& explicit_reinit = stan::math::init_threadpool_tbb(original_threads + 2); + EXPECT_EQ(&arena, &explicit_reinit); + EXPECT_EQ(original_threads, explicit_reinit.max_concurrency()); +} - tbb_init.terminate(); - EXPECT_FALSE(tbb_init.is_active()); +#ifdef STAN_THREADS +TEST(intel_tbb_new_init, validates_after_initialization) { + scoped_stan_num_threads restore_environment; + stan::math::init_threadpool_tbb(2); + EXPECT_THROW_MSG(stan::math::init_threadpool_tbb(-2), std::invalid_argument, + "positive or -1"); + set_n_threads("invalid"); + EXPECT_THROW_MSG(stan::math::init_threadpool_tbb(), std::invalid_argument, + "positive number or -1"); } +#endif diff --git a/test/unit/math/prim/core/set_num_threads_test.cpp b/test/unit/math/prim/core/set_num_threads_test.cpp index f46eeec0760..e6d2c1758bc 100644 --- a/test/unit/math/prim/core/set_num_threads_test.cpp +++ b/test/unit/math/prim/core/set_num_threads_test.cpp @@ -1,14 +1,22 @@ #include #include +#include TEST(intel_tbb_new_init, set_threads) { - int num_threads = std::thread::hardware_concurrency(); - auto& tbb_init = stan::math::init_threadpool_tbb(num_threads - 1); - EXPECT_TRUE(tbb_init.is_active()); - -#ifdef STAN_THREADS - EXPECT_EQ(num_threads - 1, tbb_init.max_concurrency()); -#else - EXPECT_EQ(1, tbb_init.max_concurrency()); -#endif + using control = tbb::global_control; + const auto previous = control::active_value(control::max_allowed_parallelism); + for (int num_threads : {1, 2, 4}) { + { + auto state = stan::math::internal::make_threadpool_tbb(num_threads); + EXPECT_TRUE(state.arena.is_active()); + EXPECT_EQ(num_threads, state.arena.max_concurrency()); + EXPECT_EQ(std::min(previous, static_cast(num_threads)), + control::active_value(control::max_allowed_parallelism)); + state.arena.execute([num_threads]() { + EXPECT_EQ(num_threads, tbb::this_task_arena::max_concurrency()); + }); + } + EXPECT_EQ(previous, + control::active_value(control::max_allowed_parallelism)); + } } diff --git a/test/unit/math/prim/functor/utils_threads.hpp b/test/unit/math/prim/functor/utils_threads.hpp index 77de14ffeb2..83187bfd439 100644 --- a/test/unit/math/prim/functor/utils_threads.hpp +++ b/test/unit/math/prim/functor/utils_threads.hpp @@ -1,9 +1,33 @@ #ifndef TEST_UNIT_MATH_PRIM_FUNCTOR_UTILS_THREADS_HPP #define TEST_UNIT_MATH_PRIM_FUNCTOR_UTILS_THREADS_HPP -#include +#include +#include #include +// Restore the caller's environment, including an originally absent variable. +class scoped_stan_num_threads { + const bool was_set_; + const std::string previous_; + + public: + scoped_stan_num_threads() + : was_set_(std::getenv("STAN_NUM_THREADS") != nullptr), + previous_(was_set_ ? std::getenv("STAN_NUM_THREADS") : "") {} + + ~scoped_stan_num_threads() { +#ifdef _WIN32 + _putenv_s("STAN_NUM_THREADS", previous_.c_str()); +#else + if (was_set_) { + setenv("STAN_NUM_THREADS", previous_.c_str(), 1); + } else { + unsetenv("STAN_NUM_THREADS"); + } +#endif + } +}; + // utility to set number of threads to use inline void set_n_threads(int num_threads) { static char env_string[256]; diff --git a/test/unit/math/rev/functor/map_rect_concurrent_prim_test.cpp b/test/unit/math/rev/functor/map_rect_concurrent_prim_test.cpp index b7fe369ae98..fcf2f8378c6 100644 --- a/test/unit/math/rev/functor/map_rect_concurrent_prim_test.cpp +++ b/test/unit/math/rev/functor/map_rect_concurrent_prim_test.cpp @@ -19,6 +19,7 @@ STAN_REGISTER_MAP_RECT(0, hard_work) STAN_REGISTER_MAP_RECT(1, hard_work) struct map_rect_con_prim : public ::testing::Test { + scoped_stan_num_threads restore_environment; Eigen::VectorXd shared_params_d; std::vector job_params_d; std::vector > x_r; diff --git a/test/unit/math/rev/functor/map_rect_concurrent_test.cpp b/test/unit/math/rev/functor/map_rect_concurrent_test.cpp index 65b0dec034a..4617f12d798 100644 --- a/test/unit/math/rev/functor/map_rect_concurrent_test.cpp +++ b/test/unit/math/rev/functor/map_rect_concurrent_test.cpp @@ -18,6 +18,7 @@ STAN_REGISTER_MAP_RECT(0, hard_work) struct map_rect_con : public ::testing::Test { + scoped_stan_num_threads restore_environment; Eigen::VectorXd shared_params_d; std::vector job_params_d; std::vector > x_r; diff --git a/test/unit/math/rev/functor/map_rect_concurrent_threads_test.cpp b/test/unit/math/rev/functor/map_rect_concurrent_threads_test.cpp index 40b20451e73..b733304050b 100644 --- a/test/unit/math/rev/functor/map_rect_concurrent_threads_test.cpp +++ b/test/unit/math/rev/functor/map_rect_concurrent_threads_test.cpp @@ -46,6 +46,7 @@ inline void setup_job(int N, Eigen::VectorXd& shared_params_d, } struct map_rect_con_threads : public ::testing::Test { + scoped_stan_num_threads restore_environment; Eigen::VectorXd shared_params_d; std::vector job_params_d; std::vector > x_r; diff --git a/test/varmat_compatibility.py b/test/varmat_compatibility.py index bfb8a5d1d59..f50c93c8698 100755 --- a/test/varmat_compatibility.py +++ b/test/varmat_compatibility.py @@ -35,8 +35,15 @@ def compile_signature(cpp_path): "cmake", "-S", str(Path(HERE).parent), "-B", str(_compile_worker.build), "-DBUILD_TESTING=OFF", "-DSTAN_TEST_OPTIMIZATION=0", "-DSTAN_COMPILE_TEST_SOURCE=" + str(_compile_worker.source)]) + if successful: + successful, stdout, stderr = run_command([ + "cmake", "--build", str(_compile_worker.build), + "--parallel", "1", "--target", "stan_compile_test"]) if not successful: - raise RuntimeError(stderr.decode("utf-8") + stdout.decode("utf-8")) + _compile_worker.directory.cleanup() + del _compile_worker.directory + raise RuntimeError("CMake compilation setup failed:\n" + + stdout.decode("utf-8") + stderr.decode("utf-8")) shutil.copyfile(cpp_path, _compile_worker.source) return run_command(["cmake", "--build", str(_compile_worker.build), "--parallel", "1", "--target", "stan_compile_test"]) diff --git a/test/varmat_compatibility_test.py b/test/varmat_compatibility_test.py index 028b10e200e..a161d3aacdd 100644 --- a/test/varmat_compatibility_test.py +++ b/test/varmat_compatibility_test.py @@ -3,10 +3,23 @@ import tempfile import unittest import os +import threading from unittest.mock import patch class VarmatCompatibilityMainTest(unittest.TestCase): + def test_initial_build_failure_propagates_compiler_output(self): + with tempfile.TemporaryDirectory() as directory, patch.object( + varmat_compatibility, "WORKING_FOLDER", directory), patch.object( + varmat_compatibility, "_compile_worker", threading.local()), patch.object( + varmat_compatibility, "run_command", side_effect=[ + (True, b"configured", b""), + (False, b"PCH build failed", b"compiler diagnostic")]) as command: + with self.assertRaisesRegex(RuntimeError, "PCH build failedcompiler diagnostic"): + varmat_compatibility.compile_signature("unused.cpp") + self.assertEqual(command.call_count, 2) + self.assertEqual(command.call_args.args[0][-1], "stan_compile_test") + def test_configuration_failure_propagates(self): with tempfile.TemporaryDirectory() as directory, patch.object( varmat_compatibility, "get_signatures", return_value=["sum(matrix) => real\n"]), patch.object( From 865d8007e46e7e350b673e608104a5489362c576 Mon Sep 17 00:00:00 2001 From: Steve Bronder Date: Thu, 15 Oct 2026 18:21:47 -0400 Subject: [PATCH 05/10] Fix OpenCL warnings with Clang 10 Remove unused declarations exposed by the Jenkins -Werror build and group conditional callback captures without changing the derivative branches. Forward empty softmax inputs and exercise reverse-mode segment exceptions. Remove discarded reduction copies that consumed temporary matrix inputs. Add regressions covering temporary inputs to all six affected factories. --- stan/math/fwd/fun/mdivide_left_tri_low.hpp | 6 - .../kernel_generator/colwise_reduction.hpp | 3 - .../kernel_generator/multi_result_kernel.hpp | 9 +- .../opencl/kernel_generator/reduction_2d.hpp | 3 - .../opencl/prim/binomial_logit_glm_lpmf.hpp | 1 - stan/math/opencl/prim/logistic_lcdf.hpp | 1 - stan/math/opencl/rev/add_diag.hpp | 32 +-- stan/math/opencl/rev/append_col.hpp | 6 +- stan/math/opencl/rev/append_row.hpp | 6 +- stan/math/opencl/rev/mdivide_left_tri_low.hpp | 6 +- .../math/opencl/rev/mdivide_right_tri_low.hpp | 6 +- stan/math/opencl/zeros_strict_tri.hpp | 1 - stan/math/prim/fun/log_softmax.hpp | 3 +- stan/math/prim/fun/softmax.hpp | 3 +- stan/math/prim/prob/student_t_qf.hpp | 1 - stan/math/rev/constraint/lub_constrain.hpp | 204 ++++++++++-------- stan/math/rev/fun/fma.hpp | 23 +- stan/math/rev/fun/inv_inc_beta.hpp | 5 +- stan/math/rev/prob/student_t_qf.hpp | 2 - .../opencl/kernel_generator/calc_if_test.cpp | 1 - .../colwise_reduction_test.cpp | 20 ++ .../kernel_generator/reduction_2d_test.cpp | 17 ++ .../opencl/prim/mdivide_left_tri_low_test.cpp | 2 - .../prim/mdivide_right_tri_low_test.cpp | 1 - .../math/opencl/rev/append_array_test.cpp | 2 - .../math/opencl/rev/bernoulli_cdf_test.cpp | 4 - .../math/opencl/rev/bernoulli_lccdf_test.cpp | 5 - .../opencl/rev/bernoulli_logit_lpmf_test.cpp | 3 - .../math/opencl/rev/bernoulli_lpmf_test.cpp | 3 - test/unit/math/opencl/rev/beta_lpdf_test.cpp | 1 - .../opencl/rev/beta_proportion_lpdf_test.cpp | 1 - .../opencl/rev/binomial_logit_lpmf_test.cpp | 2 - .../math/opencl/rev/binomial_lpmf_test.cpp | 2 - .../math/opencl/rev/cauchy_lccdf_test.cpp | 1 - .../unit/math/opencl/rev/cauchy_lpdf_test.cpp | 1 - .../rev/double_exponential_cdf_test.cpp | 1 - .../rev/double_exponential_lccdf_test.cpp | 1 - .../rev/double_exponential_lcdf_test.cpp | 1 - .../opencl/rev/exp_mod_normal_cdf_test.cpp | 2 - .../opencl/rev/exp_mod_normal_lccdf_test.cpp | 3 - .../math/opencl/rev/exponential_cdf_test.cpp | 1 - .../opencl/rev/exponential_lccdf_test.cpp | 1 - .../math/opencl/rev/exponential_lcdf_test.cpp | 1 - .../math/opencl/rev/exponential_lpdf_test.cpp | 1 - .../unit/math/opencl/rev/frechet_cdf_test.cpp | 1 - .../math/opencl/rev/frechet_lccdf_test.cpp | 1 - .../math/opencl/rev/frechet_lcdf_test.cpp | 1 - test/unit/math/opencl/rev/gumbel_cdf_test.cpp | 1 - .../math/opencl/rev/gumbel_lccdf_test.cpp | 1 - .../unit/math/opencl/rev/gumbel_lcdf_test.cpp | 1 - .../math/opencl/rev/inv_gamma_lpdf_test.cpp | 2 - test/unit/math/opencl/rev/log_mix_test.cpp | 1 - .../math/opencl/rev/logistic_cdf_test.cpp | 2 - .../math/opencl/rev/logistic_lccdf_test.cpp | 2 - .../math/opencl/rev/logistic_lcdf_test.cpp | 2 - .../math/opencl/rev/logistic_lpdf_test.cpp | 2 - .../math/opencl/rev/lognormal_cdf_test.cpp | 2 - .../math/opencl/rev/lognormal_lccdf_test.cpp | 2 - .../math/opencl/rev/lognormal_lcdf_test.cpp | 2 - .../math/opencl/rev/lognormal_lpdf_test.cpp | 2 - .../rev/multi_normal_cholesky_lpdf_test.cpp | 1 - .../rev/neg_binomial_2_log_lpmf_test.cpp | 4 - .../opencl/rev/neg_binomial_2_lpmf_test.cpp | 2 - .../opencl/rev/neg_binomial_lpmf_test.cpp | 1 - .../math/opencl/rev/normal_lccdf_test.cpp | 1 - .../unit/math/opencl/rev/normal_lcdf_test.cpp | 1 - test/unit/math/opencl/rev/pareto_cdf_test.cpp | 2 - .../math/opencl/rev/pareto_lccdf_test.cpp | 2 - .../unit/math/opencl/rev/pareto_lcdf_test.cpp | 2 - .../unit/math/opencl/rev/pareto_lpdf_test.cpp | 1 - .../opencl/rev/pareto_type_2_cdf_test.cpp | 1 - .../opencl/rev/pareto_type_2_lccdf_test.cpp | 1 - .../opencl/rev/pareto_type_2_lcdf_test.cpp | 1 - .../math/opencl/rev/poisson_log_lpmf_test.cpp | 3 - .../math/opencl/rev/poisson_lpmf_test.cpp | 3 - .../math/opencl/rev/rayleigh_cdf_test.cpp | 1 - .../math/opencl/rev/rayleigh_lccdf_test.cpp | 1 - .../math/opencl/rev/rayleigh_lcdf_test.cpp | 1 - .../math/opencl/rev/rayleigh_lpdf_test.cpp | 1 - test/unit/math/opencl/rev/segment_test.cpp | 10 +- .../rev/skew_double_exponential_cdf_test.cpp | 2 - .../skew_double_exponential_lccdf_test.cpp | 2 - .../rev/skew_double_exponential_lcdf_test.cpp | 2 - .../math/opencl/rev/skew_normal_lpdf_test.cpp | 1 - .../math/opencl/rev/std_normal_cdf_test.cpp | 1 - .../math/opencl/rev/std_normal_lccdf_test.cpp | 1 - .../math/opencl/rev/std_normal_lcdf_test.cpp | 1 - .../math/opencl/rev/std_normal_lpdf_test.cpp | 1 - .../unit/math/opencl/rev/uniform_cdf_test.cpp | 2 - .../math/opencl/rev/uniform_lccdf_test.cpp | 2 - .../math/opencl/rev/uniform_lcdf_test.cpp | 2 - .../math/opencl/rev/uniform_lpdf_test.cpp | 1 - .../unit/math/opencl/rev/weibull_cdf_test.cpp | 1 - .../math/opencl/rev/weibull_lccdf_test.cpp | 1 - .../math/opencl/rev/weibull_lcdf_test.cpp | 1 - .../math/opencl/rev/weibull_lpdf_test.cpp | 1 - .../unit/math/prim/prob/student_t_qf_test.cpp | 1 - 97 files changed, 226 insertions(+), 261 deletions(-) diff --git a/stan/math/fwd/fun/mdivide_left_tri_low.hpp b/stan/math/fwd/fun/mdivide_left_tri_low.hpp index 8dfd8f4cb2d..d17a4c385f2 100644 --- a/stan/math/fwd/fun/mdivide_left_tri_low.hpp +++ b/stan/math/fwd/fun/mdivide_left_tri_low.hpp @@ -20,9 +20,6 @@ template , std::decay_t::RowsAtCompileTime, std::decay_t::ColsAtCompileTime> mdivide_left_tri_low(T1&& A, T2&& b) { - constexpr int S1 = std::decay_t::RowsAtCompileTime; - constexpr int C2 = std::decay_t::ColsAtCompileTime; - check_square("mdivide_left_tri_low", "A", A); check_multiplicable("mdivide_left_tri_low", "A", A, "b", b); if (A.size() == 0) { @@ -47,7 +44,6 @@ template * = nullptr, inline Eigen::Matrix, std::decay_t::RowsAtCompileTime, std::decay_t::ColsAtCompileTime> mdivide_left_tri_low(T1&& A, T2&& b) { - constexpr int S1 = std::decay_t::RowsAtCompileTime; check_square("mdivide_left_tri_low", "A", A); check_multiplicable("mdivide_left_tri_low", "A", A, "b", b); if (A.size() == 0) { @@ -64,8 +60,6 @@ template * = nullptr, inline Eigen::Matrix, std::decay_t::RowsAtCompileTime, std::decay_t::ColsAtCompileTime> mdivide_left_tri_low(T1&& A, T2&& b) { - constexpr int S1 = std::decay_t::RowsAtCompileTime; - constexpr int C2 = std::decay_t::ColsAtCompileTime; check_square("mdivide_left_tri_low", "A", A); check_multiplicable("mdivide_left_tri_low", "A", A, "b", b); if (A.size() == 0) { diff --git a/stan/math/opencl/kernel_generator/colwise_reduction.hpp b/stan/math/opencl/kernel_generator/colwise_reduction.hpp index e4520b154c6..088577f8a88 100644 --- a/stan/math/opencl/kernel_generator/colwise_reduction.hpp +++ b/stan/math/opencl/kernel_generator/colwise_reduction.hpp @@ -222,7 +222,6 @@ class colwise_sum_ : public colwise_reduction, T, sum_op> { */ template * = nullptr> inline auto colwise_sum(T&& a) { - auto&& arg_copy = as_operation_cl(std::forward(a)).deep_copy(); return colwise_sum_>( as_operation_cl(std::forward(a))); } @@ -266,7 +265,6 @@ class colwise_prod_ : public colwise_reduction, T, prod_op> { */ template * = nullptr> inline auto colwise_prod(T&& a) { - auto&& arg_copy = as_operation_cl(std::forward(a)).deep_copy(); return colwise_prod_>( as_operation_cl(std::forward(a))); } @@ -315,7 +313,6 @@ class colwise_max_ : public colwise_reduction< */ template * = nullptr> inline auto colwise_max(T&& a) { - auto&& arg_copy = as_operation_cl(std::forward(a)).deep_copy(); return colwise_max_>( as_operation_cl(std::forward(a))); } diff --git a/stan/math/opencl/kernel_generator/multi_result_kernel.hpp b/stan/math/opencl/kernel_generator/multi_result_kernel.hpp index 0b0cbadec57..7c235d8af26 100644 --- a/stan/math/opencl/kernel_generator/multi_result_kernel.hpp +++ b/stan/math/opencl/kernel_generator/multi_result_kernel.hpp @@ -342,11 +342,10 @@ class results_cl { index_apply([this, &exprs](auto... Is) { auto tmp = std::tuple_cat(make_assignment_pair( std::get(results_), std::get(exprs.expressions_))...); - index_apply::value>( - [this, &tmp](auto... Is2) { - assignment_impl(std::make_tuple(std::make_pair( - std::get(tmp).first, std::get(tmp).second)...)); - }); + index_apply::value>([&tmp](auto... Is2) { + assignment_impl(std::make_tuple(std::make_pair( + std::get(tmp).first, std::get(tmp).second)...)); + }); }); } diff --git a/stan/math/opencl/kernel_generator/reduction_2d.hpp b/stan/math/opencl/kernel_generator/reduction_2d.hpp index 4d019f1012c..b028c50f60a 100644 --- a/stan/math/opencl/kernel_generator/reduction_2d.hpp +++ b/stan/math/opencl/kernel_generator/reduction_2d.hpp @@ -231,7 +231,6 @@ class sum_2d_ : public reduction_2d, T, sum_op> { */ template * = nullptr> inline auto sum_2d(T&& a) { - auto&& arg_copy = as_operation_cl(std::forward(a)).deep_copy(); return sum_2d_>(as_operation_cl(std::forward(a))); } @@ -274,7 +273,6 @@ class prod_2d_ : public reduction_2d, T, prod_op> { */ template * = nullptr> inline auto prod_2d(T&& a) { - auto&& arg_copy = as_operation_cl(std::forward(a)).deep_copy(); return prod_2d_>(as_operation_cl(std::forward(a))); } @@ -321,7 +319,6 @@ class max_2d_ */ template * = nullptr> inline auto max_2d(T&& a) { - auto&& arg_copy = as_operation_cl(std::forward(a)).deep_copy(); return max_2d_>(as_operation_cl(std::forward(a))); } diff --git a/stan/math/opencl/prim/binomial_logit_glm_lpmf.hpp b/stan/math/opencl/prim/binomial_logit_glm_lpmf.hpp index a569498f5e1..340f9250b84 100644 --- a/stan/math/opencl/prim/binomial_logit_glm_lpmf.hpp +++ b/stan/math/opencl/prim/binomial_logit_glm_lpmf.hpp @@ -34,7 +34,6 @@ inline return_type_t binomial_logit_glm_lpmf( const T_beta_cl& beta) { static const char* function = "binomial_logit_glm_lpmf(OpenCL)"; using T_partials_return = partials_return_t; - constexpr bool is_y_vector = !is_stan_scalar::value; constexpr bool is_alpha_vector = !is_stan_scalar::value; const size_t N_instances diff --git a/stan/math/opencl/prim/logistic_lcdf.hpp b/stan/math/opencl/prim/logistic_lcdf.hpp index f16c9518bd1..4bda338ded5 100644 --- a/stan/math/opencl/prim/logistic_lcdf.hpp +++ b/stan/math/opencl/prim/logistic_lcdf.hpp @@ -64,7 +64,6 @@ inline return_type_t logistic_lcdf( auto sigma_positive_finite_expr = 0 < sigma_val && isfinite(sigma_val); auto any_y_neg_inf = colwise_max(cast(y_val == NEGATIVE_INFTY)); - auto cond = y_val == INFTY; auto inv_sigma = elt_divide(1.0, sigma_val); auto mu_minus_y_div_sigma = elt_multiply(mu_val - y_val, inv_sigma); auto exp_scaled_diff = exp(mu_minus_y_div_sigma); diff --git a/stan/math/opencl/rev/add_diag.hpp b/stan/math/opencl/rev/add_diag.hpp index 00f4d7ca7a9..acc3aa2aeec 100644 --- a/stan/math/opencl/rev/add_diag.hpp +++ b/stan/math/opencl/rev/add_diag.hpp @@ -9,6 +9,8 @@ #include #include +#include + namespace stan { namespace math { @@ -34,20 +36,22 @@ inline auto add_diag(const T_m& mat, const T_a& to_add) { var_value> res = add_diag(value_of(mat), value_of(to_add)); - reverse_pass_callback([mat_arena, to_add_arena, res]() mutable { - if constexpr (is_autodiff_v) { - adjoint_of(mat_arena) += res.adj(); - } - if constexpr (is_autodiff_v) { - if constexpr (!is_stan_scalar::value) { - auto& to_add_adj = to_add_arena.adj(); - to_add_adj += diagonal(res.adj()); - } else { - auto& to_add_adj = to_add_arena.adj(); - to_add_adj += to_add_adj + sum(diagonal(res.adj())); - } - } - }); + reverse_pass_callback( + [state = std::make_tuple(mat_arena, to_add_arena, res)]() mutable { + auto& [mat_arena, to_add_arena, res] = state; + if constexpr (is_autodiff_v) { + adjoint_of(mat_arena) += res.adj(); + } + if constexpr (is_autodiff_v) { + if constexpr (!is_stan_scalar::value) { + auto& to_add_adj = to_add_arena.adj(); + to_add_adj += diagonal(res.adj()); + } else { + auto& to_add_adj = to_add_arena.adj(); + to_add_adj += to_add_adj + sum(diagonal(res.adj())); + } + } + }); return res; } } // namespace math diff --git a/stan/math/opencl/rev/append_col.hpp b/stan/math/opencl/rev/append_col.hpp index 44d735e6e4f..71c0237bfff 100644 --- a/stan/math/opencl/rev/append_col.hpp +++ b/stan/math/opencl/rev/append_col.hpp @@ -12,6 +12,8 @@ #include #include +#include + namespace stan { namespace math { @@ -45,7 +47,9 @@ inline var_value> append_col(T_a&& a, T_b&& b) { return make_callback_var( append_col(value_of(a_arena), value_of(b_arena)), - [a_arena, b_arena](const vari_value>& res) mutable { + [state = std::make_tuple(a_arena, b_arena)]( + const vari_value>& res) mutable { + auto& [a_arena, b_arena] = state; if constexpr (is_autodiff_v) { adjoint_of(a_arena) += block_zero_based( res.adj(), 0, 0, a_arena.rows(), a_arena.cols()); diff --git a/stan/math/opencl/rev/append_row.hpp b/stan/math/opencl/rev/append_row.hpp index 19a418ead62..6fc3fda8824 100644 --- a/stan/math/opencl/rev/append_row.hpp +++ b/stan/math/opencl/rev/append_row.hpp @@ -12,6 +12,8 @@ #include #include +#include + namespace stan { namespace math { @@ -43,7 +45,9 @@ inline var_value> append_row(T_a&& a, T_b&& b) { return make_callback_var( append_row(value_of(a_arena), value_of(b_arena)), - [a_arena, b_arena](const vari_value>& res) mutable { + [state = std::make_tuple(a_arena, b_arena)]( + const vari_value>& res) mutable { + auto& [a_arena, b_arena] = state; if constexpr (is_autodiff_v) { adjoint_of(a_arena) += block_zero_based( res.adj(), 0, 0, a_arena.rows(), a_arena.cols()); diff --git a/stan/math/opencl/rev/mdivide_left_tri_low.hpp b/stan/math/opencl/rev/mdivide_left_tri_low.hpp index 1431e25e49c..7d79f17ee75 100644 --- a/stan/math/opencl/rev/mdivide_left_tri_low.hpp +++ b/stan/math/opencl/rev/mdivide_left_tri_low.hpp @@ -9,6 +9,8 @@ #include #include +#include + namespace stan { namespace math { @@ -39,7 +41,9 @@ inline var_value> mdivide_left_tri_low(T1&& A, T2&& b) { = tri_inverse(value_of(A_arena)); return make_callback_var( A_tri_inv * value_of(b_arena), - [A_arena, b_arena, A_tri_inv](const vari_value>& res) { + [state = std::make_tuple(A_arena, b_arena, A_tri_inv)]( + const vari_value>& res) { + auto& [A_arena, b_arena, A_tri_inv] = state; matrix_cl adjB = transpose(A_tri_inv) * res.adj(); if constexpr (is_autodiff_v) { matrix_cl adjA = adjB * transpose(res.val()); diff --git a/stan/math/opencl/rev/mdivide_right_tri_low.hpp b/stan/math/opencl/rev/mdivide_right_tri_low.hpp index 9450ece53a3..2b4da0f1e36 100644 --- a/stan/math/opencl/rev/mdivide_right_tri_low.hpp +++ b/stan/math/opencl/rev/mdivide_right_tri_low.hpp @@ -9,6 +9,8 @@ #include #include +#include + namespace stan { namespace math { @@ -39,7 +41,9 @@ inline var_value> mdivide_right_tri_low(T2&& b, T1&& A) { = tri_inverse(value_of(A_arena)); return make_callback_var( value_of(b_arena) * A_tri_inv, - [A_arena, b_arena, A_tri_inv](const vari_value>& res) { + [state = std::make_tuple(A_arena, b_arena, A_tri_inv)]( + const vari_value>& res) { + auto& [A_arena, b_arena, A_tri_inv] = state; matrix_cl adjB = res.adj() * transpose(A_tri_inv); if constexpr (is_autodiff_v) { matrix_cl adjA = transpose(res.val()) * adjB; diff --git a/stan/math/opencl/zeros_strict_tri.hpp b/stan/math/opencl/zeros_strict_tri.hpp index 339f987cb59..8e4b9989328 100644 --- a/stan/math/opencl/zeros_strict_tri.hpp +++ b/stan/math/opencl/zeros_strict_tri.hpp @@ -44,7 +44,6 @@ inline void matrix_cl::zeros_strict_tri() try { return; } this->view_ = both(this->view_, invert(matrix_view)); - cl::CommandQueue& cmdQueue = opencl_context.queue(); opencl_kernels::fill_strict_tri(cl::NDRange(this->rows(), this->cols()), *this, 0.0, this->rows(), this->cols(), matrix_view); diff --git a/stan/math/prim/fun/log_softmax.hpp b/stan/math/prim/fun/log_softmax.hpp index d6c9017edb6..8e34955c048 100644 --- a/stan/math/prim/fun/log_softmax.hpp +++ b/stan/math/prim/fun/log_softmax.hpp @@ -7,6 +7,7 @@ #include #include #include +#include namespace stan { namespace math { @@ -53,7 +54,7 @@ inline auto log_softmax(Container&& x) { std::forward(a), [](auto&& v) -> plain_type_t { if (v.size() == 0) { - return v; + return std::forward(v); } return (v.array() - log_sum_exp(v)).matrix(); }); diff --git a/stan/math/prim/fun/softmax.hpp b/stan/math/prim/fun/softmax.hpp index 63a51876a60..a737a2e2d53 100644 --- a/stan/math/prim/fun/softmax.hpp +++ b/stan/math/prim/fun/softmax.hpp @@ -6,6 +6,7 @@ #include #include #include +#include namespace stan { namespace math { @@ -56,7 +57,7 @@ inline auto softmax(Container&& x) { std::forward(a), [](auto&& v) -> plain_type_t { if (v.size() == 0) { - return v; + return std::forward(v); } const auto theta = (v.array() - v.maxCoeff()).exp(); return (theta / theta.sum()).matrix(); diff --git a/stan/math/prim/prob/student_t_qf.hpp b/stan/math/prim/prob/student_t_qf.hpp index 5a4b9dbf920..c2c096de9a1 100644 --- a/stan/math/prim/prob/student_t_qf.hpp +++ b/stan/math/prim/prob/student_t_qf.hpp @@ -72,7 +72,6 @@ template ; - static constexpr const char* function = "student_t_qf"; const size_t max_size_all = max_size(p, nu, mu, sigma); Eigen::Matrix result(max_size_all); diff --git a/stan/math/rev/constraint/lub_constrain.hpp b/stan/math/rev/constraint/lub_constrain.hpp index df5d9f9dd46..662f6668685 100644 --- a/stan/math/rev/constraint/lub_constrain.hpp +++ b/stan/math/rev/constraint/lub_constrain.hpp @@ -8,6 +8,8 @@ #include #include +#include + namespace stan { namespace math { @@ -49,19 +51,21 @@ inline auto lub_constrain(const T& x, const L& lb, const U& ub) { check_less("lub_constrain", "lb", lb_val, ub_val); auto diff = ub_val - lb_val; double inv_logit_x = inv_logit(value_of(x)); - return make_callback_var(diff * inv_logit_x + lb_val, - [x, ub, lb, diff, inv_logit_x](auto& vi) mutable { - if constexpr (is_autodiff_v) { - x.adj() += vi.adj() * diff * inv_logit_x - * (1.0 - inv_logit_x); - } - if constexpr (is_autodiff_v) { - lb.adj() += vi.adj() * (1.0 - inv_logit_x); - } - if constexpr (is_autodiff_v) { - ub.adj() += vi.adj() * inv_logit_x; - } - }); + return make_callback_var( + diff * inv_logit_x + lb_val, + [state + = std::make_tuple(x, ub, lb, diff, inv_logit_x)](auto& vi) mutable { + auto& [x, ub, lb, diff, inv_logit_x] = state; + if constexpr (is_autodiff_v) { + x.adj() += vi.adj() * diff * inv_logit_x * (1.0 - inv_logit_x); + } + if constexpr (is_autodiff_v) { + lb.adj() += vi.adj() * (1.0 - inv_logit_x); + } + if constexpr (is_autodiff_v) { + ub.adj() += vi.adj() * inv_logit_x; + } + }); } } @@ -122,7 +126,9 @@ inline auto lub_constrain(const T& x, const L& lb, const U& ub, lp += (log(diff) + (neg_abs_x - (2.0 * log1p_exp(neg_abs_x)))); return make_callback_var( diff * inv_logit_x + lb_val, - [x, ub, lb, diff, lp, inv_logit_x](auto& vi) mutable { + [state = std::make_tuple(x, ub, lb, diff, lp, inv_logit_x)]( + auto& vi) mutable { + auto& [x, ub, lb, diff, lp, inv_logit_x] = state; if constexpr (is_autodiff_v) { x.adj() += vi.adj() * diff * inv_logit_x * (1.0 - inv_logit_x) + lp.adj() * (1.0 - 2.0 * inv_logit_x); @@ -167,7 +173,9 @@ inline auto lub_constrain(const T& x, const L& lb, const U& ub) { const auto diff = ub_val - lb_val; auto inv_logit_x = to_arena(inv_logit(arena_x.val().array())); arena_t ret = diff * inv_logit_x + lb_val; - reverse_pass_callback([arena_x, ub, lb, ret, diff, inv_logit_x]() mutable { + reverse_pass_callback([state = std::make_tuple(arena_x, ub, lb, ret, diff, + inv_logit_x)]() mutable { + auto& [arena_x, ub, lb, ret, diff, inv_logit_x] = state; if constexpr (is_autodiff_v) { arena_x.adj().array() += ret.adj().array() * diff * inv_logit_x * (1.0 - inv_logit_x); @@ -212,7 +220,9 @@ inline auto lub_constrain(const T& x, const L& lb, const U& ub, auto inv_logit_x = to_arena(inv_logit(value_of(arena_x).array())); arena_t ret = diff * inv_logit_x + lb_val; reverse_pass_callback( - [arena_x, ub, lb, ret, lp, diff, inv_logit_x]() mutable { + [state = std::make_tuple(arena_x, ub, lb, ret, lp, diff, + inv_logit_x)]() mutable { + auto& [arena_x, ub, lb, ret, lp, diff, inv_logit_x] = state; if constexpr (is_autodiff_v) { arena_x.adj().array() += ret.adj().array() * diff * inv_logit_x * (1.0 - inv_logit_x) @@ -262,8 +272,10 @@ inline auto lub_constrain(const T& x, const L& lb, const U& ub) { auto inv_logit_x = to_arena(inv_logit(value_of(arena_x).array())); arena_t ret = (is_lb_inf).select( ub_val - value_of(arena_x).array().exp(), diff * inv_logit_x + lb_val); - reverse_pass_callback([arena_x, ub, arena_lb, ret, diff, inv_logit_x, - is_lb_inf]() mutable { + reverse_pass_callback([state = std::make_tuple(arena_x, ub, arena_lb, ret, + diff, inv_logit_x, + is_lb_inf)]() mutable { + auto& [arena_x, ub, arena_lb, ret, diff, inv_logit_x, is_lb_inf] = state; if constexpr (is_autodiff_v) { arena_x.adj().array() += (is_lb_inf).select( ret.adj().array() * -value_of(arena_x).array().exp(), @@ -316,29 +328,33 @@ inline auto lub_constrain(const T& x, const L& lb, const U& ub, .select(arena_x_val, log(diff) + (neg_abs_x - (2.0 * log1p_exp(neg_abs_x)))) .sum(); - reverse_pass_callback([arena_x, arena_x_val, ub, arena_lb, ret, lp, diff, - inv_logit_x, is_lb_inf]() mutable { - const auto lp_adj = lp.adj(); - if constexpr (is_autodiff_v) { - const auto x_sign = arena_x_val.sign().eval(); - arena_x.adj().array() += (is_lb_inf).select( - ret.adj().array() * -arena_x_val.exp() + lp_adj, - ret.adj().array() * diff * inv_logit_x * (1.0 - inv_logit_x) - + lp.adj() * (1.0 - 2.0 * inv_logit_x)); - } - if constexpr (is_autodiff_v) { - arena_lb.adj().array() - += (is_lb_inf).select(0, ret.adj().array() * (1.0 - inv_logit_x) - + -(1.0 / diff) * lp_adj); - } - if constexpr (is_autodiff_v) { - ub.adj() - += (is_lb_inf) - .select(ret.adj().array(), ret.adj().array() * inv_logit_x - + (1.0 / diff) * lp_adj) - .sum(); - } - }); + reverse_pass_callback( + [state = std::make_tuple(arena_x, arena_x_val, ub, arena_lb, ret, lp, + diff, inv_logit_x, is_lb_inf)]() mutable { + auto& [arena_x, arena_x_val, ub, arena_lb, ret, lp, diff, inv_logit_x, + is_lb_inf] + = state; + const auto lp_adj = lp.adj(); + if constexpr (is_autodiff_v) { + const auto x_sign = arena_x_val.sign().eval(); + arena_x.adj().array() += (is_lb_inf).select( + ret.adj().array() * -arena_x_val.exp() + lp_adj, + ret.adj().array() * diff * inv_logit_x * (1.0 - inv_logit_x) + + lp.adj() * (1.0 - 2.0 * inv_logit_x)); + } + if constexpr (is_autodiff_v) { + arena_lb.adj().array() + += (is_lb_inf).select(0, ret.adj().array() * (1.0 - inv_logit_x) + + -(1.0 / diff) * lp_adj); + } + if constexpr (is_autodiff_v) { + ub.adj() += (is_lb_inf) + .select(ret.adj().array(), + ret.adj().array() * inv_logit_x + + (1.0 / diff) * lp_adj) + .sum(); + } + }); return ret_type(ret); } } @@ -369,24 +385,28 @@ inline auto lub_constrain(const T& x, const L& lb, const U& ub) { auto inv_logit_x = to_arena(inv_logit(arena_x_val.array())); arena_t ret = (is_ub_inf).select( arena_x_val.array().exp() + lb_val, diff * inv_logit_x + lb_val); - reverse_pass_callback([arena_x, arena_x_val, arena_ub, lb, ret, is_ub_inf, - inv_logit_x, diff]() mutable { - if constexpr (is_autodiff_v) { - arena_x.adj().array() += (is_ub_inf).select( - ret.adj().array() * arena_x_val.array().exp(), - ret.adj().array() * diff * inv_logit_x * (1.0 - inv_logit_x)); - } - if constexpr (is_autodiff_v) { - lb.adj() += (is_ub_inf) - .select(ret.adj().array(), - ret.adj().array() * (1.0 - inv_logit_x)) - .sum(); - } - if constexpr (is_autodiff_v) { - arena_ub.adj().array() - += (is_ub_inf).select(0, ret.adj().array() * inv_logit_x); - } - }); + reverse_pass_callback( + [state = std::make_tuple(arena_x, arena_x_val, arena_ub, lb, ret, + is_ub_inf, inv_logit_x, diff)]() mutable { + auto& [arena_x, arena_x_val, arena_ub, lb, ret, is_ub_inf, + inv_logit_x, diff] + = state; + if constexpr (is_autodiff_v) { + arena_x.adj().array() += (is_ub_inf).select( + ret.adj().array() * arena_x_val.array().exp(), + ret.adj().array() * diff * inv_logit_x * (1.0 - inv_logit_x)); + } + if constexpr (is_autodiff_v) { + lb.adj() += (is_ub_inf) + .select(ret.adj().array(), + ret.adj().array() * (1.0 - inv_logit_x)) + .sum(); + } + if constexpr (is_autodiff_v) { + arena_ub.adj().array() + += (is_ub_inf).select(0, ret.adj().array() * inv_logit_x); + } + }); return ret_type(ret); } } @@ -423,27 +443,31 @@ inline auto lub_constrain(const T& x, const L& lb, const U& ub, auto inv_logit_x = to_arena(inv_logit(arena_x_val.array())); arena_t ret = (is_ub_inf).select( arena_x_val.array().exp() + lb_val, diff * inv_logit_x + lb_val); - reverse_pass_callback([arena_x, arena_x_val, diff, inv_logit_x, arena_ub, - lb, ret, lp, is_ub_inf]() mutable { - const auto lp_adj = lp.adj(); - if constexpr (is_autodiff_v) { - arena_x.adj().array() += (is_ub_inf).select( - ret.adj().array() * arena_x_val.array().exp() + lp_adj, - ret.adj().array() * diff * inv_logit_x * (1.0 - inv_logit_x) - + lp.adj() * (1.0 - 2.0 * inv_logit_x)); - } - if constexpr (is_autodiff_v) { - lb.adj() += (is_ub_inf) - .select(ret.adj().array(), - ret.adj().array() * (1.0 - inv_logit_x) - + -(1.0 / diff) * lp_adj) - .sum(); - } - if constexpr (is_autodiff_v) { - arena_ub.adj().array() += (is_ub_inf).select( - 0, ret.adj().array() * inv_logit_x + (1.0 / diff) * lp_adj); - } - }); + reverse_pass_callback( + [state = std::make_tuple(arena_x, arena_x_val, diff, inv_logit_x, + arena_ub, lb, ret, lp, is_ub_inf)]() mutable { + auto& [arena_x, arena_x_val, diff, inv_logit_x, arena_ub, lb, ret, lp, + is_ub_inf] + = state; + const auto lp_adj = lp.adj(); + if constexpr (is_autodiff_v) { + arena_x.adj().array() += (is_ub_inf).select( + ret.adj().array() * arena_x_val.array().exp() + lp_adj, + ret.adj().array() * diff * inv_logit_x * (1.0 - inv_logit_x) + + lp.adj() * (1.0 - 2.0 * inv_logit_x)); + } + if constexpr (is_autodiff_v) { + lb.adj() += (is_ub_inf) + .select(ret.adj().array(), + ret.adj().array() * (1.0 - inv_logit_x) + + -(1.0 / diff) * lp_adj) + .sum(); + } + if constexpr (is_autodiff_v) { + arena_ub.adj().array() += (is_ub_inf).select( + 0, ret.adj().array() * inv_logit_x + (1.0 / diff) * lp_adj); + } + }); return ret_type(ret); } } @@ -477,9 +501,13 @@ inline auto lub_constrain(const T& x, const L& lb, const U& ub) { ub_val - arena_x.val().array().exp(), (is_ub_inf).select(arena_x_val.array().exp() + lb_val, diff * inv_logit_x + lb_val))); - reverse_pass_callback([arena_x, arena_x_val, inv_logit_x, arena_ub, arena_lb, - diff, ret, is_ub_inf, is_lb_inf, - is_lb_ub_inf]() mutable { + reverse_pass_callback([state = std::make_tuple( + arena_x, arena_x_val, inv_logit_x, arena_ub, + arena_lb, diff, ret, is_ub_inf, is_lb_inf, + is_lb_ub_inf)]() mutable { + auto& [arena_x, arena_x_val, inv_logit_x, arena_ub, arena_lb, diff, ret, + is_ub_inf, is_lb_inf, is_lb_ub_inf] + = state; // The most likely case is neither of them are infinity const bool is_none_inf = !(is_lb_inf.any() || is_ub_inf.any()); if (is_none_inf) { @@ -560,9 +588,13 @@ inline auto lub_constrain(const T& x, const L& lb, const U& ub, arena_x_val.array(), log(diff) + (neg_abs_x - (2.0 * log1p_exp(neg_abs_x))))) .sum(); - reverse_pass_callback([arena_x, arena_x_val, inv_logit_x, arena_ub, arena_lb, - diff, ret, is_ub_inf, is_lb_inf, is_lb_ub_inf, - lp]() mutable { + reverse_pass_callback([state = std::make_tuple( + arena_x, arena_x_val, inv_logit_x, arena_ub, + arena_lb, diff, ret, is_ub_inf, is_lb_inf, + is_lb_ub_inf, lp)]() mutable { + auto& [arena_x, arena_x_val, inv_logit_x, arena_ub, arena_lb, diff, ret, + is_ub_inf, is_lb_inf, is_lb_ub_inf, lp] + = state; const auto lp_adj = lp.adj(); // The most likely case is neither of them are infinity const bool is_none_inf = !(is_lb_inf.any() || is_ub_inf.any()); diff --git a/stan/math/rev/fun/fma.hpp b/stan/math/rev/fun/fma.hpp index 517eb3481d6..56e5fbcd097 100644 --- a/stan/math/rev/fun/fma.hpp +++ b/stan/math/rev/fun/fma.hpp @@ -7,6 +7,8 @@ #include #include +#include + namespace stan { namespace math { @@ -194,7 +196,8 @@ namespace internal { template * = nullptr> inline auto fma_reverse_pass(T1& arena_x, T2& arena_y, T3& arena_z, T4& ret) { - return [arena_x, arena_y, arena_z, ret]() mutable { + return [state = std::make_tuple(arena_x, arena_y, arena_z, ret)]() mutable { + auto& [arena_x, arena_y, arena_z, ret] = state; if constexpr (is_autodiff_v) { arena_x.adj().array() += ret.adj().array() * value_of(arena_y).array(); } @@ -214,7 +217,8 @@ template * = nullptr, require_stan_scalar_t* = nullptr> inline auto fma_reverse_pass(T1& arena_x, T2& arena_y, T3& arena_z, T4& ret) { - return [arena_x, arena_y, arena_z, ret]() mutable { + return [state = std::make_tuple(arena_x, arena_y, arena_z, ret)]() mutable { + auto& [arena_x, arena_y, arena_z, ret] = state; if constexpr (is_autodiff_v) { arena_x.adj() += (ret.adj().array() * value_of(arena_y).array()).sum(); } @@ -234,7 +238,8 @@ template * = nullptr, require_stan_scalar_t* = nullptr> inline auto fma_reverse_pass(T1& arena_x, T2& arena_y, T3& arena_z, T4& ret) { - return [arena_x, arena_y, arena_z, ret]() mutable { + return [state = std::make_tuple(arena_x, arena_y, arena_z, ret)]() mutable { + auto& [arena_x, arena_y, arena_z, ret] = state; if constexpr (is_autodiff_v) { arena_x.adj().array() += ret.adj().array() * value_of(arena_y); } @@ -254,7 +259,8 @@ template * = nullptr, require_all_stan_scalar_t* = nullptr> inline auto fma_reverse_pass(T1& arena_x, T2& arena_y, T3& arena_z, T4& ret) { - return [arena_x, arena_y, arena_z, ret]() mutable { + return [state = std::make_tuple(arena_x, arena_y, arena_z, ret)]() mutable { + auto& [arena_x, arena_y, arena_z, ret] = state; if constexpr (is_autodiff_v) { arena_x.adj() += (ret.adj().array() * value_of(arena_y)).sum(); } @@ -274,7 +280,8 @@ template * = nullptr, require_stan_scalar_t* = nullptr> inline auto fma_reverse_pass(T1& arena_x, T2& arena_y, T3& arena_z, T4& ret) { - return [arena_x, arena_y, arena_z, ret]() mutable { + return [state = std::make_tuple(arena_x, arena_y, arena_z, ret)]() mutable { + auto& [arena_x, arena_y, arena_z, ret] = state; if constexpr (is_autodiff_v) { arena_x.adj().array() += ret.adj().array() * value_of(arena_y).array(); } @@ -294,7 +301,8 @@ template * = nullptr, require_all_stan_scalar_t* = nullptr> inline auto fma_reverse_pass(T1& arena_x, T2& arena_y, T3& arena_z, T4& ret) { - return [arena_x, arena_y, arena_z, ret]() mutable { + return [state = std::make_tuple(arena_x, arena_y, arena_z, ret)]() mutable { + auto& [arena_x, arena_y, arena_z, ret] = state; if constexpr (is_autodiff_v) { arena_x.adj() += (ret.adj().array() * value_of(arena_y).array()).sum(); } @@ -314,7 +322,8 @@ template * = nullptr, require_all_stan_scalar_t* = nullptr> inline auto fma_reverse_pass(T1& arena_x, T2& arena_y, T3& arena_z, T4& ret) { - return [arena_x, arena_y, arena_z, ret]() mutable { + return [state = std::make_tuple(arena_x, arena_y, arena_z, ret)]() mutable { + auto& [arena_x, arena_y, arena_z, ret] = state; if constexpr (is_autodiff_v) { arena_x.adj().array() += ret.adj().array() * value_of(arena_y); } diff --git a/stan/math/rev/fun/inv_inc_beta.hpp b/stan/math/rev/fun/inv_inc_beta.hpp index 64f6b75e75f..9e0af10ea70 100644 --- a/stan/math/rev/fun/inv_inc_beta.hpp +++ b/stan/math/rev/fun/inv_inc_beta.hpp @@ -17,6 +17,7 @@ #include #include #include +#include namespace stan { namespace math { @@ -62,7 +63,9 @@ inline var inv_inc_beta(const T1& a, const T2& b, const T3& p) { double b_val = value_of(b); double p_val = value_of(p); double w = inv_inc_beta(a_val, b_val, p_val); - return make_callback_var(w, [a, b, p, a_val, b_val, w](auto& vi) { + return make_callback_var(w, [state = std::make_tuple(a, b, p, a_val, b_val, + w)](auto& vi) { + auto& [a, b, p, a_val, b_val, w] = state; double log_w = log(w); double log1m_w = log1m(w); double one_m_a = 1 - a_val; diff --git a/stan/math/rev/prob/student_t_qf.hpp b/stan/math/rev/prob/student_t_qf.hpp index e44d4cbbc9c..d9ff3e0314b 100644 --- a/stan/math/rev/prob/student_t_qf.hpp +++ b/stan/math/rev/prob/student_t_qf.hpp @@ -39,7 +39,6 @@ inline var student_t_qf(const T_p& p, const T_nu& nu, const T_mu& mu, const double p_val_flip = p_val < 0.5 ? p_val : 1.0 - p_val; const double p_sign = p_val < 0.5 ? -1.0 : 1.0; const double ibeta_arg = inv_inc_beta(0.5 * nu_val, 0.5, 2 * p_val_flip); - const double sqrt_inv_ibeta_m1 = sqrt(inv(ibeta_arg) - 1.0); double ret_val = mu_val + p_sign * sigma_val * sqrt(nu_val) * sqrt(-1.0 + 1.0 / ibeta_arg); @@ -49,7 +48,6 @@ inline var student_t_qf(const T_p& p, const T_nu& nu, const T_mu& mu, const double mu_val = value_of(mu); const double sigma_val = value_of(sigma); const double nu_val = value_of(nu); - const double p_val_flip = p_val < 0.5 ? p_val : 1.0 - p_val; const double p_sign = p_val < 0.5 ? -1.0 : 1.0; const double sqrt_inv_ibeta_m1 = sqrt(inv(ibeta_arg) - 1.0); if constexpr (is_autodiff_v) { diff --git a/test/unit/math/opencl/kernel_generator/calc_if_test.cpp b/test/unit/math/opencl/kernel_generator/calc_if_test.cpp index 0343ae0a350..89c233eb81d 100644 --- a/test/unit/math/opencl/kernel_generator/calc_if_test.cpp +++ b/test/unit/math/opencl/kernel_generator/calc_if_test.cpp @@ -50,7 +50,6 @@ TEST(KernelGenerator, calc_if_multi_result_pass_test) { } TEST(KernelGenerator, calc_if_multi_result_no_output_test) { - double eps = 0; MatrixXd m(2, 2); m << 1, 2, 3, 4; matrix_cl m_cl(m); diff --git a/test/unit/math/opencl/kernel_generator/colwise_reduction_test.cpp b/test/unit/math/opencl/kernel_generator/colwise_reduction_test.cpp index 0ee89e52094..4982efa00e2 100644 --- a/test/unit/math/opencl/kernel_generator/colwise_reduction_test.cpp +++ b/test/unit/math/opencl/kernel_generator/colwise_reduction_test.cpp @@ -59,6 +59,26 @@ TEST(KernelGenerator, colwise_prod_test) { EXPECT_MATRIX_NEAR(correct, res, 1e-9); } +TEST(KernelGenerator, colwise_reductions_rvalue_input) { + MatrixXd m(3, 2); + m << 1.1, 1.2, 1.3, 1.4, 1.5, 1.6; + + matrix_cl res_cl = stan::math::colwise_sum(matrix_cl(m)); + MatrixXd res = stan::math::from_matrix_cl(res_cl).colwise().sum(); + MatrixXd correct = m.colwise().sum(); + EXPECT_MATRIX_NEAR(correct, res, 1e-9); + + matrix_cl prod_cl = stan::math::colwise_prod(matrix_cl(m)); + MatrixXd prod = stan::math::from_matrix_cl(prod_cl).colwise().prod(); + MatrixXd correct_prod = m.colwise().prod(); + EXPECT_MATRIX_NEAR(correct_prod, prod, 1e-9); + + matrix_cl max_cl = stan::math::colwise_max(matrix_cl(m)); + MatrixXd max = stan::math::from_matrix_cl(max_cl).colwise().maxCoeff(); + MatrixXd correct_max = m.colwise().maxCoeff(); + EXPECT_MATRIX_NEAR(correct_max, max, 1e-9); +} + TEST(KernelGenerator, colwise_min_test) { MatrixXd m(3, 2); m << 1.1, 1.2, 1.3, 1.4, 1.5, 1.6; diff --git a/test/unit/math/opencl/kernel_generator/reduction_2d_test.cpp b/test/unit/math/opencl/kernel_generator/reduction_2d_test.cpp index 0569d901a1f..ca92f3a7ee6 100644 --- a/test/unit/math/opencl/kernel_generator/reduction_2d_test.cpp +++ b/test/unit/math/opencl/kernel_generator/reduction_2d_test.cpp @@ -57,6 +57,23 @@ TEST(KernelGenerator, prod_2d_test) { EXPECT_NEAR(correct, res, 1e-9); } +TEST(KernelGenerator, reductions_2d_rvalue_input) { + MatrixXd m(3, 2); + m << 1.1, 1.2, 1.3, 1.4, 1.5, 1.6; + + matrix_cl res_cl = stan::math::sum_2d(matrix_cl(m)); + double res = stan::math::from_matrix_cl(res_cl).sum(); + EXPECT_NEAR(m.sum(), res, 1e-9); + + matrix_cl prod_cl = stan::math::prod_2d(matrix_cl(m)); + double prod = stan::math::from_matrix_cl(prod_cl).prod(); + EXPECT_NEAR(m.prod(), prod, 1e-9); + + matrix_cl max_cl = stan::math::max_2d(matrix_cl(m)); + double max = stan::math::from_matrix_cl(max_cl).maxCoeff(); + EXPECT_NEAR(m.maxCoeff(), max, 1e-9); +} + TEST(KernelGenerator, min_2d_test) { MatrixXd m(3, 2); m << 1.1, 1.2, 1.3, 1.4, 1.5, 1.6; diff --git a/test/unit/math/opencl/prim/mdivide_left_tri_low_test.cpp b/test/unit/math/opencl/prim/mdivide_left_tri_low_test.cpp index c0151631ed1..93e5d029013 100644 --- a/test/unit/math/opencl/prim/mdivide_left_tri_low_test.cpp +++ b/test/unit/math/opencl/prim/mdivide_left_tri_low_test.cpp @@ -49,7 +49,6 @@ inline void mdivide_left_tri_low_Ab_test(int size) { stan::math::matrix_cl m0_cl(m1, stan::math::matrix_cl_view::Lower); stan::math::matrix_d m1_cl = stan::math::from_matrix_cl( stan::math::mdivide_left_tri_low(m0_cl, m0_cl)); - double max_error = 0; for (int i = 0; i < m1_cpu.size(); i++) { stan::test::expect_near_rel("mdivide_left_tri_low (OpenCL)", m1_cpu(i), m1_cl(i)); @@ -77,7 +76,6 @@ inline void mdivide_left_tri_low_A_test(int size) { stan::math::matrix_cl m1_A_cl(m1, stan::math::matrix_cl_view::Lower); m1_cl = stan::math::from_matrix_cl(stan::math::mdivide_left_tri_low(m1_A_cl)); - double max_error = 0; for (int i = 0; i < size; i++) { for (int j = 0; j <= i; j++) { stan::test::expect_near_rel("mdivide_left_tri_low (OpenCL)", m1_cpu(i, j), diff --git a/test/unit/math/opencl/prim/mdivide_right_tri_low_test.cpp b/test/unit/math/opencl/prim/mdivide_right_tri_low_test.cpp index a339cfbb107..d59099dc3cc 100644 --- a/test/unit/math/opencl/prim/mdivide_right_tri_low_test.cpp +++ b/test/unit/math/opencl/prim/mdivide_right_tri_low_test.cpp @@ -48,7 +48,6 @@ inline void mdivide_right_tri_low_Ab_test(int size) { stan::math::matrix_cl m1_A_cl(m1, stan::math::matrix_cl_view::Lower); stan::math::matrix_d m1_cl = stan::math::from_matrix_cl( stan::math::mdivide_right_tri_low(m1_A_cl, m1_A_cl)); - double max_error = 0; for (int i = 0; i < m1_cpu.size(); i++) { stan::test::expect_near_rel("mdivide_right_tri_low (OpenCL)", m1_cpu(i), m1_cl(i)); diff --git a/test/unit/math/opencl/rev/append_array_test.cpp b/test/unit/math/opencl/rev/append_array_test.cpp index 3b2252a767c..4e9023f5e72 100644 --- a/test/unit/math/opencl/rev/append_array_test.cpp +++ b/test/unit/math/opencl/rev/append_array_test.cpp @@ -15,8 +15,6 @@ TEST(OpenCLAppendArray, prim_rev_values_small) { } TEST(OpenCLAppendArray, prim_rev_size_0) { - int N = 0; - std::vector a{}; std::vector b{}; stan::math::test::compare_cpu_opencl_prim_rev(append_array_functor, a, b); diff --git a/test/unit/math/opencl/rev/bernoulli_cdf_test.cpp b/test/unit/math/opencl/rev/bernoulli_cdf_test.cpp index 107708c0d80..79975958028 100644 --- a/test/unit/math/opencl/rev/bernoulli_cdf_test.cpp +++ b/test/unit/math/opencl/rev/bernoulli_cdf_test.cpp @@ -45,7 +45,6 @@ auto bernoulli_cdf_functor = [](const auto& n, const auto& theta) { TEST(ProbDistributionsBernoulliCdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; std::vector n{0, 1, 3}; Eigen::VectorXd theta(N); @@ -59,7 +58,6 @@ TEST(ProbDistributionsBernoulliCdf, opencl_matches_cpu_small) { TEST(ProbDistributionsBernoulliCdf, opencl_matches_cpu_small_n_negative) { int N = 3; - int M = 2; std::vector n{0, 1, -5}; Eigen::VectorXd theta(N); @@ -83,8 +81,6 @@ TEST(ProbDistributionsBernoulliCdf, opencl_broadcast_n) { } TEST(ProbDistributionsBernoulliCdf, opencl_broadcast_theta) { - int N = 3; - std::vector n{0, 1, 0}; double theta_scal = 0.4; diff --git a/test/unit/math/opencl/rev/bernoulli_lccdf_test.cpp b/test/unit/math/opencl/rev/bernoulli_lccdf_test.cpp index 732b1e62f2c..51a2e43a4a6 100644 --- a/test/unit/math/opencl/rev/bernoulli_lccdf_test.cpp +++ b/test/unit/math/opencl/rev/bernoulli_lccdf_test.cpp @@ -45,7 +45,6 @@ auto bernoulli_lccdf_functor = [](const auto& n, const auto& theta) { TEST(ProbDistributionsBernoulliLccdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; std::vector n{0, 0, 0}; Eigen::VectorXd theta(N); @@ -59,7 +58,6 @@ TEST(ProbDistributionsBernoulliLccdf, opencl_matches_cpu_small) { TEST(ProbDistributionsBernoulliLccdf, opencl_matches_cpu_small_n_negative) { int N = 3; - int M = 2; std::vector n{0, 0, -5}; Eigen::VectorXd theta(N); @@ -73,7 +71,6 @@ TEST(ProbDistributionsBernoulliLccdf, opencl_matches_cpu_small_n_negative) { TEST(ProbDistributionsBernoulliLccdf, opencl_matches_cpu_small_n_over_one) { int N = 3; - int M = 2; std::vector n{0, 1, 5}; Eigen::VectorXd theta(N); @@ -97,8 +94,6 @@ TEST(ProbDistributionsBernoulliLccdf, opencl_broadcast_n) { } TEST(ProbDistributionsBernoulliLccdf, opencl_broadcast_theta) { - int N = 3; - std::vector n{0, 0, 0}; double theta_scal = 0.4; diff --git a/test/unit/math/opencl/rev/bernoulli_logit_lpmf_test.cpp b/test/unit/math/opencl/rev/bernoulli_logit_lpmf_test.cpp index 401b88349f7..b6d86ae5e90 100644 --- a/test/unit/math/opencl/rev/bernoulli_logit_lpmf_test.cpp +++ b/test/unit/math/opencl/rev/bernoulli_logit_lpmf_test.cpp @@ -48,7 +48,6 @@ auto bernoulli_logit_lpmf_functor_propto TEST(ProbDistributionsBernoulliLogit, opencl_matches_cpu_small) { int N = 3; - int M = 2; std::vector n{0, 1, 1}; Eigen::VectorXd theta(N); @@ -78,8 +77,6 @@ TEST(ProbDistributionsBernoulliLogit, opencl_broadcast_n) { } TEST(ProbDistributionsBernoulliLogit, opencl_broadcast_theta) { - int N = 3; - std::vector n{0, 1, 0}; double theta = 0.4; diff --git a/test/unit/math/opencl/rev/bernoulli_lpmf_test.cpp b/test/unit/math/opencl/rev/bernoulli_lpmf_test.cpp index 2dd8ae41530..7351e45d7a2 100644 --- a/test/unit/math/opencl/rev/bernoulli_lpmf_test.cpp +++ b/test/unit/math/opencl/rev/bernoulli_lpmf_test.cpp @@ -47,7 +47,6 @@ auto bernoulli_lpmf_functor_propto = [](const auto& n, const auto& theta) { TEST(ProbDistributionsBernoulli, opencl_matches_cpu_small) { int N = 3; - int M = 2; std::vector n{0, 1, 0}; Eigen::VectorXd theta(N); @@ -77,8 +76,6 @@ TEST(ProbDistributionsBernoulli, opencl_broadcast_n) { } TEST(ProbDistributionsBernoulli, opencl_broadcast_theta) { - int N = 3; - std::vector n{0, 1, 0}; double theta_scal = 0.4; diff --git a/test/unit/math/opencl/rev/beta_lpdf_test.cpp b/test/unit/math/opencl/rev/beta_lpdf_test.cpp index dd0c3fce94e..a12ef5a5c80 100644 --- a/test/unit/math/opencl/rev/beta_lpdf_test.cpp +++ b/test/unit/math/opencl/rev/beta_lpdf_test.cpp @@ -85,7 +85,6 @@ auto beta_lpdf_functor_propto TEST(ProbDistributionsBeta, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0, 0.5, 1; diff --git a/test/unit/math/opencl/rev/beta_proportion_lpdf_test.cpp b/test/unit/math/opencl/rev/beta_proportion_lpdf_test.cpp index 6709ea03e42..157d598bc09 100644 --- a/test/unit/math/opencl/rev/beta_proportion_lpdf_test.cpp +++ b/test/unit/math/opencl/rev/beta_proportion_lpdf_test.cpp @@ -65,7 +65,6 @@ auto beta_proportion_lpdf_functor_propto TEST(ProbDistributionsBetaProportion, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; diff --git a/test/unit/math/opencl/rev/binomial_logit_lpmf_test.cpp b/test/unit/math/opencl/rev/binomial_logit_lpmf_test.cpp index 92b85841292..4ee784145d0 100644 --- a/test/unit/math/opencl/rev/binomial_logit_lpmf_test.cpp +++ b/test/unit/math/opencl/rev/binomial_logit_lpmf_test.cpp @@ -114,8 +114,6 @@ TEST(ProbDistributionsBinomialLogit, opencl_broadcast_N) { } TEST(ProbDistributionsBinomialLogit, opencl_broadcast_alpha) { - int N = 3; - std::vector n{0, 1, 12}; std::vector m{0, 1, 123}; double alpha_scal = 0.4; diff --git a/test/unit/math/opencl/rev/binomial_lpmf_test.cpp b/test/unit/math/opencl/rev/binomial_lpmf_test.cpp index 7be5166eb86..e84dc9c6eab 100644 --- a/test/unit/math/opencl/rev/binomial_lpmf_test.cpp +++ b/test/unit/math/opencl/rev/binomial_lpmf_test.cpp @@ -119,8 +119,6 @@ TEST(ProbDistributionsBinomial, opencl_broadcast_N) { } TEST(ProbDistributionsBinomial, opencl_broadcast_theta) { - int N = 3; - std::vector n{0, 1, 12}; std::vector m{0, 1, 123}; double theta_scal = 0.4; diff --git a/test/unit/math/opencl/rev/cauchy_lccdf_test.cpp b/test/unit/math/opencl/rev/cauchy_lccdf_test.cpp index 342bf34c129..29fa178f33b 100644 --- a/test/unit/math/opencl/rev/cauchy_lccdf_test.cpp +++ b/test/unit/math/opencl/rev/cauchy_lccdf_test.cpp @@ -64,7 +64,6 @@ auto cauchy_lccdf_functor TEST(ProbDistributionsCauchyLccdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, -0.6, 1.0; diff --git a/test/unit/math/opencl/rev/cauchy_lpdf_test.cpp b/test/unit/math/opencl/rev/cauchy_lpdf_test.cpp index 7623819af84..a31ab65c0fe 100644 --- a/test/unit/math/opencl/rev/cauchy_lpdf_test.cpp +++ b/test/unit/math/opencl/rev/cauchy_lpdf_test.cpp @@ -67,7 +67,6 @@ auto cauchy_lpdf_functor_propto TEST(ProbDistributionsCauchy, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; diff --git a/test/unit/math/opencl/rev/double_exponential_cdf_test.cpp b/test/unit/math/opencl/rev/double_exponential_cdf_test.cpp index fc441df05e9..91f9ed8afd6 100644 --- a/test/unit/math/opencl/rev/double_exponential_cdf_test.cpp +++ b/test/unit/math/opencl/rev/double_exponential_cdf_test.cpp @@ -63,7 +63,6 @@ auto double_exponential_cdf_functor TEST(ProbDistributionsDoubleExponentialCdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << -0.3, 1.8, 1.4; diff --git a/test/unit/math/opencl/rev/double_exponential_lccdf_test.cpp b/test/unit/math/opencl/rev/double_exponential_lccdf_test.cpp index eca87505ee9..3edb3b9bb21 100644 --- a/test/unit/math/opencl/rev/double_exponential_lccdf_test.cpp +++ b/test/unit/math/opencl/rev/double_exponential_lccdf_test.cpp @@ -66,7 +66,6 @@ auto double_exponential_lccdf_functor TEST(ProbDistributionsDoubleExponentialLccdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 2.8, 1.0; diff --git a/test/unit/math/opencl/rev/double_exponential_lcdf_test.cpp b/test/unit/math/opencl/rev/double_exponential_lcdf_test.cpp index 6e8f2f57f90..88a0d8e6aa1 100644 --- a/test/unit/math/opencl/rev/double_exponential_lcdf_test.cpp +++ b/test/unit/math/opencl/rev/double_exponential_lcdf_test.cpp @@ -63,7 +63,6 @@ auto double_exponential_lcdf_functor TEST(ProbDistributionsDoubleExponentialLcdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.7; diff --git a/test/unit/math/opencl/rev/exp_mod_normal_cdf_test.cpp b/test/unit/math/opencl/rev/exp_mod_normal_cdf_test.cpp index 33c53bb16a7..711b9e38a3e 100644 --- a/test/unit/math/opencl/rev/exp_mod_normal_cdf_test.cpp +++ b/test/unit/math/opencl/rev/exp_mod_normal_cdf_test.cpp @@ -88,7 +88,6 @@ auto exp_mod_normal_cdf_functor TEST(ProbDistributionsDoubleExpModNormalCdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << -0.3, 1.8, 1.4; @@ -109,7 +108,6 @@ TEST(ProbDistributionsDoubleExpModNormalCdf, opencl_matches_cpu_small) { TEST(ProbDistributionsDoubleExpModNormalCdf, opencl_matches_cpu_small_y_neg_inf) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << -INFINITY, 1.8, 1.4; diff --git a/test/unit/math/opencl/rev/exp_mod_normal_lccdf_test.cpp b/test/unit/math/opencl/rev/exp_mod_normal_lccdf_test.cpp index 677ce2a2f89..d03eae230b3 100644 --- a/test/unit/math/opencl/rev/exp_mod_normal_lccdf_test.cpp +++ b/test/unit/math/opencl/rev/exp_mod_normal_lccdf_test.cpp @@ -88,7 +88,6 @@ auto exp_mod_normal_lccdf_functor TEST(ProbDistributionsDoubleExpModNormalLccdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << -0.3, 1.8, 1.4; @@ -109,7 +108,6 @@ TEST(ProbDistributionsDoubleExpModNormalLccdf, opencl_matches_cpu_small) { TEST(ProbDistributionsDoubleExpModNormalLccdf, opencl_matches_cpu_small_y_pos_inf) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << -0.3, 1.8, INFINITY; @@ -130,7 +128,6 @@ TEST(ProbDistributionsDoubleExpModNormalLccdf, TEST(ProbDistributionsDoubleExpModNormalLccdf, opencl_matches_cpu_small_y_neg_inf) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << -0.3, 1.8, -INFINITY; diff --git a/test/unit/math/opencl/rev/exponential_cdf_test.cpp b/test/unit/math/opencl/rev/exponential_cdf_test.cpp index 405ba03f6a5..8002466f000 100644 --- a/test/unit/math/opencl/rev/exponential_cdf_test.cpp +++ b/test/unit/math/opencl/rev/exponential_cdf_test.cpp @@ -48,7 +48,6 @@ auto exponential_cdf_functor = [](const auto& y, const auto& beta) { TEST(ProbDistributionsExponentialCdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; diff --git a/test/unit/math/opencl/rev/exponential_lccdf_test.cpp b/test/unit/math/opencl/rev/exponential_lccdf_test.cpp index 99cea24f1d4..23386c90562 100644 --- a/test/unit/math/opencl/rev/exponential_lccdf_test.cpp +++ b/test/unit/math/opencl/rev/exponential_lccdf_test.cpp @@ -48,7 +48,6 @@ auto exponential_lccdf_functor = [](const auto& y, const auto& beta) { TEST(ProbDistributionsExponentialLccdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; diff --git a/test/unit/math/opencl/rev/exponential_lcdf_test.cpp b/test/unit/math/opencl/rev/exponential_lcdf_test.cpp index 25d47a0248f..3a236b149a0 100644 --- a/test/unit/math/opencl/rev/exponential_lcdf_test.cpp +++ b/test/unit/math/opencl/rev/exponential_lcdf_test.cpp @@ -48,7 +48,6 @@ auto exponential_lcdf_functor = [](const auto& y, const auto& beta) { TEST(ProbDistributionsExponentialLcdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; diff --git a/test/unit/math/opencl/rev/exponential_lpdf_test.cpp b/test/unit/math/opencl/rev/exponential_lpdf_test.cpp index 01e5671360a..cbf8018af98 100644 --- a/test/unit/math/opencl/rev/exponential_lpdf_test.cpp +++ b/test/unit/math/opencl/rev/exponential_lpdf_test.cpp @@ -57,7 +57,6 @@ auto exponential_lpdf_functor_propto = [](const auto& y, const auto& beta) { TEST(ProbDistributionsExponential, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0, 0.5, 12.3; diff --git a/test/unit/math/opencl/rev/frechet_cdf_test.cpp b/test/unit/math/opencl/rev/frechet_cdf_test.cpp index 0fbab3d7f5d..c5302fe7f4b 100644 --- a/test/unit/math/opencl/rev/frechet_cdf_test.cpp +++ b/test/unit/math/opencl/rev/frechet_cdf_test.cpp @@ -63,7 +63,6 @@ auto frechet_cdf_functor TEST(ProbDistributionsFrechetCdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; diff --git a/test/unit/math/opencl/rev/frechet_lccdf_test.cpp b/test/unit/math/opencl/rev/frechet_lccdf_test.cpp index 36405481bac..918c6671cfc 100644 --- a/test/unit/math/opencl/rev/frechet_lccdf_test.cpp +++ b/test/unit/math/opencl/rev/frechet_lccdf_test.cpp @@ -63,7 +63,6 @@ auto frechet_lccdf_functor TEST(ProbDistributionsFrechetLccdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; diff --git a/test/unit/math/opencl/rev/frechet_lcdf_test.cpp b/test/unit/math/opencl/rev/frechet_lcdf_test.cpp index a9ab54e177c..905da61790b 100644 --- a/test/unit/math/opencl/rev/frechet_lcdf_test.cpp +++ b/test/unit/math/opencl/rev/frechet_lcdf_test.cpp @@ -63,7 +63,6 @@ auto frechet_lcdf_functor TEST(ProbDistributionsFrechetLcdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; diff --git a/test/unit/math/opencl/rev/gumbel_cdf_test.cpp b/test/unit/math/opencl/rev/gumbel_cdf_test.cpp index f65c838f4e6..572a06e0bd1 100644 --- a/test/unit/math/opencl/rev/gumbel_cdf_test.cpp +++ b/test/unit/math/opencl/rev/gumbel_cdf_test.cpp @@ -62,7 +62,6 @@ auto gumbel_cdf_functor = [](const auto& y, const auto& mu, const auto& sigma) { TEST(ProbDistributionsGumbelCdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; diff --git a/test/unit/math/opencl/rev/gumbel_lccdf_test.cpp b/test/unit/math/opencl/rev/gumbel_lccdf_test.cpp index 859e48573c9..f6aa9a09500 100644 --- a/test/unit/math/opencl/rev/gumbel_lccdf_test.cpp +++ b/test/unit/math/opencl/rev/gumbel_lccdf_test.cpp @@ -63,7 +63,6 @@ auto gumbel_lccdf_functor TEST(ProbDistributionsGumbelLccdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; diff --git a/test/unit/math/opencl/rev/gumbel_lcdf_test.cpp b/test/unit/math/opencl/rev/gumbel_lcdf_test.cpp index 9340c0b4183..b40cc75cef0 100644 --- a/test/unit/math/opencl/rev/gumbel_lcdf_test.cpp +++ b/test/unit/math/opencl/rev/gumbel_lcdf_test.cpp @@ -63,7 +63,6 @@ auto gumbel_lcdf_functor TEST(ProbDistributionsGumbelLcdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; diff --git a/test/unit/math/opencl/rev/inv_gamma_lpdf_test.cpp b/test/unit/math/opencl/rev/inv_gamma_lpdf_test.cpp index d86600bd966..08bd621ff18 100644 --- a/test/unit/math/opencl/rev/inv_gamma_lpdf_test.cpp +++ b/test/unit/math/opencl/rev/inv_gamma_lpdf_test.cpp @@ -79,7 +79,6 @@ auto inv_gamma_lpdf_functor_propto TEST(ProbDistributionsInvGamma, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 10, 0.5, 1; @@ -102,7 +101,6 @@ TEST(ProbDistributionsInvGamma, opencl_matches_cpu_small) { TEST(ProbDistributionsInvGamma, opencl_matches_cpu_small_zero_y) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0, 0.5, 1; diff --git a/test/unit/math/opencl/rev/log_mix_test.cpp b/test/unit/math/opencl/rev/log_mix_test.cpp index 68bccc12594..dc9eb721b83 100644 --- a/test/unit/math/opencl/rev/log_mix_test.cpp +++ b/test/unit/math/opencl/rev/log_mix_test.cpp @@ -59,7 +59,6 @@ auto log_mix_functor = [](const auto& theta, const auto& lambda) { TEST(OpenCLLogMix, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd theta1(N); theta1 << 0.5, 0.4, 0.1; diff --git a/test/unit/math/opencl/rev/logistic_cdf_test.cpp b/test/unit/math/opencl/rev/logistic_cdf_test.cpp index f279c45691e..385dab309b2 100644 --- a/test/unit/math/opencl/rev/logistic_cdf_test.cpp +++ b/test/unit/math/opencl/rev/logistic_cdf_test.cpp @@ -63,7 +63,6 @@ auto logistic_cdf_functor TEST(ProbDistributionsLogisticCdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; @@ -81,7 +80,6 @@ TEST(ProbDistributionsLogisticCdf, opencl_matches_cpu_small) { TEST(ProbDistributionsLogisticCdf, opencl_matches_cpu_small_y_neg_inf) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, -INFINITY, 1.0; diff --git a/test/unit/math/opencl/rev/logistic_lccdf_test.cpp b/test/unit/math/opencl/rev/logistic_lccdf_test.cpp index 50796ef91b6..00ff9dff489 100644 --- a/test/unit/math/opencl/rev/logistic_lccdf_test.cpp +++ b/test/unit/math/opencl/rev/logistic_lccdf_test.cpp @@ -63,7 +63,6 @@ auto logistic_lccdf_functor TEST(ProbDistributionsLogisticLccdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; @@ -81,7 +80,6 @@ TEST(ProbDistributionsLogisticLccdf, opencl_matches_cpu_small) { TEST(ProbDistributionsLogisticLccdf, opencl_matches_cpu_small_y_neg_inf) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, -INFINITY, 1.0; diff --git a/test/unit/math/opencl/rev/logistic_lcdf_test.cpp b/test/unit/math/opencl/rev/logistic_lcdf_test.cpp index c3cb1cc9d4e..2a0e9ecd3a0 100644 --- a/test/unit/math/opencl/rev/logistic_lcdf_test.cpp +++ b/test/unit/math/opencl/rev/logistic_lcdf_test.cpp @@ -63,7 +63,6 @@ auto logistic_lcdf_functor TEST(ProbDistributionsLogisticLcdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; @@ -81,7 +80,6 @@ TEST(ProbDistributionsLogisticLcdf, opencl_matches_cpu_small) { TEST(ProbDistributionsLogisticLcdf, opencl_matches_cpu_small_y_neg_inf) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, -INFINITY, 1.0; diff --git a/test/unit/math/opencl/rev/logistic_lpdf_test.cpp b/test/unit/math/opencl/rev/logistic_lpdf_test.cpp index 44cc923c6ea..9c3ba21f4b7 100644 --- a/test/unit/math/opencl/rev/logistic_lpdf_test.cpp +++ b/test/unit/math/opencl/rev/logistic_lpdf_test.cpp @@ -79,7 +79,6 @@ auto logistic_lpdf_functor_propto TEST(ProbDistributionsLogistic, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 10, -0.5, 1; @@ -181,7 +180,6 @@ TEST(ProbDistributionsLogistic, opencl_matches_cpu_big) { TEST(ProbDistributionsLogistic, opencl_sigma_mu_scalar) { int N = 3; - int M = 2; double sigma = 0.3; double mu = 0.5; diff --git a/test/unit/math/opencl/rev/lognormal_cdf_test.cpp b/test/unit/math/opencl/rev/lognormal_cdf_test.cpp index bb45cc07d4a..4c80c21b39e 100644 --- a/test/unit/math/opencl/rev/lognormal_cdf_test.cpp +++ b/test/unit/math/opencl/rev/lognormal_cdf_test.cpp @@ -63,7 +63,6 @@ auto lognormal_cdf_functor TEST(ProbDistributionsLognormalCdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; @@ -81,7 +80,6 @@ TEST(ProbDistributionsLognormalCdf, opencl_matches_cpu_small) { TEST(ProbDistributionsLognormalCdf, opencl_matches_cpu_small_y_zero) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.0, 1.0; diff --git a/test/unit/math/opencl/rev/lognormal_lccdf_test.cpp b/test/unit/math/opencl/rev/lognormal_lccdf_test.cpp index 324864b55c3..a3619fcb1a3 100644 --- a/test/unit/math/opencl/rev/lognormal_lccdf_test.cpp +++ b/test/unit/math/opencl/rev/lognormal_lccdf_test.cpp @@ -63,7 +63,6 @@ auto lognormal_lccdf_functor TEST(ProbDistributionsLognormalLccdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; @@ -81,7 +80,6 @@ TEST(ProbDistributionsLognormalLccdf, opencl_matches_cpu_small) { TEST(ProbDistributionsLognormalLccdf, opencl_matches_cpu_small_y_zero) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.0, 1.0; diff --git a/test/unit/math/opencl/rev/lognormal_lcdf_test.cpp b/test/unit/math/opencl/rev/lognormal_lcdf_test.cpp index b3c9163b015..9fdb5da9a8e 100644 --- a/test/unit/math/opencl/rev/lognormal_lcdf_test.cpp +++ b/test/unit/math/opencl/rev/lognormal_lcdf_test.cpp @@ -63,7 +63,6 @@ auto lognormal_lcdf_functor TEST(ProbDistributionsLognormalLcdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; @@ -81,7 +80,6 @@ TEST(ProbDistributionsLognormalLcdf, opencl_matches_cpu_small) { TEST(ProbDistributionsLognormalLcdf, opencl_matches_cpu_small_y_zero) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.0, 1.0; diff --git a/test/unit/math/opencl/rev/lognormal_lpdf_test.cpp b/test/unit/math/opencl/rev/lognormal_lpdf_test.cpp index b3c0bdbd490..b984669aa5e 100644 --- a/test/unit/math/opencl/rev/lognormal_lpdf_test.cpp +++ b/test/unit/math/opencl/rev/lognormal_lpdf_test.cpp @@ -85,7 +85,6 @@ auto lognormal_lpdf_functor_propto TEST(ProbDistributionsLognormal, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.1, 0.5, 1; @@ -107,7 +106,6 @@ TEST(ProbDistributionsLognormal, opencl_matches_cpu_small) { } TEST(ProbDistributionsLognormal, opencl_matches_cpu_small_zero_y) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0, 0.5, 1; diff --git a/test/unit/math/opencl/rev/multi_normal_cholesky_lpdf_test.cpp b/test/unit/math/opencl/rev/multi_normal_cholesky_lpdf_test.cpp index 221cec889d7..61443d022c4 100644 --- a/test/unit/math/opencl/rev/multi_normal_cholesky_lpdf_test.cpp +++ b/test/unit/math/opencl/rev/multi_normal_cholesky_lpdf_test.cpp @@ -90,7 +90,6 @@ auto multi_normal_cholesky_lpdf_functor_propto TEST(ProbDistributionsMultiNormalCholesky, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y1(N); y1 << 0.5, 0.4, 0.1; diff --git a/test/unit/math/opencl/rev/neg_binomial_2_log_lpmf_test.cpp b/test/unit/math/opencl/rev/neg_binomial_2_log_lpmf_test.cpp index d9e2e89b852..6aea791d8fe 100644 --- a/test/unit/math/opencl/rev/neg_binomial_2_log_lpmf_test.cpp +++ b/test/unit/math/opencl/rev/neg_binomial_2_log_lpmf_test.cpp @@ -69,7 +69,6 @@ auto neg_binomial_2_log_lpmf_functor_propto TEST(ProbDistributionsNegBinomial2Log, opencl_matches_cpu_small) { int N = 3; - int M = 2; std::vector n{1, 0, 12}; Eigen::VectorXd eta(N); @@ -169,9 +168,6 @@ TEST(ProbDistributionsNegBinomial2Log, opencl_matches_cpu_big) { } TEST(ProbDistributionsNegBinomial2Log, opencl_matches_cpu_eta_phi_scalar) { - int N = 3; - int M = 2; - std::vector n{1, 0, 12}; double eta = 0.3; double phi = 0.8; diff --git a/test/unit/math/opencl/rev/neg_binomial_2_lpmf_test.cpp b/test/unit/math/opencl/rev/neg_binomial_2_lpmf_test.cpp index e7e863610d9..591d1598a86 100644 --- a/test/unit/math/opencl/rev/neg_binomial_2_lpmf_test.cpp +++ b/test/unit/math/opencl/rev/neg_binomial_2_lpmf_test.cpp @@ -74,7 +74,6 @@ auto neg_binomial_2_lpmf_functor_propto TEST(muProbDistributionsNegBinomial2, opencl_matches_cpu_small) { int N = 3; - int M = 2; std::vector n{1, 0, 12}; Eigen::VectorXd mu(N); @@ -175,7 +174,6 @@ TEST(muProbDistributionsNegBinomial2, opencl_matches_cpu_big) { TEST(ProbDistributionsNegBinomial2, opencl_scalar_n_mu) { int N = 3; - int M = 2; int n = 1; double mu = 0.3; diff --git a/test/unit/math/opencl/rev/neg_binomial_lpmf_test.cpp b/test/unit/math/opencl/rev/neg_binomial_lpmf_test.cpp index 28014935e8e..ce7613b75e4 100644 --- a/test/unit/math/opencl/rev/neg_binomial_lpmf_test.cpp +++ b/test/unit/math/opencl/rev/neg_binomial_lpmf_test.cpp @@ -74,7 +74,6 @@ auto neg_binomial_lpmf_functor_propto TEST(muProbDistributionsNegBinomial, opencl_matches_cpu_small) { int N = 3; - int M = 2; std::vector n{1, 0, 12}; Eigen::VectorXd alpha(N); diff --git a/test/unit/math/opencl/rev/normal_lccdf_test.cpp b/test/unit/math/opencl/rev/normal_lccdf_test.cpp index e03341403ed..190ff5589a3 100644 --- a/test/unit/math/opencl/rev/normal_lccdf_test.cpp +++ b/test/unit/math/opencl/rev/normal_lccdf_test.cpp @@ -63,7 +63,6 @@ auto normal_lccdf_functor TEST(ProbDistributionsNormalLccdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; diff --git a/test/unit/math/opencl/rev/normal_lcdf_test.cpp b/test/unit/math/opencl/rev/normal_lcdf_test.cpp index e264c5e1d9c..a062e1304d4 100644 --- a/test/unit/math/opencl/rev/normal_lcdf_test.cpp +++ b/test/unit/math/opencl/rev/normal_lcdf_test.cpp @@ -63,7 +63,6 @@ auto normal_lcdf_functor TEST(ProbDistributionsNormalLcdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; diff --git a/test/unit/math/opencl/rev/pareto_cdf_test.cpp b/test/unit/math/opencl/rev/pareto_cdf_test.cpp index 89e7fcd3f8b..556bcb4f687 100644 --- a/test/unit/math/opencl/rev/pareto_cdf_test.cpp +++ b/test/unit/math/opencl/rev/pareto_cdf_test.cpp @@ -63,7 +63,6 @@ auto pareto_cdf_functor TEST(ProbDistributionsParetoCdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.35, 0.9, 1.5; @@ -81,7 +80,6 @@ TEST(ProbDistributionsParetoCdf, opencl_matches_cpu_small) { TEST(ProbDistributionsParetoCdf, opencl_matches_cpu_small_y_lower_than_y_min) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.35, 0.7, 1.3; diff --git a/test/unit/math/opencl/rev/pareto_lccdf_test.cpp b/test/unit/math/opencl/rev/pareto_lccdf_test.cpp index 64efae248b9..ffd41b62f82 100644 --- a/test/unit/math/opencl/rev/pareto_lccdf_test.cpp +++ b/test/unit/math/opencl/rev/pareto_lccdf_test.cpp @@ -63,7 +63,6 @@ auto pareto_lccdf_functor TEST(ProbDistributionsParetoLccdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.35, 0.9, 1.5; @@ -82,7 +81,6 @@ TEST(ProbDistributionsParetoLccdf, opencl_matches_cpu_small) { TEST(ProbDistributionsParetoLccdf, opencl_matches_cpu_small_y_lower_than_y_min) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.35, 0.7, 1.3; diff --git a/test/unit/math/opencl/rev/pareto_lcdf_test.cpp b/test/unit/math/opencl/rev/pareto_lcdf_test.cpp index 24c158cea90..1f2e27bd91d 100644 --- a/test/unit/math/opencl/rev/pareto_lcdf_test.cpp +++ b/test/unit/math/opencl/rev/pareto_lcdf_test.cpp @@ -63,7 +63,6 @@ auto pareto_lcdf_functor TEST(ProbDistributionsParetoLcdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.35, 0.9, 1.5; @@ -81,7 +80,6 @@ TEST(ProbDistributionsParetoLcdf, opencl_matches_cpu_small) { TEST(ProbDistributionsParetoLcdf, opencl_matches_cpu_small_y_lower_than_y_min) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.35, 0.7, 1.3; diff --git a/test/unit/math/opencl/rev/pareto_lpdf_test.cpp b/test/unit/math/opencl/rev/pareto_lpdf_test.cpp index 1ab39a71093..e6378bf2634 100644 --- a/test/unit/math/opencl/rev/pareto_lpdf_test.cpp +++ b/test/unit/math/opencl/rev/pareto_lpdf_test.cpp @@ -77,7 +77,6 @@ auto pareto_lpdf_functor_propto TEST(ProbDistributionsPareto, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; diff --git a/test/unit/math/opencl/rev/pareto_type_2_cdf_test.cpp b/test/unit/math/opencl/rev/pareto_type_2_cdf_test.cpp index e3bc404da15..13677126159 100644 --- a/test/unit/math/opencl/rev/pareto_type_2_cdf_test.cpp +++ b/test/unit/math/opencl/rev/pareto_type_2_cdf_test.cpp @@ -86,7 +86,6 @@ auto pareto_type_2_cdf_functor TEST(ProbDistributionsParetoType2Cdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.5, 1.8, 1.4; diff --git a/test/unit/math/opencl/rev/pareto_type_2_lccdf_test.cpp b/test/unit/math/opencl/rev/pareto_type_2_lccdf_test.cpp index fa71fc0cc8f..2d936ba78a9 100644 --- a/test/unit/math/opencl/rev/pareto_type_2_lccdf_test.cpp +++ b/test/unit/math/opencl/rev/pareto_type_2_lccdf_test.cpp @@ -86,7 +86,6 @@ auto pareto_type_2_lccdf_functor TEST(ProbDistributionsParetoType2Lccdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.5, 1.8, 1.4; diff --git a/test/unit/math/opencl/rev/pareto_type_2_lcdf_test.cpp b/test/unit/math/opencl/rev/pareto_type_2_lcdf_test.cpp index 2242c01a8bc..a6ee89e5097 100644 --- a/test/unit/math/opencl/rev/pareto_type_2_lcdf_test.cpp +++ b/test/unit/math/opencl/rev/pareto_type_2_lcdf_test.cpp @@ -86,7 +86,6 @@ auto pareto_type_2_lcdf_functor TEST(ProbDistributionsParetoType2Lcdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.5, 1.8, 1.4; diff --git a/test/unit/math/opencl/rev/poisson_log_lpmf_test.cpp b/test/unit/math/opencl/rev/poisson_log_lpmf_test.cpp index bc7dcfc3171..98ba889142e 100644 --- a/test/unit/math/opencl/rev/poisson_log_lpmf_test.cpp +++ b/test/unit/math/opencl/rev/poisson_log_lpmf_test.cpp @@ -47,7 +47,6 @@ auto poisson_log_lpmf_functor_propto = [](const auto& n, const auto& alpha) { TEST(ProbDistributionsPoissonLog, opencl_matches_cpu_small) { int N = 3; - int M = 2; std::vector n{0, 1, 6}; Eigen::VectorXd alpha(N); @@ -77,8 +76,6 @@ TEST(ProbDistributionsPoissonLog, opencl_broadcast_n) { } TEST(ProbDistributionsPoissonLog, opencl_broadcast_alpha) { - int N = 3; - std::vector n{0, 1, 5}; double alpha_scal = 0.4; diff --git a/test/unit/math/opencl/rev/poisson_lpmf_test.cpp b/test/unit/math/opencl/rev/poisson_lpmf_test.cpp index 0da588c6ab7..2176126def3 100644 --- a/test/unit/math/opencl/rev/poisson_lpmf_test.cpp +++ b/test/unit/math/opencl/rev/poisson_lpmf_test.cpp @@ -47,7 +47,6 @@ auto poisson_lpmf_functor_propto = [](const auto& n, const auto& alpha) { TEST(ProbDistributionsPoisson, opencl_matches_cpu_small) { int N = 3; - int M = 2; std::vector n{0, 1, 6}; Eigen::VectorXd alpha(N); @@ -75,8 +74,6 @@ TEST(ProbDistributionsPoisson, opencl_broadcast_n) { } TEST(ProbDistributionsPoisson, opencl_broadcast_alpha) { - int N = 3; - std::vector n{0, 1, 5}; double alpha_scal = 0.4; diff --git a/test/unit/math/opencl/rev/rayleigh_cdf_test.cpp b/test/unit/math/opencl/rev/rayleigh_cdf_test.cpp index 7db4882d0b8..ea8619bf53e 100644 --- a/test/unit/math/opencl/rev/rayleigh_cdf_test.cpp +++ b/test/unit/math/opencl/rev/rayleigh_cdf_test.cpp @@ -46,7 +46,6 @@ auto rayleigh_cdf_functor = [](const auto& y, const auto& mu) { TEST(ProbDistributionsRayleighCdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; diff --git a/test/unit/math/opencl/rev/rayleigh_lccdf_test.cpp b/test/unit/math/opencl/rev/rayleigh_lccdf_test.cpp index 4ab1ea7384d..b9c5eadea45 100644 --- a/test/unit/math/opencl/rev/rayleigh_lccdf_test.cpp +++ b/test/unit/math/opencl/rev/rayleigh_lccdf_test.cpp @@ -48,7 +48,6 @@ auto rayleigh_lccdf_functor = [](const auto& y, const auto& mu) { TEST(ProbDistributionsRayleighLccdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; diff --git a/test/unit/math/opencl/rev/rayleigh_lcdf_test.cpp b/test/unit/math/opencl/rev/rayleigh_lcdf_test.cpp index 713b5639fb4..c89f97394f3 100644 --- a/test/unit/math/opencl/rev/rayleigh_lcdf_test.cpp +++ b/test/unit/math/opencl/rev/rayleigh_lcdf_test.cpp @@ -46,7 +46,6 @@ auto rayleigh_lcdf_functor = [](const auto& y, const auto& mu) { TEST(ProbDistributionsRayleighLcdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; diff --git a/test/unit/math/opencl/rev/rayleigh_lpdf_test.cpp b/test/unit/math/opencl/rev/rayleigh_lpdf_test.cpp index 93460840c2f..1c81a0b1f84 100644 --- a/test/unit/math/opencl/rev/rayleigh_lpdf_test.cpp +++ b/test/unit/math/opencl/rev/rayleigh_lpdf_test.cpp @@ -52,7 +52,6 @@ auto rayleigh_lpdf_functor_propto = [](const auto& y, const auto& sigma) { TEST(ProbDistributionsRayleigh, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.1, 0.5, 12.3; diff --git a/test/unit/math/opencl/rev/segment_test.cpp b/test/unit/math/opencl/rev/segment_test.cpp index 8f78952a071..c87a7fb47a2 100644 --- a/test/unit/math/opencl/rev/segment_test.cpp +++ b/test/unit/math/opencl/rev/segment_test.cpp @@ -19,15 +19,15 @@ TEST(MathMatrixCL, segment_exception) { EXPECT_THROW(segment(m3_cl, 1, 5), std::invalid_argument); stan::math::var_value> m4_cl = m1_cl; - EXPECT_THROW(segment(m1_cl, 1, 5), std::invalid_argument); + EXPECT_THROW(segment(m4_cl, 1, 5), std::invalid_argument); stan::math::var_value> m5_cl = m2_cl; - EXPECT_THROW(segment(m2_cl, 0, 2), std::invalid_argument); - EXPECT_THROW(segment(m2_cl, 1, 5), std::invalid_argument); + EXPECT_THROW(segment(m5_cl, 0, 2), std::invalid_argument); + EXPECT_THROW(segment(m5_cl, 1, 5), std::invalid_argument); stan::math::var_value> m6_cl = m3_cl; - EXPECT_THROW(segment(m3_cl, 0, 2), std::invalid_argument); - EXPECT_THROW(segment(m3_cl, 1, 5), std::invalid_argument); + EXPECT_THROW(segment(m6_cl, 0, 2), std::invalid_argument); + EXPECT_THROW(segment(m6_cl, 1, 5), std::invalid_argument); } auto segment_functor = [](const auto& a, size_t i, size_t n) { diff --git a/test/unit/math/opencl/rev/skew_double_exponential_cdf_test.cpp b/test/unit/math/opencl/rev/skew_double_exponential_cdf_test.cpp index 7d82bffb838..397f97673ba 100644 --- a/test/unit/math/opencl/rev/skew_double_exponential_cdf_test.cpp +++ b/test/unit/math/opencl/rev/skew_double_exponential_cdf_test.cpp @@ -88,7 +88,6 @@ auto skew_double_exponential_cdf_functor TEST(ProbDistributionsSkewDoubleExponentialCdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << -0.3, 1.8, 1.4; @@ -109,7 +108,6 @@ TEST(ProbDistributionsSkewDoubleExponentialCdf, opencl_matches_cpu_small) { TEST(ProbDistributionsSkewDoubleExponentialCdf, opencl_matches_cpu_small_y_neg_inf) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << -INFINITY, 1.8, 1.4; diff --git a/test/unit/math/opencl/rev/skew_double_exponential_lccdf_test.cpp b/test/unit/math/opencl/rev/skew_double_exponential_lccdf_test.cpp index 0628af47960..79092f6a583 100644 --- a/test/unit/math/opencl/rev/skew_double_exponential_lccdf_test.cpp +++ b/test/unit/math/opencl/rev/skew_double_exponential_lccdf_test.cpp @@ -88,7 +88,6 @@ auto skew_double_exponential_lccdf_functor TEST(ProbDistributionsSkewDoubleExponentialLccdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << -0.3, 1.8, 1.4; @@ -109,7 +108,6 @@ TEST(ProbDistributionsSkewDoubleExponentialLccdf, opencl_matches_cpu_small) { TEST(ProbDistributionsSkewDoubleExponentialLccdf, opencl_matches_cpu_small_y_neg_inf) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << -INFINITY, 1.8, 1.4; diff --git a/test/unit/math/opencl/rev/skew_double_exponential_lcdf_test.cpp b/test/unit/math/opencl/rev/skew_double_exponential_lcdf_test.cpp index c8b5a250856..03ad9cbf643 100644 --- a/test/unit/math/opencl/rev/skew_double_exponential_lcdf_test.cpp +++ b/test/unit/math/opencl/rev/skew_double_exponential_lcdf_test.cpp @@ -88,7 +88,6 @@ auto skew_double_exponential_lcdf_functor TEST(ProbDistributionsSkewDoubleExponentialLcdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << -0.3, 1.8, 1.4; @@ -109,7 +108,6 @@ TEST(ProbDistributionsSkewDoubleExponentialLcdf, opencl_matches_cpu_small) { TEST(ProbDistributionsSkewDoubleExponentialLcdf, opencl_matches_cpu_small_y_neg_inf) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << -INFINITY, 1.8, 1.4; diff --git a/test/unit/math/opencl/rev/skew_normal_lpdf_test.cpp b/test/unit/math/opencl/rev/skew_normal_lpdf_test.cpp index 4e60315ee71..ad5220e8557 100644 --- a/test/unit/math/opencl/rev/skew_normal_lpdf_test.cpp +++ b/test/unit/math/opencl/rev/skew_normal_lpdf_test.cpp @@ -90,7 +90,6 @@ auto skew_normal_lpdf_functor_propto TEST(ProbDistributionsSkewNormal, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; diff --git a/test/unit/math/opencl/rev/std_normal_cdf_test.cpp b/test/unit/math/opencl/rev/std_normal_cdf_test.cpp index 1bf3edff524..dea8b16e9f4 100644 --- a/test/unit/math/opencl/rev/std_normal_cdf_test.cpp +++ b/test/unit/math/opencl/rev/std_normal_cdf_test.cpp @@ -26,7 +26,6 @@ auto std_normal_cdf_functor TEST(ProbDistributionsStdNormalCdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; diff --git a/test/unit/math/opencl/rev/std_normal_lccdf_test.cpp b/test/unit/math/opencl/rev/std_normal_lccdf_test.cpp index b261a062258..6f4b1e1f866 100644 --- a/test/unit/math/opencl/rev/std_normal_lccdf_test.cpp +++ b/test/unit/math/opencl/rev/std_normal_lccdf_test.cpp @@ -26,7 +26,6 @@ auto std_normal_lccdf_functor TEST(ProbDistributionsStdNormalLccdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; diff --git a/test/unit/math/opencl/rev/std_normal_lcdf_test.cpp b/test/unit/math/opencl/rev/std_normal_lcdf_test.cpp index 52f3562c6fe..97fdbb38225 100644 --- a/test/unit/math/opencl/rev/std_normal_lcdf_test.cpp +++ b/test/unit/math/opencl/rev/std_normal_lcdf_test.cpp @@ -26,7 +26,6 @@ auto std_normal_lcdf_functor TEST(ProbDistributionsStdNormalLcdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; diff --git a/test/unit/math/opencl/rev/std_normal_lpdf_test.cpp b/test/unit/math/opencl/rev/std_normal_lpdf_test.cpp index 7f8fcea56bd..ed4556ba09c 100644 --- a/test/unit/math/opencl/rev/std_normal_lpdf_test.cpp +++ b/test/unit/math/opencl/rev/std_normal_lpdf_test.cpp @@ -27,7 +27,6 @@ auto std_normal_lpdf_functor_propto TEST(ProbDistributionsStdNormal, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; diff --git a/test/unit/math/opencl/rev/uniform_cdf_test.cpp b/test/unit/math/opencl/rev/uniform_cdf_test.cpp index 262e6985897..5ed2189f1fe 100644 --- a/test/unit/math/opencl/rev/uniform_cdf_test.cpp +++ b/test/unit/math/opencl/rev/uniform_cdf_test.cpp @@ -63,7 +63,6 @@ auto uniform_cdf_functor TEST(ProbDistributionsUniformCdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; @@ -81,7 +80,6 @@ TEST(ProbDistributionsUniformCdf, opencl_matches_cpu_small) { TEST(ProbDistributionsUniformCdf, opencl_matches_cpu_small_y_neg_inf) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, -INFINITY, 1.0; diff --git a/test/unit/math/opencl/rev/uniform_lccdf_test.cpp b/test/unit/math/opencl/rev/uniform_lccdf_test.cpp index bed3d490b85..05b8d00a41e 100644 --- a/test/unit/math/opencl/rev/uniform_lccdf_test.cpp +++ b/test/unit/math/opencl/rev/uniform_lccdf_test.cpp @@ -63,7 +63,6 @@ auto uniform_lccdf_functor TEST(ProbDistributionsUniformLccdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; @@ -81,7 +80,6 @@ TEST(ProbDistributionsUniformLccdf, opencl_matches_cpu_small) { TEST(ProbDistributionsUniformLccdf, opencl_matches_cpu_small_y_neg_inf) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, -INFINITY, 1.0; diff --git a/test/unit/math/opencl/rev/uniform_lcdf_test.cpp b/test/unit/math/opencl/rev/uniform_lcdf_test.cpp index 5e028cb777e..cec558ada3f 100644 --- a/test/unit/math/opencl/rev/uniform_lcdf_test.cpp +++ b/test/unit/math/opencl/rev/uniform_lcdf_test.cpp @@ -63,7 +63,6 @@ auto uniform_lcdf_functor TEST(ProbDistributionsUniformLcdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; @@ -81,7 +80,6 @@ TEST(ProbDistributionsUniformLcdf, opencl_matches_cpu_small) { TEST(ProbDistributionsUniformLcdf, opencl_matches_cpu_small_y_neg_inf) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, -INFINITY, 1.0; diff --git a/test/unit/math/opencl/rev/uniform_lpdf_test.cpp b/test/unit/math/opencl/rev/uniform_lpdf_test.cpp index 8f58bf84d32..6a1546d3bd1 100644 --- a/test/unit/math/opencl/rev/uniform_lpdf_test.cpp +++ b/test/unit/math/opencl/rev/uniform_lpdf_test.cpp @@ -94,7 +94,6 @@ TEST(ProbDistributionsUniform, opencl_matches_cpu_small) { TEST(ProbDistributionsUniform, opencl_matches_cpu_small_y_out_of_bounds) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 14.0; diff --git a/test/unit/math/opencl/rev/weibull_cdf_test.cpp b/test/unit/math/opencl/rev/weibull_cdf_test.cpp index c8c6fe620c4..3fac364865e 100644 --- a/test/unit/math/opencl/rev/weibull_cdf_test.cpp +++ b/test/unit/math/opencl/rev/weibull_cdf_test.cpp @@ -63,7 +63,6 @@ auto weibull_cdf_functor TEST(ProbDistributionsWeibullCdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; diff --git a/test/unit/math/opencl/rev/weibull_lccdf_test.cpp b/test/unit/math/opencl/rev/weibull_lccdf_test.cpp index bf845190153..91b82eda794 100644 --- a/test/unit/math/opencl/rev/weibull_lccdf_test.cpp +++ b/test/unit/math/opencl/rev/weibull_lccdf_test.cpp @@ -63,7 +63,6 @@ auto weibull_lccdf_functor TEST(ProbDistributionsWeibullLccdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; diff --git a/test/unit/math/opencl/rev/weibull_lcdf_test.cpp b/test/unit/math/opencl/rev/weibull_lcdf_test.cpp index 4f0b1b89b63..a81c190e45a 100644 --- a/test/unit/math/opencl/rev/weibull_lcdf_test.cpp +++ b/test/unit/math/opencl/rev/weibull_lcdf_test.cpp @@ -63,7 +63,6 @@ auto weibull_lcdf_functor TEST(ProbDistributionsWeibullLcdf, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; diff --git a/test/unit/math/opencl/rev/weibull_lpdf_test.cpp b/test/unit/math/opencl/rev/weibull_lpdf_test.cpp index c6c4e13ab69..dbea51cb619 100644 --- a/test/unit/math/opencl/rev/weibull_lpdf_test.cpp +++ b/test/unit/math/opencl/rev/weibull_lpdf_test.cpp @@ -77,7 +77,6 @@ auto weibull_lpdf_functor_propto TEST(ProbDistributionsWeibull, opencl_matches_cpu_small) { int N = 3; - int M = 2; Eigen::VectorXd y(N); y << 0.3, 0.8, 1.0; diff --git a/test/unit/math/prim/prob/student_t_qf_test.cpp b/test/unit/math/prim/prob/student_t_qf_test.cpp index 4694264870d..999ef198f91 100644 --- a/test/unit/math/prim/prob/student_t_qf_test.cpp +++ b/test/unit/math/prim/prob/student_t_qf_test.cpp @@ -39,7 +39,6 @@ TEST(MathFunctions, student_t_qf_vec) { TEST(MathFunctions, student_t_qf_inf) { using stan::math::student_t_qf; - long double log_p = std::numeric_limits::min(); const double inf = std::numeric_limits::infinity(); EXPECT_EQ(student_t_qf(0, 2, 0, 1), -inf); EXPECT_EQ(student_t_qf(1, 2, 0, 1), inf); From ddc7cca18ddbe1db0372802789d9df83d3d6d6f9 Mon Sep 17 00:00:00 2001 From: Steve Bronder Date: Thu, 15 Oct 2026 19:38:14 -0400 Subject: [PATCH 06/10] Replace tuple callback captures with direct value captures Remove all 23 tuple captures from the OpenCL and CPU reverse callbacks. Use direct value captures to avoid Clang 10 unused explicit-capture warnings while keeping the original derivative bodies and callback ownership. Verify trivial callback destruction across 23 affected test translation units using validation-only compile-time assertions. --- stan/math/opencl/rev/add_diag.hpp | 32 ++- stan/math/opencl/rev/append_col.hpp | 6 +- stan/math/opencl/rev/append_row.hpp | 6 +- stan/math/opencl/rev/mdivide_left_tri_low.hpp | 6 +- .../math/opencl/rev/mdivide_right_tri_low.hpp | 6 +- stan/math/rev/constraint/lub_constrain.hpp | 215 +++++++----------- stan/math/rev/fun/fma.hpp | 23 +- stan/math/rev/fun/inv_inc_beta.hpp | 5 +- 8 files changed, 112 insertions(+), 187 deletions(-) diff --git a/stan/math/opencl/rev/add_diag.hpp b/stan/math/opencl/rev/add_diag.hpp index acc3aa2aeec..660c307312b 100644 --- a/stan/math/opencl/rev/add_diag.hpp +++ b/stan/math/opencl/rev/add_diag.hpp @@ -9,8 +9,6 @@ #include #include -#include - namespace stan { namespace math { @@ -36,22 +34,20 @@ inline auto add_diag(const T_m& mat, const T_a& to_add) { var_value> res = add_diag(value_of(mat), value_of(to_add)); - reverse_pass_callback( - [state = std::make_tuple(mat_arena, to_add_arena, res)]() mutable { - auto& [mat_arena, to_add_arena, res] = state; - if constexpr (is_autodiff_v) { - adjoint_of(mat_arena) += res.adj(); - } - if constexpr (is_autodiff_v) { - if constexpr (!is_stan_scalar::value) { - auto& to_add_adj = to_add_arena.adj(); - to_add_adj += diagonal(res.adj()); - } else { - auto& to_add_adj = to_add_arena.adj(); - to_add_adj += to_add_adj + sum(diagonal(res.adj())); - } - } - }); + reverse_pass_callback([=]() mutable { + if constexpr (is_autodiff_v) { + adjoint_of(mat_arena) += res.adj(); + } + if constexpr (is_autodiff_v) { + if constexpr (!is_stan_scalar::value) { + auto& to_add_adj = to_add_arena.adj(); + to_add_adj += diagonal(res.adj()); + } else { + auto& to_add_adj = to_add_arena.adj(); + to_add_adj += to_add_adj + sum(diagonal(res.adj())); + } + } + }); return res; } } // namespace math diff --git a/stan/math/opencl/rev/append_col.hpp b/stan/math/opencl/rev/append_col.hpp index 71c0237bfff..82e2d2eab8f 100644 --- a/stan/math/opencl/rev/append_col.hpp +++ b/stan/math/opencl/rev/append_col.hpp @@ -12,8 +12,6 @@ #include #include -#include - namespace stan { namespace math { @@ -47,9 +45,7 @@ inline var_value> append_col(T_a&& a, T_b&& b) { return make_callback_var( append_col(value_of(a_arena), value_of(b_arena)), - [state = std::make_tuple(a_arena, b_arena)]( - const vari_value>& res) mutable { - auto& [a_arena, b_arena] = state; + [=](const vari_value>& res) mutable { if constexpr (is_autodiff_v) { adjoint_of(a_arena) += block_zero_based( res.adj(), 0, 0, a_arena.rows(), a_arena.cols()); diff --git a/stan/math/opencl/rev/append_row.hpp b/stan/math/opencl/rev/append_row.hpp index 6fc3fda8824..395835fe133 100644 --- a/stan/math/opencl/rev/append_row.hpp +++ b/stan/math/opencl/rev/append_row.hpp @@ -12,8 +12,6 @@ #include #include -#include - namespace stan { namespace math { @@ -45,9 +43,7 @@ inline var_value> append_row(T_a&& a, T_b&& b) { return make_callback_var( append_row(value_of(a_arena), value_of(b_arena)), - [state = std::make_tuple(a_arena, b_arena)]( - const vari_value>& res) mutable { - auto& [a_arena, b_arena] = state; + [=](const vari_value>& res) mutable { if constexpr (is_autodiff_v) { adjoint_of(a_arena) += block_zero_based( res.adj(), 0, 0, a_arena.rows(), a_arena.cols()); diff --git a/stan/math/opencl/rev/mdivide_left_tri_low.hpp b/stan/math/opencl/rev/mdivide_left_tri_low.hpp index 7d79f17ee75..ad848c4ea92 100644 --- a/stan/math/opencl/rev/mdivide_left_tri_low.hpp +++ b/stan/math/opencl/rev/mdivide_left_tri_low.hpp @@ -9,8 +9,6 @@ #include #include -#include - namespace stan { namespace math { @@ -41,9 +39,7 @@ inline var_value> mdivide_left_tri_low(T1&& A, T2&& b) { = tri_inverse(value_of(A_arena)); return make_callback_var( A_tri_inv * value_of(b_arena), - [state = std::make_tuple(A_arena, b_arena, A_tri_inv)]( - const vari_value>& res) { - auto& [A_arena, b_arena, A_tri_inv] = state; + [=](const vari_value>& res) { matrix_cl adjB = transpose(A_tri_inv) * res.adj(); if constexpr (is_autodiff_v) { matrix_cl adjA = adjB * transpose(res.val()); diff --git a/stan/math/opencl/rev/mdivide_right_tri_low.hpp b/stan/math/opencl/rev/mdivide_right_tri_low.hpp index 2b4da0f1e36..a9717623565 100644 --- a/stan/math/opencl/rev/mdivide_right_tri_low.hpp +++ b/stan/math/opencl/rev/mdivide_right_tri_low.hpp @@ -9,8 +9,6 @@ #include #include -#include - namespace stan { namespace math { @@ -41,9 +39,7 @@ inline var_value> mdivide_right_tri_low(T2&& b, T1&& A) { = tri_inverse(value_of(A_arena)); return make_callback_var( value_of(b_arena) * A_tri_inv, - [state = std::make_tuple(A_arena, b_arena, A_tri_inv)]( - const vari_value>& res) { - auto& [A_arena, b_arena, A_tri_inv] = state; + [=](const vari_value>& res) { matrix_cl adjB = res.adj() * transpose(A_tri_inv); if constexpr (is_autodiff_v) { matrix_cl adjA = transpose(res.val()) * adjB; diff --git a/stan/math/rev/constraint/lub_constrain.hpp b/stan/math/rev/constraint/lub_constrain.hpp index 662f6668685..8e6099b1165 100644 --- a/stan/math/rev/constraint/lub_constrain.hpp +++ b/stan/math/rev/constraint/lub_constrain.hpp @@ -8,8 +8,6 @@ #include #include -#include - namespace stan { namespace math { @@ -52,10 +50,7 @@ inline auto lub_constrain(const T& x, const L& lb, const U& ub) { auto diff = ub_val - lb_val; double inv_logit_x = inv_logit(value_of(x)); return make_callback_var( - diff * inv_logit_x + lb_val, - [state - = std::make_tuple(x, ub, lb, diff, inv_logit_x)](auto& vi) mutable { - auto& [x, ub, lb, diff, inv_logit_x] = state; + diff * inv_logit_x + lb_val, [=](auto& vi) mutable { if constexpr (is_autodiff_v) { x.adj() += vi.adj() * diff * inv_logit_x * (1.0 - inv_logit_x); } @@ -125,10 +120,7 @@ inline auto lub_constrain(const T& x, const L& lb, const U& ub, double inv_logit_x = inv_logit(value_of(x)); lp += (log(diff) + (neg_abs_x - (2.0 * log1p_exp(neg_abs_x)))); return make_callback_var( - diff * inv_logit_x + lb_val, - [state = std::make_tuple(x, ub, lb, diff, lp, inv_logit_x)]( - auto& vi) mutable { - auto& [x, ub, lb, diff, lp, inv_logit_x] = state; + diff * inv_logit_x + lb_val, [=](auto& vi) mutable { if constexpr (is_autodiff_v) { x.adj() += vi.adj() * diff * inv_logit_x * (1.0 - inv_logit_x) + lp.adj() * (1.0 - 2.0 * inv_logit_x); @@ -173,9 +165,7 @@ inline auto lub_constrain(const T& x, const L& lb, const U& ub) { const auto diff = ub_val - lb_val; auto inv_logit_x = to_arena(inv_logit(arena_x.val().array())); arena_t ret = diff * inv_logit_x + lb_val; - reverse_pass_callback([state = std::make_tuple(arena_x, ub, lb, ret, diff, - inv_logit_x)]() mutable { - auto& [arena_x, ub, lb, ret, diff, inv_logit_x] = state; + reverse_pass_callback([=]() mutable { if constexpr (is_autodiff_v) { arena_x.adj().array() += ret.adj().array() * diff * inv_logit_x * (1.0 - inv_logit_x); @@ -219,30 +209,27 @@ inline auto lub_constrain(const T& x, const L& lb, const U& ub, lp += (log(diff) + (neg_abs_x - (2.0 * log1p_exp(neg_abs_x)))).sum(); auto inv_logit_x = to_arena(inv_logit(value_of(arena_x).array())); arena_t ret = diff * inv_logit_x + lb_val; - reverse_pass_callback( - [state = std::make_tuple(arena_x, ub, lb, ret, lp, diff, - inv_logit_x)]() mutable { - auto& [arena_x, ub, lb, ret, lp, diff, inv_logit_x] = state; - if constexpr (is_autodiff_v) { - arena_x.adj().array() - += ret.adj().array() * diff * inv_logit_x * (1.0 - inv_logit_x) - + lp.adj() * (1.0 - 2.0 * inv_logit_x); - } - if constexpr (is_autodiff_v && is_autodiff_v) { - const auto lp_calc = lp.adj() * ret.size(); - const auto one_over_diff = 1.0 / diff; - lb.adj() += (ret.adj().array() * (1.0 - inv_logit_x)).sum() - + -one_over_diff * lp_calc; - ub.adj() += (ret.adj().array() * inv_logit_x).sum() - + one_over_diff * lp_calc; - } else if constexpr (is_autodiff_v) { - lb.adj() += (ret.adj().array() * (1.0 - inv_logit_x)).sum() - + -(1.0 / diff) * lp.adj() * ret.size(); - } else if constexpr (is_autodiff_v) { - ub.adj() += (ret.adj().array() * inv_logit_x).sum() - + (1.0 / diff) * lp.adj() * ret.size(); - } - }); + reverse_pass_callback([=]() mutable { + if constexpr (is_autodiff_v) { + arena_x.adj().array() + += ret.adj().array() * diff * inv_logit_x * (1.0 - inv_logit_x) + + lp.adj() * (1.0 - 2.0 * inv_logit_x); + } + if constexpr (is_autodiff_v && is_autodiff_v) { + const auto lp_calc = lp.adj() * ret.size(); + const auto one_over_diff = 1.0 / diff; + lb.adj() += (ret.adj().array() * (1.0 - inv_logit_x)).sum() + + -one_over_diff * lp_calc; + ub.adj() += (ret.adj().array() * inv_logit_x).sum() + + one_over_diff * lp_calc; + } else if constexpr (is_autodiff_v) { + lb.adj() += (ret.adj().array() * (1.0 - inv_logit_x)).sum() + + -(1.0 / diff) * lp.adj() * ret.size(); + } else if constexpr (is_autodiff_v) { + ub.adj() += (ret.adj().array() * inv_logit_x).sum() + + (1.0 / diff) * lp.adj() * ret.size(); + } + }); return ret_type(ret); } } @@ -272,10 +259,7 @@ inline auto lub_constrain(const T& x, const L& lb, const U& ub) { auto inv_logit_x = to_arena(inv_logit(value_of(arena_x).array())); arena_t ret = (is_lb_inf).select( ub_val - value_of(arena_x).array().exp(), diff * inv_logit_x + lb_val); - reverse_pass_callback([state = std::make_tuple(arena_x, ub, arena_lb, ret, - diff, inv_logit_x, - is_lb_inf)]() mutable { - auto& [arena_x, ub, arena_lb, ret, diff, inv_logit_x, is_lb_inf] = state; + reverse_pass_callback([=]() mutable { if constexpr (is_autodiff_v) { arena_x.adj().array() += (is_lb_inf).select( ret.adj().array() * -value_of(arena_x).array().exp(), @@ -328,33 +312,28 @@ inline auto lub_constrain(const T& x, const L& lb, const U& ub, .select(arena_x_val, log(diff) + (neg_abs_x - (2.0 * log1p_exp(neg_abs_x)))) .sum(); - reverse_pass_callback( - [state = std::make_tuple(arena_x, arena_x_val, ub, arena_lb, ret, lp, - diff, inv_logit_x, is_lb_inf)]() mutable { - auto& [arena_x, arena_x_val, ub, arena_lb, ret, lp, diff, inv_logit_x, - is_lb_inf] - = state; - const auto lp_adj = lp.adj(); - if constexpr (is_autodiff_v) { - const auto x_sign = arena_x_val.sign().eval(); - arena_x.adj().array() += (is_lb_inf).select( - ret.adj().array() * -arena_x_val.exp() + lp_adj, - ret.adj().array() * diff * inv_logit_x * (1.0 - inv_logit_x) - + lp.adj() * (1.0 - 2.0 * inv_logit_x)); - } - if constexpr (is_autodiff_v) { - arena_lb.adj().array() - += (is_lb_inf).select(0, ret.adj().array() * (1.0 - inv_logit_x) - + -(1.0 / diff) * lp_adj); - } - if constexpr (is_autodiff_v) { - ub.adj() += (is_lb_inf) - .select(ret.adj().array(), - ret.adj().array() * inv_logit_x - + (1.0 / diff) * lp_adj) - .sum(); - } - }); + reverse_pass_callback([=]() mutable { + const auto lp_adj = lp.adj(); + if constexpr (is_autodiff_v) { + const auto x_sign = arena_x_val.sign().eval(); + arena_x.adj().array() += (is_lb_inf).select( + ret.adj().array() * -arena_x_val.exp() + lp_adj, + ret.adj().array() * diff * inv_logit_x * (1.0 - inv_logit_x) + + lp.adj() * (1.0 - 2.0 * inv_logit_x)); + } + if constexpr (is_autodiff_v) { + arena_lb.adj().array() + += (is_lb_inf).select(0, ret.adj().array() * (1.0 - inv_logit_x) + + -(1.0 / diff) * lp_adj); + } + if constexpr (is_autodiff_v) { + ub.adj() + += (is_lb_inf) + .select(ret.adj().array(), ret.adj().array() * inv_logit_x + + (1.0 / diff) * lp_adj) + .sum(); + } + }); return ret_type(ret); } } @@ -385,28 +364,23 @@ inline auto lub_constrain(const T& x, const L& lb, const U& ub) { auto inv_logit_x = to_arena(inv_logit(arena_x_val.array())); arena_t ret = (is_ub_inf).select( arena_x_val.array().exp() + lb_val, diff * inv_logit_x + lb_val); - reverse_pass_callback( - [state = std::make_tuple(arena_x, arena_x_val, arena_ub, lb, ret, - is_ub_inf, inv_logit_x, diff)]() mutable { - auto& [arena_x, arena_x_val, arena_ub, lb, ret, is_ub_inf, - inv_logit_x, diff] - = state; - if constexpr (is_autodiff_v) { - arena_x.adj().array() += (is_ub_inf).select( - ret.adj().array() * arena_x_val.array().exp(), - ret.adj().array() * diff * inv_logit_x * (1.0 - inv_logit_x)); - } - if constexpr (is_autodiff_v) { - lb.adj() += (is_ub_inf) - .select(ret.adj().array(), - ret.adj().array() * (1.0 - inv_logit_x)) - .sum(); - } - if constexpr (is_autodiff_v) { - arena_ub.adj().array() - += (is_ub_inf).select(0, ret.adj().array() * inv_logit_x); - } - }); + reverse_pass_callback([=]() mutable { + if constexpr (is_autodiff_v) { + arena_x.adj().array() += (is_ub_inf).select( + ret.adj().array() * arena_x_val.array().exp(), + ret.adj().array() * diff * inv_logit_x * (1.0 - inv_logit_x)); + } + if constexpr (is_autodiff_v) { + lb.adj() += (is_ub_inf) + .select(ret.adj().array(), + ret.adj().array() * (1.0 - inv_logit_x)) + .sum(); + } + if constexpr (is_autodiff_v) { + arena_ub.adj().array() + += (is_ub_inf).select(0, ret.adj().array() * inv_logit_x); + } + }); return ret_type(ret); } } @@ -443,31 +417,26 @@ inline auto lub_constrain(const T& x, const L& lb, const U& ub, auto inv_logit_x = to_arena(inv_logit(arena_x_val.array())); arena_t ret = (is_ub_inf).select( arena_x_val.array().exp() + lb_val, diff * inv_logit_x + lb_val); - reverse_pass_callback( - [state = std::make_tuple(arena_x, arena_x_val, diff, inv_logit_x, - arena_ub, lb, ret, lp, is_ub_inf)]() mutable { - auto& [arena_x, arena_x_val, diff, inv_logit_x, arena_ub, lb, ret, lp, - is_ub_inf] - = state; - const auto lp_adj = lp.adj(); - if constexpr (is_autodiff_v) { - arena_x.adj().array() += (is_ub_inf).select( - ret.adj().array() * arena_x_val.array().exp() + lp_adj, - ret.adj().array() * diff * inv_logit_x * (1.0 - inv_logit_x) - + lp.adj() * (1.0 - 2.0 * inv_logit_x)); - } - if constexpr (is_autodiff_v) { - lb.adj() += (is_ub_inf) - .select(ret.adj().array(), - ret.adj().array() * (1.0 - inv_logit_x) - + -(1.0 / diff) * lp_adj) - .sum(); - } - if constexpr (is_autodiff_v) { - arena_ub.adj().array() += (is_ub_inf).select( - 0, ret.adj().array() * inv_logit_x + (1.0 / diff) * lp_adj); - } - }); + reverse_pass_callback([=]() mutable { + const auto lp_adj = lp.adj(); + if constexpr (is_autodiff_v) { + arena_x.adj().array() += (is_ub_inf).select( + ret.adj().array() * arena_x_val.array().exp() + lp_adj, + ret.adj().array() * diff * inv_logit_x * (1.0 - inv_logit_x) + + lp.adj() * (1.0 - 2.0 * inv_logit_x)); + } + if constexpr (is_autodiff_v) { + lb.adj() += (is_ub_inf) + .select(ret.adj().array(), + ret.adj().array() * (1.0 - inv_logit_x) + + -(1.0 / diff) * lp_adj) + .sum(); + } + if constexpr (is_autodiff_v) { + arena_ub.adj().array() += (is_ub_inf).select( + 0, ret.adj().array() * inv_logit_x + (1.0 / diff) * lp_adj); + } + }); return ret_type(ret); } } @@ -501,13 +470,7 @@ inline auto lub_constrain(const T& x, const L& lb, const U& ub) { ub_val - arena_x.val().array().exp(), (is_ub_inf).select(arena_x_val.array().exp() + lb_val, diff * inv_logit_x + lb_val))); - reverse_pass_callback([state = std::make_tuple( - arena_x, arena_x_val, inv_logit_x, arena_ub, - arena_lb, diff, ret, is_ub_inf, is_lb_inf, - is_lb_ub_inf)]() mutable { - auto& [arena_x, arena_x_val, inv_logit_x, arena_ub, arena_lb, diff, ret, - is_ub_inf, is_lb_inf, is_lb_ub_inf] - = state; + reverse_pass_callback([=]() mutable { // The most likely case is neither of them are infinity const bool is_none_inf = !(is_lb_inf.any() || is_ub_inf.any()); if (is_none_inf) { @@ -588,13 +551,7 @@ inline auto lub_constrain(const T& x, const L& lb, const U& ub, arena_x_val.array(), log(diff) + (neg_abs_x - (2.0 * log1p_exp(neg_abs_x))))) .sum(); - reverse_pass_callback([state = std::make_tuple( - arena_x, arena_x_val, inv_logit_x, arena_ub, - arena_lb, diff, ret, is_ub_inf, is_lb_inf, - is_lb_ub_inf, lp)]() mutable { - auto& [arena_x, arena_x_val, inv_logit_x, arena_ub, arena_lb, diff, ret, - is_ub_inf, is_lb_inf, is_lb_ub_inf, lp] - = state; + reverse_pass_callback([=]() mutable { const auto lp_adj = lp.adj(); // The most likely case is neither of them are infinity const bool is_none_inf = !(is_lb_inf.any() || is_ub_inf.any()); diff --git a/stan/math/rev/fun/fma.hpp b/stan/math/rev/fun/fma.hpp index 56e5fbcd097..e1865ead30e 100644 --- a/stan/math/rev/fun/fma.hpp +++ b/stan/math/rev/fun/fma.hpp @@ -7,8 +7,6 @@ #include #include -#include - namespace stan { namespace math { @@ -196,8 +194,7 @@ namespace internal { template * = nullptr> inline auto fma_reverse_pass(T1& arena_x, T2& arena_y, T3& arena_z, T4& ret) { - return [state = std::make_tuple(arena_x, arena_y, arena_z, ret)]() mutable { - auto& [arena_x, arena_y, arena_z, ret] = state; + return [=]() mutable { if constexpr (is_autodiff_v) { arena_x.adj().array() += ret.adj().array() * value_of(arena_y).array(); } @@ -217,8 +214,7 @@ template * = nullptr, require_stan_scalar_t* = nullptr> inline auto fma_reverse_pass(T1& arena_x, T2& arena_y, T3& arena_z, T4& ret) { - return [state = std::make_tuple(arena_x, arena_y, arena_z, ret)]() mutable { - auto& [arena_x, arena_y, arena_z, ret] = state; + return [=]() mutable { if constexpr (is_autodiff_v) { arena_x.adj() += (ret.adj().array() * value_of(arena_y).array()).sum(); } @@ -238,8 +234,7 @@ template * = nullptr, require_stan_scalar_t* = nullptr> inline auto fma_reverse_pass(T1& arena_x, T2& arena_y, T3& arena_z, T4& ret) { - return [state = std::make_tuple(arena_x, arena_y, arena_z, ret)]() mutable { - auto& [arena_x, arena_y, arena_z, ret] = state; + return [=]() mutable { if constexpr (is_autodiff_v) { arena_x.adj().array() += ret.adj().array() * value_of(arena_y); } @@ -259,8 +254,7 @@ template * = nullptr, require_all_stan_scalar_t* = nullptr> inline auto fma_reverse_pass(T1& arena_x, T2& arena_y, T3& arena_z, T4& ret) { - return [state = std::make_tuple(arena_x, arena_y, arena_z, ret)]() mutable { - auto& [arena_x, arena_y, arena_z, ret] = state; + return [=]() mutable { if constexpr (is_autodiff_v) { arena_x.adj() += (ret.adj().array() * value_of(arena_y)).sum(); } @@ -280,8 +274,7 @@ template * = nullptr, require_stan_scalar_t* = nullptr> inline auto fma_reverse_pass(T1& arena_x, T2& arena_y, T3& arena_z, T4& ret) { - return [state = std::make_tuple(arena_x, arena_y, arena_z, ret)]() mutable { - auto& [arena_x, arena_y, arena_z, ret] = state; + return [=]() mutable { if constexpr (is_autodiff_v) { arena_x.adj().array() += ret.adj().array() * value_of(arena_y).array(); } @@ -301,8 +294,7 @@ template * = nullptr, require_all_stan_scalar_t* = nullptr> inline auto fma_reverse_pass(T1& arena_x, T2& arena_y, T3& arena_z, T4& ret) { - return [state = std::make_tuple(arena_x, arena_y, arena_z, ret)]() mutable { - auto& [arena_x, arena_y, arena_z, ret] = state; + return [=]() mutable { if constexpr (is_autodiff_v) { arena_x.adj() += (ret.adj().array() * value_of(arena_y).array()).sum(); } @@ -322,8 +314,7 @@ template * = nullptr, require_all_stan_scalar_t* = nullptr> inline auto fma_reverse_pass(T1& arena_x, T2& arena_y, T3& arena_z, T4& ret) { - return [state = std::make_tuple(arena_x, arena_y, arena_z, ret)]() mutable { - auto& [arena_x, arena_y, arena_z, ret] = state; + return [=]() mutable { if constexpr (is_autodiff_v) { arena_x.adj().array() += ret.adj().array() * value_of(arena_y); } diff --git a/stan/math/rev/fun/inv_inc_beta.hpp b/stan/math/rev/fun/inv_inc_beta.hpp index 9e0af10ea70..01567fa7ed4 100644 --- a/stan/math/rev/fun/inv_inc_beta.hpp +++ b/stan/math/rev/fun/inv_inc_beta.hpp @@ -17,7 +17,6 @@ #include #include #include -#include namespace stan { namespace math { @@ -63,9 +62,7 @@ inline var inv_inc_beta(const T1& a, const T2& b, const T3& p) { double b_val = value_of(b); double p_val = value_of(p); double w = inv_inc_beta(a_val, b_val, p_val); - return make_callback_var(w, [state = std::make_tuple(a, b, p, a_val, b_val, - w)](auto& vi) { - auto& [a, b, p, a_val, b_val, w] = state; + return make_callback_var(w, [=](auto& vi) { double log_w = log(w); double log1m_w = log1m(w); double one_m_a = 1 - a_val; From e2c91d633e821b0e1a3e97c2d63a0bc2a509dc3b Mon Sep 17 00:00:00 2001 From: Steve Bronder Date: Fri, 16 Oct 2026 13:19:29 -0400 Subject: [PATCH 07/10] Fix remaining CI platform, solver, and MPI failures Link ARKODE for the new ODE wrappers and mirror Make's Windows math constant definition. Pair symmetric finite differences before weighting to avoid FMA cancellation noise without relaxing test tolerances. Keep MPI command registration objects in every test executable and use Stan's test flags for its customized GoogleTest main. Validated on the exact CI merge: 11 numerical/ODE executables and five MPI executables pass. The gradient suite also passes with Clang 10 and -Werror; Windows definition propagation and CMake 4 configuration checked. --- CMakeLists.txt | 2 +- cmake/CMakeLists.txt | 7 +++++-- .../prim/functor/finite_diff_gradient_auto.hpp | 15 ++++++++++----- test/CMakeLists.txt | 15 +++++++++------ .../functor/finite_diff_gradient_auto_test.cpp | 17 +++++++++++++++++ 5 files changed, 42 insertions(+), 14 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4f37c83f532..9ad1238edfa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -111,7 +111,7 @@ if(STAN_MPI) target_link_libraries(stan_boost_mpi PUBLIC Boost::serialization MPI::MPI_CXX) endif() target_link_libraries(stanmath INTERFACE Boost::mpi Boost::serialization MPI::MPI_CXX) - add_library(stan_mpi STATIC stan/math/prim/functor/mpi_cluster_inst.cpp) + add_library(stan_mpi OBJECT stan/math/prim/functor/mpi_cluster_inst.cpp) target_link_libraries(stan_mpi PRIVATE stanmath) endif() diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt index 12a3e927446..3a20cff1d2b 100644 --- a/cmake/CMakeLists.txt +++ b/cmake/CMakeLists.txt @@ -35,6 +35,7 @@ else() endif() if(WIN32) + target_compile_definitions(stanmath INTERFACE _USE_MATH_DEFINES) # Match Make's windows.h macro probe, without requiring an executable link. function(stan_detect_ucrt) include(CheckCXXSourceCompiles) @@ -113,9 +114,10 @@ endif() target_link_libraries(stanmath INTERFACE TBB::tbb) if(STAN_USE_SYSTEM_SUNDIALS) - find_package(SUNDIALS REQUIRED COMPONENTS cvodes idas kinsol nvecserial) + find_package(SUNDIALS REQUIRED COMPONENTS cvodes idas kinsol arkode nvecserial) target_link_libraries(stanmath INTERFACE - SUNDIALS::cvodes SUNDIALS::idas SUNDIALS::kinsol SUNDIALS::nvecserial) + SUNDIALS::cvodes SUNDIALS::idas SUNDIALS::kinsol SUNDIALS::arkode + SUNDIALS::nvecserial) else() # The pruned vendored distribution has no upstream CMakeLists.txt. # Match make/libraries, including Stan's printf override and common modules. @@ -123,6 +125,7 @@ else() "${STAN_SUNDIALS_ROOT}/src/cvodes/*.c" "${STAN_SUNDIALS_ROOT}/src/idas/*.c" "${STAN_SUNDIALS_ROOT}/src/kinsol/*.c" + "${STAN_SUNDIALS_ROOT}/src/arkode/*.c" "${STAN_SUNDIALS_ROOT}/src/sundials/*.c" "${STAN_SUNDIALS_ROOT}/src/sunmatrix/band/*.c" "${STAN_SUNDIALS_ROOT}/src/sunmatrix/dense/*.c" diff --git a/stan/math/prim/functor/finite_diff_gradient_auto.hpp b/stan/math/prim/functor/finite_diff_gradient_auto.hpp index 8c9951ae37b..002f094ed1a 100644 --- a/stan/math/prim/functor/finite_diff_gradient_auto.hpp +++ b/stan/math/prim/functor/finite_diff_gradient_auto.hpp @@ -51,8 +51,8 @@ template ; - static constexpr int h_scale[6] = {3, 2, 1, -3, -2, -1}; - static constexpr int mults[6] = {1, -9, 45, -1, 9, -45}; + static constexpr int h_scale[3] = {3, 2, 1}; + static constexpr int mults[3] = {1, -9, 45}; fx = f(x); grad_fx.resize(x.size()); @@ -61,12 +61,17 @@ inline void finite_diff_gradient_auto(const F& f, VectorT&& x, ScalarT& fx, grad_map = EigT::NullaryExpr(x.size(), [&f, &x](Eigen::Index i) { double h = finite_diff_stepsize(value_of_rec(x[i])); ScalarT delta_f = 0; - for (int j = 0; j < 6; ++j) { - auto x_temp + for (int j = 0; j < 3; ++j) { + auto x_plus = EigT::NullaryExpr(x.size(), [&x, &i, &h, &j](Eigen::Index k) { return k == i ? x[i] + h * h_scale[j] : x[k]; }); - delta_f += f(std::move(x_temp)) * mults[j]; + auto x_minus + = EigT::NullaryExpr(x.size(), [&x, &i, &h, &j](Eigen::Index k) { + return k == i ? x[i] - h * h_scale[j] : x[k]; + }); + // Cancel constant terms before weighting the symmetric differences. + delta_f += (f(std::move(x_plus)) - f(std::move(x_minus))) * mults[j]; } return delta_f / (60 * h); }); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d471f0cf489..f20f01d846f 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -3,14 +3,11 @@ # Stan's vendored gtest_main initializes the MPI cluster and includes Stan # headers even when MPI is disabled. target_link_libraries(gtest_main PRIVATE stanmath) -if(STAN_MPI) - target_link_libraries(gtest_main PRIVATE stan_mpi) -endif() +# Stan's customized main includes Stan headers. Use the test flags below +# instead of GoogleTest's internal warning policy for its own sources. +set_property(TARGET gtest_main PROPERTY COMPILE_FLAGS "") add_library(stan_test_options INTERFACE) target_link_libraries(stan_test_options INTERFACE stanmath gtest_main) -if(STAN_MPI) - target_link_libraries(stan_test_options INTERFACE stan_mpi) -endif() if(MSVC) target_compile_options(stan_test_options INTERFACE ${STAN_TEST_OPTIMIZATION_FLAGS} /W3) else() @@ -21,6 +18,8 @@ else() -Werror=invalid-pch -Wno-misleading-indentation) endif() endif() +target_compile_options(gtest_main PRIVATE + "$") function(stan_add_pch target) if(STAN_EFFECTIVE_TEST_PCH) @@ -54,6 +53,10 @@ endfunction() function(stan_add_test_binary target) # Xcode needs a real source alongside object-library references. add_executable(${target} "${PROJECT_SOURCE_DIR}/cmake/pch.cpp" ${ARGN}) + if(STAN_MPI) + # A static archive can discard the unreferenced registration initializer. + target_sources(${target} PRIVATE $) + endif() stan_configure_test(${target} "") set_property(TARGET ${target} PROPERTY STAN_TEST_OBJECTS "${ARGN}") if(WIN32) diff --git a/test/unit/math/mix/functor/finite_diff_gradient_auto_test.cpp b/test/unit/math/mix/functor/finite_diff_gradient_auto_test.cpp index eeae792742a..18fed3f7d2d 100644 --- a/test/unit/math/mix/functor/finite_diff_gradient_auto_test.cpp +++ b/test/unit/math/mix/functor/finite_diff_gradient_auto_test.cpp @@ -48,3 +48,20 @@ TEST(MathMixMatFunctor, FiniteDiffGradientAuto) { w << 1, 2, 3, 4, 5; expect_match_autodiff(log_fun, w); } + +TEST(MathMixMatFunctor, FiniteDiffGradientAutoConstant) { + // Keep the evaluations observable so the compiler cannot fold the stencil. + const volatile double constant = -937.6; + auto f = [&constant](const auto&) { return constant; }; + Eigen::VectorXd x(3); + x << 0, 1.7, -1000; + double fx; + Eigen::VectorXd grad; + stan::math::finite_diff_gradient_auto(f, x, fx, grad); + + EXPECT_DOUBLE_EQ(-937.6, fx); + ASSERT_EQ(x.size(), grad.size()); + for (int i = 0; i < grad.size(); ++i) { + EXPECT_DOUBLE_EQ(0, grad(i)); + } +} From f1a0dc71b3dccc0dfc56d1c5e64d47a7e7426501 Mon Sep 17 00:00:00 2001 From: Steve Bronder Date: Fri, 16 Oct 2026 16:01:43 -0400 Subject: [PATCH 08/10] Fix remaining threading and Windows ARM numerical tests Remove the unused capture and two profiling-test variables rejected by Clang 10. Pair symmetric fixed-step finite differences before weighting to cancel constant terms, with constant-function and analytic-polynomial regressions. Use absolute bounds for zero-valued matrix sums and solver finite differences, and verify the degenerate solver's analytic gradient independently. Preserve public APIs and thread-pool initialization. Validated all 20 Jenkins threading executables with Clang 10 and -Werror, seven numerical suites with FMA enabled for C and C++, and ten shuffled grouped-core repetitions each with threading on and off. --- .../prim/functor/finite_diff_gradient.hpp | 26 +++++--------- .../constraint/sum_to_zero_transform_test.cpp | 2 +- .../core/init_threadpool_tbb_late_test.cpp | 2 +- .../functor/finite_diff_gradient_test.cpp | 35 +++++++++++++++++++ .../rev/core/profiling_threading_test.cpp | 2 -- .../rev/functor/algebra_solver_fp_test.cpp | 5 ++- 6 files changed, 50 insertions(+), 22 deletions(-) create mode 100644 test/unit/math/prim/functor/finite_diff_gradient_test.cpp diff --git a/stan/math/prim/functor/finite_diff_gradient.hpp b/stan/math/prim/functor/finite_diff_gradient.hpp index 72237fd6c0e..7ec59cdb36c 100644 --- a/stan/math/prim/functor/finite_diff_gradient.hpp +++ b/stan/math/prim/functor/finite_diff_gradient.hpp @@ -43,6 +43,8 @@ template inline void finite_diff_gradient(const F& f, const Eigen::VectorXd& x, double& fx, Eigen::VectorXd& grad_fx, double epsilon = 1e-03) { + static constexpr int h_scale[3] = {3, 2, 1}; + static constexpr int mults[3] = {1, -9, 45}; Eigen::VectorXd x_temp(x); int d = x.size(); grad_fx.resize(d); @@ -52,23 +54,13 @@ inline void finite_diff_gradient(const F& f, const Eigen::VectorXd& x, for (int i = 0; i < d; ++i) { double delta_f = 0.0; - x_temp(i) = x(i) + 3.0 * epsilon; - delta_f = f(x_temp); - - x_temp(i) = x(i) + 2.0 * epsilon; - delta_f -= 9.0 * f(x_temp); - - x_temp(i) = x(i) + epsilon; - delta_f += 45.0 * f(x_temp); - - x_temp(i) = x(i) + -3.0 * epsilon; - delta_f -= f(x_temp); - - x_temp(i) = x(i) + -2.0 * epsilon; - delta_f += 9.0 * f(x_temp); - - x_temp(i) = x(i) + -epsilon; - delta_f -= 45.0 * f(x_temp); + for (int j = 0; j < 3; ++j) { + x_temp(i) = x(i) + h_scale[j] * epsilon; + const double f_plus = f(x_temp); + x_temp(i) = x(i) - h_scale[j] * epsilon; + // Cancel constant terms before weighting the symmetric differences. + delta_f += (f_plus - f(x_temp)) * mults[j]; + } delta_f /= 60 * epsilon; diff --git a/test/unit/math/prim/constraint/sum_to_zero_transform_test.cpp b/test/unit/math/prim/constraint/sum_to_zero_transform_test.cpp index 0184b77fc57..ebea34da8eb 100644 --- a/test/unit/math/prim/constraint/sum_to_zero_transform_test.cpp +++ b/test/unit/math/prim/constraint/sum_to_zero_transform_test.cpp @@ -90,7 +90,7 @@ TEST(prob_transform, sum_to_zero_mat_rt) { EXPECT_EQ(x.rows() + 1, y.rows()); EXPECT_EQ(x.cols() + 1, y.cols()); - EXPECT_FLOAT_EQ(y.sum(), 0.0); + EXPECT_NEAR(y.sum(), 0.0, 1E-15); EXPECT_MATRIX_NEAR(y.rowwise().sum(), Eigen::VectorXd::Zero(4), 1E-15); EXPECT_MATRIX_NEAR(y.colwise().sum(), Eigen::RowVectorXd::Zero(5), 1E-15); EXPECT_NO_THROW(stan::math::check_sum_to_zero("checkSumToZero", "y", y)); diff --git a/test/unit/math/prim/core/init_threadpool_tbb_late_test.cpp b/test/unit/math/prim/core/init_threadpool_tbb_late_test.cpp index f9fc73e924d..fa5c338bf64 100644 --- a/test/unit/math/prim/core/init_threadpool_tbb_late_test.cpp +++ b/test/unit/math/prim/core/init_threadpool_tbb_late_test.cpp @@ -15,7 +15,7 @@ TEST(intel_tbb_new_late_init, check_status) { auto state = stan::math::internal::make_threadpool_tbb(num_threads - 1); EXPECT_TRUE(state.arena.is_active()); EXPECT_EQ(num_threads - 1, state.arena.max_concurrency()); - state.arena.execute([num_threads]() { + state.arena.execute([]() { EXPECT_EQ(num_threads - 1, tbb::this_task_arena::max_concurrency()); }); EXPECT_EQ(num_threads, tbb::this_task_arena::max_concurrency()); diff --git a/test/unit/math/prim/functor/finite_diff_gradient_test.cpp b/test/unit/math/prim/functor/finite_diff_gradient_test.cpp new file mode 100644 index 00000000000..61beb1d2834 --- /dev/null +++ b/test/unit/math/prim/functor/finite_diff_gradient_test.cpp @@ -0,0 +1,35 @@ +#include +#include + +TEST(MathPrimFunctor, FiniteDiffGradientConstant) { + // Preserve the evaluations when testing cancellation under FMA contraction. + const volatile double constant = -937.6; + auto f = [&constant](const Eigen::VectorXd&) { return constant; }; + Eigen::VectorXd x(3); + x << 0, 1.7, -1000; + double fx; + Eigen::VectorXd grad; + stan::math::finite_diff_gradient(f, x, fx, grad); + + EXPECT_DOUBLE_EQ(-937.6, fx); + ASSERT_EQ(x.size(), grad.size()); + for (int i = 0; i < grad.size(); ++i) { + EXPECT_DOUBLE_EQ(0, grad(i)); + } +} + +TEST(MathPrimFunctor, FiniteDiffGradientPolynomial) { + auto f = [](const Eigen::VectorXd& x) { + return x(0) * x(0) * x(0) + x(1) * x(1) * x(1) * x(1); + }; + Eigen::VectorXd x(2); + x << 1.7, -0.3; + double fx; + Eigen::VectorXd grad; + stan::math::finite_diff_gradient(f, x, fx, grad); + + EXPECT_DOUBLE_EQ(f(x), fx); + ASSERT_EQ(2, grad.size()); + EXPECT_NEAR(3 * x(0) * x(0), grad(0), 1e-10); + EXPECT_NEAR(4 * x(1) * x(1) * x(1), grad(1), 1e-10); +} diff --git a/test/unit/math/rev/core/profiling_threading_test.cpp b/test/unit/math/rev/core/profiling_threading_test.cpp index b3c2c78fa2b..2e15bbf9779 100644 --- a/test/unit/math/rev/core/profiling_threading_test.cpp +++ b/test/unit/math/rev/core/profiling_threading_test.cpp @@ -30,7 +30,6 @@ struct grouped_count_lpdf { TEST_F(AgradRev, Profiling_profile_threading) { using stan::math::var; using stan::math::test::get_new_msg; - double lambda_d = 10.0; const std::size_t groups = 10; const std::size_t elems_per_group = 1000; const std::size_t elems = groups * elems_per_group; @@ -47,7 +46,6 @@ TEST_F(AgradRev, Profiling_profile_threading) { for (std::size_t i = 0; i != groups; ++i) vlambda_v[i] = i + 0.2; - var lambda_v = vlambda_v[0]; var poisson_lpdf = stan::math::reduce_sum>( diff --git a/test/unit/math/rev/functor/algebra_solver_fp_test.cpp b/test/unit/math/rev/functor/algebra_solver_fp_test.cpp index fd31b9da8d4..079026ace2c 100644 --- a/test/unit/math/rev/functor/algebra_solver_fp_test.cpp +++ b/test/unit/math/rev/functor/algebra_solver_fp_test.cpp @@ -471,7 +471,10 @@ TEST_F(FP_degenerated_func_test, algebra_solver_fp) { auto f_fd = fd_functor(i); finite_diff_gradient_auto(f_fd, y, fx, grad_fx); for (int j = 0; j < M; ++j) { - EXPECT_FLOAT_EQ(grad_fx(j), yp(j).adj()); + // This solution is (y(0), y(0)), independent of y(1). + EXPECT_NEAR(j == 0 ? 1.0 : 0.0, yp(j).adj(), 1e-12); + // Differencing amplifies the numerical solver's convergence error. + EXPECT_NEAR(grad_fx(j), yp(j).adj(), 1e-8); } } } From cf12429f44f1b654de69e15fe925c854ba54c804 Mon Sep 17 00:00:00 2001 From: Steve Bronder Date: Fri, 16 Oct 2026 17:32:04 -0400 Subject: [PATCH 09/10] Read Jenkins parallelism from the active container runPod supplies PARALLEL in the container environment, while the Groovy env lookup fell back to one. Resolve it through sh and reuse that count for CMake builds and CTest. Apply the same lookup to header and doxygen builds, retaining the default of one and explicit serial test overrides. Validated the old failure and repaired commands with a Groovy harness that separates the controller binding from the container shell environment. Checked unset, empty, four, six, and nonpositive values, plus serial test overrides. All eight CMake command-planning tests pass. --- Jenkinsfile | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index d62d56107c7..ff7240e5397 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -25,6 +25,12 @@ def shellQuote(String value) { return "'" + value.replace("'", "'\"'\"'") + "'" } +def containerJobs() { + // runPod sets PARALLEL in the container, outside Groovy's env binding. + def jobs = sh(returnStdout: true, script: 'printf "%s" "${PARALLEL:-1}"').trim() + return Math.max(1, jobs.toInteger()) +} + def configureCmake(List arguments) { def flags = ['-S', '.', '-B', 'build'] + arguments if (fileExists('build/CMakeCache.txt')) { @@ -36,7 +42,7 @@ def configureCmake(List arguments) { def runCmakeTests(Map config) { def buildDir = 'build' def reportDir = "test-results/${config.name}" - def jobs = Math.max(1, (env.PARALLEL ?: '1').toInteger()) + def jobs = containerJobs() def flags = [ '-DCMAKE_BUILD_TYPE=Release', '-DSTAN_TEST_HEADERS=OFF', '-DSTAN_BUILD_PROB_TESTS=OFF', @@ -142,7 +148,7 @@ up the autoformatter locally. (Check console output at ${env.BUILD_URL}) }, Documentation: { configureCmake(['-DBUILD_TESTING=OFF', '-DSTAN_BUILD_DOCS=ON']) - sh "cmake --build build --parallel ${env.PARALLEL ?: '1'} --target doxygen" + sh "cmake --build build --parallel ${containerJobs()} --target doxygen" }, ) } @@ -157,7 +163,7 @@ up the autoformatter locally. (Check console output at ${env.BUILD_URL}) "-DCMAKE_C_COMPILER=${CLANG_CC}", "-DCMAKE_CXX_COMPILER=${CLANG_CXX}", '-DCMAKE_CXX_FLAGS=-Werror', '-DSTAN_BUILD_PROB_TESTS=OFF', '-DSTAN_TEST_HEADERS=ON']) - sh "cmake --build build --parallel ${env.PARALLEL ?: '1'} --target test-headers" + sh "cmake --build build --parallel ${containerJobs()} --target test-headers" } } }, unit: { @@ -322,7 +328,7 @@ up the autoformatter locally. (Check console output at ${env.BUILD_URL}) stage('Upload doxygen') { def branch = 'gh-pages' configureCmake(['-DBUILD_TESTING=OFF', '-DSTAN_BUILD_DOCS=ON']) - sh "cmake --build build --parallel ${env.PARALLEL ?: '1'} --target doxygen" + sh "cmake --build build --parallel ${containerJobs()} --target doxygen" dir('gh-pages') { def scm = scmGit( branches: [[name: "refs/heads/$branch"]], From ae42a39ac4d966a11bd5bb827ea0612cd4b88933 Mon Sep 17 00:00:00 2001 From: Steve Bronder Date: Fri, 16 Oct 2026 18:12:23 -0400 Subject: [PATCH 10/10] Rename the shared empty CMake source to empty.cpp Update standalone header checks, test executables, PCH producers, and compile-only PCH producers to use the clearer source filename. Verified before renaming that header targets compile ordinary ELF objects with PCH enabled on Clang 18, without producing PCH artifacts. After renaming, rebuilt three header checks, a test executable, and the unit/compile-only PCH paths. The test and CMake planning checks pass. --- CMakeLists.txt | 2 +- cmake/{pch.cpp => empty.cpp} | 0 test/CMakeLists.txt | 6 +++--- 3 files changed, 4 insertions(+), 4 deletions(-) rename cmake/{pch.cpp => empty.cpp} (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ad1238edfa..1602133d1f3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -138,7 +138,7 @@ if(STAN_COMPILE_TEST_SOURCE) add_library(stan_compile_test OBJECT EXCLUDE_FROM_ALL "${STAN_COMPILE_TEST_SOURCE}") set(compile_test_targets stan_compile_test) if(STAN_EFFECTIVE_TEST_PCH) - add_library(stan_compile_test_pch OBJECT EXCLUDE_FROM_ALL cmake/pch.cpp) + add_library(stan_compile_test_pch OBJECT EXCLUDE_FROM_ALL cmake/empty.cpp) list(APPEND compile_test_targets stan_compile_test_pch) target_precompile_headers(stan_compile_test_pch PRIVATE ) target_precompile_headers(stan_compile_test REUSE_FROM stan_compile_test_pch) diff --git a/cmake/pch.cpp b/cmake/empty.cpp similarity index 100% rename from cmake/pch.cpp rename to cmake/empty.cpp diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index f20f01d846f..07454a8ffe7 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -23,7 +23,7 @@ target_compile_options(gtest_main PRIVATE function(stan_add_pch target) if(STAN_EFFECTIVE_TEST_PCH) - add_library(${target} OBJECT EXCLUDE_FROM_ALL "${PROJECT_SOURCE_DIR}/cmake/pch.cpp") + add_library(${target} OBJECT EXCLUDE_FROM_ALL "${PROJECT_SOURCE_DIR}/cmake/empty.cpp") set_target_properties(${target} PROPERTIES POSITION_INDEPENDENT_CODE FALSE) target_link_libraries(${target} PRIVATE stan_test_options) target_precompile_headers(${target} PRIVATE ${ARGN}) @@ -52,7 +52,7 @@ endfunction() function(stan_add_test_binary target) # Xcode needs a real source alongside object-library references. - add_executable(${target} "${PROJECT_SOURCE_DIR}/cmake/pch.cpp" ${ARGN}) + add_executable(${target} "${PROJECT_SOURCE_DIR}/cmake/empty.cpp" ${ARGN}) if(STAN_MPI) # A static archive can discard the unreferenced registration initializer. target_sources(${target} PRIVATE $) @@ -205,7 +205,7 @@ if(STAN_TEST_HEADERS) endif() string(REPLACE "/" "_" name "${relative_header}") string(REGEX REPLACE "\\.hpp$" "-test" name "${name}") - add_library(${name} OBJECT EXCLUDE_FROM_ALL "${PROJECT_SOURCE_DIR}/cmake/pch.cpp") + add_library(${name} OBJECT EXCLUDE_FROM_ALL "${PROJECT_SOURCE_DIR}/cmake/empty.cpp") target_link_libraries(${name} PRIVATE stanmath) if(MSVC) target_compile_options(${name} PRIVATE "/FI${header}")