Skip to content

Commit 73d53ba

Browse files
theo-brownTorax team
authored andcommitted
Add backtracking options to solver config.
PiperOrigin-RevId: 911274846
1 parent 5049b6b commit 73d53ba

10 files changed

Lines changed: 509 additions & 102 deletions

docs/configuration.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2270,6 +2270,21 @@ specific solver are defined in the relevant section below.
22702270
Number of corrector steps for the predictor-corrector linear solver. 0 means a
22712271
pure linear solve with no corrector steps. Must be a positive integer.
22722272
2273+
``atol`` (float | None [default = None])
2274+
Absolute tolerance for fixed-point iterations in the predictor-corrector solver.
2275+
If specified, iterations can exit early when the normalized residual falls below this threshold.
2276+
2277+
``rtol`` (float | None [default = None])
2278+
Relative tolerance for fixed-point iterations in the predictor-corrector solver.
2279+
If specified, iterations can exit early when the normalized residual falls below this fraction of the initial residual.
2280+
2281+
``use_backtracking`` (bool [default = True])
2282+
Enables backtracking linesearch to improve stability. Can be used with any
2283+
solver. For the Newton-Raphson solver, this option is always enforced as True.
2284+
2285+
``delta_reduction_factor`` (float [default = 0.5])
2286+
Factor by which the step size is reduced during backtracking.
2287+
22732288
``use_pereverzev`` (bool [default = False])
22742289
Use Pereverzev-Corrigan terms in the heat and particle flux when using the
22752290
linear solver. Critical for stable calculation of stiff transport, at the cost

docs/solver_details.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,24 @@ these coefficients become known at every iteration step, describing a `linear`
234234
system of equations. :math:`\mathbf{x}_{t+\Delta t}^k` can then be solved using
235235
standard linear algebra methods implemented in JAX.
236236

237+
Optionally, the fixed-point iteration can be configured to terminate early
238+
once a specified tolerance is achieved, rather than running for a fixed number
239+
of iterations. This is controlled by user-configurable absolute and relative
240+
tolerances on the residual norm, denoted by :math:`\varepsilon_{abs}` and
241+
:math:`\varepsilon_{rel}` respectively. The solve iterates until the normalized
242+
residual falls below the absolute tolerance
243+
:math:`\| \mathbf{R} \|_{norm} < \varepsilon_{abs}` or becomes smaller than the
244+
relative tolerance multiplied by the initial residual, i.e.,
245+
:math:`\| \mathbf{R} \|_{norm} < \varepsilon_{rel} \| \mathbf{R}_{0} \|_{norm}`.
246+
247+
Additionally, a backtracking linesearch can be used to improve stability in
248+
the solvers. When enabled in fixed-point iteration, if an iteration results
249+
in an increase in the residual or an invalid state (e.g., NaN values), the
250+
solver will backtrack along the update direction by reducing the step size.
251+
For the Newton-Raphson solver, this backtracking linesearch is always required
252+
and enforced to ensure robustness.
253+
254+
237255
To further enhance the stability of the linear solver, particularly in the
238256
presence of stiff transport coefficients (e.g., when using the QLKNN turbulent
239257
transport model, see :ref:`physics_models`), the |pereverzev-corrigan-method|

torax/_src/solver/jax_fixed_point.py

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import jax
1919
import jax.numpy as jnp
2020
from torax._src import jax_utils
21+
from torax._src.solver import linesearch
2122

2223
PyTree: TypeAlias = Any
2324

