fix: compare log likelihoods, not figures of merit, in the resume sanity check - #1474
Merged
Merged
Conversation
…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
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.
MultiStartGradient(af.MultiStartAdam/af.MultiStartProdigy) raisedSearchExceptionwhen resuming a search killed mid-run, making the resume path unusable — the exact scenario long pixelized campaigns need it for.Diagnosis
Fitness.check_log_likelihoodcompared the log likelihood stored in a previous run's samples summary againstfitness(parameters), which returns the figure of merit in the search's own convention. Those agree only whenfom_is_log_likelihood=Trueandconvert_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:This was not multi-start specific.
LBFGS,Emcee,Zeus,NUTSandDrawerall buildFitnesswithfom_is_log_likelihood=Falseand 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_fromexplicitly stores-0.5 * best_fom - log_prior, a genuine log likelihood, and every downstream consumer (aggregator, database, results) readsSample.log_likelihoodas one.So the comparison side is converted instead, through a new
Fitness.log_likelihood_fromhelper applying the exact inverse of the figure-of-merit mapping.call_wrapalready 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
-=. Whenconvert_to_chi_squaredisFalse,log_likelihoodisfigure_of_merit, so acall_wrapover 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 inautofit_workspace_testas originally scoped.Note
test_autofit/config/general.yamlsetscheck_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:
log_likelihood_frominverts the conventioncall_wrapreturns the figure of merit, not the log likelihoodWith 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 -9mid-run leavingsearch_internal.dilland no.completed:Old = -56.3875 / New = 112.7750(-2.0000x), exit 1Full suite: 1719 passed. Two failures (
test_betamissing optional dep,test_nautiluspool) confirmed pre-existing on unmodifiedmain.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_stepsaccumulation across a resume remains covered only by unit tests over hand-builtsearch_internaldicts. Demonstrating it end-to-end needs a NaN-producing analysis — worth a follow-up now that this blocker is gone.Generated by Claude Code