Skip to content

scipy.optimize: fix signed bracket width in line search zoom - #40228

Open
VaggelisGian wants to merge 1 commit into
jax-ml:mainfrom
VaggelisGian:fix-bfgs-zoom-threshold
Open

scipy.optimize: fix signed bracket width in line search zoom#40228
VaggelisGian wants to merge 1 commit into
jax-ml:mainfrom
VaggelisGian:fix-bfgs-zoom-threshold

Conversation

@VaggelisGian

Copy link
Copy Markdown

Description

jax.scipy.optimize.minimize(method="BFGS") reports a line-search failure (status 3) on the classical 2D Rosenbrock function from the standard start points, in both float32 and float64, where scipy.optimize.minimize converges:

JAX_ENABLE_X64=1, x0=[0,0]:     success=False status=3 nit=1 fun=0.77657
JAX_ENABLE_X64=1, x0=[-1.2,1]:  success=False status=3 nit=8 fun=1.74729
float32,       x0=[-1.2,1]:     success=False status=3 nit=8 fun=1.74729

Root cause is in the zoom stage of the Wolfe line search (jax/_src/scipy/optimize/line_search.py). The failure guard compared a signed bracket width dalpha <= threshold. zoom2 is always entered with a_lo > a_hi whenever the outer loop doubled alpha, and zoom1 legitimately inverts its bracket after swaps, so on those iterations a negative width compared against a positive threshold set the sticky failed flag even while the bracket still contained steps satisfying the strong Wolfe conditions - scipy's line_search_wolfe2 returns the identical step for the same inputs.

Second, the absolute thresholds (1e-5 narrow dtypes, 1e-10 otherwise) were neither scale nor dtype aware and rejected legitimate deep bisection (widths near 1e-12 on steep quadratics).

The guard now compares abs(dalpha) against eps(dtype) * max(|a_lo|, |a_hi|), the width below which bisection cannot produce a distinct midpoint, and the zoom iteration cap moves from 30 to 60 so pure-bisection searches over wide dynamic ranges can finish.

Deliberately unchanged: the Wolfe condition definitions, the outer loop, and the float32 alpha floor in line_search. One interaction worth noting: the newly reachable deep zooms can end on an alpha below that pre-existing floor (|alpha_k| < 1e-8 is floored to 1e-8 for narrow dtypes), so on problems with extreme curvature in float32 the L-BFGS path can report convergence at a step whose stored alpha was floored. That is pre-existing floor behavior on a previously unreachable path and is left to a separate change.

AI assistance disclosure: this fix was developed with AI coding assistance under my direction; I verified the root cause analysis, ran all tests shown below, and take responsibility for the change.

Test Plan

New tests use the classical Rosenbrock valley (the file's existing helper squares plain differences and never forms it):

python -m pytest tests/scipy_optimize_test.py -k "curved_valley or steep_quadratic" -q
  default flags: 2 failed before the fix, 2 passed + 3 skipped after.
JAX_ENABLE_X64=1 python -m pytest tests/scipy_optimize_test.py -k "curved_valley or steep_quadratic" -q
  x64: 5 failed before the fix, 5 passed after.
python -m pytest tests/scipy_optimize_test.py -q -n auto
  default flags: 15 passed, 4 skipped.
JAX_ENABLE_X64=1 python -m pytest tests/scipy_optimize_test.py -q -n auto
  x64: 19 passed.

All previously-converging cases were checked for drift before/after: standard objectives converge to bit-identical results or shift by at most one iteration.

References

The zoom stage of the Wolfe line search marked itself as failed when
`a_hi - a_lo <= threshold`, where threshold was an absolute constant
(1e-5 for dtypes narrower than float64, 1e-10 otherwise). Two problems:

1. dalpha is signed. zoom2 is always called as _zoom(a_i, ..., a_i1),
   i.e. with a_lo > a_hi whenever the outer loop doubled alpha, and
   zoom1 inverts its bracket after hi_to_lo swaps. On an inverted-
   bracket iteration `negative <= positive threshold` evaluated true
   and set failed after that iteration's trial point, cutting off any
   further refinement of a bracket that still contained Wolfe-satisfying
   steps. On problems where overshoot triggers zoom2 (Rosenbrock from
   [-1.2, 1] or from [0, 0], Beale), minimize reported a line search
   failure (status 3 for BFGS, 5 for
   l-bfgs-experimental-do-not-rely-on-this) for line searches that had
   in fact found a point satisfying the strong Wolfe conditions;
   scipy's line_search_wolfe2 returns the identical step for the same
   inputs.

2. The absolute thresholds were neither scale nor dtype aware. They are
   replaced by eps(dtype) * max(|a_lo|, |a_hi|): the width below which
   bisection can no longer produce a distinct midpoint. This lets deep
   but legitimate zooms finish (BFGS on sum((1e6*(x-1))^2) needs widths
   near 1e-12 that the old 1e-10 floor rejected) while still catching
   genuinely collapsed brackets.

The zoom iteration cap moves from 30 to 60: when interpolation
degenerates to pure bisection over a wide dynamic range
(l-bfgs-experimental starts at alpha=1 and needs alpha ~ 5e-13 on the
steep quadratic above), 30 halvings are not enough to reach such a
step, so the old cap aborted searches that were still contracting.

Deliberately unchanged: the Wolfe condition definitions, the outer line
search loop, and the float32 alpha_k floor in line_search. Note one
interaction worth knowing: the newly reachable deep zooms can end on an
alpha below that floor (|alpha_k| < 1e-8 is floored to 1e-8 for narrow
dtypes), so on problems with extreme curvature in float32 the L-BFGS
path can now report convergence at a step whose stored alpha was
floored; the returned fun/jac come from the true point while the saved
step uses the floored one. This is pre-existing floor behavior on a
previously unreachable path and is left to a separate change.

Test Plan:
python -m pytest tests/scipy_optimize_test.py -k "curved_valley or steep_quadratic" -q
  default flags: 2 failed before the fix, 2 passed + 3 skipped after.
JAX_ENABLE_X64=1 python -m pytest tests/scipy_optimize_test.py -k "curved_valley or steep_quadratic" -q
  x64: 5 failed before the fix, 5 passed after.
python -m pytest tests/scipy_optimize_test.py -q -n auto
  default flags: 15 passed, 4 skipped.
JAX_ENABLE_X64=1 python -m pytest tests/scipy_optimize_test.py -q -n auto
  x64: 19 passed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant