Skip to content

fix: compare log likelihoods, not figures of merit, in the resume sanity check - #1474

Merged
Jammy2211 merged 1 commit into
mainfrom
claude/multistart-gradient-resume-fom-vvlcm0
Aug 15, 2026
Merged

fix: compare log likelihoods, not figures of merit, in the resume sanity check#1474
Jammy2211 merged 1 commit into
mainfrom
claude/multistart-gradient-resume-fom-vvlcm0

Conversation

@Jammy2211

Copy link
Copy Markdown
Collaborator

MultiStartGradient (af.MultiStartAdam / af.MultiStartProdigy) raised SearchException when resuming a search killed mid-run, making the resume path unusable — the exact scenario long pixelized campaigns need it for.

autofit.exc.SearchException:
    Figure of merit sanity check failed.
    Old Figure of Merit = -2692.9547224874896
    New Figure of Merit = 5390.281252235021

Diagnosis

Fitness.check_log_likelihood compared the log likelihood stored in a previous run's samples summary against fitness(parameters), which returns the figure of merit in the search's own convention. Those agree only when fom_is_log_likelihood=True and convert_to_chi_squared=False.

For the multi-start searches (fom_is_log_likelihood=False, convert_to_chi_squared=True) the fresh value is -2 * log_posterior. The reported ratio decomposes exactly:

5390.281 = -2 x (-2692.955 + (-2.186))
             ^^                ^^^^^^
     convert_to_chi_squared    folded-in log prior

This was not multi-start specific. LBFGS, Emcee, Zeus, NUTS and Drawer all build Fitness with fom_is_log_likelihood=False and carried the same mismatch. Multi-start is simply where it surfaced, being the search whose resume path gets exercised.

Which side was wrong

The persisted side is correct and was left alone. MultiStartGradient.samples_via_internal_from explicitly stores -0.5 * best_fom - log_prior, a genuine log likelihood, and every downstream consumer (aggregator, database, results) reads Sample.log_likelihood as one.

So the comparison side is converted instead, through a new Fitness.log_likelihood_from helper applying the exact inverse of the figure-of-merit mapping. call_wrap already needed that identical inverse for its quick-update / history bookkeeping and now shares it rather than duplicating it.

The guard still fires on a genuinely changed likelihood function — which is what it exists for, and what the documented "multi-start resume chains do not survive library upgrades that touch FoM bookkeeping" behaviour depends on. That is asserted for all three conventions.

Incidental fix

The log-prior subtraction was an in-place -=. When convert_to_chi_squared is False, log_likelihood is figure_of_merit, so a call_wrap over a NumPy array mutated the value it was about to return, handing the search a log likelihood where it asked for a log posterior. Now out-of-place, with a test.

Testing

The defect is entirely inside Fitness, which is pure NumPy — no jax, optax or subprocess needed — so the regression test lives in the library unit suite (test_autofit/non_linear/test_fitness_check_log_likelihood.py, 10 tests, 0.16s) rather than in autofit_workspace_test as originally scoped.

Note test_autofit/config/general.yaml sets check_likelihood_function: false, which is why nothing caught this; the tests flip it on via fixture.

Coverage, parametrised over all three figure-of-merit conventions:

  • resuming an unchanged likelihood function does not raise
  • resuming a changed likelihood function still raises
  • log_likelihood_from inverts the convention
  • call_wrap returns the figure of merit, not the log likelihood

With the fix reverted, the log-posterior and chi-squared resume cases fail while the log-likelihood case passes either way.

End-to-end, reproducing the original report — MultiStartAdam, iterations_per_full_update=2, kill -9 mid-run leaving search_internal.dill and no .completed:

before after
resume Old = -56.3875 / New = 112.7750 (-2.0000x), exit 1 runs to completion, exit 0

Full suite: 1719 passed. Two failures (test_beta missing optional dep, test_nautilus pool) confirmed pre-existing on unmodified main.

Follow-up not covered here

Resume now works, and the resumed run was confirmed to continue from the checkpointed step. But the run was a clean Gaussian fit with zero NaN lanes, so the #1472 n_value_nan_lane_steps / n_grad_nan_lane_steps accumulation across a resume remains covered only by unit tests over hand-built search_internal dicts. Demonstrating it end-to-end needs a NaN-producing analysis — worth a follow-up now that this blocker is gone.


Generated by Claude Code

…ity check

`Fitness.check_log_likelihood` compared the log likelihood stored in a previous
run's samples summary against `fitness(parameters)`, which returns the figure of
merit in the search's *own* convention. Those agree only when
`fom_is_log_likelihood=True` and `convert_to_chi_squared=False`.

For `MultiStartAdam` / `MultiStartProdigy` (`fom_is_log_likelihood=False`,
`convert_to_chi_squared=True`) the fresh value is `-2 * log_posterior`, so
resuming a search killed mid-run raised `SearchException` at roughly `-2x` the
stored value on a completely unchanged likelihood function, making the resume
path unusable. `LBFGS`, `Emcee`, `Zeus`, `NUTS` and `Drawer` carry the same
mismatch.

The persisted side is correct: `Sample.log_likelihood` is a true log likelihood
(`MultiStartGradient.samples_via_internal_from` explicitly stores
`-0.5 * best_fom - log_prior`), and every downstream consumer reads it as one.
So the comparison side is converted instead, via a new `log_likelihood_from`
helper that applies the exact inverse of the figure-of-merit mapping. `call_wrap`
already needed that same inverse for its quick-update / history bookkeeping and
now shares it.

The guard still fires on a genuinely changed likelihood function, which is what
it exists for — that is asserted for all three figure-of-merit conventions.

Also makes the log-prior subtraction out-of-place. As an in-place `-=` it aliased
and mutated `figure_of_merit` whenever `convert_to_chi_squared` was `False`, so a
`call_wrap` over a NumPy array returned the log likelihood in place of the figure
of merit the search asked for.

Verified end-to-end by killing a `MultiStartAdam` mid-run and resuming: before,
`Old = -56.3875 / New = 112.7750` (exactly -2x) and exit 1; after, the resume
runs to completion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNbe6eLQbxUtY52EGqej5o
@Jammy2211
Jammy2211 merged commit a0af857 into main Aug 15, 2026
3 of 5 checks 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.

2 participants