`wiener_lpdf`: faster term counts in the 4-parameter density and less repeated work in the 5-parameter gradients

Đang mở
#3,481 0 bình luận 1 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 1 ngày

Chưa có ai nhận issue này.

Đánh giá

Độ khó
5/5
Thời gian dự kiến
Hơn một tuần
Mức phù hợp với người mới
35/100
Loại issue
Tái cấu trúc
Độ rõ ràng
Khá rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
cpp
Lĩnh vực
backend, performance

Hướng nghiên cứu

Start with stan/math/prim/prob/wiener_lpdf.hpp for the term-count changes and stan/math/prim/prob/wiener5_lpdf.hpp for the gradient and series changes; item 7 is an unprototyped larger change. The report compares against #3442, which it says should be merged before items 3–5. Benchmark with the supplied setup and validate accuracy and gradients; the example says patches 1 and 2 have not yet been run through unit tests.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

numerics performance

(assisted by Claude)

Description

I looked at the run time of the wiener_lpdf overloads and found some changes that would make them faster without losing accuracy. With per-observation a, t0, w and v, value plus gradient on develop takes:

  • about 900 ns per observation for the 4-parameter wiener_lpdf(y | a, t0, w, v);
  • about 3,600 ns per observation for the 5-parameter wiener_lpdf(y | a, t0, w, v, sv) with sv = 0, for the same values and gradients. That's about 4× slower.

This is separate from #3442, which fixes accuracy in the same files. #3442 already fixes two things we'd notice, so they aren't repeated here: the LogResult == false check in estimate_with_err_check never fires, and the gradient series use an absolute tolerance at small densities. #3442 reports the 5-parameter lpdf at 1.09–1.13× develop's run time. The 5-parameter changes below touch the same functions, so they're probably best done on top of it.

The setup, patches and benchmark code are under Example below.

4-parameter density (wiener_lpdf.hpp)

1. The term counts and the loop counter are on the autodiff stack. kl, ks, K, k and the values used to compute them are T_return_type. With var arguments, each of them, and each k++, creates a vari. They only feed ceil and floor, so their derivative is zero.

https://github.com/stan-dev/math/blob/579253050c2302549e9ca2fb6c893d5a3c96dec1/stan/math/prim/prob/wiener_lpdf.hpp#L153-L157

Computing them in double from value_of_rec(x) gives the same values and the same gradients (to 4e-15), and makes value plus gradient 1.35–1.41× faster (patch 1 below).

2. The large-time bound doesn't match Navarro & Fuss (2009). With $x = (y - \tau)/\alpha^2$ and $\epsilon = 10^{-6}$, the number of large-time terms should be

\kappa_l = \max\left(\sqrt{\frac{-2\log(\pi x \epsilon)}{\pi^2 x}},\ \frac{1}{\pi\sqrt{x}}\right)

This is the form wiener5_density_large_reaction_time_terms uses:

https://github.com/stan-dev/math/blob/579253050c2302549e9ca2fb6c893d5a3c96dec1/stan/math/prim/prob/wiener5_lpdf.hpp#L105-L110

The 4-parameter code has $\sqrt{\pi}$ where the formula has $1/\pi^2$, and $\sqrt{x}/\pi$ where it has $1/(\pi\sqrt{x})$:

https://github.com/stan-dev/math/blob/579253050c2302549e9ca2fb6c893d5a3c96dec1/stan/math/prim/prob/wiener_lpdf.hpp#L157-L168

The small-time bound matches the paper. The square-root part of the large-time bound is about 4.2× too large, so the function uses more large-time terms than it needs. It also picks the small-time series in places where the large-time series would need fewer terms:

x develop Navarro & Fuss
0.05 small time, 4 terms small time, 4 terms
0.2 small time, 5 large time, 4
0.5 small time, 6 large time, 3
1.0 large time, 7 large time, 2
2.0 large time, 5 large time, 2

On top of patch 1, the corrected bound (patch 2) makes value plus gradient 1.6–1.7× faster than develop. It is also more accurate, because the large-time series converges quickly where it is now chosen. I compared both against the 5-parameter internals with sv = 0 and a 1e-20 series tolerance, on 20,000 random points (gradient errors relative to max(|ref|, 1)):

develop patch 1 + 2
log density (absolute) 3.4e-8 3.0e-9
d/da 6.5e-7 6.6e-8
d/dt0 1.9e-7 1.1e-7
d/dw 3.5e-7 5.0e-8
d/dv 8.9e-16 8.9e-16

