Repository navigation
fix(contrib.hsgp): numerically stable spectral densities with finite gradients for large m - #2365
Conversation
In float32, exp(-w^2 length^2 / 2) underflows to exactly 0 for most of the
first m eigenvalues once m is a few dozen (92/100 zeros at length=1, ell=1).
The Matern density also overflows through gamma(nu + dim/2) and (2 nu)^nu
for nu >~ 30. Evaluate both densities in log space and expose
log_diag_spectral_density_{squared_exponential,matern}; the existing
functions become exp() of the log-space result.
log(exp(-z) * kve(nu, z)) underflows to log(0) for large z, which gives NaN gradients once sqrt() is applied; use log_bessel_kve(nu, z) - z instead. The regular branch of the jnp.where at w=0 is now evaluated at a safe argument so its log(0) can no longer leak NaN into the gradient of the S(0) branch. Expose log_diag_spectral_density_rational_quadratic and drop the unused modified_bessel_second_kind helper.
exp(a) * ive(j, a) / exp(a) with a = 1 / length^2 overflows to inf / inf = nan in float32 for length <~ 0.1. Use ive(j, a) = I_j(a) exp(-a) directly and drop the unused modified_bessel_first_kind helper.
sqrt(S) has an infinite derivative at S = 0, so once the spectral density underflows (most basis functions for m ~ 100 in float32) the gradient of the approximation w.r.t. the kernel hyperparameters is inf * 0 = nan and initialize_model cannot find valid initial parameters, in both the centered and the non-centered parameterization (the clip in the centered path zeroes the cotangent but 0 * inf is still nan). Build the square root of the density as exp(0.5 * log S) instead, whose gradient at an underflowed entry is exactly 0. Document the remaining limitation of the centered parameterization with degenerate scales.
…c wording Address review: jnp.asarray(length) ** (-2) raised for integer length, which master accepted; use a float exponent. Tighten the centered-path note and cross-reference the scale_mixture gradient limitation from hsgp_rational_quadratic. Drop the stale vector-length TODO on the Matern diag density (vector length scales are already supported and tested).
Benchmark reportthis PR + run time: 3 faster
compile time: unchanged across 32 benchmarksSignificant changes (3) ─────── run time ─────── ────── compile time ─────
benchmark baseline this PR Δ baseline this PR Δ
──────────────────────────────────────────────────────────────────────────────────
+ biject_to_constraints 2.3 ms 2.1 ms -6.1% 188.8 ms 195.3 ms +3.4%
+ categorical_log_prob 1.3 ms 1.3 ms -6.0% 53.7 ms 48.6 ms -9.6%
+ svi_multi_particle_elbo 1.10 s 1.02 s -7.1% 2.83 s 2.73 s -3.5%Red is slower, green is faster; a row is coloured by the worse of its two columns. A delta in parentheses cleared the threshold on a measurement below the resolution floor, so it is shown without being called a change. † marks a benchmark that could not be compared — see below. Full results
|
| baseline | this PR | |
|---|---|---|
| ref | master |
fix/hsgp-log-space-spectral-densities |
| commit | 02556b0e |
5b97a054 |
| numpyro | 0.22.0 | 0.22.0 |
| jax | 0.11.2 | 0.11.2 |
| backend | cpu | cpu |
| python | 3.14.8 | 3.14.8 |
Runner: Linux-6.17.0-1022-azure-x86_64-with-glibc2.39, 4 CPUs.
Produced by this benchmark run.
… log API Flooring the (strictly positive) spectral densities at the smallest positive normal number of their dtype makes jnp.sqrt(diag_spectral_density_*) safe to differentiate: at a floored entry the finite cotangent of sqrt (0.5 / sqrt(tiny)) is routed into the constant floor instead of being multiplied as inf * 0 into the hyperparameters. The gradient matches the float64 reference to 6 digits. This fixes existing user code that follows the documented pattern spd = jnp.sqrt(diag_spectral_density_*(...)) without any change on their side, so the log_diag_spectral_density_* functions added earlier in this branch are dropped again and hsgp_* go back to the same sqrt(diag) pattern. The log-space evaluation stays internal.
Replace the hand-derived float64 reference for the rational quadratic gradient (which needed the Bessel recurrence for d/dz log K_nu) with jax.test_util.check_grads against finite differences of the public function, parametrized over all three kernels. The NaN regression is in the reverse-mode path while the primal is fine, which is exactly what check_grads exercises.
Keep the public docstrings as they were; the floor is an implementation detail explained in a code comment in _exp_floored.
fehiepsi
left a comment
There was a problem hiding this comment.
Thanks for the fix, @juanitorduz!
- define diag_spectral_density_periodic as the floored exp of the new private _log_diag_spectral_density_periodic helper (pyro-pplgh-2365 convention), keeping int-dtype length support and the TFP dtype handling - drop the alpha != 0 special-casing in the log helper, matching the other _log_* helpers - scale the non-centered betas with sqrt(diag_spectral_density_periodic(...)) in the library and the example, matching the other kernels and avoiding private imports in approximation.py - document the linear model in the hsgp_periodic_non_centered docstring - fold the periodic kernel into test_hsgp_large_m_finite_gradients and drop the zero-amplitude public-API test Signed-off-by: Juan Orduz <juanitorduz@gmail.com>
* fix(contrib.hsgp): scale periodic coefficients by standard deviation Assisted-by: OpenAI Codex Signed-off-by: Vamsi-1 <vamsi-1@Vamsis-MacBook-Pro.local> * fix(example): correct periodic HSGP coefficient scaling Assisted-by: OpenAI Codex Signed-off-by: Vamsi-1 <vamsi-1@Vamsis-MacBook-Pro.local> * test(contrib.hsgp): verify periodic prior covariance Assisted-by: OpenAI Codex Signed-off-by: Vamsi-1 <vamsi-1@Vamsis-MacBook-Pro.local> * fix(contrib.hsgp): compute periodic scales in log space Assisted-by: OpenAI Codex Signed-off-by: Vamsi-1 <vamsi-1@Vamsis-MacBook-Pro.local> * fix(contrib.hsgp): compute periodic scales in log space Assisted-by: OpenAI Codex Signed-off-by: Vamsi-1 <vamsi-1@Vamsis-MacBook-Pro.local> * fix(contrib.hsgp): compute periodic scales in log space Assisted-by: OpenAI Codex Signed-off-by: Vamsi-1 <vamsi-1@Vamsis-MacBook-Pro.local> * fix(contrib.hsgp): compute periodic scales in log space Assisted-by: OpenAI Codex Signed-off-by: Vamsi-1 <vamsi-1@Vamsis-MacBook-Pro.local> * refactor(contrib.hsgp): remove unused periodic density values Assisted-by: OpenAI Codex Signed-off-by: Vamsi-1 <vamsi-1@Vamsis-MacBook-Pro.local> * refactor(contrib.hsgp): remove unused periodic density values Assisted-by: OpenAI Codex Signed-off-by: Vamsi-1 <vamsi-1@Vamsis-MacBook-Pro.local> * refactor(contrib.hsgp): remove unused periodic density values Assisted-by: OpenAI Codex Signed-off-by: Vamsi-1 <vamsi-1@Vamsis-MacBook-Pro.local> * chore(contrib.hsgp): remove unused periodic helpers Assisted-by: OpenAI Codex Signed-off-by: Vamsi-1 <vamsi-1@Vamsis-MacBook-Pro.local> * chore(contrib.hsgp): remove unused periodic helpers Assisted-by: OpenAI Codex Signed-off-by: Vamsi-1 <vamsi-1@Vamsis-MacBook-Pro.local> * fix(contrib.hsgp): preserve zero periodic amplitude Assisted-by: OpenAI Codex Signed-off-by: Vamsi-1 <vamsi-1@Vamsis-MacBook-Pro.local> * test(contrib.hsgp): preserve zero periodic amplitude Assisted-by: OpenAI Codex Signed-off-by: Vamsi-1 <vamsi-1@Vamsis-MacBook-Pro.local> * test(contrib.hsgp): check zero-amplitude gradient Assisted-by: OpenAI Codex Signed-off-by: Vamsi-1 <vamsi-1@Vamsis-MacBook-Pro.local> * style(contrib.hsgp): normalize test spacing Assisted-by: OpenAI Codex Signed-off-by: Vamsi-1 <vamsi-1@Vamsis-MacBook-Pro.local> * refactor(hsgp): keep log density helper private * refactor(hsgp): keep log density helper private * fix(hsgp): import private periodic log density helper * test(hsgp): preserve upstream underflow coverage Assisted-by: OpenAI Codex Signed-off-by: Vamsi-1 <245859299+Vamsi-2581@users.noreply.github.com> * test: remove duplicate periodic HSGP tests Signed-off-by: Vamsi-1 <245859299+Vamsi-2581@users.noreply.github.com> Assisted-by: OpenAI Codex * fix(contrib.hsgp): address review on periodic coefficient scaling - define diag_spectral_density_periodic as the floored exp of the new private _log_diag_spectral_density_periodic helper (gh-2365 convention), keeping int-dtype length support and the TFP dtype handling - drop the alpha != 0 special-casing in the log helper, matching the other _log_* helpers - scale the non-centered betas with sqrt(diag_spectral_density_periodic(...)) in the library and the example, matching the other kernels and avoiding private imports in approximation.py - document the linear model in the hsgp_periodic_non_centered docstring - fold the periodic kernel into test_hsgp_large_m_finite_gradients and drop the zero-amplitude public-API test Signed-off-by: Juan Orduz <juanitorduz@gmail.com> --------- Signed-off-by: Vamsi-1 <vamsi-1@Vamsis-MacBook-Pro.local> Signed-off-by: Vamsi-1 <245859299+Vamsi-2581@users.noreply.github.com> Signed-off-by: Juan Orduz <juanitorduz@gmail.com> Co-authored-by: Juan Orduz <juanitorduz@gmail.com>
Problem
In float32 the HSGP spectral densities under/overflow well before
m ~ 100, andjnp.sqrtof an underflowed density has an infinite derivative, so the gradient w.r.t. the kernel hyperparameters isinf * 0 = nanandinitialize_modelfails with "Cannot find valid initial parameters" (centered and non-centered alike). (related to #2143)exp(-w^2 l^2 / 2)underflows to 0length=1, ell=1; NaN gradientslog(exp(-z) * kve)->log(0);jnp.whereatw = 0leaks NaN from the unselected branchdS/dlengthatw = 0andw >= 100gamma(nu + D/2),(2 nu)^nuoverflownu >= 30.5exp(a) * ive(j, a) / exp(a)->inf / inflength <= 0.1Fix
No API changes. All densities are evaluated in log space and the public
spectral_density_*/diag_spectral_density_*returnmax(exp(log S), finfo.tiny), i.e. they are strictly positive as they are mathematically. This makes the usual patternjnp.sqrt(diag_spectral_density_*(...))safe to differentiate (the finitesqrtcotangent is routed into the constant floor instead of multiplyinginf * 0), sohsgp_*and existing user code are fixed without changes on the user's side.gammalninstead ofgamma.log_bessel_kve(nu, z) - z; the regular branch is evaluated at a safe argument nearw = 0so thejnp.wherecannot leak NaN gradients.bessel_ive(j, a)directly, noexp(a)round trip.modified_bessel_first_kind/modified_bessel_second_kind.Values agree with master to float32 noise wherever master did not under/overflow.
Tests
diag_spectral_density_*atm=100match float64 NumPy/SciPy references and thetinyfloor (SE, RQ); Matérn correct atnu in {30.5, 60.5}; periodic correct atlength in {0.1, 0.05}.check_gradsofsum(sqrt(diag_spectral_density_*))w.r.t.lengthatm=100for all three kernels; RQ gradient atw = 0exact.initialize_model+grad(potential_fn)finite forhsgp_{squared_exponential,matern,rational_quadratic}withm=100.pytest test/contrib/hsgp: 141 passed (also withJAX_ENABLE_X64=1). NUTS onhsgp_squared_exponentialwithm=100: 0 divergences.make lintandpre-commitpass.Notes
hsgp_periodic_non_centeredusesq2instead ofsqrt(q2)).scale_mixturethrough the TFP Bessel function are silently dropped.log_diag_spectral_density_*API; happy to squash.