Inference: stage-aware retries (run manifest, supersede-on-retry) + visualization suite#129
Inference: stage-aware retries (run manifest, supersede-on-retry) + visualization suite#129zachtheyek wants to merge 9 commits into
Conversation
Layering a saved training config under CLI flags copied its checkpoint section (save_tag, load_dir, load_tag, start_round) onto the singleton, so an inference run without an explicit --save-tag would masquerade under the training run's tag, corrupting DB provenance and output paths. A saved training config's checkpoint fields are never what an inference run wants — skip the section entirely; the CLI --save-tag (or the default import-time timestamp) stays authoritative. Regression test pins the skip; the old test asserting the documented sharp edge is updated accordingly. Part of #124 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One row per (cadence, stage transition): 'preprocessed' when the stamp .npy lands, a superseding 'inferred' row with aggregate stats (n_stamps, n_candidates, JSON confidence-quantile summary, duration) when inference completes, and 'failed' when a cadence's inference stage dies. A live 'inferred' row is the stage-aware retry skip signal. Created via the PRAGMA user_version migration mechanism (v2; new tables need no ALTER step since CREATE TABLE IF NOT EXISTS covers old and new databases alike). The table joins _SUPERSEDE_TABLES with the npy_path filter, gets a writer-queue write method, a whitelisted query method, and a (tag, npy_path, status) index for the resume lookup. Part of #124 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ssing Three additions feeding PR-08's stage-aware retries and viz suite: - _energy_detect_channel_worker now also returns a histogram of ALL finite window statistics over fixed log-spaced bins (ED_STAT_HIST_EDGES) — a handful of numpy ops per channel, so the full statistic distribution (not just hits) is recoverable per ON file for the ed_stat_distributions figure. - Cadence metadata JSON gains ed_stat_hist (per-ON-file counts + edges), n_raw_hits / n_merged_hits, and pre-/post-dedup hit frequency lists (hit spectrum + preprocessing funnel figures). - _process_cadence writes a status='preprocessed' row (n_stamps, cadence key, csv path, duration) to the inference_cadences run manifest on completion; the resume path never re-writes it. Part of #124 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New module rendering the nine end-of-run figures for a streaming CSV
inference run, each saved under plots/inference/{save_tag}/ and
uploaded to Slack (mirroring train.py's plot pattern):
- ed_stat_distributions: all-window D'Agostino k2 histogram (log-log,
per-ON-file overlay, threshold line) from the worker-accumulated
fixed-bin histograms in each cadence's metadata
- ed_hit_spectrum: hit density vs frequency, pre- vs post-dedup
- bandpass_flattening: raw vs flattened integrated spectra with the
removed model overlaid (formalizes PR-07's opt-in debug artifact)
- stamp_gallery: top-K stamps by statistic as 6-obs waterfall grids,
with overlap-offset near-duplicates greedily skipped
- preproc_funnel: raw hits -> merged -> stamps -> snippets per cadence,
storage annotated
- confidence_distribution: P(true) histogram (log-y, threshold line,
per-cadence overlay when <= 10 cadences)
- candidate_gallery + capped per-candidate figures (implements the
long-standing plot_candidate stub: 6-panel waterfall, latent bar
chart, provenance annotations), sourced from inference_results so
resumed cadences are covered
- inference_latent_projection: cadence-level latents through the
persisted training UMAP (located via the training config JSON's
model_path + save_tag; skips gracefully with a log when absent)
- inference_summary: table card with counts, per-stage durations from
the run manifest, throughput, and per-target/band candidate counts
InferenceVizCollector keeps bounded per-cadence state (fixed-bin
confidence histograms; latent features subsampled to a global budget
with candidates always kept). Every figure runs under _viz_safe — a
plot bug can never kill a science run. Strictly the OO Figure API, no
pyplot (background threads exist; the global registry is unsafe).
Config: inference_viz_enabled (default on), stamp_gallery_top_k (12),
max_candidate_plots (50) + --inference-viz/--no-inference-viz,
--stamp-gallery-top-k, --max-candidate-plots flags, validation, and
to_dict() entries. README CLI Reference regenerated verbatim.
Part of #124
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The streaming CSV loop now resumes off the inference_cadences run
manifest instead of re-running every cadence:
- Before streaming, cadences with a live status='inferred' row for
(tag, npy_path) are skipped entirely — no preprocessing, no model
load when nothing is pending — and their stored aggregates fold into
the run totals. Preprocessing-artifact-only cadences still skip
straight to inference via the existing .npy resume.
- Per-cadence inference failures are contained like preprocessing's:
log, write a status='failed' manifest row, continue. The pass raises
at the end so inference_command's retry loop (bounded by
--max-retries) re-attempts — and re-runs only the failed cadences.
- Supersede-on-retry: mark_superseded(inference_results, tag,
npy_path) runs before each cadence's fresh rows land, so partial
positives from a dead attempt can't mix in; the manifest follows
new-row-plus-supersede on completion ('preprocessed'/'failed'/stale
'inferred' rows retired, fresh 'inferred' row written with n_stamps,
n_candidates, confidence-quantile summary, duration).
- run_inference computes predict_proba once and now also returns the
P(true) vector, predictions, and latents (per-cadence transients for
the manifest summary + viz suite). summarize_confidences provides
the JSON-serializable quantile summary.
- The obsolete cross-attempt cadence_data cache on the legacy
--test-files path is dropped (the manifest made it obsolete; holding
a catalog-sized array across attempts was its only remaining use).
- The visualization suite renders after a fully successful pass
(gated on inference_viz_enabled; every figure exception-guarded).
State-machine unit tests cover fresh-run manifest writes, skip-on-
retry, failed-cadence containment + solo re-attempt, stale-positive
supersede, artifact-only resume, and the no-stamps non-retryable path,
with the GPU pipeline stubbed.
Closes the inference-resume NOTE at main.py:478 (pre-change numbering).
Part of #124
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Stamp gallery selection now collapses overlap-search offset copies by grouping each cadence's stamps on their (exact) shared statistic and taking the median-start representative — the previous greedy distance check missed copies sitting exactly (or twice) the offset apart, so a strong hit could fill three gallery cells. Regression test added. - ed_stat_distributions' above-threshold count now comes from the workers' exact hit counts (metadata n_raw_hits) instead of summing histogram bins around a threshold that falls mid-bin. - The per-cadence completion log line regained its snippet/candidate counts (dropped in the streaming-loop restructure). - plot_inference_summary reuses the metadata dict the suite entry point already loaded instead of re-reading every JSON. Part of #124 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude finished @zachtheyek's task in 2m 36s —— View job PR Review: Inference stage-aware retries + visualization suite
Overall assessmentThis is a well-structured, thoroughly tested PR. The retry overhaul and visualization suite are cleanly separated, the manifest-driven resume logic is sound, and the test coverage is comprehensive. The cluster validation evidence (kill/retry across 3 runs) demonstrates the correctness of the stage-aware resume mechanism convincingly. No blocking issues found — the inline comments are suggestions and observations, not required changes. Architecture & designRetry/resume mechanism — The manifest-driven approach (one row per Visualization suite — The
Specific findings (inline comments posted)
Code quality
Test coverageStrong coverage across all new code paths:
SecurityNo concerns. The new PerformanceThe pre-binning strategy for ED stat histograms (a few numpy ops per channel in the worker, combined by addition in the parent) is well-designed and avoids transferring large arrays. The latent reservoir sampling ( |
|
Contribution check: This PR mentions |
…count, notes - _save_and_upload eagerly releases each figure's artists/render buffers (fig.clear()) right after savefig instead of waiting for garbage collection — relevant for dense stamp/candidate galleries. - cadence_metadata_path promoted to a public DataPreprocessor method (main.py and tests no longer reach into a _-prefixed helper). - The 'failed' manifest row's n_stamps is documented at the write site and on CadenceResult.n_hits: despite the historical field name, both paths that build a CadenceResult set it to the .npy's stamp-row count, so the value is already consistent with the 'preprocessed'/'inferred' rows' n_stamps. - NOTEs added on the raw hit-frequency accumulation asymmetry (stored and accumulated raw, unlike the pre-binned ED stat histograms; bounded at current scale — pre-bin onto a fixed frequency grid if catalogs grow to hundreds of RFI-dense cadences). Part of #124 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Review round applied in 505cabb — thanks for the careful pass:
Affected tests + full pre-commit pass; all inline threads answered individually. |
|
Re: contribution check — this is a stacked PR: its base is the coordinator-maintained |
The per-candidate inference plot drew the right-column latent bar chart and
the provenance text block as two halves of the shared n_obs x 2 gridspec, so
the bar chart's x-axis label ("latent dimension (colored per observation)")
landed directly on the first metadata line ("confidence: ..."), rendering them
illegibly on top of each other.
Give the right column its own nested 2x1 sub-gridspec with a dedicated vertical
gap (hspace) between the bar chart and a taller text panel, and anchor the text
at the top of that panel. The axis label and all metadata lines now have clear
separation, and the layout scales with the number of provenance lines.
Verified by rendering plot_candidate's layout against synthetic inputs (48-dim
latent, full provenance dict) with numpy+matplotlib and inspecting the PNG.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Fixed a layout collision in the per-candidate inference plot ( Bug: the right column split the shared Fix: the right column now uses its own nested Verification: rendered Verified render by Claude Code. |
|
@claude please perform a second-pass code review of this PR. Re-review the current diff for any remaining correctness bugs, security issues, or significant quality problems, and post your findings. If the diff is clean, please say so explicitly. (Final verification round before merge.) |
| embedding[non_candidates, 0], | ||
| embedding[non_candidates, 1], | ||
| s=6, | ||
| c="tab:blue", | ||
| alpha=0.5, | ||
| linewidths=0, | ||
| label=f"snippets ({int(non_candidates.sum()):,})", | ||
| rasterized=True, |
There was a problem hiding this comment.
Bug: preprocessing duration will always be 0 for cadences that completed inference.
query_inference_cadences(tag=tag) defaults to include_superseded=False. But _infer_cadence (main.py) calls mark_superseded("inference_cadences", tag, npy_path=...) before writing the 'inferred' row — this supersedes the 'preprocessed' row too (it supersedes ALL prior rows for that (tag, npy_path)). So by the time this summary runs, every successfully-inferred cadence's 'preprocessed' row has superseded=1 and is invisible to this query.
The result: preproc_duration will only accumulate durations from cadences that failed inference (their 'preprocessed' rows are never superseded). For a fully successful run, preprocessing time shows 0.0 s.
Fix: pass include_superseded=True here so superseded 'preprocessed' rows contribute their durations:
| embedding[non_candidates, 0], | |
| embedding[non_candidates, 1], | |
| s=6, | |
| c="tab:blue", | |
| alpha=0.5, | |
| linewidths=0, | |
| label=f"snippets ({int(non_candidates.sum()):,})", | |
| rasterized=True, | |
| for row in db.query_inference_cadences(tag=tag, include_superseded=True): |
There was a problem hiding this comment.
Fixed in 293ef96. Passed include_superseded=True on the query_inference_cadences(tag=tag) call so the superseded 'preprocessed' rows are visible to the summary again.
Verified this does not introduce double-counting: the aggregation loop already keys off status ('preprocessed' -> preproc_duration, 'inferred' -> inference_duration), so a cadence's superseded 'preprocessed' row and its live 'inferred' row land in separate buckets, each summed exactly once. A single row carries exactly one status and one duration_s, so nothing is counted twice. Added a NOTE at the query explaining why include_superseded is required here (contrary to most callers, which rely on the default).
Added a regression test test_inference_summary_counts_superseded_preprocessing that reproduces the post-success manifest state (a 'preprocessed' row superseded by its 'inferred' row, in the _infer_cadence order) and asserts the rendered preprocessing time is the real non-zero value (12.0 s), summed once — not 0.0 s (the bug) and not 24.0 s (a double-count). Confirmed it fails against the pre-fix code with exactly ['0.0', 's'] and passes after the one-token fix.
Second-pass code review — PR #129Performed a thorough re-review of the current diff: config/CLI, DB schema + migration, Finding: one correctness bug
Everything else is cleanSpecifically verified:
No security issues, no other correctness bugs, no significant quality problems beyond the one inline comment above. The diff is ready to merge after that one-line fix. |
plot_inference_summary summed per-stage durations from query_inference_cadences(tag=tag), which defaults to include_superseded=False. But _infer_cadence supersedes each cadence's 'preprocessed' row just before writing its live 'inferred' row, so on a fully successful run every 'preprocessed' row is superseded and hidden by the default query -> preprocessing time always rendered 0.0 s (only failed cadences, whose 'preprocessed' rows are never superseded, could contribute). Pass include_superseded=True on that query so the 'preprocessed' rows are visible again. The aggregation already keys off status (preprocessed vs inferred), so each metric is summed from its own row with no double-counting between a cadence's superseded 'preprocessed' row and its live 'inferred' row. Add a regression test that reproduces the post-success manifest state (a 'preprocessed' row superseded by its 'inferred' row) and asserts the rendered preprocessing time is the real non-zero value, summed once. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@claude the finding from your previous review has been addressed in a new commit. Please re-review the current diff once more and confirm whether it is now clean, or report any remaining issues. |
|
Re-reviewed the current diff. The finding from my previous review (preprocessing duration always reading 0.0 s on a fully successful run because superseded
No new issues found. The diff is clean — well-structured code, comprehensive test coverage for the new features, and all my earlier informational comments (memory/mmap, pre-binning asymmetry, private API usage) remain valid observations that don't need changes now. 🤖 Generated with Claude Code |
Summary
Stacked on #120 (
feature/inference-energy-detection-perf), #122 (feature/training-retry-overhaul), and #125 (feature/pfb-bandpass-equalization). The PR baseintegration/pr08-baseis coordinator scaffolding that merges those three chains; it will be rebased ontomasteras the parents merge — review only the commits unique to this branch.Implements inference-side stage-aware retries plus the inference visualization suite. Closes #124.
Retry overhaul
apply_saved_configsave_tag clobber fixed: thecheckpointsection of a saved training config is now skipped entirely when layering under CLI flags — an inference run can no longer masquerade under the training run's tag (regression test; confirmed live on blpc3: the run logged under its datetime tag despite--config-path config_test_v17.json).inference_cadencesrun manifest (schema v2 via thePRAGMA user_versionmechanism from Training fault tolerance: stage-based resume, run manifest, DB supersede semantics #122; new tables need no ALTER step — theCREATE TABLE IF NOT EXISTSin_init_databasecovers old and new databases, the version stamp records the change): one row per (cadence, stage transition) —preprocessedwhen the stamp.npylands (written by_process_cadence, never re-written on resume), a supersedinginferredrow on inference completion carrying the aggregate stats (n_stamps,n_candidates, JSON confidence-quantile summary from the full P(true) vector, stageduration_s), andfailedwhen a cadence's inference stage dies. Aggregates in the manifest mean run-level artifacts don't depend on the positives-onlyinference_resultstable. Writer-queue writes, whitelistedquery_inference_cadences,mark_supersededsupport (npy_path filter),(tag, npy_path, status)index.inferredrow for(tag, npy_path)skips the cadence entirely (stored aggregates fold into the totals; when nothing is pending the models aren't even loaded); preprocessing artifacts only → straight to inference via the existing.npyskip; else preprocess.mark_superseded(inference_results, tag, npy_path=...)runs before each cadence's fresh rows land, so partial positives from a dead attempt can't mix in; the manifest follows new-row-plus-supersede on completion.failedmanifest row → continue; the pass raises at the end soinference_command's retry loop (bounded by--max-retries) re-attempts — and the manifest skip means only failed cadences re-run.cadence_datacache on the legacy--test-filespath is dropped, andrun_inferencecomputespredict_probaonce (returning P(true)/predictions/latents as per-cadence transients) instead of a second RF pass.Visualization suite (
src/aetherscan/inference_viz.py)Nine deliverables, each saved under
plots/inference/{save_tag}/+ Slack upload (train.py's pattern), all individually exception-guarded (_viz_safe) — a plot bug can never kill a science run. Strictly the OO Figure API, never pyplot (background threads exist; the global registry is not thread-safe).ed_stat_distributions— all-window D'Agostino k² histogram (log-log, per-ON-file overlay, threshold line). The vectorized ED workers now also return a fixed log-spaced summary histogram of every finite window statistic (a few numpy ops per channel), accumulated per ON file into each cadence's metadata JSON.ed_hit_spectrum— hit density vs frequency (MHz), pre- vs post-dedup (raw/merged hit frequency lists added to metadata).bandpass_flattening— raw vs flattened integrated spectra with the removed model (scaled PFB response H or spline fit) overlaid; formalizes PFB static passband equalization as default bandpass flattener; tag-scoped preprocess output dir #125's opt-in debug artifact as a standard per-run figure (reuses the preprocessor's channel-read/flattener helpers so it can't drift from what detection does).stamp_gallery— top-K stamps by statistic (--stamp-gallery-top-k, default 12) as the 6-obs ON/OFF waterfall grids scientists inspect; overlap-search offset copies collapsed via their shared statistic (median-start representative).preproc_funnel— per cadence: raw hits → merged → stamps (+overlap) → snippets inferred, with per-cadence stamp storage annotated.confidence_distribution— P(true) histogram over all snippets (log-y, threshold line, per-cadence overlay when ≤ 10 cadences), from fixed-bin histograms folded per cadence (memory stays O(#cadences)).candidate_gallery+ per-candidatecandidate_{i}_{tag}.png— implements the long-standingplot_candidate()stub: 6-panel cadence waterfall, 48-dim latent bar chart (colored per observation), confidence/frequency/target/session/band/tstart annotations. Sourced frominference_results(covers resume-skipped cadences); per-candidate figures capped by--max-candidate-plots(default 50).inference_latent_projection— this run's cadence-level latent features projected through the persisted training cadence-level UMAP (located via the training config JSON'smodel_path+save_tag+ umap sweep values; skips gracefully with a log when absent), over the UMAP's training embedding as a faint backdrop; candidates starred. Latent pool bounded (20k rows, candidates always kept).inference_summary— table card: cadence/snippet/candidate counts, resumed-cadence count, raw/merged hits, stamp storage, per-stage durations + throughput from the manifest, per-target/band candidate counts.Config:
inference_viz_enabled(default on) /stamp_gallery_top_k/max_candidate_plots+--inference-viz|--no-inference-viz,--stamp-gallery-top-k,--max-candidate-plots(validation,to_dict(), README CLI Reference regenerated viaPYTHONPATH=src python utils/print_cli_help.py all).Cluster validation — kill/retry + figures (blpc3, subset_test.csv = 2 cadences, dummy test_v17 model,
--max-retries 1)Run 1 (tag
20260711_224251): fresh run; the real cluster DB migratedversion 1 → 2on open. Cadence 1 (DDO210) preprocessed (790 s, 73,004 stamps) and inferred (102.7 s); cadence 2 (NGC7454) preprocessed (336.6 s, 21,663 stamps). A remote watcher SIGKILLed the whole process tree 10 s into cadence 2's inference (trigger: the second "Running inference on" log line).Run 2 (identical command, same tag):
Cadence ('DDO210', ...): already inferred under tag 20260711_224251 (73004 snippets, 0 candidate(s)); skipping→Streaming inference over 1 cadence(s) (1 already inferred, resumed from manifest)— cadence 1 skipped entirely (no ED, no load, no encode)..npyand went straight to inference (32.0 s), then the full viz suite rendered and the run completed successfully. Retry wall time ~3 m 14 s vs ~21 min for the original pass.DB assertions after run 2 (sqlite):
inference_cadencesfor the tag: exactly one liveinferredrow per cadence; bothpreprocessedrows superseded (preprocessed|1 / inferred|0× 2).inference_resultsrows:GROUP BY npy_path, snippet_index HAVING COUNT(*) > 1withsuperseded = 0returns nothing.inferredrows, e.g. DDO210:{"n": 73004, "threshold": 0.99, "n_above_threshold": 0, "mean": 0.0517, "max": 0.915, "quantiles": {"p01": 0.0, ..., "p99": 0.461}}.Figures (run 2): 8 of 9 rendered under
plots/inference/20260711_224251/and uploaded to Slack (#aetherscan-logs; zero upload-failure signatures in the log — upload success is silent by design, same as train.py's plots): ed_stat_distributions, ed_hit_spectrum, bandpass_flattening, stamp_gallery, preproc_funnel, confidence_distribution, inference_latent_projection (through the real persistedumap_cadence_nn5_md0.1_test_v17.joblib, 20,000 features), inference_summary. The candidate figures correctly loggedViz: no candidates recorded; skipping candidate plots— the dummy model's max P(true) is 0.918, below the 0.99 threshold (0 candidates, consistent with #125's runs).Run 3 (bonus, fresh tag
20260711_231234,--classification-threshold 0.8,--preprocess-output-dirpointing at run 1's directory — the documented reuse escape hatch): exercised the candidate path on-cluster. 109 candidates written (26 DDO210 + 83 NGC7454), 109 liveinference_resultsrows, and all nine deliverables rendered underplots/inference/20260711_231234/: the eight run-2 figures pluscandidate_galleryand exactly 50 per-candidate figures (Viz: rendered 50 of 109 candidate figures (--max-candidate-plots cap)), zero Slack upload failures. End-to-end ~6.5 min with ED skipped via the reused directory.Local verification
PYTHONPATH=src pytest -m "not gpu and not cluster" -q→ 402 passed, 3 skipped (integration), 2 deselected (arm64 venv, Python 3.12, TF 2.17.1), after rebasing onto the refreshedintegration/pr08-base(8e42ce5, incl. PFB static passband equalization as default bandpass flattener; tag-scoped preprocess output dir #125's PFB sidecar-cache memory fix).summarize_confidencesmath (quantiles, strict threshold boundary, JSON round-trip);apply_saved_configcheckpoint-skip regression; ED-worker histogram invariants; viz-flag routing + validation; and smoke tests for every figure (files exist, non-empty) including graceful-skip paths and a suite-entry test that survives fully broken records.ruff check/ruff format/ all pre-commit hooks pass; all commits GPG-signed.AI disclosure
Implemented end-to-end with Claude Code (model: Claude Fable 5): design-from-spec, implementation, unit tests, cluster kill/retry validation, and this PR description. Human oversight: repo owner reviews/merges.
🤖 Generated with Claude Code