|
| 1 | +# MultiStartGradient cannot resume a killed mid-run search — FoM sanity check compares log-likelihood against chi-squared |
| 2 | + |
| 3 | +Type: bug |
| 4 | +Target: PyAutoFit |
| 5 | +Repos: |
| 6 | +- PyAutoFit |
| 7 | +Difficulty: small |
| 8 | +Autonomy: supervised |
| 9 | +Priority: normal |
| 10 | +Status: formalised |
| 11 | + |
| 12 | +`MultiStartGradient` (`af.MultiStartAdam` / `af.MultiStartProdigy`) raises |
| 13 | +`SearchException` when resuming a search that was killed mid-run, so the resume |
| 14 | +path is unusable. Long pixelized campaigns are exactly the runs that get |
| 15 | +interrupted, and this is the mechanism meant to recover them. |
| 16 | + |
| 17 | +## Reproduce |
| 18 | + |
| 19 | +Confirmed on `main` (PyAutoFit `39a187b2d`), with the **stock** `af.ex.Analysis` |
| 20 | +— no custom analysis needed. |
| 21 | + |
| 22 | +1. Run a named `af.MultiStartAdam` with `iterations_per_full_update=2` so |
| 23 | + checkpoints are written during the run (the default single-chunk cadence only |
| 24 | + checkpoints at the end, so a mid-run kill leaves nothing to resume from). |
| 25 | +2. `kill -9` it around step 10 of 400. This leaves |
| 26 | + `files/search_internal/search_internal.dill` and **no** `.completed` marker — |
| 27 | + the only state a real resume starts from, since a search that finishes |
| 28 | + deletes its checkpoint. |
| 29 | +3. Re-run the identical script. It fails: |
| 30 | + |
| 31 | +``` |
| 32 | +autofit.exc.SearchException: |
| 33 | + Figure of merit sanity check failed. |
| 34 | + Old Figure of Merit = -2692.9547224874896 |
| 35 | + New Figure of Merit = 5390.281252235021 |
| 36 | +``` |
| 37 | + |
| 38 | +Reproduced twice, with matching structure: |
| 39 | + |
| 40 | +| analysis | old FoM | new FoM | ratio | |
| 41 | +|---|---|---|---| |
| 42 | +| stock `af.ex.Analysis` | -2692.9547224874896 | 5390.281252235021 | -2.0000x | |
| 43 | +| a custom analysis | -2915.2793638973044 | 5834.81444744653 | -2.0006x | |
| 44 | + |
| 45 | +## Diagnosis (starting point, not a conclusion) |
| 46 | + |
| 47 | +The consistent **-2x** relationship points at a units mismatch rather than a |
| 48 | +genuine likelihood change. `MultiStartGradient` builds its `Fitness` with |
| 49 | +`fom_is_log_likelihood=False` and `convert_to_chi_squared=True`, i.e. its |
| 50 | +figure-of-merit is `-2 * log_posterior` (a chi-squared). The stored "old" value |
| 51 | +looks like a **log-likelihood** while the freshly computed "new" value looks |
| 52 | +like the **chi-squared**, so `Fitness.check_log_likelihood` |
| 53 | +(`autofit/non_linear/fitness.py:614`, called from `__init__` at line 171) |
| 54 | +appears to be comparing a stored log-likelihood against a value in the |
| 55 | +multi-start FoM convention. |
| 56 | + |
| 57 | +Verify that before fixing — the fix is either at the point the old value is |
| 58 | +persisted or at the point the comparison converts, and picking the wrong one |
| 59 | +would paper over a real check. Note `check_log_likelihood` exists to catch a |
| 60 | +genuinely changed likelihood function between runs (the documented |
| 61 | +"multi-start resume chains do not survive library upgrades that touch FoM |
| 62 | +bookkeeping" behaviour in `autolens_profiling/scripts/misc/searches/README.md`). |
| 63 | +The fix must keep that guard working for the case it was built for; the bug is |
| 64 | +that it fires on an **unchanged** likelihood in the same process generation. |
| 65 | + |
| 66 | +## Scope |
| 67 | + |
| 68 | +- Confirm which side carries the wrong convention, with a test that resumes a |
| 69 | + killed run and asserts it continues rather than raising. |
| 70 | +- The library unit suite is NumPy-only and `_fit` needs jax + optax, so the |
| 71 | + regression test likely belongs in `autofit_workspace_test` alongside |
| 72 | + `scripts/searches/MultiStartResurrect.py` (`ENV: real_search jax`), with the |
| 73 | + kill/resume driven as a subprocess. |
| 74 | + |
| 75 | +## Provenance |
| 76 | + |
| 77 | +Found while verifying the resume path of the value-NaN / gradient-NaN step |
| 78 | +counters added in PyAutoFit#1472. Those counters restore via |
| 79 | +`search_internal.get(..., 0)` and are designed to keep accumulating across a |
| 80 | +resume; that behaviour **cannot be demonstrated end-to-end until this is |
| 81 | +fixed**, so #1472 ships with the resume accumulation covered only by unit tests |
| 82 | +over hand-built `search_internal` dicts. Re-check it here once resume works. |
| 83 | + |
| 84 | +This bug is independent of #1472 — reproduced on `main` without those changes. |
0 commit comments