Skip to content

Commit 2f9930b

Browse files
Jammy2211claude
authored andcommitted
prompt: ship multistart-nan-step-diagnostics (#1472) → complete
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 26ea101 commit 2f9930b

4 files changed

Lines changed: 122 additions & 50 deletions

File tree

active.md

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -61,19 +61,3 @@
6161
- artifacts-are-laptop-only: Actions artifact downloads are blocked from cloud/mobile sessions (egress policy 403s `productionresultssa2.blob.core.windows.net` on CONNECT) — this is what stopped the cloud session finishing the ingest. Both wiki drift reports were captured to `~/.pyauto-heart/release_20260807_wiki_drift/` while on the laptop.
6262
- do-not: do NOT use the nightly driver for a manual release — AUTONOMY.md forbids converting a manual release into the scheduled-nightly exception.
6363
- repos-none-claimed: this entry claims NO repos — deliberately on one line, NOT as 2-space ` - Repo` bullets, because `worktree_check_conflict` treats any such bullet as a live claim.
64-
65-
## multistart-nan-step-diagnostics
66-
- issue: https://github.com/PyAutoLabs/PyAutoFit/issues/1472
67-
- prompt: active/multistart_gradient_nan_step_diagnostics.md
68-
- session: claude 2026-08-14 (start_dev; feature agent -> library workflow)
69-
- status: library-shipped, awaiting-merge — PRs open, smoke clean (3 autolens_test failures reproduced identically on main)
70-
- library-pr: https://github.com/PyAutoLabs/PyAutoFit/pull/1473
71-
- profiling-pr: https://github.com/PyAutoLabs/autolens_profiling/pull/127 (merge AFTER the library PR)
72-
- heart-ack (2026-08-14): shipped on YELLOW score 70, red_reasons []. Acknowledged reasons: "workspace validation not passing (45 failed, cloud#31356506626 ...)" and "manifest drift: tenant firewall (organ code) — 9 mismatch(es) vs PyAutoMind/repos.yaml". Both pre-existing and unrelated; the branch was never pushed when that cloud run executed.
73-
- spun-off bugs: draft/bug/autofit/multistart_gradient_resume_fom_sanity_check.md (blocks end-to-end resume verification) · draft/bug/workspaces/jax_likelihood_pins_stale_by_1e4.md
74-
- classification: library (PyAutoFit, primary) + profiling artifact (autolens_profiling)
75-
- plan: fused-in-jit gradient-finiteness reduction (variant B). CPU microbenchmark showed the eager-outside-the-jit variant is the WORST of three (+3.3% vs a +1.5% noise floor on a 1.9ms/step objective); fused measured +0.05%, below noise. CPU understates the host-pull variant — on CPU device→host is a same-address-space memcpy — so re-run under the GPU profile before merging.
76-
- worktree: ~/Code/PyAutoLabs-wt/multistart-nan-step-diagnostics
77-
- repos:
78-
- PyAutoFit: feature/multistart-nan-step-diagnostics
79-
- autolens_profiling: feature/multistart-nan-step-diagnostics

active/multistart_gradient_nan_step_diagnostics.md

Lines changed: 0 additions & 33 deletions
This file was deleted.
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
## multistart-nan-step-diagnostics
2+
3+
- issue: https://github.com/PyAutoLabs/PyAutoFit/issues/1472 (closed)
4+
- completed: 2026-08-14
5+
- library-pr: https://github.com/PyAutoLabs/PyAutoFit/pull/1473 (MERGED fbfcece3)
6+
- profiling-pr: https://github.com/PyAutoLabs/autolens_profiling/pull/127 (MERGED a34d6191)
7+
8+
`MultiStartGradient` detected a dead lane only via the VALUE. A lane whose
9+
likelihood was finite but whose gradient was non-finite counted as alive, had
10+
its update zeroed by `optax.apply_if_finite`, and froze in place — a
11+
differentiability failure that looks exactly like convergence in the FoM trace.
12+
Now counted per step, disjointly (gradient-NaN only for lanes still alive by
13+
value), persisted into `search_internal` + `samples_info`, and surfaced in
14+
`search.summary` with rates normalised by `n_starts * total_steps`.
15+
16+
Measurement only: `resurrect` still triggers on value-NaN alone, so the
17+
wsdev #117/#125 benchmark numbers stay comparable. The resurrection policy is
18+
deliberately deferred until the counters show how often frozen lanes occur.
19+
20+
### The finding that changed the design
21+
22+
Reducing gradient finiteness on device but OUTSIDE the jit is the WORST of three
23+
options, not the best — it buys a kernel dispatch plus a host round-trip to
24+
avoid a transfer that was never the cost. Measured ~+3%/step against a ~1.5%
25+
noise floor, worse than pulling the whole `(n_starts, ndim)` gradient to host.
26+
Fused into the jitted call it is +0.05%. On a real MGE lens likelihood the
27+
shipped variant costs **4.1us on a 1.03s step = 0.0004% of run time**.
28+
See [[feedback_eager_jnp_reduction_outside_jit_costs_more]].
29+
30+
### Traps hit
31+
32+
- **A benchmark can be too coarse to see its own subject.** The first profiling
33+
harness diffed two end-to-end loops: a ~4us effect against a ~1s step, with
34+
~10-35ms jitter. Variants came out NEGATIVE (faster than a baseline doing
35+
strictly less work) and the verdict would have passed a 9ms regression as
36+
"below the noise floor". The duplicate-baseline CONTROL is what exposed it —
37+
without a measured noise floor the script could only make an unfalsifiable
38+
claim. Fixed by measuring numerator and denominator where each is resolvable.
39+
- **`_broad_starts` already filters draws on gradient finiteness.** A forced
40+
NaN-gradient fixture covering the start range gets every start rejected
41+
outright (`could not draw any finite-gradient starting points`). Lanes must
42+
BEGIN differentiable and cross the cliff mid-descent — which is precisely why
43+
mid-search freezing was the invisible gap.
44+
- **`| tail -N` on a long background run destroys the results.** Cost a 25-minute
45+
six-workspace smoke re-run. Redirect to a file.
46+
- **Verifying the source on disk is not verifying the source that got imported.**
47+
Probing the smoke env's interpreter directly resolved `autofit` to the
48+
canonical checkout, which looked like the run had graded `main`. It hadn't —
49+
`heart/smoke.py` replaces `PYTHONPATH` from `--root` and preflights module
50+
ownership. Check the log, not a probe outside the runner's environment.
51+
- A completed search DELETES its `search_internal`, and re-running a completed
52+
named search returns the cached result via `.completed` rather than resuming.
53+
The only real resume scenario is a search killed mid-run with
54+
`iterations_per_full_update` small enough to have checkpointed.
55+
56+
### Verification beyond the suite
57+
58+
Device reduction cross-checked against host recomputation on real gradients
59+
(exact agreement); gradient-NaN counter proven to FIRE via the `jnp.where` AD
60+
trap (14/80 lane-steps, `n_resurrections` unchanged); `search.summary` verified
61+
ON DISK with rates recomputed from the file; both guards mutation-tested.
62+
Full suite 1747 passed / 2 skipped. Smoke: all six workspaces, the only three
63+
failures reproduced byte-identically against `main`.
64+
65+
### Spun off, not absorbed
66+
67+
- `draft/bug/autofit/multistart_gradient_resume_fom_sanity_check.md`
68+
MultiStartGradient cannot resume a killed mid-run search on `main`; the FoM
69+
sanity check compares a stored log-likelihood against the multi-start
70+
chi-squared convention (clean -2x). This BLOCKS end-to-end verification of the
71+
counters' resume accumulation, which therefore ships unit-test-only.
72+
- `draft/bug/workspaces/jax_likelihood_pins_stale_by_1e4.md` — three
73+
`jax_likelihood` pins stale by 1.24e-4 against rtol 1e-4, failing on `main`.
74+
75+
### Still owed
76+
77+
GPU row for the profiling artifact (unchecked box in autolens_profiling#127):
78+
laptop GPU first, then A100 with `jax_enable_x64` set EXPLICITLY — it is not
79+
inherited under `sbatch`, and float32 would halve the gradient array and
80+
understate the exact quantity under test. CPU cannot see the `host` variant's
81+
real cost at all (same-address-space memcpy).
82+
83+
Shipped on Heart YELLOW (score 70, `red_reasons: []`), human-acknowledged; both
84+
reasons pre-existing and unrelated.
85+
86+
## Original prompt
87+
88+
# MultiStartGradient value-NaN and gradient-NaN step diagnostics
89+
90+
Type: feature
91+
Target: PyAutoFit
92+
Repos:
93+
- PyAutoFit
94+
Difficulty: small
95+
Autonomy: supervised
96+
Priority: normal
97+
Status: formalised
98+
99+
MultiStartGradient value-NaN and gradient-NaN step diagnostics.
100+
101+
Add per-step non-finite accounting to MultiStartGradient and surface it. Scope is MEASUREMENT ONLY — no change to resurrection behaviour (see 'Deliberately out of scope' below).
102+
103+
PART 1 (the key diagnostic) — split and record value-NaN vs gradient-NaN steps.
104+
In autofit/non_linear/search/mle/multi_start_gradient/search.py the fit loop detects a dead lane ONLY via 'alive = np.isfinite(np.asarray(foms))' (~line 627) — the VALUE. The gradient is never checked inside the loop; gradient finiteness is tested only when drawing initial starts (~line 876). So a lane whose value is finite but whose gradient is non-finite is not counted dead and not resurrected: optax.apply_if_finite zeroes its update and the lane silently freezes in place while still counted alive. That failure mode is currently invisible, and it is exactly a differentiability failure rather than an evaluation failure. Count both, per step, and record them separately:
105+
- value-NaN lane-steps: where the likelihood is UNDEFINED (today's resurrection trigger)
106+
- gradient-NaN lane-steps: where the likelihood is defined but NOT DIFFERENTIABLE (new; currently unmeasured)
107+
'grads' is already in hand at that point in the loop. Persist both counters into search_internal alongside n_resurrections, and into samples_info in samples_via_internal_from. Measure, do not assume, the cost: pulling grads to host is a larger device-to-host transfer than foms (n_starts x n_params vs n_starts) and sits inside the stepped loop — the step already syncs on foms, so it is likely in the noise, but benchmark it.
108+
109+
PART 2 — surface the counters in search.summary.
110+
autofit/text/text_util.py:115 search_summary_from_samples(samples) already receives the samples object, and samples_via_internal_from already puts n_resurrections, n_starts, n_steps, total_steps and resurrect into samples_info. Add a guarded block emitting the resurrection count, the two NaN counters, and NORMALIZED rates (divide by n_starts * total_steps — raw counts are not comparable across runs: 797 on a 16x3000 run vs 10 on an 8x300 run differ 80x raw and ~2x by rate). Follow the existing duck-typed precedent three lines above, 'if hasattr(samples, total_accepted_samples)', which adds Total Accepted Samples / Acceptance Ratio for MCMC searches — search-specific blocks are already the idiom. Guard with .get() so other searches are unaffected.
111+
112+
Naming constraint: emit these as neutral factual counts. Do NOT label them a smoothness metric in user-facing output — the resurrection-rate to HMC-divergence-rate correlation is unvalidated (that validation is a separate ideas.md item, wsdev#117 resurrection diagnostics).
113+
114+
Deliberately out of scope: making resurrect trigger on non-finite gradients. That would change search behaviour and shift every existing benchmark number, so the wsdev #117/#125 pix results would stop being comparable without re-running. Decide the resurrection policy AFTER the counters show how often frozen lanes actually occur.
115+
116+
Motivation: pixelized-mesh MultiStartProdigy campaigns (wsdev #117/#125) judge mesh differentiability indirectly, from final logL and raw resurrection counts. The value/gradient NaN split directly answers the open question in pix_prodigy_laptop_gpu_findings.md section 6.2 — whether DelaunayNN's 109 free-AdaptSplit lane deaths were NaN deaths like plain Delaunay or survivable over-regularized-floor deaths like knn — and would have caught frozen zombie lanes in every run to date, some of which may have been misattributed to regularization plateaus.
117+
118+
Sizing note: this is SMALL despite the prose length — roughly a counter plus two dict keys in search.py, and a ~5-line guarded block in text_util.py, plus a benchmark and unit tests.
119+
120+
<!-- formalised by the Intake (Conception) Agent on 2026-08-14 from user-intake -->

complete/index.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Token-light navigation over the finished-work records (schema:
66
only then grep a dated bucket. Curators: edit the band between the CURATED
77
markers; everything below GENERATED is rebuilt.
88

9-
989 records across 7 buckets.
9+
990 records across 7 buckets.
1010

1111
<!-- CURATED:START -->
1212
## Highlights
@@ -72,6 +72,7 @@ _(curate hard-won records here — survives regeneration.)_
7272
- [mge-sigma-min-workspace-sweep](2026/08/mge-sigma-min-workspace-sweep.md)
7373
- [missing-auto-simulate-guards](2026/08/missing-auto-simulate-guards.md)
7474
- [multi-start-auto-convergence-real-search](2026/08/multi-start-auto-convergence-real-search.md)`scripts/jax_assertions/multi_start_gradient_auto_convergence.py` failed
75+
- [multistart-nan-step-diagnostics](2026/08/multistart-nan-step-diagnostics.md)
7576
- [nautilus-1core-serial-pool](2026/08/nautilus-1core-serial-pool.md) — corrective for the Heart RED "release validation FAILED (stage
7677
- [normalise-auto-simulate-guard-idiom](2026/08/normalise-auto-simulate-guard-idiom.md)
7778
- [notebook-setup-notebook-regen-drift](2026/08/notebook-setup-notebook-regen-drift.md)

0 commit comments

Comments
 (0)