Skip to content

Commit 42c164b

Browse files
committed
fix integrate_1d_gauss_kronrod gradient speed
1 parent 29e4630 commit 42c164b

3 files changed

Lines changed: 107 additions & 6 deletions

File tree

stan/math/rev/functor/integrate_1d_adjoint.hpp

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ namespace internal {
3838
* zero. Any other NaN propagates into the component integral and is reported as
3939
* a `domain_error` naming the (flattened) parameter index.
4040
*
41+
* `shift_gradient_integrand`: when true (and the value integral `I` is finite
42+
* and non-zero) each component adjoint is computed as
43+
* `integrator(d f / d arg + f) - I` instead of `integrator(d f / d arg)`,
44+
* which can make the computation better behaving.
45+
*
46+
* @tparam shift_gradient_integrand see above
4147
* @tparam F Type of f
4248
* @tparam T_a type of first limit
4349
* @tparam T_b type of second limit
@@ -52,8 +58,8 @@ namespace internal {
5258
* @param args additional arguments to pass to f
5359
* @return numeric integral of function f
5460
*/
55-
template <typename F, typename T_a, typename T_b, typename Integrator,
56-
typename... Args>
61+
template <bool shift_gradient_integrand = false, typename F, typename T_a,
62+
typename T_b, typename Integrator, typename... Args>
5763
inline return_type_t<T_a, T_b, Args...> integrate_1d_adjoint(
5864
const char* function, const F& f, const T_a& a, const T_b& b,
5965
Integrator&& integrator, std::ostream* msgs, const Args&... args) {
@@ -94,13 +100,15 @@ inline return_type_t<T_a, T_b, Args...> integrate_1d_adjoint(
94100

95101
// Argument adjoints.
96102
if constexpr (is_any_var_scalar_v<Args...>) {
103+
const bool shift = shift_gradient_integrand && integral != 0.0
104+
&& !is_inf(integral) && !is_nan(integral);
97105
auto args_adj = make_zeroed_arena(std::forward_as_tuple(args...));
98106
{
99107
nested_rev_autodiff argument_nest;
100108
auto args_copy = deep_copy_vargs<var>(std::forward_as_tuple(args...));
101109
auto args_copy_filter = filter_var_scalar_types(args_copy);
102110
auto integrate_grad = [&](auto&& target) -> double {
103-
return integrator([&](const auto& x, const auto& xc) {
111+
const double result = integrator([&](const auto& x, const auto& xc) {
104112
argument_nest.set_zero_all_adjoints();
105113
nested_rev_autodiff gradient_nest;
106114
var fx = stan::math::apply(
@@ -113,8 +121,9 @@ inline return_type_t<T_a, T_b, Args...> integrate_1d_adjoint(
113121
if (is_nan(gradient) && fx.val() == 0) {
114122
gradient = 0.0;
115123
}
116-
return gradient;
124+
return shift ? gradient + fx.val() : gradient;
117125
});
126+
return shift ? result - integral : result;
118127
};
119128
std::size_t param_index = 0;
120129
auto assign_grad = [&](auto&& adj, auto&& target) {

stan/math/rev/functor/integrate_1d_gauss_kronrod.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ inline return_type_t<T_a, T_b, Args...> integrate_1d_gauss_kronrod_tol(
4545
check_less_or_equal(function, "lower limit", a, b);
4646
check_nonnegative(function, "max_depth", max_depth);
4747
check_nonnegative(function, "absolute_tolerance", absolute_tolerance);
48-
return internal::integrate_1d_adjoint(
48+
// `true`: gradient integrands are shifted by f
49+
return internal::integrate_1d_adjoint<true>(
4950
function, f, a, b,
5051
[&](auto &&integrand) {
5152
return integrate_gk(std::forward<decltype(integrand)>(integrand),

test/unit/math/rev/functor/integrate_1d_gauss_kronrod_test.cpp

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -481,4 +481,95 @@ TEST_F(AgradRev, StanMath_integrate_1d_gk_rev_TestUniform) {
481481
EXPECT_FLOAT_EQ(1, 1 + g[1]);
482482
}
483483

484-
} // namespace integrate_1d_gk_test
484+
// ── gradient-integrand shift (integrate_1d_adjoint<true>) ────────────
485+
//
486+
// The wrapper integrates (d f / d theta_i + f) and subtracts the
487+
// value integral. These tests pin (a) the evaluation count on such a
488+
// component, which the accuracy tests above cannot see, and (b) the
489+
// guard paths: an integral that is exactly zero (shift disabled) and
490+
// a negative integral (shift active with I < 0).
491+
492+
long n_evals = 0;
493+
494+
// d f / d theta is analytically zero (cos^2 + sin^2 = 1) but autodiff
495+
// evaluates it as round-off noise of order 1e-16 * x * f.
496+
struct f_noisy_gradient_counted {
497+
template <typename T1, typename T2, typename T3>
498+
inline stan::return_type_t<T1, T2, T3> operator()(
499+
const T1 &x, const T2 &xc, std::ostream *msgs,
500+
const std::vector<T3> &theta, const std::vector<double> &x_r,
501+
const std::vector<int> &x_i) const {
502+
++n_evals;
503+
auto tx = theta[0] * x;
504+
return exp(-x * x) * (cos(tx) * cos(tx) + sin(tx) * sin(tx));
505+
}
506+
};
507+
508+
TEST_F(AgradRev, StanMath_integrate_1d_gk_rev_GradientShift_noisy_gradient) {
509+
using stan::math::var;
510+
const double I_ref = std::sqrt(stan::math::pi()) * std::erf(1.0);
511+
std::vector<var> theta = {2.5};
512+
n_evals = 0;
513+
var I = stan::math::integrate_1d_gauss_kronrod_tol(
514+
f_noisy_gradient_counted{}, -1.0, 1.0, 1e-6, 0.0, 15, msgs, theta,
515+
std::vector<double>{}, std::vector<int>{});
516+
std::vector<double> g;
517+
I.grad(theta, g);
518+
EXPECT_NEAR(I_ref, I.val(), 1e-6);
519+
EXPECT_NEAR(0.0, g[0], 1e-6);
520+
// Value + one gradient component. Without the shift the gradient
521+
// integral hits max_depth (2^15 * 21 ≈ 6.9e5 evaluations).
522+
EXPECT_LT(n_evals, 5000L);
523+
}
524+
525+
struct f_odd {
526+
template <typename T1, typename T2, typename T3>
527+
inline stan::return_type_t<T1, T2, T3> operator()(
528+
const T1 &x, const T2 &xc, std::ostream *msgs,
529+
const std::vector<T3> &theta, const std::vector<double> &x_r,
530+
const std::vector<int> &x_i) const {
531+
return theta[0] * x * exp(-x * x); // odd: integral over [-1, 1] is 0
532+
}
533+
};
534+
535+
struct f_negative {
536+
template <typename T1, typename T2, typename T3>
537+
inline stan::return_type_t<T1, T2, T3> operator()(
538+
const T1 &x, const T2 &xc, std::ostream *msgs,
539+
const std::vector<T3> &theta, const std::vector<double> &x_r,
540+
const std::vector<int> &x_i) const {
541+
return -theta[0] * exp(-x * x); // negative everywhere, I < 0
542+
}
543+
};
544+
545+
TEST_F(AgradRev, StanMath_integrate_1d_gk_rev_GradientShift_guards) {
546+
using stan::math::var;
547+
// (a) exactly-zero integral: the shift is disabled and the gradient is
548+
// the (zero) integral of x exp(-x^2).
549+
{
550+
std::vector<var> theta = {2.5};
551+
var I;
552+
EXPECT_NO_THROW(I = stan::math::integrate_1d_gauss_kronrod_tol(
553+
f_odd{}, -1.0, 1.0, 1e-6, 0.0, 15, msgs, theta,
554+
std::vector<double>{}, std::vector<int>{}));
555+
std::vector<double> g;
556+
I.grad(theta, g);
557+
EXPECT_NEAR(0.0, I.val(), 1e-8);
558+
EXPECT_NEAR(0.0, g[0], 1e-8);
559+
}
560+
// (b) negative integral: shift active with I < 0; d I / d theta = I / theta.
561+
{
562+
const double th = 2.5;
563+
const double I_ref = -th * std::sqrt(stan::math::pi()) * std::erf(1.0);
564+
std::vector<var> theta = {th};
565+
var I = stan::math::integrate_1d_gauss_kronrod_tol(
566+
f_negative{}, -1.0, 1.0, 1e-8, 0.0, 15, msgs, theta,
567+
std::vector<double>{}, std::vector<int>{});
568+
std::vector<double> g;
569+
I.grad(theta, g);
570+
EXPECT_NEAR(I_ref, I.val(), 1e-7);
571+
EXPECT_NEAR(I_ref / th, g[0], 1e-7);
572+
}
573+
}
574+
575+
}

0 commit comments

Comments
 (0)