# MultiStartGradient runs silently for their whole duration — no per-step progress
Type: feature
Target: autofit
Repos:
- PyAutoFit
Difficulty: small
Autonomy: supervised
Priority: medium
Status: formalised
## Original request (verbatim)
> might not be feasible but if this could have a verbose update would be nice:
> 2026-07-30 19:45:31,924 - start_here - INFO - Starting new prodigy MultiStartGradient search (48 starts, no previous samples found)
> e.g. can Prodigy display its updates a bit more so a user knows it progressing
## Problem
`AbstractMultiStartGradient._fit` emits exactly two log lines for an entire run:
`"Starting new <rule> MultiStartGradient search (N starts, ...)"` and, however many
hours later, `"<rule> MultiStartGradient sampling complete."` Nothing in between.
A user cannot tell a live 300-step fit from a hung one.
Both of the standard autofit progress channels are inert for this search:
- `iterations_per_full_update` defaults to `1e99` (`autofit/config/general.yaml:3`),
so `_steps_until_full_update` returns the whole budget — one chunk, one
`perform_update`, which `_is_final_boundary` then correctly suppresses as
duplicated work. Zero intermediate output by construction.
- `Fitness.call`'s quick-update counter never fires: `fitness.call` is traced inside
`jax.jit(jax.vmap(...))` (`search.py:260-261`), so the Python-side counting runs
once at trace time, never per step.
Other searches sidestep this by delegating to their sampler's own progress bar
(emcee `progress=True`, dynesty `print_progress=not self.silence`). The gradient
searches own their step loop, so they have no such fallback.
## Why it is cheap
The step loop (`search.py:358-400`) is plain Python and **already forces a device
sync every step** at line 361 (`np.isfinite(np.asarray(foms))`). Everything a
progress line wants is already materialised on the host each step — `total_steps`
vs `self.n_steps`, `best_fom` (best log posterior is `-0.5 * best_fom`), this
step's `foms_np[best_index]`, `alive.sum()` live starts, `n_resurrections`. No
extra sync, no recompile, no new device work.
## Ask
Add a cadence-controlled progress log line to `AbstractMultiStartGradient`'s step
loop, gated on the existing `silence` flag.
Prodigy-specific extra: `optax.contrib.prodigy`'s state carries `estim_lr` — its
`d`, the self-estimated step scale — confirmed against the installed optax
(`ProdigyState(exp_avg, exp_avg_sq, grad_sum, params0, estim_lr,
numerator_weighted, count)`). It is reachable through the `apply_if_finite`
wrapper via `optax.tree_utils.tree_get(opt_state, "estim_lr")`, the same helper
the resume path already uses at `search.py:298`. State is vmapped per-start, so
this gives min/median/max across the 48 starts — the one genuinely
Prodigy-specific diagnostic that is currently invisible (has `d` finished ramping,
or is it still climbing?). Adam/ADABelief/Lion have no such field; `tree_get`
returns `None` and they get the generic line.
## Constraints / decisions to make
- **Log line, not a tqdm bar.** tqdm is installed only transitively (not declared
in `pyproject.toml`), a bar's ETA would mislead because auto-convergence makes
`n_steps` a ceiling rather than a target, and progress lines survive SLURM/HPC
log capture where bars do not.
- The knob lands on `AbstractMultiStartGradient`, so it applies to all four
subclasses (Adam / ADABelief / Lion / Prodigy), not Prodigy alone. That is the
right scope — the silence is a base-class problem — but it is a shared-search
change, not a Prodigy-only tweak.
- Must not perturb numerics, add a device sync, or trigger an XLA recompile.
- Cadence default should be sane for both a 300-step CPU run and a long GPU run.
Overview
af.MultiStartProdigy/MultiStartAdam/MultiStartADABelief/MultiStartLionemit exactly two log lines for an entire run —"Starting new <rule> MultiStartGradient search (N starts, ...)"and, however many hours later,"<rule> MultiStartGradient sampling complete."Nothing in between, so a user cannot tell a live 300-step fit from a hung one.Both standard autofit progress channels are inert here:
iterations_per_full_updatedefaults to1e99, so the whole run is one chunk whose singleperform_updateis (correctly) suppressed by_is_final_boundary; andFitness.call's quick-update counter never fires becausefitness.callis traced insidejax.jit(jax.vmap(...)), so its Python-side counting runs once at trace time. Other searches sidestep this by delegating to their sampler's own progress bar (emceeprogress=True, dynestyprint_progress); the gradient searches own their step loop and have no such fallback.The auto-convergence arc (#1406 / #1409) already ships
fom_historyinsamples_infoplus thefigure_of_merit_vs_iterationplotter — but that is the post-hoc view. You can see the descent after the run, never during it. This issue closes that gap.Plan
AbstractMultiStartGradient's step loop, gated on the existingsilenceflag.d(min/median/max across starts) — invisible today, and the one thing that says whether it is still ramping.KeyErroron it.Detailed implementation plan
Affected Repositories
Branch Survey
No worktree claim on PyAutoFit — the three tasks in
active.mdclaim PyAutoLens, autolens_workspace_test and PyAutoMind.Suggested branch:
feature/multi-start-gradient-progress-loggingWorktree root:
~/Code/PyAutoLabs-wt/multi-start-gradient-progress-logging/Work Classification: Library
Implementation Steps
__init__(autofit/non_linear/search/mle/multi_start_gradient/search.py) — newiterations_per_log: int = 10, stored asself.iterations_per_log. Name follows the houseiterations_per_*_updateconvention. Carried throughto_dict/from_dictautomatically, likeresurrect/batch_size.New pure seam
_should_log_progress(self, total_steps, converged) -> bool— true ontotal_steps == 1, ontotal_steps % self.iterations_per_log == 0, or onconverged; false whenself.silence.New pure seam
_progress_message(self, total_steps, best_fom, previous_fom, n_alive, estim_lr=None) -> str— formats e.g.log_post = -0.5 * best_fom, the same convention assamples_via_internal_from(search.py:628). Non-finitebest_fom(no basin found yet) renders asbest log_post —.estim_lr=Nonedrops thedfield entirely.Wire into the step loop — after
fom_history.append(best_fom), before the resurrect block: call the two seams andself.logger.info(...).estim_lrcomes fromoptax.tree_utils.tree_get(opt_state, "estim_lr"), pulled only on a logging step, converted vianp.asarray.Verified against the installed optax that
tree_getreachesestim_lrthrough theapply_if_finitewrapper: returns a per-start(n_starts,)array for prodigy (initialised at1e-6, ramps up), andNonefor adam — so Adam / ADABelief / Lion get the generic line with no special-casing.Cost
The loop already forces a device sync every step at
search.py:361(np.isfinite(np.asarray(foms))), so the fom / alive / step fields are free. Theestim_lrpull is an extra(n_starts,)device→host transfer on 1-in-iterations_per_logsteps, which forces the optimizer-state sync marginally earlier than the next iteration would anyway. Not literally zero, but negligible and constant — and it adds no traced operation, so no XLA recompile.Tests (
test_autofit/non_linear/search/mle/test_multi_start_gradient.py, numpy-only — no JAX)_should_log_progress: first step, cadence hits/misses,converged=Trueoff-cadence,silence=Truesuppresses everything._progress_message: with and withoutestim_lr; the-0.5 *sign convention; non-finitebest_fom.iterations_per_logsurvivesto_dict/from_dict.test__fit_uses_both_seams_and_keeps_the_converged_loop_guard, pinning both call sites so the seams cannot be silently orphaned.Beyond the unit suite, the numpy-only tests cannot prove the line fires in a live JAX run — verify by running the
searches/mle.pysmoke script inautofit_workspacewith a Prodigy search and pasting the real output.Key Files
autofit/non_linear/search/mle/multi_start_gradient/search.py—AbstractMultiStartGradient.__init__+_fitstep loop; the two new seams.test_autofit/non_linear/search/mle/test_multi_start_gradient.py— the numpy-only seam tests + wiring guard.Scope notes
AbstractMultiStartGradient, so all four subclasses get the line, not Prodigy alone. That is the right fix — the silence is a base-class problem — but it is a shared-search change, not a Prodigy-only tweak.autofit_workspace_testPR. Both auto-convergence phases shipped ajax_assertions/validation script, so there is precedent for a second leg. Deliberately declined here: a permanent JAX regression net for a log line's wording is a maintenance cost out of proportion to the risk, and the source-level wiring guard already catches the orphaning failure mode.Sizing
Brain sized this
large (score 8); overridden tosmall, no phase split. Every point came from prompt prose — 521 words (+3), the words dynesty / emcee / gradient / jax / sampler (+3), jax / vmap (+1), memory-context-required (+1) — whilerepos_affected=1andarchitectural_risk=[]contributed nothing. The heuristic penalised a well-researched prompt rather than measuring scope.Original Prompt
Click to expand starting prompt