Skip to content

Commit 4ec1508

Browse files
SteveBronderclaude
andcommitted
Use the asymptotic Mills ratio in the far lower tail (DLMF 7.12.1)
The scaled_diff < -29 branch took the Abramowitz & Stegun negative-tail approximation and added a cubic residual fit whose leading term is 0.0015065154280332 * x2. That term grows quadratically in scaled_diff. DLMF 7.12.1 gives, for y = -scaled_diff > 0, erfc(y) ~ (exp(-y^2) / (sqrt(pi) y)) * sum_m (-1)^m (1/2)_m / y^(2m) and since dncdf_log = (2/sqrt(pi)) exp(-y^2) / erfc(y), this is dncdf_log ~ 2y / (1 - 1/(2y^2) + 3/(4y^4) - 15/(8y^6) + ...) so dncdf_log grows LINEARLY, as -2*scaled_diff. A quadratic residual fit cannot track a linear asymptote; it must eventually dominate. Measured relative error of the old branch against a 60-digit mpmath reference: scaled_diff -29 -40 -75 -100 -1000 old 9.5e-06 1.9e-04 1.0e-02 2.3e-02 6.8e-01 new 1.3e-11 1.0e-12 6.5e-15 7.0e-16 7.8e-18 At scaled_diff = -1000 the old branch is 68% wrong. In y terms (mu=0, sigma=1) the gradient passes 1e-4 relative error at y = -54.46 and 1e-2 at y = -106.06. This is a plain first-order var gradient, so it corrupts HMC transitions, not just Hessians -- and PR #3363's reflection newly exposes it on the lccdf side at large positive y. Replacing the branch with the truncated asymptotic is also exponential-free: no erf, no exp, nothing that can overflow. DLMF states the remainder is bounded by the first neglected term for real argument, which at scaled_diff = -29 is 6.5625/29^8 = 1.3e-11, matching the measured error. The seam at scaled_diff = -29 moves from 3.3e-06 to 6.2e-06 relative, still well inside the existing inter-branch jumps (~2.7e-5 at scaled_diff = 2.9). Also corrects the sqrt(2) scaling in the new rev sweep assertions: dncdf_log is d/d(scaled_diff), while the reported partial is dncdf_log/(sigma*sqrt(2)). The discrete mpmath-referenced cases were unaffected. Verified the corrected sweep is still red without this fix and green with it. All P1, P2 and P3 tests now pass across prim, rev, fwd and mix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 31e5a69 commit 4ec1508

8 files changed

Lines changed: 45 additions & 21 deletions

File tree