I also truncated the standardised density at each bound on 19,551 (x, w) grid points. The largest truncation errors were 1.0e-8 for develop and 1.4e-9 for Navarro & Fuss, both within the 1e-6 target.

5-parameter density (wiener5_lpdf.hpp)

For each observation, develop evaluates six series:

  • the density three times: once directly, and again inside wiener5_grad_t and wiener5_grad_a;
  • the t, a and w gradient series, once each.

Times per point on 100k random points: wiener5_density 548 ns, wiener5_grad_t 1,323 ns, wiener5_grad_a 1,298 ns, wiener5_grad_w 415 ns.

3. The t gradient is computed even when nothing needs it. Unlike the other partials, deriv_y isn't guarded by if constexpr (is_autodiff_v<T_y> || is_autodiff_v<T_t0>):

https://github.com/stan-dev/math/blob/579253050c2302549e9ca2fb6c893d5a3c96dec1/stan/math/prim/prob/wiener5_lpdf.hpp#L866-L873

A call with all arguments double, such as log_lik in generated quantities, takes 1,807 ns per observation. About 1,300 ns of that is this gradient. Guarding it would make those calls about 3× faster.

4. wiener5_grad_a can be derived from wiener5_grad_t. The series depends on y and a only through u = y/a². Write LET for wiener5_compute_log_error_term, which includes the −2 log a. In both branches, log p = LET(y, a, v, w, sv) + h(y/a², w), so

\partial_a \log p = \partial_a \mathrm{LET} - \frac{2y}{a}\left(\partial_y \log p - \partial_y \mathrm{LET}\right)

Here $\partial_y \mathrm{LET}$ is density_part_one in wiener5_grad_t, and $\partial_a \mathrm{LET}$ is density_part_one in wiener5_grad_a minus 2/a. Once grad_t is known, this takes 4 ns instead of 1,298 ns. I checked it on 20k points (relative to max(|g|, 1)):

  • against wiener5_grad_a with a 1e-20 tolerance: 3.2e-14;
  • against a central difference of the log density, where the density is above 1e-6: 1.3e-9;
  • against wiener5_grad_a at the default tolerance, where the density is above 1e-6: 9.0e-11.

To keep the error guarantee of the a gradient, the t series needs the a gradient's tolerance when that one is tighter. In develop, grad_a already uses exactly grad_t's log tolerance minus log(2y/a):

https://github.com/stan-dev/math/blob/579253050c2302549e9ca2fb6c893d5a3c96dec1/stan/math/prim/prob/wiener5_lpdf.hpp#L362

https://github.com/stan-dev/math/blob/579253050c2302549e9ca2fb6c893d5a3c96dec1/stan/math/prim/prob/wiener5_lpdf.hpp#L426-L427

So the t series could use the smaller of the two tolerances. The re-estimate in estimate_with_err_check would then have to check both gradients.

5. The gradient functions compute the density again. wiener5_grad_t and wiener5_grad_a each call wiener5_density, at a slightly different tolerance from the outer call:

https://github.com/stan-dev/math/blob/579253050c2302549e9ca2fb6c893d5a3c96dec1/stan/math/prim/prob/wiener5_lpdf.hpp#L375-L376

https://github.com/stan-dev/math/blob/579253050c2302549e9ca2fb6c893d5a3c96dec1/stan/math/prim/prob/wiener5_lpdf.hpp#L441-L442

Computing the density once, at the tightest of the three tolerances, and passing it in would save one or two density series per observation. From the component timings, items 4 and 5 together should roughly halve the time for value plus gradient. I haven't measured that end to end.

6. The series are summed in log space, one term at a time. For each term, wiener5_log_sum_exp calls log, plus log_sum_exp, which itself costs an exp and a log1p:

https://github.com/stan-dev/math/blob/579253050c2302549e9ca2fb6c893d5a3c96dec1/stan/math/prim/prob/wiener5_lpdf.hpp#L175-L177

An alternative is to factor out the largest exponent and sum in linear space, as wiener5_grad_w has done since #3324. This gives the same log density (7.1e-15 over 20k points) and makes wiener5_density 30–40% faster (548 → 344 ns). Cancellation is the same as now, where the positive and negative sums are formed separately and combined with log_diff_exp. The scaling also protects against overflow and underflow the way the log scale does.

