`wiener_lpdf`: faster term counts in the 4-parameter density and less repeated work in the 5-parameter gradients
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ả
(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)withsv = 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.
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:
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})$:
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_tandwiener5_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>):
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_awith 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_aat 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):
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:
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:
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)andsin/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:
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
- Items 1 and 2. They only touch
wiener_lpdf.hpp, so they don't depend on #3442. - After #3442 is merged: items 3, 4 and 5 in
wiener5_lpdf.hpp. - 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
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- 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.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của stan-dev/math
-
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 54/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 54/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 35/100
Maintainer thường phản hồi trong vòng 1 ngày
-
`eigendecompose_sym`, `eigenvalues_sym` and `eigenvectors_sym` do not compile for complex matrices of `fvar<var>` or `fvar<fvar<double>>`Có thể làm lại được Pull request cho issue này đã bị đóng mà không được merge. Đang mởbug
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 55/100
stan-dev/math#3477 · 5 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 35/100
stan-dev/math#3474 · 1 bình luận · 1 reaction ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của stan-dev/math
Issue tương tự
-
Upstream changes to reviewĐang mở
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 72/100
rhalbersma/xstd-ints#395 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
[Bug] Root filesystem scope (/) incorrectly rejects all descendant pathsCó thể đã có người làm @mugenkyou đã nhận hôm nay. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
neutralinojs/neutralino.js#304 · 1 bình luận ·
-
zq prints a raw SQL 'no such table: index_default' error when the index has no default indexĐang mởbug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 73/100
mattgodbolt/zindex#67 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 67/100
NKI-AI/fastslide#105 · 2 bình luận ·
Maintainer thường phản hồi trong vòng 3 ngày