@@ -29,6 +30,11 @@ def fixed_point(
2930
xtol: float | None = 1e-08,
3031
maxiter: int = 500,
3132
method: Literal['del2', 'iteration'] = 'del2',
33+
atol: float | None = None,
34+
rtol: float | None = None,
35+
use_backtracking: bool = False,
36+
delta_reduction_factor: float = 0.5,
37+
max_backtrack_steps: int = 10,
3238
) -> PyTree:
3339
"""A JAX version of `scipy.optimize.fixed_point`.
3440
@@ -47,6 +53,12 @@ def fixed_point(
4753
with Aitken’s Del^2 convergence acceleration, taken from Burden, Faires,
4854
“Numerical Analysis”, 5th edition, pg. 80. 'iteration' just iterates the
4955
function until the tolerance is reached.
56+
atol: Absolute tolerance on the residual norm.
57+
rtol: Relative tolerance on the residual norm.
58+
use_backtracking: If true, use backtracking linesearch in 'iteration'
59+
method.
60+
delta_reduction_factor: Factor by which step_size is reduced each step.
61+
max_backtrack_steps: Maximum number of backtracking steps.
5062
5163
Returns:
5264
The fixed point `jax.Array`.
@@ -56,8 +68,22 @@ def fixed_point(
5668
if maxiter <= 0:
5769
raise ValueError(f'Invalid maxiter: {maxiter} must be positive.')
5870

59-
def body(x):
60-
x, count, _ = x
71+
def residual_fn(x):
72+
return jax.tree.map(lambda a, b: a - b, func(x, *args), x)
73+
74+
def norm_fn(res):
75+
return jnp.sqrt(sum(jnp.sum(leaf**2) for leaf in jax.tree.leaves(res)))
76+
77+
def residual_norm(x):
78+
return norm_fn(residual_fn(x))
79+
80+
if rtol is not None:
81+
initial_residual_norm = residual_norm(x0)
82+
else:
83+
initial_residual_norm = jnp.array(0.0)
84+
85+
def body(x_state):
86+
x, count, _ = x_state
6187
out1 = func(x, *args)
6288
if method == 'del2':
6389
out2 = func(out1, *args)
@@ -69,9 +95,45 @@ def _del2(p0, p1, p2):
6995

7096
out = jax.tree.map(_del2, x, out1, out2)
7197
else:
72-
out = out1
73-
74-
if xtol:
98+
if use_backtracking:
99+
direction = jax.tree.map(lambda a, b: a - b, out1, x)
100+
101+
init_res = direction
102+
init_norm = norm_fn(init_res)
103+
104+
decrease = 1e-4
105+
current_norm_sq = init_norm**2
106+
107+
def accept_fn(step_size, trial_norm):
108+
target = (1.0 - 2.0 * decrease * step_size) * current_norm_sq
109+
return (trial_norm**2) <= target
110+
111+
ls_state = linesearch.backtracking_linesearch(
112+
residual_fn=residual_fn,
113+
x_init=x,
114+
direction=direction,
115+
accept_fn=accept_fn,
116+
norm_fn=norm_fn,
117+
initial_residual=init_res,
118+
initial_residual_norm=init_norm,
119+
delta_reduction_factor=delta_reduction_factor,
120+
max_steps=max_backtrack_steps,
121+
)
122+
out = ls_state.x
123+
else:
124+
out = out1
125+
126+
# Terminate based on residual norm.
127+
if atol is not None or rtol is not None:
128+
res_norm = residual_norm(out)
129+
converged = jnp.array(False, dtype=jnp.bool_)
130+
if atol is not None:
131+
converged = converged | (res_norm <= atol)
132+
if rtol is not None:
133+
converged = converged | (res_norm <= rtol * initial_residual_norm)
134+
stop = converged
135+
# Terminate based on relative error.
136+
elif xtol:
75137

76138
def _relative_error(actual, expected):
77139
relative_error = (actual - expected) / expected
@@ -93,7 +155,7 @@ def cond(x):
93155
stop = jnp.array(False, dtype=jnp.bool_)
94156
x_init = (x0, count, stop)
95157

96-
if xtol is None:
158+
if xtol is None and atol is None and rtol is None:
97159
return jax.lax.fori_loop(0, maxiter, lambda i, val: body(val), x_init)[0]
98160
else:
99161
return jax.lax.while_loop(cond, body, x_init)[0]

torax/_src/solver/jax_root_finding.py

Lines changed: 31 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
"""JAX root finding functions."""
16+
1617
import dataclasses
1718
import functools
1819
from typing import Callable, Final
@@ -21,6 +22,7 @@
2122
import jax.numpy as jnp
2223
import numpy as np
2324
from torax._src import jax_utils
25+
from torax._src.solver import linesearch
2426

2527
# Delta is a vector. If no entry of delta is above this magnitude, we terminate
2628
# the delta loop. This is to avoid getting stuck in an infinite loop in edge
@@ -123,9 +125,7 @@ def back(g, y):
123125

124126
if use_jax_custom_root:
125127
if custom_jac is not None:
126-
raise ValueError(
127-
'custom_jac is not compatible with use_jax_custom_root.'
128-
)
128+
raise ValueError('custom_jac is not compatible with use_jax_custom_root.')
129129
x_out, metadata = jax.lax.custom_root(
130130
f=fun,
131131
initial_guess=x0,
@@ -199,109 +199,44 @@ def _body(
199199
dtype = input_state['x'].dtype
200200
a_mat = jacobian_fun(input_state['x'])
201201
rhs = -input_state['residual']
202-
# delta = x_new - x_old
203-
# tau = delta/delta0, where delta0 is the delta that sets the linearized
204-
# residual to zero. tau < 1 when needed such that x_new meets
205-
# conditions of reduced residual and valid state quantities.
206-
# If tau < taumin while residual > tol, then the routine exits with an
207-
# error flag, leading to either a warning or recalculation at lower dt
208-
initial_delta_state = {
209-
'x': input_state['x'],
210-
'delta': jnp.linalg.solve(a_mat, rhs),
211-
'residual_old': input_state['residual'],
212-
'residual_new': input_state['residual'],
213-
'tau': jnp.array(1.0, dtype=dtype),
214-
}
215-
output_delta_state = _compute_output_delta_state(
216-
initial_delta_state, residual_fun, delta_reduction_factor
202+
203+
direction = jnp.linalg.solve(a_mat, rhs)
204+
205+
def norm_fn(res):
206+
return jnp.mean(jnp.abs(res))
207+
208+
init_norm = norm_fn(input_state['residual'])
209+
210+
def accept_fn(step_size, trial_norm):
211+
del step_size # Unused
212+
return (trial_norm <= init_norm) & (~jnp.isnan(trial_norm))
213+
214+
ls_state = linesearch.backtracking_linesearch(
215+
residual_fn=residual_fun,
216+
x_init=input_state['x'],
217+
direction=direction,
218+
accept_fn=accept_fn,
219+
norm_fn=norm_fn,
220+
initial_residual=input_state['residual'],
221+
initial_residual_norm=init_norm,
222+
delta_reduction_factor=delta_reduction_factor,
223+
max_steps=100,
224+
min_step_norm=MIN_DELTA,
217225
)
218226

219227
output_state = {
220-
'x': input_state['x'] + output_delta_state['delta'],
221-
'residual': output_delta_state['residual_new'],
228+
'x': ls_state.x,
229+
'residual': ls_state.residual,
222230
'iterations': jnp.array(input_state['iterations'][...], dtype=dtype) + 1,
223-
'last_tau': output_delta_state['tau'],
231+
'last_tau': ls_state.step_size,
224232
}
233+
225234
if log_iterations:
226235
jax.debug.print(
227236
'Iteration: {iteration:d}. Residual: {residual:.16f}. tau = {tau:.6f}',
228237
iteration=output_state['iterations'].astype(jax_utils.get_int_dtype()),
229238
residual=_residual_scalar(output_state['residual']),
230-
tau=output_delta_state['tau'],
239+
tau=ls_state.step_size,
231240
)
232241

233242
return output_state
234-
235-
236-
def _compute_output_delta_state(
237-
initial_state: dict[str, jax.Array],
238-
residual_fun: Callable[[jax.Array], jax.Array],
239-
delta_reduction_factor: float,
240-
):
241-
"""Updates output delta state."""
242-
delta_body_fun = functools.partial(
243-
_delta_body,
244-
delta_reduction_factor=delta_reduction_factor,
245-
)
246-
delta_cond_fun = functools.partial(
247-
_delta_cond,
248-
residual_fun=residual_fun,
249-
)
250-
output_delta_state = jax.lax.while_loop(
251-
delta_cond_fun, delta_body_fun, initial_state
252-
)
253-
254-
x_new = output_delta_state['x'] + output_delta_state['delta']
255-
residual_vec_x_new = residual_fun(x_new)
256-
output_delta_state |= dict(
257-
residual_new=residual_vec_x_new,
258-
)
259-
return output_delta_state
260-
261-
262-
def _delta_cond(
263-
delta_state: dict[str, jax.Array],
264-
residual_fun: Callable[[jax.Array], jax.Array],
265-
) -> bool:
266-
"""Check if delta obtained from Newton step is valid.
267-
268-
Args:
269-
delta_state: see `delta_body`.
270-
residual_fun: Residual function.
271-
272-
Returns:
273-
True if the new value of `x` causes any NaNs or has increased the residual
274-
relative to the old value of `x`.
275-
"""
276-
x_old = delta_state['x']
277-
x_new = x_old + delta_state['delta']
278-
residual_vec_x_old = delta_state['residual_old']
279-
residual_scalar_x_old = _residual_scalar(residual_vec_x_old)
280-
# Avoid sanity checking inside residual, since we directly
281-
# afterwards check sanity on the output (NaN checking)
282-
# TODO(b/312453092) consider instead sanity-checking x_new
283-
with jax_utils.enable_errors(False):
284-
residual_vec_x_new = residual_fun(x_new)
285-
residual_scalar_x_new = _residual_scalar(residual_vec_x_new)
286-
delta_state['residual_new'] = residual_vec_x_new
287-
return jnp.bool_(
288-
jnp.logical_and(
289-
jnp.max(jnp.abs(delta_state['delta'])) > MIN_DELTA,
290-
jnp.logical_or(
291-
residual_scalar_x_old < residual_scalar_x_new,
292-
jnp.isnan(residual_scalar_x_new),
293-
),
294-
),
295-
)
296-
297-
298-
def _delta_body(
299-
input_delta_state: dict[str, jax.Array],
300-
delta_reduction_factor: float,
301-
) -> dict[str, jax.Array]:
302-
"""Reduces step size for this Newton iteration."""
303-
return input_delta_state | dict(
304-
delta=input_delta_state['delta'] * delta_reduction_factor,
305-
tau=jnp.array(input_delta_state['tau'][...], dtype=jax_utils.get_dtype())
306-
* delta_reduction_factor,
307-
)

0 commit comments

Comments
 (0)