From 35566840f2f57e7fb04c89086e9ab5212117bdd9 Mon Sep 17 00:00:00 2001 From: Steve Bronder Date: Thu, 27 Aug 2026 15:23:03 -0400 Subject: [PATCH 1/4] Add red tests for reduce_sum tuple arguments --- .../functor/reduce_sum_tuple_data_test.cpp | 34 ++++++++++++++++ .../rev/functor/reduce_sum_tuple_var_test.cpp | 40 +++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 test/unit/math/rev/functor/reduce_sum_tuple_data_test.cpp create mode 100644 test/unit/math/rev/functor/reduce_sum_tuple_var_test.cpp diff --git a/test/unit/math/rev/functor/reduce_sum_tuple_data_test.cpp b/test/unit/math/rev/functor/reduce_sum_tuple_data_test.cpp new file mode 100644 index 00000000000..984bcecb979 --- /dev/null +++ b/test/unit/math/rev/functor/reduce_sum_tuple_data_test.cpp @@ -0,0 +1,34 @@ +#include +#include +#include +#include +#include + +namespace { + +struct sum_with_laplace_options { + template + auto operator()(const std::vector& slice, std::size_t start, + std::size_t end, std::ostream* msgs, + const stan::math::var& shared, + const Options& laplace_options) const { + return slice.size() + * (shared + static_cast(std::get<2>(laplace_options))); + } +}; + +// Regression test for https://github.com/stan-dev/math/issues/3359. +TEST_F(AgradRev, reduce_sum_accepts_const_laplace_options_tuple) { + stan::math::var shared = 1.0; + const auto laplace_options = stan::math::generate_laplace_options(1); + + stan::math::var result = stan::math::reduce_sum( + std::vector{0, 1}, 1, nullptr, shared, laplace_options); + + const double expected = 2.0 * (shared.val() + std::get<2>(laplace_options)); + EXPECT_FLOAT_EQ(expected, result.val()); + result.grad(); + EXPECT_FLOAT_EQ(2.0, shared.adj()); +} + +} // namespace diff --git a/test/unit/math/rev/functor/reduce_sum_tuple_var_test.cpp b/test/unit/math/rev/functor/reduce_sum_tuple_var_test.cpp new file mode 100644 index 00000000000..26cfe8bdd67 --- /dev/null +++ b/test/unit/math/rev/functor/reduce_sum_tuple_var_test.cpp @@ -0,0 +1,40 @@ +#include +#include +#include +#include +#include + +namespace { + +struct sum_tuple_var { + template + auto operator()(const std::vector& slice, std::size_t start, + std::size_t end, std::ostream* msgs, + const Tuple& shared) const { + return slice.size() + * (stan::math::sum(std::get<0>(shared)) + + stan::math::sum(std::get<1>(shared))); + } +}; + +// Regression test for https://github.com/stan-dev/math/issues/3041. +TEST_F(AgradRev, reduce_sum_accepts_tuple_containing_vars) { + using stan::math::var; + + Eigen::Matrix first(2); + first << 1.0, 2.0; + Eigen::Matrix second(1); + second << 3.0; + auto shared = std::make_tuple(first, second); + + var result = stan::math::reduce_sum(std::vector{0, 1}, 1, + nullptr, shared); + + EXPECT_FLOAT_EQ(12.0, result.val()); + result.grad(); + EXPECT_FLOAT_EQ(2.0, first(0).adj()); + EXPECT_FLOAT_EQ(2.0, first(1).adj()); + EXPECT_FLOAT_EQ(2.0, second(0).adj()); +} + +} // namespace From e3822e7b3940979aa244220a05d05d61f8079556 Mon Sep 17 00:00:00 2001 From: Steve Bronder Date: Thu, 27 Aug 2026 16:18:20 -0400 Subject: [PATCH 2/4] Add tuple support to reverse-mode helpers --- stan/math/rev/core/accumulate_adjoints.hpp | 25 +++++++++++ stan/math/rev/core/count_vars.hpp | 37 +++++++++++----- stan/math/rev/core/deep_copy_vars.hpp | 20 +++++++++ stan/math/rev/core/save_varis.hpp | 23 ++++++++++ .../rev/core/accumulate_adjoints_test.cpp | 37 ++++++++++++++++ test/unit/math/rev/core/count_vars_test.cpp | 16 +++++++ .../math/rev/core/deep_copy_vars_test.cpp | 44 +++++++++++++++++++ test/unit/math/rev/core/save_varis_test.cpp | 32 ++++++++++++++ 8 files changed, 222 insertions(+), 12 deletions(-) diff --git a/stan/math/rev/core/accumulate_adjoints.hpp b/stan/math/rev/core/accumulate_adjoints.hpp index e5b27354ebd..30e4869db76 100644 --- a/stan/math/rev/core/accumulate_adjoints.hpp +++ b/stan/math/rev/core/accumulate_adjoints.hpp @@ -1,6 +1,7 @@ #ifndef STAN_MATH_REV_CORE_ACCUMULATE_ADJOINTS_HPP #define STAN_MATH_REV_CORE_ACCUMULATE_ADJOINTS_HPP +#include #include #include #include @@ -33,6 +34,9 @@ template * = nullptr, typename... Pargs> inline double* accumulate_adjoints(double* dest, Arith&& x, Pargs&&... args); +template * = nullptr, typename... Pargs> +inline double* accumulate_adjoints(double* dest, Tuple&& x, Pargs&&... args); + inline double* accumulate_adjoints(double* dest); /** @@ -140,6 +144,27 @@ inline double* accumulate_adjoints(double* dest, Arith&& x, Pargs&&... args) { return accumulate_adjoints(dest, std::forward(args)...); } +/** + * Accumulate adjoints from a tuple into storage pointed to by dest, then + * recursively accumulate adjoints from the remaining arguments. + * + * @tparam Tuple A tuple type + * @tparam Pargs Types of remaining arguments + * @param dest Pointer to where adjoints are to be accumulated + * @param x A tuple containing arguments whose adjoints are accumulated + * @param args Further args to accumulate over + * @return Final position of adjoint storage pointer + */ +template *, typename... Pargs> +inline double* accumulate_adjoints(double* dest, Tuple&& x, Pargs&&... args) { + dest = stan::math::apply( + [dest](auto&&... tuple_args) { + return accumulate_adjoints(dest, tuple_args...); + }, + std::forward(x)); + return accumulate_adjoints(dest, std::forward(args)...); +} + /** * End accumulate_adjoints recursion and return pointer * diff --git a/stan/math/rev/core/count_vars.hpp b/stan/math/rev/core/count_vars.hpp index 3edd209399a..d108f4b475a 100644 --- a/stan/math/rev/core/count_vars.hpp +++ b/stan/math/rev/core/count_vars.hpp @@ -33,6 +33,9 @@ template >* = nullptr, typename... Pargs> inline size_t count_vars_impl(size_t count, Arith& x, Pargs&&... args); +template * = nullptr, typename... Pargs> +inline size_t count_vars_impl(size_t count, Tuple&& x, Pargs&&... args); + inline size_t count_vars_impl(size_t count); /** * Count the number of vars in x (a std::vector of vars), @@ -132,23 +135,33 @@ inline size_t count_vars_impl(size_t count, Arith& x, Pargs&&... args) { inline size_t count_vars_impl(size_t count, std::basic_ostream*&) { return count; } + +/** + * Count the vars in a tuple, add them to the running total, and count the vars + * in the remaining arguments. + * + * @tparam Tuple A tuple type + * @tparam Pargs Types of remaining arguments + * @param[in] count The current count of the number of vars + * @param[in] x A tuple containing arguments to count + * @param[in] args Objects to be forwarded to the recursive call + * @return The total number of vars + */ +template *, typename... Pargs> +inline size_t count_vars_impl(size_t count, Tuple&& x, Pargs&&... args) { + count = stan::math::apply( + [count](auto&&... tuple_args) { + return count_vars_impl(count, tuple_args...); + }, + std::forward(x)); + return count_vars_impl(count, std::forward(args)...); +} + /** * End count_vars_impl recursion and return total number of counted vars */ inline size_t count_vars_impl(size_t count) { return count; } -template -inline size_t count_vars_impl(std::size_t count, - const std::tuple& arg, Args&&... args) { - return count_vars_impl( - stan::math::apply( - [count](auto&&... inner_args) { - return (count_vars_impl(0, inner_args) + ... + count); - }, - arg), - std::forward(args)...); -} - } // namespace internal /** diff --git a/stan/math/rev/core/deep_copy_vars.hpp b/stan/math/rev/core/deep_copy_vars.hpp index 06561d1a9e0..ae0fe4e8574 100644 --- a/stan/math/rev/core/deep_copy_vars.hpp +++ b/stan/math/rev/core/deep_copy_vars.hpp @@ -1,10 +1,12 @@ #ifndef STAN_MATH_REV_CORE_DEEP_COPY_VARS_HPP #define STAN_MATH_REV_CORE_DEEP_COPY_VARS_HPP +#include #include #include #include +#include #include #include @@ -81,6 +83,24 @@ inline auto deep_copy_vars(EigT&& arg) { .eval(); } +/** + * Copy the vars in a tuple but reallocate new varis for them. + * + * @tparam Tuple A tuple type + * @param arg A tuple containing arguments to copy + * @return A tuple containing copied arguments + */ +template * = nullptr> +inline auto deep_copy_vars(Tuple&& arg) { + return stan::math::apply( + [](auto&&... tuple_args) { + return std::tuple(tuple_args)))...>{ + deep_copy_vars(std::forward(tuple_args))...}; + }, + std::forward(arg)); +} + } // namespace math } // namespace stan diff --git a/stan/math/rev/core/save_varis.hpp b/stan/math/rev/core/save_varis.hpp index c53a5390539..494447cdd20 100644 --- a/stan/math/rev/core/save_varis.hpp +++ b/stan/math/rev/core/save_varis.hpp @@ -2,6 +2,7 @@ #define STAN_MATH_REV_CORE_SAVE_VARIS_HPP #include +#include #include #include #include @@ -33,6 +34,9 @@ template * = nullptr, typename... Pargs> inline vari** save_varis(vari** dest, Arith&& x, Pargs&&... args); +template * = nullptr, typename... Pargs> +inline vari** save_varis(vari** dest, Tuple&& x, Pargs&&... args); + inline vari** save_varis(vari** dest); /** @@ -136,6 +140,25 @@ inline vari** save_varis(vari** dest, Arith&& x, Pargs&&... args) { return save_varis(dest, std::forward(args)...); } +/** + * Save the vari pointers in a tuple into the memory pointed to by dest, then + * recursively save the varis in the remaining arguments. + * + * @tparam Tuple A tuple type + * @tparam Pargs Types of remaining arguments + * @param[in, out] dest Pointer to where vari pointers are saved + * @param[in] x A tuple containing arguments whose varis are saved + * @param[in] args Additional arguments to have their varis saved + * @return Final position of dest pointer + */ +template *, typename... Pargs> +inline vari** save_varis(vari** dest, Tuple&& x, Pargs&&... args) { + dest = stan::math::apply( + [dest](auto&&... tuple_args) { return save_varis(dest, tuple_args...); }, + std::forward(x)); + return save_varis(dest, std::forward(args)...); +} + /** * End save_varis recursion and return pointer * diff --git a/test/unit/math/rev/core/accumulate_adjoints_test.cpp b/test/unit/math/rev/core/accumulate_adjoints_test.cpp index e8017993f73..810d88e7acc 100644 --- a/test/unit/math/rev/core/accumulate_adjoints_test.cpp +++ b/test/unit/math/rev/core/accumulate_adjoints_test.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include TEST_F(AgradRev, Rev_accumulate_adjoints_zero_args) { @@ -437,3 +438,39 @@ TEST_F(AgradRev, Rev_accumulate_adjoints_sum) { EXPECT_EQ(ptr, storage.data() + num_vars); stan::math::recover_memory(); } + +TEST_F(AgradRev, Rev_accumulate_adjoints_tuple_args) { + const std::tuple<> empty; + const auto data = std::make_tuple(1, Eigen::VectorXd::Ones(2)); + Eigen::VectorXd data_storage = Eigen::VectorXd::Zero(2); + double* data_ptr + = stan::math::accumulate_adjoints(data_storage.data(), empty, data); + EXPECT_EQ(data_storage.data(), data_ptr); + EXPECT_FLOAT_EQ(0.0, data_storage(0)); + EXPECT_FLOAT_EQ(0.0, data_storage(1)); + + stan::math::var before = 1.0; + stan::math::var first = 2.0; + Eigen::Matrix vars(2); + vars << 3.0, 4.0; + stan::math::var last = 5.0; + stan::math::var after = 6.0; + before.vi_->adj_ = 1.0; + first.vi_->adj_ = 2.0; + vars(0).vi_->adj_ = 3.0; + vars(1).vi_->adj_ = 4.0; + last.vi_->adj_ = 5.0; + after.vi_->adj_ = 6.0; + auto nested = std::make_tuple(first, std::make_tuple(vars, 7), last); + Eigen::VectorXd storage = Eigen::VectorXd::Constant(8, 10.0); + + double* ptr = stan::math::accumulate_adjoints(storage.data(), before, nested, + std::make_tuple(after)); + + EXPECT_EQ(storage.data() + 6, ptr); + for (int i = 0; i < 6; ++i) { + EXPECT_FLOAT_EQ(11.0 + i, storage(i)); + } + EXPECT_FLOAT_EQ(10.0, storage(6)); + EXPECT_FLOAT_EQ(10.0, storage(7)); +} diff --git a/test/unit/math/rev/core/count_vars_test.cpp b/test/unit/math/rev/core/count_vars_test.cpp index 25157012714..7f3eed053f2 100644 --- a/test/unit/math/rev/core/count_vars_test.cpp +++ b/test/unit/math/rev/core/count_vars_test.cpp @@ -3,6 +3,7 @@ #include #include +#include #include using stan::math::var; @@ -161,3 +162,18 @@ TEST_F(AgradRev, Rev_count_vars_sum) { count_vars(arg1, arg18, arg17, arg2, arg16, arg3, arg15, arg4, arg14, arg5, arg13, arg12, arg6, arg11, arg7, arg10, arg8, arg9)); } + +TEST_F(AgradRev, Rev_count_vars_tuple_args) { + const std::tuple<> empty; + const auto data = std::make_tuple(1, Eigen::VectorXd::Ones(2)); + EXPECT_EQ(0, stan::math::count_vars(empty)); + EXPECT_EQ(0, stan::math::count_vars(data)); + + Eigen::Matrix vars(2); + auto nested = std::make_tuple(var(1.0), std::make_tuple(vars, 2.0), var(3.0)); + var before = 4.0; + + EXPECT_EQ(5, stan::math::count_vars(before, nested)); + EXPECT_EQ(2, stan::math::count_vars( + std::make_tuple(var(5.0), std::make_tuple(var(6.0))))); +} diff --git a/test/unit/math/rev/core/deep_copy_vars_test.cpp b/test/unit/math/rev/core/deep_copy_vars_test.cpp index 99f78c9fb9c..632625fbbf5 100644 --- a/test/unit/math/rev/core/deep_copy_vars_test.cpp +++ b/test/unit/math/rev/core/deep_copy_vars_test.cpp @@ -2,6 +2,8 @@ #include #include #include +#include +#include #include using stan::math::var; @@ -306,3 +308,45 @@ TEST_F(AgradRev, Rev_deep_copy_vars_std_vector_eigen_matrix_var_arg) { EXPECT_NE(out[i](j).vi_, arg[i](j).vi_); } } + +TEST_F(AgradRev, Rev_deep_copy_vars_tuple_data_arg) { + const std::tuple<> empty; + const auto arg = std::make_tuple(5, Eigen::VectorXd::Ones(2).eval()); + + auto empty_out = stan::math::deep_copy_vars(empty); + auto out = stan::math::deep_copy_vars(arg); + + static_assert(std::is_same_v>); + static_assert(std::is_same_v>); + EXPECT_EQ(&std::get<0>(out), &std::get<0>(arg)); + EXPECT_EQ(&std::get<1>(out), &std::get<1>(arg)); +} + +TEST_F(AgradRev, Rev_deep_copy_vars_nested_tuple_var_arg) { + Eigen::Matrix vars(2); + vars << 2.0, 3.0; + auto arg = std::make_tuple(1.0, var(4.0), vars, std::make_tuple(var(5.0), 6)); + + auto out = stan::math::deep_copy_vars(arg); + + static_assert(std::is_reference_v>); + EXPECT_EQ(&std::get<0>(out), &std::get<0>(arg)); + EXPECT_FLOAT_EQ(std::get<1>(out).val(), std::get<1>(arg).val()); + EXPECT_NE(std::get<1>(out).vi_, std::get<1>(arg).vi_); + for (int i = 0; i < vars.size(); ++i) { + EXPECT_FLOAT_EQ(std::get<2>(out)(i).val(), std::get<2>(arg)(i).val()); + EXPECT_NE(std::get<2>(out)(i).vi_, std::get<2>(arg)(i).vi_); + } + EXPECT_FLOAT_EQ(std::get<0>(std::get<3>(out)).val(), + std::get<0>(std::get<3>(arg)).val()); + EXPECT_NE(std::get<0>(std::get<3>(out)).vi_, + std::get<0>(std::get<3>(arg)).vi_); + EXPECT_EQ(&std::get<1>(std::get<3>(out)), &std::get<1>(std::get<3>(arg))); + + auto rvalue_out = stan::math::deep_copy_vars( + std::make_tuple(7.0, var(8.0), std::make_tuple(9))); + static_assert(std::is_same_v>>); + EXPECT_FLOAT_EQ(8.0, std::get<1>(rvalue_out).val()); +} diff --git a/test/unit/math/rev/core/save_varis_test.cpp b/test/unit/math/rev/core/save_varis_test.cpp index 5f8a69ce0d9..55a0c30353b 100644 --- a/test/unit/math/rev/core/save_varis_test.cpp +++ b/test/unit/math/rev/core/save_varis_test.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include using stan::math::var; @@ -424,3 +425,34 @@ TEST_F(AgradRev, Rev_save_varis_sum) { EXPECT_EQ(ptr, storage.data() + num_vars); } + +TEST_F(AgradRev, Rev_save_varis_tuple_args) { + const std::tuple<> empty; + const auto data = std::make_tuple(1, Eigen::VectorXd::Ones(2)); + std::vector data_storage(2, nullptr); + vari** data_ptr = stan::math::save_varis(data_storage.data(), empty, data); + EXPECT_EQ(data_storage.data(), data_ptr); + EXPECT_EQ(nullptr, data_storage[0]); + EXPECT_EQ(nullptr, data_storage[1]); + + var before = 1.0; + var first = 2.0; + Eigen::Matrix vars(2); + vars << 3.0, 4.0; + var last = 5.0; + var after = 6.0; + auto nested = std::make_tuple(first, std::make_tuple(vars, 7), last); + std::vector storage(8, nullptr); + + vari** ptr = stan::math::save_varis(storage.data(), before, nested, + std::make_tuple(after)); + + std::vector expected{before.vi_, first.vi_, vars(0).vi_, + vars(1).vi_, last.vi_, after.vi_}; + EXPECT_EQ(storage.data() + expected.size(), ptr); + for (size_t i = 0; i < expected.size(); ++i) { + EXPECT_EQ(expected[i], storage[i]); + } + EXPECT_EQ(nullptr, storage[expected.size()]); + EXPECT_EQ(nullptr, storage[expected.size() + 1]); +} From ba09f405f57e1e234aa7afb01bee433e2203f13d Mon Sep 17 00:00:00 2001 From: Steve Bronder Date: Thu, 27 Aug 2026 16:28:50 -0400 Subject: [PATCH 3/4] cleanup --- stan/math/rev/core/deep_copy_vars.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stan/math/rev/core/deep_copy_vars.hpp b/stan/math/rev/core/deep_copy_vars.hpp index ae0fe4e8574..d96b0c68f00 100644 --- a/stan/math/rev/core/deep_copy_vars.hpp +++ b/stan/math/rev/core/deep_copy_vars.hpp @@ -94,9 +94,9 @@ template * = nullptr> inline auto deep_copy_vars(Tuple&& arg) { return stan::math::apply( [](auto&&... tuple_args) { - return std::tuple(tuple_args)))...>{ - deep_copy_vars(std::forward(tuple_args))...}; + return std::tuple{ + deep_copy_vars( + std::forward(tuple_args))...}; }, std::forward(arg)); } From 119a1cffef1401c298a3c38478c0e4104a9c459f Mon Sep 17 00:00:00 2001 From: Steve Bronder Date: Thu, 27 Aug 2026 16:39:22 -0400 Subject: [PATCH 4/4] Use holder tuple for copied tuple arguments --- stan/math/rev/core/deep_copy_vars.hpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/stan/math/rev/core/deep_copy_vars.hpp b/stan/math/rev/core/deep_copy_vars.hpp index d96b0c68f00..1fb4e18f313 100644 --- a/stan/math/rev/core/deep_copy_vars.hpp +++ b/stan/math/rev/core/deep_copy_vars.hpp @@ -2,11 +2,11 @@ #define STAN_MATH_REV_CORE_DEEP_COPY_VARS_HPP #include +#include #include #include #include -#include #include #include @@ -94,9 +94,8 @@ template * = nullptr> inline auto deep_copy_vars(Tuple&& arg) { return stan::math::apply( [](auto&&... tuple_args) { - return std::tuple{ - deep_copy_vars( - std::forward(tuple_args))...}; + return make_holder_tuple( + deep_copy_vars(std::forward(tuple_args))...); }, std::forward(arg)); }