Repository navigation
scg: make the Steihaug CG forcing term invariant to the scale of f - #89
Merged
Merged
Conversation
The residual tolerance min(0.5, sqrt(|g|)) * |g| of the scg subproblem solver stays at 0.5 * |g| while |g| >= 0.25. On ill-conditioned problems a single CG iteration (a steepest descent step) often meets it, and the resulting small decrease in f triggered FTOL short of the optimum, depending on the scale of f. Use min(0.5, sqrt(|sg| / |sg0|)) with sg0 the scaled gradient at the first iteration instead. Early steps stay as cheap as before, the tolerance no longer depends on the scale of f and still decreases like sqrt(|sg|) towards the optimum. Only scg is affected. Fixes #77 Co-Authored-By: Claude Opus 5.5 <[email protected]>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #89 +/- ##
==========================================
+ Coverage 96.98% 97.16% +0.18%
==========================================
Files 10 10
Lines 1159 1445 +286
==========================================
+ Hits 1124 1404 +280
- Misses 35 41 +6 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #77
Change
Only
subspace_solver='scg'is affected. The Steihaug CG residual tolerance changes frommin(0.5, sqrt(|sg|)) * |sg|tomin(0.5, sqrt(|sg| / |sg0|)) * |sg|. Heresg0is the scaled gradient at the first iteration. This is option 2 in the issue.Optimizer.minimizestores|sg0|at iteration 1 asself.sgnorm0. It passes the value through a new optionalsgnorm0argument oftrust_region()toTRStepSteihaug.CGStep/TRStepSteihaugtake an optionalsgnorm0. Without it, the step counts as the first step, so eta = 0.5.scipy.linalg.norm, which does not overflow for the huge gradients covered bytest_huge_gradient_and_hessian.Why option 2
I measured options 1, 2 and 3 on the reproducer, on the 300-problem fuzz set from #75 and on larger-n problems.
|g| >= 0.25, so the first steps still take one CG iteration. It tightens only as|sg|falls relative to|sg0|, and it still goes to zero likesqrt(|sg|), so the asymptotic rate stays superlinear.|g| / |g0|.eta = 1e-2leaves 5 of the issue's cases. Witheta = 1e-6it fixes them but costs +17% CG iterations on those same runs (median ×1.30), and it is a blanket tighter tolerance.|sg|as the reference, to guard against a tiny|sg0|whenx0sits next to a bound. It gave identical results on all 3600 fuzz runs, and|sg|never rose above 1.8|sg0|. I kept the simpler fixed reference.Reproducer from the issue
fatol=gatol=0, f ×1e-4 / ×1 / ×1e4Other options on the reproducer with default options:
eta = 1e-2gives FTOL after 9 iterations at a gap of 4e-8.eta = 1e-6and option 3 both give GTOL after 12 iterations.Fuzz set
300 problems from #75 (bounded quadratics and Rosenbrock, n ≤ 12), each run with
scg× 4 step-back strategies × exact/BFGS/SR1, for 3600 runs per variant. "Optimum" meansf − fref ≤ 1e-6 + 1e-4 |fref|.reflect, exact/BFGS/SR1By problem type, this PR changes:
Total CG iterations over all 3600 runs go from 455k to 880k. Almost all of the increase comes from the 306 runs where main stops with a false convergence:
8 runs reach the optimum on main but not here, against 209 gained. Seed 164 (quadratic, BFGS, all 4 step-backs) gets caught in the bound crawl of #78, with steps of ~3e-5 next to two bounds. Seed 213 is a Rosenbrock problem in a ±1e10 box.
Larger n
Exact Hessian (except the last row),
scg/reflect, default options. Gap is relative to the best value over all variants. Each cell shows outer iterations / CG iterations (gap).Where this PR costs more. The ill-conditioned n=500 quadratic needs 6.7× the CG iterations. Main stops there with FTOL at a gap of 4e-7 to 5e-7, while this PR converges to below 1e-9. The last 4 steps take about 4000 CG iterations, capped at 2n+10. Option 1 costs as much or more here. Option 3 stays cheap but also stops early.
Cost of a CG iteration in fides. Measured with single-threaded BLAS, taking the best of 200 runs:
minimizeBuilding the dense scaled Hessian and the diagonal scaling matrices for every candidate step dominates the per-iteration cost. So the worst case above, about +9000 CG iterations at n=500, costs roughly as much as 9 outer iterations, or 0.14 s. Elsewhere the reduction in outer iterations outweighs the extra CG iterations. I'm not reporting whole-run CPU times, because they were too noisy on the loaded machine. For example, two runs with the same iteration count differed by 7×.
Behaviour change
scgusers will see different iteration counts and results. 2D and full are unchanged. The newsgnorm0arguments oftrust_region(),CGStepandTRStepSteihaugare optional.Tests
test_minimize.py::test_steihaug_ill_conditioned_no_premature_ftol[1e-4, 1, 1e4]uses the issue's reproducer at three scales of f. It fails on main (FTOL at f = 46.4256 for scale 1) and passes here.test_steps.py::test_steihaug_tolerance_invariant_to_objective_scalechecks that the CG step is identical for f scaled by 2^-40, 1 and 2^40. On main the step is the Newton step for 2^-40 and a steepest-descent step for 1 and 2^40.pytest testsreports 1206 passed and 3 skipped. The skips are the existing scg + refine combination.Not addressed here: DELTA_TOO_SMALL at the optimum, which 11 of 48 starts on the reproducer hit regardless of the forcing term; and the bound crawl in seed 164 (#78).
🤖 Generated with Claude Code