Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Inversion strategies

Bisection vs. Newton for monotone CDFs, the safeguarded hybrid, differentiating the inverse, and vectorising it

04 — Inversion strategies

Notebooks 01–03 all reduced “invert the marginal” to the same problem: solve the monotone equation F(x)=uF(x) = u for xx. This closing notebook of Part 1 is about how to solve it well — the trade-off between bisection and Newton, the safeguarded hybrid that production solvers use, and how to run the solve on a whole batch at once.

What you will see

1. Bisection vs. Newton

Both solve F(x)=uF(x) = u for a monotone FF, but they trade robustness against speed.

  • Bisection keeps a bracket [lo,hi][\text{lo}, \text{hi}] straddling the root and halves it each step. The error falls like 2−n2^{-n} — linear convergence — so reaching float64 takes ~50 steps. It needs no derivative and cannot fail for a monotone FF.
  • Newton iterates xn+1=xn−(F(xn)−u)/F′(xn)x_{n+1} = x_n - (F(x_n) - u)/F'(x_n). Near the root the error roughly squares each step — quadratic convergence, ~5 steps — but it needs F′F' and, with no bracket, can overshoot into a flat tail where F′≈0F'\approx 0 and diverge. We record the per-iteration error of each.
bisection: 55 steps to 1.1e-16  (linear)
newton:    reaches 1.1e-16 in 5 steps        (quadratic)
newton from a bad tail start: errors = ['1e+00', '1e-03', '1e+00', 'nan', 'nan', 'nan']  -> diverges (nan)
<Figure size 1100x420 with 2 Axes>

Left: Newton (green) reaches machine precision in ~5 steps where bisection (blue) needs ~50 — quadratic vs. linear. Right: starting Newton far out in the tail, the tiny F′F' throws the iterate even further out and it blows up to nan. Bisection would simply have kept halving. We want Newton’s speed without its fragility.

2. The safeguarded hybrid

The standard fix (Brent-style Brent (1973)) keeps a bracket like bisection but tries a Newton step each iteration, accepting it only if it lands inside the bracket; otherwise it falls back to the bisection midpoint. This keeps the guaranteed convergence of bisection and the speed of Newton near the root — and it is essentially what robust library solvers do.

safeguarded Newton on the hard tail target: 1.1e-16, all finite = True
<Figure size 700x400 with 1 Axes>

On the very target that sent plain Newton to nan, the safeguarded version stays finite and converges quickly — the bracket catches every bad Newton step. optimistix Kidger (2021) ships both primitives (optx.Bisection, optx.Newton) and composes them; gauss_flows uses the robust bracketing bisection by default (we saw its internals in notebook 03).

3. Differentiating the inverse

To train a flow through its sampling path we need to differentiate the inverse — ∂x⋆/∂u\partial x_\star/\partial u and ∂x⋆/∂θ\partial x_\star/\partial\theta. Crucially, whether you get the right gradient depends on the solver. There are four options:

strategyideaproscons
unroll bisectionbackprop the bracketing looptrivialwrong (zero) — the where(F<u) comparisons are piecewise constant
unroll Newtonbackprop the smooth updatecorrect gradient; trivialmemory O(#iters)O(\#\text{iters}) — stores every iterate
one-stepdetach the root, add one Newton stepexact; O(1)O(1) memory; ~3 linesneeds F′F' at x⋆x_\star
adjoint (implicit-fn thm)differentiate F(x⋆)=uF(x_\star)=u directlyexact; O(1)O(1); automatic in optimistix; scales to vector rootsa linear solve

The last three all compute the implicit-function gradient Blondel et al. (2022), which depends only on the solution:

∂x⋆∂u=1F′(x⋆),∂x⋆∂θ=−∂θF(x⋆)F′(x⋆).\frac{\partial x_\star}{\partial u} = \frac{1}{F'(x_\star)}, \qquad \frac{\partial x_\star}{\partial \theta} = -\frac{\partial_\theta F(x_\star)}{F'(x_\star)} .

Unrolling instead differentiates the path to x⋆x_\star — which is why it is correct for smooth Newton but useless for comparison-based bisection.

max |unroll-Newton - analytic| = 6.00e-12   (correct)
max |one-step     - analytic| = 0.00e+00   (correct)
max |adjoint      - analytic| = 3.55e-15   (correct)
unroll-bisection grad range   = [0e+00, 0e+00]   <- zero (wrong)
<Figure size 800x440 with 1 Axes>

Three of the four land on the analytic curve; only unrolled bisection is flat at zero, because its iterates depend on the inputs solely through < comparisons. Unrolled Newton is correct — its update is smooth — but pays O(#iters)O(\#\text{iters}) memory, so the O(1)O(1) one-step and adjoint methods win for deep flows. This trap is live: gauss_flows’ MixtureGaussianCDF.inverse is an unrolled bisection, so differentiating it returns zero (gauss_flows#111):

grad through gauss_flows .inverse = 0.0000   <- 0 (unrolled bisection)
correct implicit value 1/F'(x*)   = 1.1619
=> fine for density/log_prob (forward); wrap with optimistix or one-step to train through sampling

4. Vectorising across a batch

A flow inverts a whole array of values at once (every sample, every dimension). We never write a Python loop over elements — we write the scalar solve once and jax.vmap it, which fuses the batch into a single compiled kernel. (Bisection vectorises especially naturally: the bracket updates are jnp.where over arrays — exactly what gauss_flows’ bisection_inverse does.)

vmap :   5.19 ms for 4000 inversions  (1.30 µs each)
loop : 119.17 ms for 400 inversions (297.9 µs each)
=> vmap is ~229x faster per inversion
batched inversion accuracy: max|F(x*) - u| = 2.6e-11
<Figure size 620x400 with 1 Axes>

Same solver, but vmap amortises tracing and dispatch across the whole batch — orders of magnitude faster per element than looping, and it composes with jax.jit/grad. For a dd-dimensional marginal layer you simply vmap over the leading (batch) axes and apply the scalar solve per coordinate.

Recap

Solving F(x)=uF(x)=u:

methodconvergenceneeds F′F'?robust?use
bisectionlinear (2−n2^{-n})noalways (monotone)default; gauss_flows inverse
Newtonquadraticyesno (tails)when bracketed & well-scaled
safeguarded Newtonquadratic + safeyesyesbest of both (Brent-style)

Differentiating the inverse, and scaling it:

techniquegradientmemorynote
unroll bisectionwrong (0)O(n)O(n)comparisons kill the gradient
unroll NewtoncorrectO(n)O(n)smooth update; memory-heavy
one-stepcorrectO(1)O(1)detach + 1 Newton step
adjointcorrectO(1)O(1)optimistix ImplicitAdjoint; automatic
jax.vmap——batch the scalar solve into one kernel

Next up — end of Part 1. We can now build, fit, train, and invert a single 1D marginal transform every way that matters. Part 2 stacks them with rotations (PCA, Householder, random) so that marginal Gaussianization can attack multivariate dependence — the second half of the RBIG loop from Part 0 notebook 04.

References
  1. Brent, R. P. (1973). Algorithms for Minimization Without Derivatives. Prentice-Hall.
  2. Kidger, P. (2021). On Neural Differential Equations [Phdthesis]. University of Oxford.
  3. Blondel, M., Berthet, Q., Cuturi, M., Frostig, R., Hoyer, S., Llinares-López, F., Pedregosa, F., & Vert, J.-P. (2022). Efficient and Modular Implicit Differentiation. Advances in Neural Information Processing Systems (NeurIPS).