Overview
AbstractSearch.__init__ unconditionally coerces iterations_per_full_update to
float (abstract_search.py:219), so the MultiStart gradient step loop builds
its per-chunk step count as a float and range(float) raises
TypeError: 'float' object cannot be interpreted as an integer. The crash fires
only when the checkpoint cadence is below the remaining step budget — the
packaged config default is 1e99, so min(1e99, steps_remaining) always returns
the int operand and every default run took the safe branch. Any user who passes a
real cadence (e.g. iterations_per_full_update=50 with n_steps=3000) crashes on
step-loop entry. Six RAL chain jobs (331182–331190) died on it back-to-back during
the wsdev#117 Pix-Prodigy CPU campaign.
Reproduced on main (2026-07-27):
>>> s = af.MultiStartAdam(n_steps=3000, iterations_per_full_update=50)
>>> s.iterations_per_full_update
50.0
>>> range(min(s.iterations_per_full_update or s.n_steps, 3000))
TypeError: 'float' object cannot be interpreted as an integer
Plan
- Cast the per-chunk step count to
int at the consumer in the MultiStart
gradient _fit, rather than changing the shared float coercion in
abstract_search.py — that coercion exists to accept the inf-like 1e99
config default and is shared by every other search, so changing it there is the
regression-prone option.
- Put the computation behind a tiny named helper on the abstract MultiStart class
so the defect is unit-testable without JAX — _fit itself requires jax,
optax and a JAX-traceable Analysis, and the library unit suite is NumPy-only
by standing rule.
- Add NumPy-only unit tests: a real cadence below the budget returns an
int
usable by range(); the 1e99 default still yields the remaining-steps branch;
the iterations_per_full_update=None fallback to n_steps is preserved.
- Run the full PyAutoFit suite (Python 3.12/3.13 on CI) — no public API change,
so there is no downstream workspace impact.
- Two follow-ups are recorded below rather than bundled into this PR (Mind rule:
one prompt = one task = one PR).
Detailed implementation plan
Affected Repositories
- PyAutoFit (primary, and only)
Branch Survey
| Repository |
Current Branch |
Dirty? |
| ./PyAutoFit |
main |
clean |
Worktree claim on PyAutoFit by task testmode-env-drift was verified stale
(PyAutoFit#1417 merged, PyAutoCTI#96 merged, PyAutoCTI#95 closed) and released
into complete/2026/07/testmode-env-drift.md (PyAutoMind 3a99904) before this
task started. No live conflict.
Suggested branch: feature/multistart-cadence-int-cast
Implementation Steps
-
In autofit/non_linear/search/mle/multi_start_gradient/search.py, add a
method to AbstractMultiStartGradient:
def _steps_in_chunk(self, steps_remaining: int) -> int:
"""
Number of gradient steps to run before the next ``perform_update``
checkpoint boundary.
``iterations_per_full_update`` is stored as a **float** by
``AbstractSearch.__init__`` so the inf-like ``1e99`` config default is
representable; ``range`` needs an ``int``, so the chunk size is cast
here at the consumer. Without the cast the loop only survives because
``min(1e99, steps_remaining)`` returns the int operand — a real cadence
below the remaining budget raises ``TypeError``.
"""
return int(min(self.iterations_per_full_update or self.n_steps, steps_remaining))
-
Replace the inline computation in _fit (currently search.py:284-286) with
iterations = self._steps_in_chunk(steps_remaining).
-
Add unit tests to
test_autofit/non_linear/search/mle/test_multi_start_gradient.py (NumPy-only,
consistent with that file's stated JAX-free contract):
iterations_per_full_update=50, n_steps=3000 → _steps_in_chunk(3000) is
50 and isinstance(..., int); range() over it yields 50 items (the
assertion that actually pins the reported crash).
- the same search near the end of its budget → chunk clamps to
steps_remaining.
- default (config
1e99) → returns steps_remaining as an int.
iterations_per_full_update=None → falls back to n_steps.
-
Run python -m pytest test_autofit/ in the task worktree.
Key Files
autofit/non_linear/search/mle/multi_start_gradient/search.py — the crash site
(_fit step loop) and the new _steps_in_chunk helper.
autofit/non_linear/search/abstract_search.py:219 — the shared float()
coercion; deliberately unchanged.
test_autofit/non_linear/search/mle/test_multi_start_gradient.py — the
NumPy-only test surface.
Explicitly out of scope (follow-ups, not this PR)
-
Sibling searches share the same latent float-cadence class. A sweep of
every consumer of self.iterations_per_full_update found five more sites that
feed the float straight into an iteration budget, each masked today by the
same 1e99 default:
autofit/non_linear/search/mcmc/emcee/search.py:203-206 → float iterations
into EnsembleSampler.sample(iterations=...)
autofit/non_linear/search/mcmc/zeus/search.py:239-242 → same shape
autofit/non_linear/search/mcmc/blackjax/nuts/search.py:291 →
chunk_n = min(float, int) into the chunked NUTS runner
autofit/non_linear/search/mle/bfgs/search.py:171 → float options["maxiter"]
into scipy.optimize.minimize
autofit/non_linear/search/nest/nautilus/search.py:477 and
nest/dynesty/search/abstract.py:365 → float iteration budgets returned
None of these is reproduced yet — each needs its own repro before a fix, and
bundling six unverified changes into a hotfix PR is the wrong trade. Filed as
a separate Mind prompt.
-
The workspace hotfix must be removed once this merges. The post-construction
int overwrite in
autolens_workspace_developer/searches_minimal/pix_prodigy.py (branch
feature/pix-prodigy-cpu, task pix-prodigy-cpu, wsdev#117) exists only to
work around this bug. That repo is claimed by a live task, so the removal
belongs to it — noted on wsdev#117 rather than done here.
Original Prompt
Click to expand starting prompt
# MultiStart gradient step loop crashes when iterations_per_full_update < remaining steps
Type: bug
Target: autofit
Repos:
- PyAutoFit
Difficulty: small
Autonomy: safe
Priority: high
Status: draft
## The bug (hit live on RAL, 2026-07-27, wsdev#117 campaign)
`abstract_search.py:219` unconditionally coerces `iterations_per_full_update`
to **float** (`float(iterations_per_full_update or conf...)`). The multi-start
gradient `_fit` (`autofit/non_linear/search/mle/multi_start_gradient/search.py:284`)
then computes
iterations = min(self.iterations_per_full_update or self.n_steps, steps_remaining)
...
for _ in range(iterations): # line 298
`range(float)` → `TypeError: 'float' object cannot be interpreted as an
integer` — but **only when the cadence is below the remaining budget**, because
`min(float, int)` returns the int operand otherwise. The config default is huge,
so every existing run took the `steps_remaining` branch and the crash never
fired; any user who passes a real checkpoint cadence (e.g.
`iterations_per_full_update=50` with `n_steps=3000`) crashes on step-loop entry.
Six RAL chain jobs (331182-331190) died on it back-to-back.
Evidence: `/mnt/ral/jnightin/pixgrad_logs/pix_prod_*-33118[5-9].err`; workspace
hotfix (post-construction int overwrite) in
`autolens_workspace_developer/searches_minimal/pix_prodigy.py` (branch
`feature/pix-prodigy-cpu`) — remove it when this ships.
## Fix
`iterations = int(min(...))` in `_fit` (the float coercion in abstract_search
serves inf-like config values and is shared by other searches — casting at the
consumer is the minimal, non-regressing change). Add a unit test that runs a
MultiStart search with `iterations_per_full_update` smaller than `n_steps`
(numpy objective — library unit tests stay JAX-free) and asserts the step loop
executes + checkpoints.
Autonomy: launched with --auto. Prompt header is Autonomy: safe, but
AUTONOMY.md caps work-type bug at supervised, so the effective level is
min(safe, supervised) = supervised. This run proceeds through
implementation and tests, then parks at ship sign-off with a question on this
issue rather than opening the PR unattended.
Overview
AbstractSearch.__init__unconditionally coercesiterations_per_full_updatetofloat (
abstract_search.py:219), so the MultiStart gradient step loop buildsits per-chunk step count as a float and
range(float)raisesTypeError: 'float' object cannot be interpreted as an integer. The crash firesonly when the checkpoint cadence is below the remaining step budget — the
packaged config default is
1e99, somin(1e99, steps_remaining)always returnsthe int operand and every default run took the safe branch. Any user who passes a
real cadence (e.g.
iterations_per_full_update=50withn_steps=3000) crashes onstep-loop entry. Six RAL chain jobs (331182–331190) died on it back-to-back during
the wsdev#117 Pix-Prodigy CPU campaign.
Reproduced on
main(2026-07-27):Plan
intat the consumer in the MultiStartgradient
_fit, rather than changing the shared float coercion inabstract_search.py— that coercion exists to accept the inf-like1e99config default and is shared by every other search, so changing it there is the
regression-prone option.
so the defect is unit-testable without JAX —
_fititself requiresjax,optaxand a JAX-traceableAnalysis, and the library unit suite is NumPy-onlyby standing rule.
intusable by
range(); the1e99default still yields the remaining-steps branch;the
iterations_per_full_update=Nonefallback ton_stepsis preserved.so there is no downstream workspace impact.
one prompt = one task = one PR).
Detailed implementation plan
Affected Repositories
Branch Survey
Worktree claim on PyAutoFit by task
testmode-env-driftwas verified stale(PyAutoFit#1417 merged, PyAutoCTI#96 merged, PyAutoCTI#95 closed) and released
into
complete/2026/07/testmode-env-drift.md(PyAutoMind3a99904) before thistask started. No live conflict.
Suggested branch:
feature/multistart-cadence-int-castImplementation Steps
In
autofit/non_linear/search/mle/multi_start_gradient/search.py, add amethod to
AbstractMultiStartGradient:Replace the inline computation in
_fit(currentlysearch.py:284-286) withiterations = self._steps_in_chunk(steps_remaining).Add unit tests to
test_autofit/non_linear/search/mle/test_multi_start_gradient.py(NumPy-only,consistent with that file's stated JAX-free contract):
iterations_per_full_update=50,n_steps=3000→_steps_in_chunk(3000)is50andisinstance(..., int);range()over it yields 50 items (theassertion that actually pins the reported crash).
steps_remaining.1e99) → returnssteps_remainingas anint.iterations_per_full_update=None→ falls back ton_steps.Run
python -m pytest test_autofit/in the task worktree.Key Files
autofit/non_linear/search/mle/multi_start_gradient/search.py— the crash site(
_fitstep loop) and the new_steps_in_chunkhelper.autofit/non_linear/search/abstract_search.py:219— the sharedfloat()coercion; deliberately unchanged.
test_autofit/non_linear/search/mle/test_multi_start_gradient.py— theNumPy-only test surface.
Explicitly out of scope (follow-ups, not this PR)
Sibling searches share the same latent float-cadence class. A sweep of
every consumer of
self.iterations_per_full_updatefound five more sites thatfeed the float straight into an iteration budget, each masked today by the
same
1e99default:autofit/non_linear/search/mcmc/emcee/search.py:203-206→ floatiterationsinto
EnsembleSampler.sample(iterations=...)autofit/non_linear/search/mcmc/zeus/search.py:239-242→ same shapeautofit/non_linear/search/mcmc/blackjax/nuts/search.py:291→chunk_n = min(float, int)into the chunked NUTS runnerautofit/non_linear/search/mle/bfgs/search.py:171→ floatoptions["maxiter"]into
scipy.optimize.minimizeautofit/non_linear/search/nest/nautilus/search.py:477andnest/dynesty/search/abstract.py:365→ float iteration budgets returnedNone of these is reproduced yet — each needs its own repro before a fix, and
bundling six unverified changes into a hotfix PR is the wrong trade. Filed as
a separate Mind prompt.
The workspace hotfix must be removed once this merges. The post-construction
int overwrite in
autolens_workspace_developer/searches_minimal/pix_prodigy.py(branchfeature/pix-prodigy-cpu, taskpix-prodigy-cpu, wsdev#117) exists only towork around this bug. That repo is claimed by a live task, so the removal
belongs to it — noted on wsdev#117 rather than done here.
Original Prompt
Click to expand starting prompt
Autonomy: launched with
--auto. Prompt header isAutonomy: safe, butAUTONOMY.mdcaps work-typebugatsupervised, so the effective level ismin(safe, supervised)=supervised. This run proceeds throughimplementation and tests, then parks at ship sign-off with a question on this
issue rather than opening the PR unattended.