Overview
Give the multi-start gradient searches (af.MultiStartProdigy / MultiStartAdam / MultiStartADABelief / MultiStartLion) a first-class auto-convergence (early-stopping) mode, default ON, so users no longer hand-tune n_steps. Today AbstractMultiStartGradient runs a fixed n_steps budget with no convergence criterion. n_steps becomes a hard ceiling. Phase 1 of 2 (phase 2 = results-contract + aggregator hardening, issued as this nears shipping). Successor to the multi-start gradient v2 work (Fit#1398/#1400).
Plan
- Add a
MultiStartGradientConvergence settings object mirroring the AutoCorrelationsSettings precedent (Emcee/Zeus early-stop): check_for_convergence=True default, plateau on the GLOBAL best figure-of-merit over a trailing window (rtol/atol + min_steps), with an is_test_mode() shrink hook.
- Wire the check into the search loop at the
iterations_per_full_update boundary; n_steps stays a hard ceiling (never runs forever). Carry a stop_reason local (surfacing into results is phase 2).
- Scope: PARAMETRIC SOURCES ONLY (MGE/Sersic). Pixelized is out of scope — when
resurrect=True the auto behavior stays conservative/off and leans on the ceiling.
- Verify + guard the JAX compile-cache path: recall/resume must reuse the disk-cached compiled value_and_grad (already default-on via
autonerves), not recompile. Prove the resumed HLO is stable so the persistent cache hits.
- Tests: numpy-only unit tests for the plateau logic in PyAutoFit; JAX early-stop + resume-no-recompile validation in autofit_workspace_test.
Detailed implementation plan
Affected Repositories
- PyAutoFit (primary)
- autofit_workspace_test (JAX validation)
Branch Survey
| Repository |
Current Branch |
Dirty? |
| ./PyAutoFit |
main |
clean |
| ./autofit_workspace_test |
main |
6 regenerated dataset artifacts (inconsequential) |
Suggested branch: feature/multistart-gradient-auto-convergence-phase-1
Implementation Steps
- New file
autofit/non_linear/search/mle/multi_start_gradient/convergence.py: MultiStartGradientConvergence(check_for_convergence=True, window, rtol, atol, min_steps) + check_if_converged(fom_history) (plateau: |best_fom[-1] - best_fom[-window]| <= atol + rtol*|best_fom[-window]| once len >= max(min_steps, window)); is_test_mode() shrinks window/min_steps.
search.py AbstractMultiStartGradient.__init__ (~line 28): add convergence: Optional[MultiStartGradientConvergence] = None (default -> auto ON); export the class via autofit/__init__.py.
search.py loop (~line 302-321, after fom_history extends + save_search_internal, before perform_update): break when check_if_converged passes AND NOT resurrect; carry stop_reason ('converged'|'max_steps') so during_analysis/final state reflect early stop.
- Add convergence defaults to packaged
autofit/config/.
- JAX cache guarantee (verify, do NOT build): confirm
batched_value_and_grad (search.py:198-208) is built once per fit; on resume (search.py:216-253) prove the rebuilt closure's HLO is byte-identical so the autonerves persistent cache (JAX_COMPILATION_CACHE_DIR) HITS — no recompile on recall.
- Tests: numpy-only unit tests (plateau boundaries,
min_steps, resurrect-off, ceiling honoured); autofit_workspace_test JAX script (MGE early-stop + resume asserts cache HIT / near-zero recompile).
Key Files
autofit/non_linear/search/mle/multi_start_gradient/search.py — loop + __init__ + samples_info
autofit/non_linear/search/mle/multi_start_gradient/convergence.py — NEW settings + check
autofit/non_linear/search/mcmc/auto_correlations.py — precedent to mirror
autonerves/jax_wrapper.py — existing persistent compile cache (reference, not edited)
autofit/__init__.py, autofit/config/ — export + defaults
Testing
python -m pytest test_autofit/non_linear/.../multi_start_gradient/ (numpy-only)
- autofit_workspace_test JAX validation (out of curated smoke subset)
Original Prompt
Click to expand starting prompt
Auto-convergence (early stopping) for the multi-start gradient searches — Phase 1: convergence loop + settings
Type: feature
Target: PyAutoFit
Repos:
- PyAutoFit
- autofit_workspace_test
Difficulty: large
Autonomy: supervised
Priority: normal
Status: formalised
Phase 1 of 2 (see multistart_gradient_auto_convergence_phase_2.md for the
results-contract + aggregator-hardening follow-up; issue phase 2 only as this one
nears shipping — no bulk-issuing). Successor to the multi-start gradient v2 work
(contrib rules + resurrection, Fit#1398/#1400).
Goal
Give af.MultiStartProdigy / MultiStartAdam / MultiStartADABelief / MultiStartLion
a first-class auto-convergence (early-stopping) mode (default on) so users no
longer hand-tune n_steps. Today AbstractMultiStartGradient runs a FIXED
n_steps budget with NO convergence criterion. n_steps stays a HARD
CEILING / max budget (never runs forever).
Scope
- In scope: PARAMETRIC SOURCES ONLY (MGE / Sersic — the smooth, well-behaved
regime where a global-best plateau genuinely means converged).
- Out of scope: pixelized. Its best-fom climbs in long plateaus punctuated by
breakthrough jumps (resurrection churn), so plateau detection false-stops there.
So when resurrect=True the auto behavior stays conservative / off and leans
on the ceiling. (The real pixelized goal is smoothing the LIKELIHOOD, not
band-aiding the optimizer — tracked separately.)
Design (mirror the AutoCorrelationsSettings precedent)
Emcee/Zeus terminate early via AutoCorrelationsSettings
(autofit/non_linear/search/mcmc/auto_correlations.py:
check_for_convergence=True, a companion check_if_converged, and an
is_test_mode() shrink hook). Mirror that shape:
- New file
autofit/non_linear/search/mle/multi_start_gradient/convergence.py
— MultiStartGradientConvergence(check_for_convergence=True, window, rtol, atol, min_steps) + a check_if_converged(fom_history) method. Plateau on the GLOBAL
best figure-of-merit over the trailing window:
|best_fom[-1] - best_fom[-window]| <= atol + rtol * |best_fom[-window]|,
only once len(fom_history) >= max(min_steps, window). is_test_mode()
shrinks window/min_steps so tests terminate fast.
search.py __init__ (AbstractMultiStartGradient, ~line 28): add a
convergence: Optional[MultiStartGradientConvergence] = None param; default
constructs the settings (auto ON). Store on self. Export
MultiStartGradientConvergence via autofit/__init__.py.
search.py loop (~line 302–321, at the iterations_per_full_update
boundary, right after fom_history extends + save_search_internal, before
perform_update): if convergence enabled AND NOT resurrect, break when
check_if_converged passes. n_steps remains the hard ceiling. Carry a
stop_reason local ('converged' | 'max_steps') so during_analysis and
the final state reflect the early stop. (stop_reason / converged surfacing
into samples_info + result artifacts is phase 2 — this phase only needs the
local so the loop terminates correctly.)
- Config: add convergence defaults to the packaged
autofit/config/ so
downstream workspaces inherit them.
JAX compile-cache guarantee (recall must NOT recompile)
Requirement: when a likelihood function is recalled (resume / restart), the
JIT/grad-compiled value_and_grad must be reused, not recompiled.
The stack ALREADY ships the durable lever: autonerves/jax_wrapper.py sets
JAX_COMPILATION_CACHE_DIR (→ ~/.cache/pyauto_jax) +
JAX_PERSISTENT_CACHE_MIN_COMPILE_TIME_SECS=1 for every user (compile-time arc,
PyAutoConf#128). XLA serializes each compiled executable (jit AND grad) to disk
keyed on its HLO hash, so a fresh process loads the binary instead of recompiling.
Do NOT build redundant caching machinery (prefer the lean existing lever).
Phase-1 work is to verify + guard, not to build:
- In
_fit (search.py:198–208) batched_value_and_grad is built once per fit
and reused across all loop iterations — confirm no per-iteration re-tracing.
- On resume (
search.py:216–253) the closure is rebuilt from a fresh
fitness.call; the on-disk cache only HITS if the HLO is byte-identical across
processes. Auto-convergence introduces variable-length/resume paths — prove the
resumed value_and_grad HLO is stable so the persistent cache hits (no recompile
on recall). If early-stop perturbs traced shapes and busts the cache, keep the
traced computation shape-stable (the actual work, if any).
- Add a JAX-validation script in autofit_workspace_test that runs an MGE
multi-start fit to early-stop, resumes in a fresh process, and asserts the
resume does NOT recompile (cache HIT / near-zero compile time).
Testing
- Unit tests (numpy-only, PyAutoFit): plateau boundaries — converges on a flat
history, does NOT on a climbing history, respects min_steps, disabled when
resurrect=True, n_steps ceiling still honoured. NEVER use JAX in library
unit tests.
- JAX validation (autofit_workspace_test): end-to-end MGE early-stop +
resume-no-recompile check (above). Cluster-scale scripts stay out of the curated
smoke subset.
Deliverable
Library PR (PyAutoFit) shipped first so the workspace can consume the API-change
summary, then the JAX-validation follow-up in autofit_workspace_test.
Overview
Give the multi-start gradient searches (
af.MultiStartProdigy / MultiStartAdam / MultiStartADABelief / MultiStartLion) a first-class auto-convergence (early-stopping) mode, default ON, so users no longer hand-tunen_steps. TodayAbstractMultiStartGradientruns a fixedn_stepsbudget with no convergence criterion.n_stepsbecomes a hard ceiling. Phase 1 of 2 (phase 2 = results-contract + aggregator hardening, issued as this nears shipping). Successor to the multi-start gradient v2 work (Fit#1398/#1400).Plan
MultiStartGradientConvergencesettings object mirroring theAutoCorrelationsSettingsprecedent (Emcee/Zeus early-stop):check_for_convergence=Truedefault, plateau on the GLOBAL best figure-of-merit over a trailing window (rtol/atol+min_steps), with anis_test_mode()shrink hook.iterations_per_full_updateboundary;n_stepsstays a hard ceiling (never runs forever). Carry astop_reasonlocal (surfacing into results is phase 2).resurrect=Truethe auto behavior stays conservative/off and leans on the ceiling.autonerves), not recompile. Prove the resumed HLO is stable so the persistent cache hits.Detailed implementation plan
Affected Repositories
Branch Survey
Suggested branch:
feature/multistart-gradient-auto-convergence-phase-1Implementation Steps
autofit/non_linear/search/mle/multi_start_gradient/convergence.py:MultiStartGradientConvergence(check_for_convergence=True, window, rtol, atol, min_steps)+check_if_converged(fom_history)(plateau:|best_fom[-1] - best_fom[-window]| <= atol + rtol*|best_fom[-window]|oncelen >= max(min_steps, window));is_test_mode()shrinkswindow/min_steps.search.pyAbstractMultiStartGradient.__init__(~line 28): addconvergence: Optional[MultiStartGradientConvergence] = None(default -> auto ON); export the class viaautofit/__init__.py.search.pyloop (~line 302-321, afterfom_historyextends +save_search_internal, beforeperform_update): break whencheck_if_convergedpasses AND NOTresurrect; carrystop_reason('converged'|'max_steps') soduring_analysis/final state reflect early stop.autofit/config/.batched_value_and_grad(search.py:198-208) is built once per fit; on resume (search.py:216-253) prove the rebuilt closure's HLO is byte-identical so theautonervespersistent cache (JAX_COMPILATION_CACHE_DIR) HITS — no recompile on recall.min_steps, resurrect-off, ceiling honoured); autofit_workspace_test JAX script (MGE early-stop + resume asserts cache HIT / near-zero recompile).Key Files
autofit/non_linear/search/mle/multi_start_gradient/search.py— loop +__init__+ samples_infoautofit/non_linear/search/mle/multi_start_gradient/convergence.py— NEW settings + checkautofit/non_linear/search/mcmc/auto_correlations.py— precedent to mirrorautonerves/jax_wrapper.py— existing persistent compile cache (reference, not edited)autofit/__init__.py,autofit/config/— export + defaultsTesting
python -m pytest test_autofit/non_linear/.../multi_start_gradient/(numpy-only)Original Prompt
Click to expand starting prompt
Auto-convergence (early stopping) for the multi-start gradient searches — Phase 1: convergence loop + settings
Type: feature
Target: PyAutoFit
Repos:
Difficulty: large
Autonomy: supervised
Priority: normal
Status: formalised
Phase 1 of 2 (see
multistart_gradient_auto_convergence_phase_2.mdfor theresults-contract + aggregator-hardening follow-up; issue phase 2 only as this one
nears shipping — no bulk-issuing). Successor to the multi-start gradient v2 work
(contrib rules + resurrection, Fit#1398/#1400).
Goal
Give
af.MultiStartProdigy / MultiStartAdam / MultiStartADABelief / MultiStartLiona first-class auto-convergence (early-stopping) mode (default on) so users no
longer hand-tune
n_steps. TodayAbstractMultiStartGradientruns a FIXEDn_stepsbudget with NO convergence criterion.n_stepsstays a HARDCEILING / max budget (never runs forever).
Scope
regime where a global-best plateau genuinely means converged).
breakthrough jumps (resurrection churn), so plateau detection false-stops there.
So when
resurrect=Truethe auto behavior stays conservative / off and leanson the ceiling. (The real pixelized goal is smoothing the LIKELIHOOD, not
band-aiding the optimizer — tracked separately.)
Design (mirror the
AutoCorrelationsSettingsprecedent)Emcee/Zeus terminate early via
AutoCorrelationsSettings(
autofit/non_linear/search/mcmc/auto_correlations.py:check_for_convergence=True, a companioncheck_if_converged, and anis_test_mode()shrink hook). Mirror that shape:autofit/non_linear/search/mle/multi_start_gradient/convergence.py—
MultiStartGradientConvergence(check_for_convergence=True, window, rtol, atol, min_steps)+ acheck_if_converged(fom_history)method. Plateau on the GLOBALbest figure-of-merit over the trailing window:
|best_fom[-1] - best_fom[-window]| <= atol + rtol * |best_fom[-window]|,only once
len(fom_history) >= max(min_steps, window).is_test_mode()shrinks
window/min_stepsso tests terminate fast.search.py__init__(AbstractMultiStartGradient, ~line 28): add aconvergence: Optional[MultiStartGradientConvergence] = Noneparam; defaultconstructs the settings (auto ON). Store on self. Export
MultiStartGradientConvergenceviaautofit/__init__.py.search.pyloop (~line 302–321, at theiterations_per_full_updateboundary, right after
fom_historyextends +save_search_internal, beforeperform_update): if convergence enabled AND NOTresurrect, break whencheck_if_convergedpasses.n_stepsremains the hard ceiling. Carry astop_reasonlocal ('converged'|'max_steps') soduring_analysisandthe final state reflect the early stop. (
stop_reason/convergedsurfacinginto
samples_info+ result artifacts is phase 2 — this phase only needs thelocal so the loop terminates correctly.)
autofit/config/sodownstream workspaces inherit them.
JAX compile-cache guarantee (recall must NOT recompile)
Requirement: when a likelihood function is recalled (resume / restart), the
JIT/grad-compiled value_and_grad must be reused, not recompiled.
The stack ALREADY ships the durable lever:
autonerves/jax_wrapper.pysetsJAX_COMPILATION_CACHE_DIR(→~/.cache/pyauto_jax) +JAX_PERSISTENT_CACHE_MIN_COMPILE_TIME_SECS=1for every user (compile-time arc,PyAutoConf#128). XLA serializes each compiled executable (jit AND grad) to disk
keyed on its HLO hash, so a fresh process loads the binary instead of recompiling.
Do NOT build redundant caching machinery (prefer the lean existing lever).
Phase-1 work is to verify + guard, not to build:
_fit(search.py:198–208)batched_value_and_gradis built once per fitand reused across all loop iterations — confirm no per-iteration re-tracing.
search.py:216–253) the closure is rebuilt from a freshfitness.call; the on-disk cache only HITS if the HLO is byte-identical acrossprocesses. Auto-convergence introduces variable-length/resume paths — prove the
resumed value_and_grad HLO is stable so the persistent cache hits (no recompile
on recall). If early-stop perturbs traced shapes and busts the cache, keep the
traced computation shape-stable (the actual work, if any).
multi-start fit to early-stop, resumes in a fresh process, and asserts the
resume does NOT recompile (cache HIT / near-zero compile time).
Testing
history, does NOT on a climbing history, respects
min_steps, disabled whenresurrect=True,n_stepsceiling still honoured. NEVER use JAX in libraryunit tests.
resume-no-recompile check (above). Cluster-scale scripts stay out of the curated
smoke subset.
Deliverable
Library PR (PyAutoFit) shipped first so the workspace can consume the API-change
summary, then the JAX-validation follow-up in autofit_workspace_test.