Skip to content

Commit 08708a5

Browse files
authored
Merge pull request #1392 from PyAutoLabs/feature/fitness-nan-guard-contract
docs: pin Fitness's NaN guard contract — value-only, never gradient
2 parents 5865957 + e7de318 commit 08708a5

1 file changed

Lines changed: 24 additions & 1 deletion

File tree

autofit/non_linear/fitness.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,27 @@ def call(self, parameters):
207207
A private method that calls the fitness function with the given parameters and additional keyword arguments.
208208
This method is intended for internal use only.
209209
210+
The NaN/inf guard below protects the **value only, never the gradient**.
211+
212+
A model whose likelihood is NaN or inf is mapped to `resample_figure_of_merit`, so searches that read only
213+
the figure of merit (nested samplers, MCMC) get the resample sentinel and never select the point. Gradient
214+
consumers get no such protection: under `jax.grad`, reverse-mode differentiates *both* branches of an
215+
`xp.where` and multiplies the unselected one by zero, so if the likelihood's derivative is also non-finite
216+
the guard yields `0 * NaN = NaN` and the returned gradient is NaN even though the value looks handled.
217+
218+
This bites only when the masked branch's *derivative* is non-finite — not merely its value. `sqrt(x)` at
219+
x < 0 and `cholesky(A)` for non-positive-definite `A` are NaN in both value and derivative, so they trigger
220+
it; `log(x)` at x < 0 is NaN in value but its derivative `1/x` stays finite, so it does not.
221+
222+
A guard here **cannot** repair this. By the time this method receives `log_likelihood` the non-finite
223+
derivative is already recorded on the autodiff tape, and no transformation of the output can remove it —
224+
an output-side "double-where" does not work. Gradient-safety must be established at the site that creates
225+
the NaN, by never *evaluating* the offending operation at the invalid input.
226+
227+
See autolens_workspace_developer#104, where this was diagnosed, and
228+
`autofit_workspace_test/scripts/jax_assertions/fitness_nan_gradient_contract.py`, which pins the behaviour
229+
described here.
230+
210231
Parameters
211232
----------
212233
parameters
@@ -235,7 +256,9 @@ def call(self, parameters):
235256
except exc.FitException:
236257
return self.resample_figure_of_merit
237258

238-
# Penalize NaNs in the log-likelihood
259+
# Penalize NaNs in the log-likelihood. Value-only: under jax.grad these `where`s still differentiate the
260+
# masked branch, so a non-finite derivative propagates as `0 * NaN = NaN`. See the contract in the
261+
# docstring above -- gradient-safety belongs at the site that creates the NaN, not here.
239262
log_likelihood = self._xp.where(self._xp.isnan(log_likelihood), self.resample_figure_of_merit, log_likelihood)
240263
log_likelihood = self._xp.where(self._xp.isinf(log_likelihood), self.resample_figure_of_merit, log_likelihood)
241264

0 commit comments

Comments
 (0)