Finding the MAP point
Inexact Newton-CG
params = hm.ReducedSpaceNewtonCG_ParameterList()
params["rel_tolerance"] = 1e-9
params["max_iter"] = 30
solver = hm.ReducedSpaceNewtonCG(model, params)
x = solver.solve([None, prior.mean.copy(), None])
print(solver.termination_reasons[solver.reason], solver.it,
solver.final_grad_norm / solver.initial_grad_norm)
The Newton system is solved with conjugate gradients against the matrix-free reduced Hessian, truncated by the Eisenstat-Walker criterion so early iterations do not pay for accuracy they cannot use (The CG of a Newton step). The prior precision is the preconditioner, which is what makes the iteration count depend on the information content of the data rather than on the mesh.
Two globalizations, selected by globalization:
|
Armijo backtracking line search (the default) |
|
Steihaug trust region, for strongly indefinite Hessians |
The Gauss-Newton approximation, the full Hessian with the terms involving the
second derivative of the forward operator dropped, is used for the first
GN_iter iterations, because it is positive definite everywhere while the full
Hessian is legitimately indefinite away from a minimum.
The CG of a Newton step
The CG iteration keeps every new residual orthogonal to all the earlier ones
explicitly (cg_reorthogonalize, on by default;
CGSolverSteihaug,
reorthogonalize). The recurrence of CG gives that orthogonality only for one
symmetric operator in exact arithmetic. A prior-preconditioned Hessian has a few
large, well separated eigenvalues, for which rounding destroys it within a few tens of
iterations, and a Hessian action computed with inexact solves is not one symmetric
operator at all. In both cases the recurrence alone is delayed, and by an amount that
changes with the last digit of the operands: the model problem of the benchmarks with
2.1 million state dofs took 193 to 210 CG iterations for its twelve Newton steps,
depending on the GPU and the number of ranks, and with the incremental solves stopped
at 1e-8 instead of 1e-12 a third more. With the residuals orthogonalized it
took 131 on every GPU and rank count, and still 131 with the incremental solves stopped
at 1e-6. The cost is k inner products and updates of parameter vectors at
iteration k and two stored vectors per iteration, nothing next to a Hessian action.
Two consequences. The incremental solves of a Hessian action need a loose tolerance
only, which halves their iterations, and Newton-CG stops them at 1e-6 in the CG of a
Newton step (cg_incremental_tolerance, the default): never more than a thousandth of
that CG’s tolerance, and their own tolerance where that is the looser. The forward and
adjoint solves keep their tolerance: they give the gradient, which must be exact for
Newton to converge to the MAP point, while the Hessian only shapes the steps. The
solvers keep theirs outside the Newton steps as well, so the Hessian of a Laplace
approximation at the MAP point has the accuracy of the solvers;
cg_incremental_tolerance = 0 keeps it in the Newton steps too. Newton-CG to a
relative gradient norm of 1e-6 on that problem, the same twelve Newton steps and the
same cost functional to nine digits in every column (the second row is
cg_incremental_tolerance = 0 with the solvers at 1e-12, the third the default):
H100 |
L40S |
Blackwell instance |
|
|---|---|---|---|
recurrence alone, incremental solves to 1e-12 |
63.8 s |
161.5 s |
163.3 s |
residuals orthogonalized |
47.4 s |
110.5 s |
122.2 s |
and incremental solves to 1e-6 |
32.0 s |
73.3 s |
83.8 s |
(one GPU each, or one MIG instance of an RTX PRO 6000 Blackwell, with MFEM’s CG for the
solves in all three rows; hypre’s own PCG, Linear solvers, takes another 7 to 12 % off
the last. At 1e-4 the H100 took 27.8 s, but one of the three GPUs then needed a
thirteenth Newton step, so 1e-6 is the default.) And single precision becomes usable for those
solves (Single precision in the GPU guide), since their rounding no longer
disturbs the iteration.
Two more solves inside that CG are cheaper than their tolerances say.
The prior’s solves, where they precondition the CG, stop at 1e-6
(cg_preconditioner_tolerance, the default). On the model problem that is eleven CG
iterations for each of the two solves of \(R^{-1} = A^{-1} M A^{-1}\) instead of
twenty-one, with the same Newton and CG counts and the same gradient norm after every
Newton step to four digits; with single-precision solves the Newton-CG solve took
23.6 s instead of 25.3 s on an H100 and 43.2 s instead of 46.1 s on an L40S. It cannot
be much looser. A solve stopped at a tolerance is not one linear map of its right-hand
side, so what the orthogonalization removes from a residual is then not all error of
the iterate, and the residual the CG stops on is off by about that tolerance times the
first residual (times the iteration count to the power 1.5): at 1e-5 the twelfth
Newton step of the model problem no longer met its tolerance. The library never uses
more than a thousandth of the CG’s own tolerance, and it leaves the solves as they are
without cg_reorthogonalize and in the trust-region method; 0 leaves them as
they are everywhere.
Or they are a fixed polynomial of multigrid cycles (cg_preconditioner_cycles,
off by default). What the argument above asks of a preconditioner is that it be one
linear map, and a Krylov solve is one only in the limit of its tolerance. k steps
of the Chebyshev iteration on the eigenvalues of the cycle times the operator are one
by construction: k cycles, each from the iterate before it, with coefficients that
do not depend on the right-hand side
(ChebyshevSolver), symmetric and positive
definite where the cycle is, with an error of at most
\(1/T_k((\lambda_{\max}+\lambda_{\min})/(\lambda_{\max}-\lambda_{\min}))\) of the
solution in the energy norm. The interval is estimated once, from twelve CG iterations
on a vector of random signs. For the prior of the Newton benchmark on 64 GPUs (4803
elements; eigenvalues of the cycle in [0.48, 1.05]) the bound is 1.4 % with three cycles,
0.3 % with four and 0.05 % with five, where the solve to 1e-6 takes eleven iterations.
How many to take is a measurement and not that bound: the first Newton steps are CG
stopped after a few iterations, and such an iterate moves with the preconditioner.
After two steps of that benchmark (eight CG iterations in every case) the cost
functional was the one of the Krylov solves to 4e-4 with five cycles, where two runs
with the Krylov solves differ by 2e-4; it was 1.3 % from it with four and 4 % with
three, with a gradient norm half as large again. With five the prior’s part of a CG
iteration took 0.07 s instead of 0.16 s there.
It replaces the solves only where they precondition: a sample of the prior and the
gradient’s norm take the prior’s own solver, as before, and a prior solved by a
factorization has nothing to replace.
The Hessian actions may lose accuracy as the CG converges
(cg_hessian_relaxation, off by default). The error of a product enters the
residual in proportion to the step it multiplies, and the steps shrink with the
residual (inexact Krylov methods). With cg_hessian_relaxation = c the two
incremental solves of the action at CG iteration k stop at c times the CG’s
tolerance times \(\|r_0\| / \|r_k\|\), where that is looser than their own
tolerance. The prior’s part of the action keeps its accuracy: a relative error of its
mass solve is the same relative error of \(R x\), with nothing to damp it. With
c = 1e-2 the incremental solves of the model problem took 6.5 iterations on average
instead of eleven, in the same Newton and CG counts at 323 and 643 with
the gradient norm after each Newton step within two percent, and the Newton-CG solve
with single-precision solves took 32.4 s instead of 39.9 s on an L40S; with
c = 1e-1 the iteration took a different path (twelve Newton steps instead of eleven
at 323). It is off by default because it was not as safe on other problems.
With ten times more observations the steps stayed fifteen at c = 1e-2 and became
sixteen at 1e-3. With noise ten times smaller, a more informative problem whose CG
runs into cg_max_iter in most late steps, the eighteen Newton steps and 488 CG
iterations became 23 and 597 at 1e-2 (20 and 514 at 1e-3), slower overall. Linear
elements at 1283 kept their twelve steps and 149 CG iterations at 1e-2 and
took 9 % less time. The incremental solves’ error enters through the misfit Hessian,
whose largest eigenvalues grow with the information in the data, so the c that is
safe shrinks with it, and nothing here knows them in advance.
Counting the last Newton step. The twelve steps of the model problem at 643 end with a gradient norm of 0.0976 against a tolerance of 0.1158, and the CG of the twelfth step passes its own test at its 33rd iteration by less than a percent. A change of the path of that size (the prior’s solves at 1e-4, a relaxation of 3e-3, another GPU) ends that CG at its 32nd iteration and the step at 0.127, and a thirteenth step of 37 CG iterations follows. Thirteen steps and 167 CG iterations in a table are then not a slower method: compare the gradient norms step by step.
cg_reorthogonalize = False gives hIPPYlib’s iteration, for a comparison step by
step. The low-rank eigensolvers of the Laplace approximation are not Krylov
recurrences and never had this sensitivity; the accuracy of their eigenpairs is that of
the incremental solves: at 643 the 50 leading eigenvalues came out to 1e-8 with
the solves at 1e-8 and to 1e-5 (the pointwise posterior variance to 6e-6) with the
solves at 1e-5, in either precision (Single precision).
Recommended settings in the GPU guide collects these settings.
Gradient norms
gradient_norm selects the norm the stopping test uses: "M", the discrete
\(L^2\) norm through the mass matrix, is the default and matches hIPPYlib;
"R" uses the prior precision. This is not cosmetic: the iteration history
differs visibly between the two, and matching hIPPYlib’s choice is what makes the
two libraries’ histories agree step for step.
BFGS
params = hm.BFGS_ParameterList()
params["BFGS_op"]["memory_limit"] = 25
solver = hm.BFGS(model, params)
x = solver.solve([None, prior.mean.copy(), None], H0inv=prior.Rsolver)
Limited-memory BFGS with the prior solver as the initial inverse Hessian. It needs only the cost and the gradient, so agreement between BFGS and Newton-CG on the minimizer is strong evidence that both the gradient and the Hessian are right: two optimizers with nothing in common beyond the cost and the gradient cannot agree by accident. The test suite requires them to agree to 2e-3 and observes 1e-5 to 1e-6.
Expect BFGS to need hundreds of iterations where Newton-CG needs tens. That is a statement about preconditioning, not correctness, and the iteration count moves by about 10% under round-off-level changes, so do not treat it as a regression signal.
Verifying a new problem
Before trusting a gradient on a new residual:
eps, err_grad, err_H = hm.modelVerify(model, m0, eps=np.logspace(-1, -6, 11))
print(hm.best_slope(eps, err_grad), hm.best_slope(eps, err_H))
Both slopes should be 1.0. best_slope takes the median over the step sizes where
round-off has not taken over; choose the window from the measured error curve rather
than leaving the default, because the Hessian difference reaches its floor much
earlier than the gradient’s when the misfit is nearly quadratic. An error that is not a
finite number at a step of the window gives NaN, so that a cost which is not a number
at one step cannot pass on the slope of the others.