stan/math/opencl/kernels/device_functions/std_normal_lcdf.hpp

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,14 @@ static constexpr const char* std_normal_lcdf_device_function
121121
dnlcdf = 0.6245634904 - 0.9521866949 * t + 0.3986215682 * t2
122122
+ 0.04700850676 * t2 * t - 0.03478651979 * t4
123123
- 0.01772675404 * t4 * t + 0.0006577254811 * pow(t, 6);
124+
} else if (scaled_y < -29.0) {
125+
// asymptotic Mills ratio, DLMF 7.12.1: grows linearly as
126+
// -2*scaled_y, so no quadratic residual fit can track it
127+
const double inv_x2 = 1.0 / x2;
128+
dnlcdf
129+
= -2.0 * scaled_y
130+
/ (1.0
131+
+ inv_x2 * (-0.5 + inv_x2 * (0.75 + inv_x2 * -1.875)));
124132
} else if (10.0 * log(fabs(scaled_y)) < log(DBL_MAX)) {
125133
t = 1.0 / (1.0 - 0.3275911 * scaled_y);
126134
t2 = t * t;
@@ -129,10 +137,7 @@ static constexpr const char* std_normal_lcdf_device_function
129137
= M_2_SQRTPI
130138
/ (0.254829592 * t - 0.284496736 * t2 + 1.421413741 * t2 * t
131139
- 1.453152027 * t4 + 1.061405429 * t4 * t);
132-
if (scaled_y < -29.0) {
133-
dnlcdf += 0.0015065154280332 * x2
134-
- 0.3993154819705530 * scaled_y - 4.2919418242931700;
135-
} else if (scaled_y < -17.0) {
140+
if (scaled_y < -17.0) {
136141
dnlcdf += 0.0001263257217272 * x2 * scaled_y
137142
+ 0.0123586859488623 * x2
138143
- 0.0860505264736028 * scaled_y - 1.252783383752970;

stan/math/opencl/prim/normal_lcdf.hpp

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const char opencl_normal_lcdf_impl[] = STRINGIFY(
6060
const char opencl_normal_lcdf_ldncdf_impl[] = STRINGIFY(
6161
double normal_ldncdf = 0.0; double t = 0.0; double t2 = 0.0;
6262
double t4 = 0.0; double normal_lcdf_exp_m_x2 = 0.0;
63+
double normal_lcdf_inv_x2 = 0.0;
6364

6465
// calculate using piecewise function
6566
// (due to instability / inaccuracy in the various approximations)
@@ -120,6 +121,17 @@ const char opencl_normal_lcdf_ldncdf_impl[] = STRINGIFY(
120121
normal_ldncdf = 0.6245634904 - 0.9521866949 * t + 0.3986215682 * t2
121122
+ 0.04700850676 * t2 * t - 0.03478651979 * t4
122123
- 0.01772675404 * t4 * t + 0.0006577254811 * pow(t, 6);
124+
} else if (normal_lcdf_deriv_scaled_diff < -29.0) {
125+
// asymptotic Mills ratio, DLMF 7.12.1: grows linearly as -2*scaled_diff,
126+
// so no quadratic residual fit can track it
127+
normal_lcdf_inv_x2 = 1.0 / x2;
128+
normal_ldncdf
129+
= -2.0 * normal_lcdf_deriv_scaled_diff
130+
/ (1.0
131+
+ normal_lcdf_inv_x2
132+
* (-0.5
133+
+ normal_lcdf_inv_x2
134+
* (0.75 + normal_lcdf_inv_x2 * -1.875)));
123135
} else if (10.0 * log(fabs(normal_lcdf_deriv_scaled_diff)) < log(DBL_MAX)) {
124136
// approximation derived from Abramowitz and Stegun (1964) 7.1.26
125137
// use fact that erf(x)=-erf(-x)
@@ -134,11 +146,7 @@ const char opencl_normal_lcdf_ldncdf_impl[] = STRINGIFY(
134146
- 1.453152027 * t4 + 1.061405429 * t4 * t);
135147
// check if we need to add a correction term
136148
// (from cubic fit of residuals)
137-
if (normal_lcdf_deriv_scaled_diff < -29.0) {
138-
normal_ldncdf += 0.0015065154280332 * x2
139-
- 0.3993154819705530 * normal_lcdf_deriv_scaled_diff
140-
- 4.2919418242931700;
141-
} else if (normal_lcdf_deriv_scaled_diff < -17.0) {
149+
if (normal_lcdf_deriv_scaled_diff < -17.0) {
142150
normal_ldncdf += 0.0001263257217272 * x2 * normal_lcdf_deriv_scaled_diff
143151
+ 0.0123586859488623 * x2
144152
- 0.0860505264736028 * normal_lcdf_deriv_scaled_diff

stan/math/prim/prob/normal_lcdf.hpp

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,13 @@ inline return_type_t<T_y, T_loc, T_scale> normal_lcdf(const T_y& y,
191191
dncdf_log = 0.6245634904 - 0.9521866949 * t + 0.3986215682 * t2
192192
+ 0.04700850676 * t2 * t - 0.03478651979 * t4
193193
- 0.01772675404 * t4 * t + 0.0006577254811 * pow(t, 6);
194+
} else if (scaled_diff < -29.0) {
195+
// asymptotic Mills ratio, DLMF 7.12.1: dncdf_log grows linearly as
196+
// -2*scaled_diff, so no quadratic residual fit can track it
197+
const T_partials_return inv_x2 = 1.0 / x2;
198+
dncdf_log
199+
= -2.0 * scaled_diff
200+
/ (1.0 + inv_x2 * (-0.5 + inv_x2 * (0.75 + inv_x2 * -1.875)));
194201
} else if (10.0 * log(fabs(scaled_diff))
195202
< log(std::numeric_limits<T_partials_return>::max())) {
196203
// approximation derived from Abramowitz and Stegun (1964) 7.1.26
@@ -207,10 +214,7 @@ inline return_type_t<T_y, T_loc, T_scale> normal_lcdf(const T_y& y,
207214
- 1.453152027 * t4 + 1.061405429 * t4 * t));
208215
// check if we need to add a correction term
209216
// (from cubic fit of residuals)
210-
if (scaled_diff < -29.0) {
211-
dncdf_log += 0.0015065154280332 * x2
212-
- 0.3993154819705530 * scaled_diff - 4.2919418242931700;
213-
} else if (scaled_diff < -17.0) {
217+
if (scaled_diff < -17.0) {
214218
dncdf_log += 0.0001263257217272 * x2 * scaled_diff
215219
+ 0.0123586859488623 * x2
216220
- 0.0860505264736028 * scaled_diff - 1.252783383752970;

stan/math/prim/prob/std_normal_lcdf.hpp

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,12 @@ inline return_type_t<T_y> std_normal_lcdf(const T_y& y) {
169169
dnlcdf = 0.6245634904 - 0.9521866949 * t + 0.3986215682 * t2
170170
+ 0.04700850676 * t2 * t - 0.03478651979 * t4
171171
- 0.01772675404 * t4 * t + 0.0006577254811 * pow(t, 6);
172+
} else if (scaled_y < -29.0) {
173+
// asymptotic Mills ratio, DLMF 7.12.1: dnlcdf grows linearly as
174+
// -2*scaled_y, so no quadratic residual fit can track it
175+
const T_partials_return inv_x2 = 1.0 / x2;
176+
dnlcdf = -2.0 * scaled_y
177+
/ (1.0 + inv_x2 * (-0.5 + inv_x2 * (0.75 + inv_x2 * -1.875)));
172178
} else if (10.0 * log(fabs(scaled_y))
173179
< log(std::numeric_limits<T_partials_return>::max())) {
174180
// approximation derived from Abramowitz and Stegun (1964) 7.1.26
@@ -183,10 +189,7 @@ inline return_type_t<T_y> std_normal_lcdf(const T_y& y) {
183189
- 1.453152027 * t4 + 1.061405429 * t4 * t);
184190
// check if we need to add a correction term
185191
// (from cubic fit of residuals)
186-
if (scaled_y < -29.0) {
187-
dnlcdf += 0.0015065154280332 * x2 - 0.3993154819705530 * scaled_y
188-
- 4.2919418242931700;
189-
} else if (scaled_y < -17.0) {
192+
if (scaled_y < -17.0) {
190193
dnlcdf += 0.0001263257217272 * x2 * scaled_y + 0.0123586859488623 * x2
191194
- 0.0860505264736028 * scaled_y - 1.252783383752970;
192195
} else if (scaled_y < -7.0) {

test/unit/math/rev/prob/normal_ccdf_log_test.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ TEST(ProbDistributionsNormalLccdf, var_gradient_matches_mills_ratio) {
4949
const double mills
5050
= -2.0 * s
5151
/ (1.0 + inv * (-0.5 + inv * (0.75 + inv * (-1.875 + inv * 6.5625))));
52-
EXPECT_LT(std::fabs(yv.adj() / -mills - 1.0), 1e-9)
52+
// dncdf_log is d/ds; the partial is dncdf_log / (sigma*sqrt(2))
53+
EXPECT_LT(std::fabs(yv.adj() / (-mills / std::sqrt(2.0)) - 1.0), 1e-9)
5354
<< "gradient drifts from the asymptotic Mills ratio at y = " << y;
5455
stan::math::set_zero_all_adjoints();
5556
}

test/unit/math/rev/prob/normal_cdf_log_test.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ TEST(ProbDistributionsNormalLcdf, var_gradient_matches_mills_ratio) {
6060
const double mills
6161
= -2.0 * s
6262
/ (1.0 + inv * (-0.5 + inv * (0.75 + inv * (-1.875 + inv * 6.5625))));
63-
EXPECT_LT(std::fabs(yv.adj() / mills - 1.0), 1e-9)
63+
// dncdf_log is d/ds; the partial is dncdf_log / (sigma*sqrt(2))
64+
EXPECT_LT(std::fabs(yv.adj() / (mills / std::sqrt(2.0)) - 1.0), 1e-9)
6465
<< "gradient drifts from the asymptotic Mills ratio at y = " << y;
6566
stan::math::set_zero_all_adjoints();
6667
}

test/unit/math/rev/prob/std_normal_ccdf_log_test.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ TEST(ProbDistributionsStdNormalLccdf, var_gradient_matches_mills_ratio) {
4848
const double mills
4949
= -2.0 * s
5050
/ (1.0 + inv * (-0.5 + inv * (0.75 + inv * (-1.875 + inv * 6.5625))));
51-
EXPECT_LT(std::fabs(yv.adj() / -mills - 1.0), 1e-9)
51+
// dncdf_log is d/ds; the partial is dncdf_log / (sigma*sqrt(2))
52+
EXPECT_LT(std::fabs(yv.adj() / (-mills / std::sqrt(2.0)) - 1.0), 1e-9)
5253
<< "gradient drifts from the asymptotic Mills ratio at y = " << y;
5354
stan::math::set_zero_all_adjoints();
5455
}

test/unit/math/rev/prob/std_normal_cdf_log_test.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ TEST(ProbDistributionsStdNormalLcdf, var_gradient_matches_mills_ratio) {
5050
const double mills
5151
= -2.0 * s
5252
/ (1.0 + inv * (-0.5 + inv * (0.75 + inv * (-1.875 + inv * 6.5625))));
53-
EXPECT_LT(std::fabs(yv.adj() / mills - 1.0), 1e-9)
53+
// dncdf_log is d/ds; the partial is dncdf_log / (sigma*sqrt(2))
54+
EXPECT_LT(std::fabs(yv.adj() / (mills / std::sqrt(2.0)) - 1.0), 1e-9)
5455
<< "gradient drifts from the asymptotic Mills ratio at y = " << y;
5556
stan::math::set_zero_all_adjoints();
5657
}

0 commit comments

Comments
 (0)