7. (Larger change) One pass for all the series. The density, t and w series use the same exponentials:

  • small time: exp(-z_k^2 / (2u));
  • large time: exp(-k^2 pi^2 u / 2) and sin/cos(k pi (1 - w)).

A single loop up to the largest of the term counts could produce all three sums. That is what autodiff effectively does through the 4-parameter series, which is probably why the 4-parameter version is faster. I haven't prototyped this.

7-parameter density (wiener_full_lpdf.hpp)

The density and each gradient are separate hcubature integrations:

https://github.com/stan-dev/math/blob/579253050c2302549e9ca2fb6c893d5a3c96dec1/stan/math/prim/prob/wiener_full_lpdf.hpp#L486-L560

Each integrand evaluates the 5-parameter density again, because the WrtLog gradient functions call wiener5_density themselves. hcubature only integrates scalar functions, so the integrands can't share evaluations. A vector-valued hcubature would allow that. I've left it out of this issue.

Suggested order
  1. Items 1 and 2. They only touch wiener_lpdf.hpp, so they don't depend on #3442.
  2. After #3442 is merged: items 3, 4 and 5 in wiener5_lpdf.hpp.
  3. Items 6 and 7 as a follow-up.

Example

Setup:

  • develop at 5792530 (v5.4.0);
  • Windows 11, GCC 14.3.0 (RTools45), -O3 -march=native -DNDEBUG;
  • tables show the median of 3 runs on one laptop, and runs varied by about ±10%.

I compiled and benchmarked patches 1 and 2 but haven't run them through the unit tests.

Value plus gradient, N = 2,000 observations, per-observation a, t0, w, v (y is data):

ns/obs speed-up vs 4-param develop
4-param, develop 909 1
4-param, patch 1 643 1.35–1.41×
4-param, patch 1 + 2 542 1.59–1.74×
5-param with sv = 0, develop 3,583 0.24–0.26×
5-param, all arguments double (value only) 1,807
Patch 1: term counts in double (no change in values)
--- a/stan/math/prim/prob/wiener_lpdf.hpp
+++ b/stan/math/prim/prob/wiener_lpdf.hpp
@@ -43,6 +43,7 @@
 #include <stan/math/prim/fun/square.hpp>
 #include <stan/math/prim/fun/sqrt.hpp>
 #include <stan/math/prim/fun/value_of.hpp>
+#include <stan/math/prim/fun/value_of_rec.hpp>
 #include <algorithm>
 #include <cmath>
 #include <string>
@@ -150,17 +151,20 @@
     typename scalar_type<T_beta>::type one_minus_beta = 1.0 - beta_vec[i];
     typename scalar_type<T_alpha>::type alpha2 = square(alpha_vec[i]);
     T_return_type x = (y_vec[i] - tau_vec[i]) / alpha2;
-    T_return_type kl, ks, tmp = 0;
-    T_return_type k, K;
-    T_return_type sqrt_x = sqrt(x);
-    T_return_type log_x = log(x);
-    T_return_type one_over_pi_times_sqrt_x = 1.0 / pi() * sqrt_x;
+    T_return_type tmp = 0;
+    // The number of terms has zero derivative, so compute it on values and
+    // keep it, and the loop counter, off the autodiff stack
+    const double x_val = value_of_rec(x);
+    const double sqrt_x = std::sqrt(x_val);
+    const double log_x = std::log(x_val);
+    const double one_over_pi_times_sqrt_x = 1.0 / pi() * sqrt_x;
+    double kl, ks;
 
     // calculate number of terms needed for large t:
     // if error threshold is set low enough
