Repository navigation
Conversation
This was referenced Oct 8, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #3153. Fixes #1578. Addresses #2508 partially.
This PR improves
beta_binomial_lpmf,beta_binomial_cdf,beta_binomial_lcdf,beta_binomial_lccdf,beta_neg_binomial_lpmf,beta_neg_binomial_cdf,beta_neg_binomial_lcdf,beta_neg_binomial_lccdf,neg_binomial_lpmf,neg_binomial_2_lpmf,neg_binomial_2_log_lpmf,neg_binomial_2_log_glm_lpmf,dirichlet_multinomial_lpmf,lkj_corr_lpdf,lkj_corr_cholesky_lpdf,lkj_cov_lpdf,student_t_lpdf,multi_student_t_lpdf,multi_student_t_cholesky_lpdf,yule_simon_lpmf,yule_simon_cdf,yule_simon_lcdf,yule_simon_lccdf, and the derivatives oflbeta.Summary
Several distributions form a difference
f(x + k) - f(x), withfone oflgamma,lbetaordigamma, a shape, concentration, dispersion or degrees-of-freedom parameterx, and a count or offsetk. Whenxis large, the two terms are large and nearly equal, and the difference keeps no correct digits. For the values the terms are of the size ofx, so the error is abouteps * x. For the gradients the terms are aboutlog(x), and a model that puts the parameter on the log scale multiplies the error byx.In the model of #3153,
beta_binomial_lpmf(57 | 117, s, s)withs = exp(lc) / 2,developreturns a log probability 81 too high fromlc = 42on, and its gradient inlcis 0 or noise fromlc = 24on. Warmup then finds a flat plateau that it cannot leave. On this branch the value is within2.3e-14of the exact value and the gradient within3.7e-14forlcfrom 0 to 44.lcd/dlcFrom
lc = 34on, the exact values are below the rounding error of any form (about2e-14):57 log p + 60 log qis about -81 there.#2508 reports the same defect:
beta_binomial_lpmf(500 | 1000, s, s)fors = 0.5e10, 0.5e13, 0.5e19, wheredevelopis wrong by5.3e-8,2.8e-4and693(it returns+689.47at the largest). This PR fixes all three; they are now rows of thebeta_binomial_lpmftest. This PR does not close #2508: the comment there aboutbeta_binomial_lcdf(3 | 10, 298.3, 146.75)returningNaN(exact-3.93335) has a different cause.hypergeometric_3F2does not stop at the zero term of the terminating series3F2({1, 302.3, -6}; {5, -151.75}; 1)(exact17.63), and later0 * infgivesNaN. A separate PR will repair that and close #2508.#1578 reports wrong derivatives of
neg_binomial_lpmffor largealpha. Its example (n = 0,alpha = 1e10 + 1,beta = 0.8) is already correct ondevelop, because the Poisson approximation it describes is no longer used. This PR repairs the loss that remains ind/dalpha = digamma(alpha + n) - digamma(alpha) + log(beta / (1 + beta)). It matters when the meanalpha / betastays fixed: then the error ofalpha * d/dalphaondevelopreaches 3.0 atalpha = 1e15, and on this branch it is1e-14(rows of the new large-shape test).The same root cause is in more functions. This PR repairs:
beta_binomial_lpmfbeta_neg_binomial_lpmfneg_binomial_lpmf,neg_binomial_2_lpmf,neg_binomial_2_log_lpmfneg_binomial_2_log_glm_lpmfdirichlet_multinomial_lpmflkj_corr_lpdf,lkj_corr_cholesky_lpdfyule_simon_lccdf(and_cdf,_lcdfin relative terms)beta_binomial_{cdf,lcdf,lccdf}inforNaNbeta_neg_binomial_{cdf,lccdf}student_t_lpdfnugradient 0.14multi_student_t_lpdf,multi_student_t_cholesky_lpdfnugradient 2e-7yule_simon_lpmflbeta(var,fvar), derivativeThe shape is
1e15for the first nine rows and up to1e15for the last four (nudecades forstudent_t_lpdf). "Log-shape gradient" isx * d/dx, the gradient a sampler sees when the model's parameter islog(x). All references are mpmath at 80 to 140 digits, certified by two independent routes (value byloggamma, gradients by closed-formdigammadifferences and bymp.diff; the cdfs as sums of the pmf).propto. In some of the more stable forms, a constant thatproptodrops cancels against terms that depend on a parameter inside one expression, so the code cannot skip it (for example,lbeta(n, alpha + 1)containslgamma(n)). To keep theproptobehavior as before, the code adds such constants back with the opposite sign:lgamma(n)inyule_simon_lpmf, andlgamma(y + 1),phi log(phi)(dataphi) andy theta(datax,alphaandbeta) inneg_binomial_2_log_glm_lpmf, also in OpenCL. For the GLM with dataphi, this brings back a rounding error of abouteps * phi log(phi)in theproptovalue. A probe of all 14 changed densities, with every combination of data and parameter arguments, matches theproptovalues ofdevelop, and a new test checksfull - proptoagainst the dropped terms.Most of the added source lines are in six files:
prim/fun/log_rising_factorial_ratio.hpp(new helpers)prim/fun/digamma_diff.hpp(new)log_beta_ratio(new)digamma_diff(new)opencl/kernel_generator/elt_function_cl.hpp(registrations)student_t_lpdf(series), CPU and OpenCLThe remaining 23 distribution files change by 2 to 48 lines each.
What this PR adds
digamma_diff(x, d) = digamma(x + d) - digamma(x), a new function inprim/fun. It's public (but not exposed in the language) because it is used in 19 distribution files and in the derivatives oflbeta;log_rising_factorialcan use it in a follow-up. Three paths: a non-autodiff integerdfrom 0 to 8 (a count) usessum_{j < d} 1 / (x + j), which has only positive terms and gives exactly1 / xford = 1; forx < 10andd >= 10the plain difference, which loses only a few ulp there (the result is at least 0.72 anddigamma(x) <= 2.25); otherwise both arguments are shifted up to at least 10 with the recurrence, andlog1p(d / y)plus the asymptotic series is used, with the differencesy^-2i - (y + d)^-2iformed as(u - v) h_iso that nothing cancels. Measured over 7 281 points,xfrom1e-3to1e18,dfrom1e-6to1e7: at most 4.2 ulp; the plain difference reaches6.5e16ulp. It is vectorized and works with all autodiff types through generic arithmetic;expect_adtests included. An OpenCL device function with the same paths and a kernel-generator registration are included.internal::log_rising_factorial_ratio,internal::log_beta_ratioand their helpers inprim/fun/log_rising_factorial_ratio.hpp. They write eachlgammathroughlgamma_stirling_diff, so that the terms linear in the arguments cancel exactly, and form the rest without cancellation (thex log1p(k / x) - kparts are split off fork < x, and the integer parts are added exactly).log_beta_ratio(alpha, beta, n, m) = lbeta(alpha + n, beta + m) - lbeta(alpha, beta)chooses between this form and the plainlbetaform by the size of their terms: the Stirling form has rounding errors of the size ofn + m, thelbetaform of the size of the smaller shape. Without the choice,N = 1e6points with one small shape lost up to 4e-11 wheredevelopwas correct.The
revandfwdderivatives oflbeta. The partial in an argument of at least 10 is now-digamma_diff(a, b); below 10 it staysdigamma(a) - digamma(a + b), withdigamma(a + b)shared between the two partials as before.lbetaitself is accurate at large arguments, but its derivative had the same cancellation, so every model that callslbetawith a large parameter, andmulti_student_t_lpdf, which gets itsnugradient by autodiff throughlbeta, had inaccurate gradients. Below 10 the plain difference has an absolute error of a feweps, which stays at the rounding level on the log scale too.Changes per function
beta_binomial_lpmf: value bylog_beta_ratio, partials bydigamma_diff.beta_neg_binomial_lpmf: value as-log(n) - lbeta(n, beta) + log((r)_n / (r + alpha + beta)_n) + log((alpha)_r / (alpha + beta)_r), partials bydigamma_diff.neg_binomial_lpmf:alphapartial bydigamma_diff; thebetapartial termalpha / beta - alpha / (1 + beta)is nowalpha / beta / (1 + beta).neg_binomial_2_lpmf,neg_binomial_2_log_lpmf:phipartial bydigamma_diff. Values unchanged (bitwise).neg_binomial_2_log_glm_lpmf: value per instance aslchoose(y + phi - 1, y) - y log1p_exp(log(phi) - theta) - phi log1p_exp(theta - log(phi)), the form ofneg_binomial_2_log_lpmf, instead ofphi log(phi) - lgamma(phi) + lgamma(y + phi) - (y + phi) log(mu + phi) + y theta. Thephipartial no longer forms1 - (y + phi) / (mu + phi)andlog(phi) - logsumexp(theta, log(phi)).dirichlet_multinomial_lpmf: partials bydigamma_diff.lkj_corr_lpdf(andlkj_corr_cholesky_lpdf,lkj_cov_lpdfthroughdo_lkj_constant): the constant is a sum oflgamma(x + k/2) - lgamma(x); each is nowlgamma(k/2) - lbeta(k/2, x), and theetapartial is set explicitly withdigamma_diff.yule_simon_{cdf,lcdf,lccdf}:lgamma(alpha + 1) + lgamma(n + 1) - lgamma(n + alpha + 1)is nowlog(n) + lbeta(n, alpha + 1); partial bydigamma_diff.yule_simon_lpmf: valuelog(alpha) + lbeta(n, alpha + 1); partial1 / alpha - digamma_diff(alpha + 1, n).beta_binomial_{cdf,lcdf,lccdf}: the leadinglbeta(nu, mu) - lbeta(alpha, beta)bylog_beta_ratio, the digamma differences bydigamma_diff.beta_neg_binomial_{cdf,lcdf,lccdf}: the leading term is the log pmf atn + 1, now computed as inbeta_neg_binomial_lpmf; partials bydigamma_diff.student_t_lpdf: fornu < 40unchanged; fornu >= 40the termslgamma((nu + 1) / 2) - lgamma(nu / 2) - log(nu) / 2anddigamma((nu + 1) / 2) - digamma(nu / 2)come from the asymptotic series inh = nu / 2:-log(2) / 2 - 1/(8h) + 1/(192h^3) - 1/(640h^5) + 17/(14336h^7) - 31/(18432h^9)and its derivative (at most 0.9 and 2.4 eps against mpmath fromh = 20to1e15). This is also cheaper than twolgammaordigammacalls.multi_student_t_lpdf,multi_student_t_cholesky_lpdf: the constantlgamma((nu + p) / 2) - lgamma(nu / 2)is nowlgamma(p / 2) - lbeta(p / 2, nu / 2); the cholesky version sets thenupartial withdigamma_diff(nu / 2, p / 2), the other one gets it throughlbeta.beta_binomial_lpmf,neg_binomial_lpmf,neg_binomial_2_lpmf,neg_binomial_2_log_lpmf,neg_binomial_2_log_glm_lpmf(prim file and kernel) andstudent_t_lpdfcompute the same formulas.Related defects fixed on the way
lkj_corr_lpdfdroppedd constant / d etaateta == 1.0. The special case foreta == 1returned the constant as a number without derivative. Ateta = 1withK = 3and the matrix in the new test,developreturnsd/deta = -0.165, the exact value is1.2214.eta = exp(0) = 1is exactly the value at the default initialization of a positive parameter.beta_binomial_lcdfcomputedlog(1 - C)withCthe complementary series. Where the cdf is small this keeps no digits, and with the accurateCit can round tologof a value at or below 0. WhereC > 1/2the function now uses the mirrorX <= nif and only ifN - X > N - n - 1, withN - Xdistributed asbeta_binomial(N, beta, alpha): the same series with the shapes swapped, without the complement. Ondevelopthis function returnedNaNat 16 of 81 test points and was wrong by up to 61; it now has the accuracy of the series everywhere. Foralpha = 1exactly the mirrored series is0 / 0at one term, so that case keepslog(1 - C).beta_binomial_lpmfdid not check0 <= n <= N. The check was built, but its result was never computed, so the function never returnedLOG_ZERO. Withoutproptothe value was-infby accident (throughbinomial_coefficient_log), but with a nonzero gradient; underproptoit was finite:-8.216forn = 6,N = 5in the new test. The CPU version returnsLOG_ZEROwithout a gradient; the OpenCL version now does too.neg_binomial_2_log_glm_lpmfwith a scalaralphaand a vectorphifailed. Whether the kernel sums thephiderivative was decided fromalphainstead ofphi, so it returned work-group sums where per-instance derivatives were needed, and the gradient assignment threw a size mismatch. The decision now depends onphionly, and thephiderivative is no longer computed whenphiis data. A new test covers both mixed cases (scalaralphawith vectorphi, and vectoralphawith scalarphi).Limits that remain, by a different cause
beta_binomial_{cdf,lcdf,lccdf}andbeta_neg_binomial_{cdf,lcdf,lccdf}are limited by the tolerance of their3F2series, about1e-8relative. They throw atalpha = beta = 1for someN, and thebeta_neg_binomialones throw from shape1e4and can take seconds per call.beta_neg_binomial_lcdfstill forms a complement and is wrong where its cdf is small. None of this is changed here.d/dalphaandd/dbeta, and autodiff formsalpha d/dalpha + beta d/dbetaoutside it. For tied shapes in numerical instability in beta_binomial_lpmf at large shape parameters #3153 the two products are-1.5and+1.5and their sum is27 / exp(lc). Each partial is accurate to a fewepsof its own terms; the sum is then accurate to a fewepsof 1.5, not of its own size.log_rising_factorialis stilllgamma(x + n) - lgamma(x); no Stan Math function calls it. A follow-up can use the new helpers there.Tests
test/unit/math/prim/fun/digamma_diff_test.cpp,test/unit/math/mix/fun/digamma_diff_test.cpp: special cases, exact finite sums, 17 mpmath values, the count path and the plain path (digamma_diff(x, 1) == 1 / xexactly fromx = 1e-300to1e300);expect_adat 8 points.test/unit/math/prim/prob/beta_binomial_test.cpp: values at the reporter's points of numerical instability in beta_binomial_lpmf at large shape parameters #3153 and beta_binomial_lpmf is unstable for large shape arguments #2508 and at large unequal shapes; the difference to the binomial limit atlc = 42.test/unit/math/rev/prob/beta_binomial_lpmf_test.cpp(new): log-shape gradients at 8 points; thelcgradient of the reporter's model forlc = 16 ... 32. Fromlc = 34ondevelopreturns exactly 0, which is within1e-13of the exact gradient, so a test there cannot see the defect.test/unit/math/rev/prob/large_shapes_test.cpp(new): 82 rows over the other functions (includingstudent_t_lpdf, bothmulti_student_tfunctions,yule_simon_lpmfandlbeta), each with points wheredevelopwas wrong by more than 100 times the tolerance and one point at a small shape wheredevelopwas right; thelkj_corrgradient ateta = 1; andfull - proptoagainst the dropped terms forstudent_t_lpdf,yule_simon_lpmf,beta_neg_binomial_lpmfand three cases of the GLM.test/unit/math/opencl/rev/(six files), because the existing differential tests compare OpenCL with the CPU and cannot see an error both share; a device-function test fordigamma_diff; a test thatbeta_binomial_lpmfgivesLOG_ZEROwithout a gradient fornoutside0...N, also underpropto; a test ofneg_binomial_2_log_glm_lpmfwith a scalaralphaand a vectorphiand the reverse.The tests find the defect. With the 23
developheaders swapped in (21prim/probfiles and therev/fwdlbeta), all 4 newbeta_binomialcases and both large-shape cases fail; the 3 old cases and theproptocase pass (theproptocase statesdevelop's rule). With the branch headers restored, everything passes again. OpenCL: with thedevelopOpenCL files every new absolute test fails (6 files), and so do the two new tests of the related defects 3 and 4. Every old differential test passes, including theproptocomparisons of the GLM andstudent_t_lpdf, exceptopencl_matches_cpu_bigof the GLM: it runs after the exception of defect 4 in the same binary, which leaves the autodiff stack dirty.Full suites. All unit tests of the touched functions pass (51 files:
prim,rev,fwd,mix, includinglbeta,digamma_diff,student_t,multi_student_tandlkj_cov), and so do the generatedtest/probtests ofbeta_binomial,beta_neg_binomial,neg_binomial,neg_binomial_2,yule_simonandstudent_t(271 306 cases). The OpenCL suites of the six distributions,digamma_diff,lbetaand the kernel generator pass on a V100 (126 cases).Side effects
student_t_lpdfexecutes the same operations asdevelopfornu < 40(plus one comparison: +2.4 % instructions for value and gradient) and fewer fromnu = 40on (-52 % instructions for value and gradient atnu >= 100; the scalar value takes 0.35 times as long there). Gradients that usedigamma_diff, at shapes below 10, ratio todevelop(median of 5 rounds, Xeon E5-2680 v3):neg_binomial,neg_binomial_2,neg_binomial_2_loggradient, counts 0 to 8neg_binomial_2gradient, counts 9 to 100beta_binomialgradient,N = 20lbetagradient, both arguments below 10lbetagradient, one argument from 10 to 1000neg_binomial_2, 1000 counts from 0 to 20, onephiRelease notes
Improved stability of
beta_binomial_lpmf,beta_binomial_cdf,beta_binomial_lcdf,beta_binomial_lccdf,beta_neg_binomial_lpmf,beta_neg_binomial_cdf,beta_neg_binomial_lcdf,beta_neg_binomial_lccdf,neg_binomial_lpmf,neg_binomial_2_lpmf,neg_binomial_2_log_lpmf,neg_binomial_2_log_glm_lpmf,dirichlet_multinomial_lpmf,lkj_corr_lpdf,lkj_corr_cholesky_lpdf,lkj_cov_lpdf,student_t_lpdf,multi_student_t_lpdf,multi_student_t_cholesky_lpdf,yule_simon_lpmf,yule_simon_cdf,yule_simon_lcdf,yule_simon_lccdf, and the derivatives oflbeta.Checklist
Copyright holder: Aalto University
The copyright holder is typically you or your assignee, such as a university or company. By submitting this pull request, the copyright holder is agreeing to the license the submitted work under the following licenses:
- Code: BSD 3-clause (https://opensource.org/licenses/BSD-3-Clause)
- Documentation: CC-BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
the basic tests are passing
./runTests.py test/unit)make test-headers)make test-math-dependencies)make doxygen)make cpplint)the code is written in idiomatic C++ and changes are documented in the doxygen
the new changes are tested