-    if (PI_TIMES_WIENER_ERR * x < 1) {
+    if (PI_TIMES_WIENER_ERR * x_val < 1) {
       // compute bound
-      kl = sqrt(-2.0 * SQRT_PI * (LOG_PI_LOG_WIENER_ERR + log_x)) / sqrt_x;
+      kl = std::sqrt(-2.0 * SQRT_PI * (LOG_PI_LOG_WIENER_ERR + log_x)) / sqrt_x;
       // ensure boundary conditions met
       kl = (kl > one_over_pi_times_sqrt_x) ? kl : one_over_pi_times_sqrt_x;
     } else {
@@ -168,28 +172,28 @@
     }
     // calculate number of terms needed for small t:
     // if error threshold is set low enough
-    T_return_type tmp_expr0 = TWO_TIMES_SQRT_TWO_PI_TIMES_WIENER_ERR * sqrt_x;
+    const double tmp_expr0 = TWO_TIMES_SQRT_TWO_PI_TIMES_WIENER_ERR * sqrt_x;
     if (tmp_expr0 < 1) {
       // compute bound
-      ks = 2.0 + sqrt_x * sqrt(-2 * log(tmp_expr0));
+      ks = 2.0 + sqrt_x * std::sqrt(-2 * std::log(tmp_expr0));
       // ensure boundary conditions are met
-      T_return_type sqrt_x_plus_one = sqrt_x + 1.0;
+      const double sqrt_x_plus_one = sqrt_x + 1.0;
       ks = (ks > sqrt_x_plus_one) ? ks : sqrt_x_plus_one;
     } else {     // if error threshold was set too high
       ks = 2.0;  // minimal kappa for that case
     }
-    if (ks < kl) {   // small t
-      K = ceil(ks);  // round to smallest integer meeting error
-      T_return_type tmp_expr1 = (K - 1.0) / 2.0;
-      T_return_type tmp_expr2 = ceil(tmp_expr1);
-      for (k = -floor(tmp_expr1); k <= tmp_expr2; k++) {
-        tmp += (one_minus_beta + 2.0 * k)
-               * exp(-(square(one_minus_beta + 2.0 * k)) * 0.5 / x);
+    if (ks < kl) {                     // small t
+      const double K = std::ceil(ks);  // round to smallest integer meeting error
+      const double tmp_expr1 = (K - 1.0) / 2.0;
+      const double tmp_expr2 = std::ceil(tmp_expr1);
+      for (double k = -std::floor(tmp_expr1); k <= tmp_expr2; k++) {
+        const auto w_plus_2_k = one_minus_beta + 2.0 * k;
+        tmp += w_plus_2_k * exp(-(square(w_plus_2_k)) * 0.5 / x);
       }
-      tmp = log(tmp) - LOG_TWO_OVER_TWO_PLUS_LOG_SQRT_PI - 1.5 * log_x;
-    } else {         // if large t is better...
-      K = ceil(kl);  // round to smallest integer meeting error
-      for (k = 1; k <= K; ++k) {
+      tmp = log(tmp) - LOG_TWO_OVER_TWO_PLUS_LOG_SQRT_PI - 1.5 * log(x);
+    } else {                           // if large t is better...
+      const double K = std::ceil(kl);  // round to smallest integer meeting error
+      for (double k = 1; k <= K; ++k) {
         tmp += k * exp(-(square(k)) * (SQUARE_PI_OVER_TWO * x))
                * sin(k * pi() * one_minus_beta);
       }
Patch 2, on top of patch 1: Navarro & Fuss large-time bound
--- a/stan/math/prim/prob/wiener_lpdf.hpp
+++ b/stan/math/prim/prob/wiener_lpdf.hpp
@@ -157,14 +157,14 @@
     const double x_val = value_of_rec(x);
     const double sqrt_x = std::sqrt(x_val);
     const double log_x = std::log(x_val);
-    const double one_over_pi_times_sqrt_x = 1.0 / pi() * sqrt_x;
+    const double one_over_pi_times_sqrt_x = 1.0 / (pi() * sqrt_x);
     double kl, ks;
 
     // calculate number of terms needed for large t:
     // if error threshold is set low enough
     if (PI_TIMES_WIENER_ERR * x_val < 1) {
       // compute bound
-      kl = std::sqrt(-2.0 * SQRT_PI * (LOG_PI_LOG_WIENER_ERR + log_x)) / sqrt_x;
+      kl = std::sqrt(-2.0 * (LOG_PI_LOG_WIENER_ERR + log_x)) / (pi() * sqrt_x);
       // ensure boundary conditions met
       kl = (kl > one_over_pi_times_sqrt_x) ? kl : one_over_pi_times_sqrt_x;
     } else {
Benchmark and checks for items 4 and 6

wiener_lpdf_b.hpp and wiener_lpdf_c.hpp are copies of wiener_lpdf.hpp with patch 1, and with patches 1 + 2. In each copy, the include guard and the function are renamed (wiener_lpdf_b, wiener_lpdf_c).

#include <stan/math/rev.hpp>
#include "wiener_lpdf_b.hpp"  // patch 1
#include "wiener_lpdf_c.hpp"  // patch 1 + 2
#include <chrono>
#include <cstdio>
#include <random>
#include <vector>
using namespace stan::math;
using internal::GradientCalc;

template <typename F>
double best_ns(F&& f, int reps, int n) {
  double best = 1e300;
  for (int r = 0; r < reps; ++r) {
    auto t0 = std::chrono::steady_clock::now();
    f();
    auto t1 = std::chrono::steady_clock::now();
    best = std::min(best, std::chrono::duration<double, std::nano>(t1 - t0).count() / n);
  }
  return best;
}

// d/da log p from d/dy log p: the series depends on y and a only through y/a^2
double grad_a_from_t(double y, double a, double v, double w, double sv, double gt) {
  const double q = 1.0 - w, sv2 = sv * sv, opsy = 1.0 + sv2 * y;
  const double dlet_da = (v * q + sv2 * q * q * a) / opsy - 2.0 / a;
  const double dlet_dy = -0.5 * (sv2 * sv2 * (y + a * a * q * q) + sv2 * (1.0 + 2.0 * a * v * q) + v * v)
                         / (opsy * opsy);
  return dlet_da - (2.0 * y / a) * (gt - dlet_dy);
}

// wiener5_density with the series summed in linear space after factoring out
// the largest exponent (as wiener5_grad_w already does)
double density_linear(double y, double a, double v, double w, double sv, double log_err) {
  const double let = internal::wiener5_compute_log_error_term(y, a, v, w, sv);
  const double n_small
      = internal::wiener5_n_terms_small_t<true, GradientCalc::OFF>(y, a, w, log_err - let);
  const double n_large = internal::wiener5_density_large_reaction_time_terms(y, a, w, log_err - let);
  const double u = y / (a * a), q = 1.0 - w;
  if (2 * n_small <= n_large) {
    const double m = -q * q / (2.0 * u);
    double s = q;
    for (int k = 1; k <= static_cast<int>(n_small); ++k) {
      const double zp = q + 2.0 * k, zm = q - 2.0 * k;
      s += zp * std::exp(-zp * zp / (2.0 * u) - m) + zm * std::exp(-zm * zm / (2.0 * u) - m);
    }
    return let - 0.5 * LOG_TWO - LOG_SQRT_PI - 1.5 * std::log(u) + std::log(std::fabs(s)) + m;
  }
  const double c = 0.5 * pi() * pi() * u;
  double s = 0.0;
  for (int k = 1; k <= static_cast<int>(n_large); ++k)
    s += k * std::sin(k * pi() * q) * std::exp(-(k * k - 1.0) * c);
  return let + std::log(std::fabs(s)) - c + LOG_PI;
}

int main() {
  // Part 1: public functions, value + gradient, reverse mode
  const int N = 2000, reps = 200;
  std::mt19937 rng(1);
  std::uniform_real_distribution<double> ua(0.8, 2.0), ut0(0.15, 0.35), uw(0.3, 0.7), uv(-1.5, 2.5),
      udt(0.15, 1.5);
  std::vector<double> y(N), a0(N), t00(N), w0(N), v0(N);
  for (int i = 0; i < N; ++i) {
    a0[i] = ua(rng); t00[i] = ut0(rng); w0[i] = uw(rng); v0[i] = uv(rng); y[i] = t00[i] + udt(rng);
  }
  std::vector<double> g_ref;
  double lp_ref = 0, t_ref = 0;
  auto run = [&](const char* name, auto&& f) {
    double lp_val = 0;
    std::vector<double> g;
    double t = best_ns([&] {
      std::vector<var> a(a0.begin(), a0.end()), t0(t00.begin(), t00.end()), w(w0.begin(), w0.end()),
          v(v0.begin(), v0.end());
      var lp = f(a, t0, w, v);
      lp.grad();
      lp_val = lp.val();
      g.clear();
      for (int i = 0; i < N; ++i) {
        g.push_back(a[i].adj()); g.push_back(t0[i].adj()); g.push_back(w[i].adj()); g.push_back(v[i].adj());
      }
      recover_memory();
    }, reps, N);
    if (g_ref.empty()) { g_ref = g; lp_ref = lp_val; t_ref = t; }
    double md = 0;
    for (size_t i = 0; i < g.size(); ++i) md = std::max(md, std::fabs(g[i] - g_ref[i]));
    std::printf("| %-46s | %7.0f | %5.2fx | %.1e | %.1e |\n", name, t, t_ref / t, std::fabs(lp_val - lp_ref), md);
  };
  run("4-param, develop", [&](auto& a, auto& t0, auto& w, auto& v) { return wiener_lpdf(y, a, t0, w, v); });
  run("4-param, patch 1", [&](auto& a, auto& t0, auto& w, auto& v) { return wiener_lpdf_b(y, a, t0, w, v); });
  run("4-param, patch 1 + 2", [&](auto& a, auto& t0, auto& w, auto& v) { return wiener_lpdf_c(y, a, t0, w, v); });
  run("5-param with sv = 0", [&](auto& a, auto& t0, auto& w, auto& v) { return wiener_lpdf(y, a, t0, w, v, 0.0); });
  volatile double sink = 0;
  double t_dbl = best_ns([&] { sink = sink + wiener_lpdf(y, a0, t00, w0, v0, 0.0); }, reps, N);
  std::printf("5-param, all arguments double (value only): %.0f ns/obs\n\n", t_dbl);

  // Part 2: 5-parameter internals on 100k random points
  const double le = std::log(1e-12);
  std::mt19937 rng2(42);
  std::uniform_real_distribution<double> uy(0.05, 2.5), ua2(0.5, 3.0), uv2(-4, 4), uw2(0.1, 0.9), usv(0.0, 1.0);
  struct Pt { double y, a, v, w, sv; };
  std::vector<Pt> pts(100000);
  for (auto& p : pts) p = {uy(rng2), ua2(rng2), uv2(rng2), uw2(rng2), (rng2() % 2) ? usv(rng2) : 0.0};
  auto per_point = [&](const char* name, auto&& f) {
    double t = best_ns([&] {
      double acc = 0;
      for (auto& p : pts) acc += f(p);
      sink = sink + acc;
    }, 5, pts.size());
    std::printf("| %-40s | %7.0f |\n", name, t);
  };
  per_point("wiener5_density", [&](const Pt& p) { return internal::wiener5_density<false>(p.y, p.a, p.v, p.w, p.sv, le); });
  per_point("density, linear-space sum", [&](const Pt& p) { return density_linear(p.y, p.a, p.v, p.w, p.sv, le); });
  per_point("wiener5_grad_t", [&](const Pt& p) { return internal::wiener5_grad_t<false>(p.y, p.a, p.v, p.w, p.sv, le); });
  per_point("wiener5_grad_a", [&](const Pt& p) { return internal::wiener5_grad_a<false>(p.y, p.a, p.v, p.w, p.sv, le); });
  per_point("grad_a from grad_t (identity)", [&](const Pt& p) { return grad_a_from_t(p.y, p.a, p.v, p.w, p.sv, 0.3); });
  per_point("wiener5_grad_w", [&](const Pt& p) { return internal::wiener5_grad_w<false>(p.y, p.a, p.v, p.w, p.sv, le); });

  // Part 3: accuracy of the identity and of the linear-space sum
  const double lt = std::log(1e-20);
  double id_tight = 0, id_fd = 0, id_def = 0, lin = 0;
  for (size_t i = 0; i < 20000; ++i) {
    const auto& p = pts[i];
    const double ga_t = internal::wiener5_grad_a<false>(p.y, p.a, p.v, p.w, p.sv, lt);
    const double gi_t = grad_a_from_t(p.y, p.a, p.v, p.w, p.sv,
                                      internal::wiener5_grad_t<false>(p.y, p.a, p.v, p.w, p.sv, lt));
    const double h = 1e-5 * p.a;
    const double fd = (internal::wiener5_density<false>(p.y, p.a + h, p.v, p.w, p.sv, lt)
                       - internal::wiener5_density<false>(p.y, p.a - h, p.v, p.w, p.sv, lt))
                      / (2 * h);
    id_tight = std::max(id_tight, std::fabs(ga_t - gi_t) / std::max(1.0, std::fabs(ga_t)));
    const double lp = internal::wiener5_density<false>(p.y, p.a, p.v, p.w, p.sv, le);
    if (lp > std::log(1e-6)) {
      id_fd = std::max(id_fd, std::fabs(gi_t - fd) / std::max(1.0, std::fabs(fd)));
      const double ga = internal::wiener5_grad_a<false>(p.y, p.a, p.v, p.w, p.sv, le);
      const double gi = grad_a_from_t(p.y, p.a, p.v, p.w, p.sv,
                                      internal::wiener5_grad_t<false>(p.y, p.a, p.v, p.w, p.sv, le));
      id_def = std::max(id_def, std::fabs(ga - gi) / std::max(1.0, std::fabs(ga)));
    }
    lin = std::max(lin, std::fabs(lp - density_linear(p.y, p.a, p.v, p.w, p.sv, le)));
  }
  std::printf("identity vs wiener5_grad_a, 1e-20 tolerance: %.1e\n", id_tight);
  std::printf("identity vs wiener5_grad_a, default tolerance, density > 1e-6: %.1e\n", id_def);
  std::printf("identity vs central difference, density > 1e-6: %.1e\n", id_fd);
  std::printf("linear-space vs log-space log density: %.1e\n", lin);
}
Accuracy check for patches 1 + 2
// Accuracy of the 4-parameter wiener_lpdf (develop, patch 1 + 2) against the
// 5-parameter internals with sv = 0 and a 1e-20 series tolerance
#include <stan/math/rev.hpp>
#include "wiener_lpdf_c.hpp"
#include <cstdio>
#include <random>
using namespace stan::math;

struct Res { double lp, ga, gt0, gw, gv; };

template <typename F>
Res eval(F&& f, double y, double a, double t0, double w, double v) {
  var av = a, t0v = t0, wv = w, vv = v;
  var lp = f(y, av, t0v, wv, vv);
  lp.grad();
  Res r{lp.val(), av.adj(), t0v.adj(), wv.adj(), vv.adj()};
  recover_memory();
  return r;
}

int main() {
  const double lt = std::log(1e-20);
  std::mt19937 rng(7);
  std::uniform_real_distribution<double> ua(0.5, 3.0), uw(0.2, 0.8), uv(-4, 4), udt(0.05, 3.0);
  double e_dev[5] = {0}, e_new[5] = {0};
  for (int i = 0; i < 20000; ++i) {
    const double a = ua(rng), w = uw(rng), v = uv(rng), dt = udt(rng), t0 = 0.2, y = t0 + dt;
    const double ref[5] = {internal::wiener5_density<false>(dt, a, v, w, 0.0, lt),
                           internal::wiener5_grad_a<false>(dt, a, v, w, 0.0, lt),
                           -internal::wiener5_grad_t<false>(dt, a, v, w, 0.0, lt),
                           internal::wiener5_grad_w<false>(dt, a, v, w, 0.0, lt),
                           internal::wiener5_grad_v<false>(dt, a, v, w, 0.0, lt)};
    const Res d = eval([](auto&&... x) { return wiener_lpdf(x...); }, y, a, t0, w, v);
    const Res c = eval([](auto&&... x) { return wiener_lpdf_c(x...); }, y, a, t0, w, v);
    const double dv[5] = {d.lp, d.ga, d.gt0, d.gw, d.gv}, cv[5] = {c.lp, c.ga, c.gt0, c.gw, c.gv};
    for (int k = 0; k < 5; ++k) {
      const double scale = k == 0 ? 1.0 : std::max(1.0, std::fabs(ref[k]));
      e_dev[k] = std::max(e_dev[k], std::fabs(dv[k] - ref[k]) / scale);
      e_new[k] = std::max(e_new[k], std::fabs(cv[k] - ref[k]) / scale);
    }
  }
  const char* names[5] = {"log density (abs)", "d/da", "d/dt0", "d/dw", "d/dv"};
  for (int k = 0; k < 5; ++k) std::printf("| %-18s | %9.1e | %13.1e |\n", names[k], e_dev[k], e_new[k]);
}

Expected Output

  • Items 1, 3, 4, 5 and 6 should give the same values to floating-point precision.
  • Item 2 gives slightly different values, and they are more accurate (table above).
  • Expected speed-ups:
    • 4-parameter value plus gradient: about 1.4× with patch 1, about 1.65× with patches 1 + 2.
    • 5-parameter value with all arguments double: about 3× (item 3).
    • 5-parameter value plus gradient: roughly half the time (items 4 and 5, projected).
    • wiener5_density: a further 30–40% faster (item 6).
Current Version:

v5.4.0 (develop at 5792530)

Ngôn ngữ chính
C++
Star
845
Fork
225
Merge trung bình
3 ngày 9 giờ
Pull request đã merge (30 ngày)
12

Chuẩn bị môi trường

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của stan-dev/math

Tất cả issue của stan-dev/math

Issue tương tự

Thêm issue về C++

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.