From c83a310f51efe988bf56437b052beb0d76244433 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Wed, 8 Jul 2026 16:19:01 +0100 Subject: [PATCH 01/10] =?UTF-8?q?maint:=20vram=20batch=20resolution=20?= =?UTF-8?q?=E2=80=94=20probe=20override,=20sparse=20table,=20provenance=20?= =?UTF-8?q?(#54)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - vram/config.py: VMAP_BATCH_SPARSE table (w-tilde rows; unprobed cells fall back dense), PROVENANCE record (dense table probed 2026-05-24 — stale vs current source, re-probe submits staged), and resolve_vmap_batch(): runtime cells now prefer a fresh cell-matching vmap_probe_[_sparse].json over the static table, so a stale table cannot OOM a job whose submit re-probed in Phase A - all 8 vmap-capable runtime cells wired to resolve_vmap_batch with the winning source logged; probe JSON basenames normalised to vmap_probe_[_sparse].json across interferometer/point_source/ datacube (imaging cells already followed it) - hpc/batch_gpu/submit_probe_{fast,delaunay}_a100: probe-only sweeps re-validating every campaign vmap cell (dense + sparse imaging) against current source; ingest workflow documented in vram/README.md, which also loses its stale z_projects path - test/test_vram_config.py: 11 new tests (sparse fallback, probe override, mismatch/corrupt-JSON handling) Co-Authored-By: Claude Fable 5 --- hpc/batch_gpu/submit_probe_delaunay_a100 | 55 +++++++ hpc/batch_gpu/submit_probe_fast_a100 | 61 ++++++++ likelihood_runtime/datacube/delaunay.py | 4 +- likelihood_runtime/imaging/delaunay.py | 23 ++- likelihood_runtime/imaging/mge.py | 12 +- likelihood_runtime/imaging/pixelization.py | 13 +- likelihood_runtime/interferometer/delaunay.py | 29 +++- likelihood_runtime/interferometer/mge.py | 15 +- .../interferometer/pixelization.py | 19 ++- .../point_source/image_plane.py | 19 ++- .../point_source/source_plane.py | 19 ++- test/test_vram_config.py | 136 ++++++++++++++++++ vram/README.md | 46 ++++-- vram/__init__.py | 11 +- vram/config.py | 85 ++++++++++- 15 files changed, 505 insertions(+), 42 deletions(-) create mode 100644 hpc/batch_gpu/submit_probe_delaunay_a100 create mode 100644 hpc/batch_gpu/submit_probe_fast_a100 create mode 100644 test/test_vram_config.py diff --git a/hpc/batch_gpu/submit_probe_delaunay_a100 b/hpc/batch_gpu/submit_probe_delaunay_a100 new file mode 100644 index 0000000..ed5e64c --- /dev/null +++ b/hpc/batch_gpu/submit_probe_delaunay_a100 @@ -0,0 +1,55 @@ +#!/bin/bash -l +# +# Probe-only sweep — DELAUNAY cells (10-30 min per XLA compile; scipy +# Delaunay via pure_callback bloats the graph, so probes are single-point +# per the vram/README.md methodology). +# +# Re-validates the vram/config.py batch-size table for the +# PreOptimizationTimes campaign (autolens_profiling#54). Each probe writes +# results/runtime//delaunay/vmap_probe_delaunay[_sparse].json +# and exits before any timing loop. See submit_probe_fast_a100 for the +# fast cells and vram/README.md "Probe-only submits" for the ingest step. + +#SBATCH -J probe_delaunay_a100 +#SBATCH --partition=gpu +#SBATCH --gres=gpu:1 +#SBATCH --ntasks=1 +#SBATCH --cpus-per-task=4 +#SBATCH --mem=64gb +#SBATCH --time=8:00:00 +#SBATCH -o output/output.%A.out +#SBATCH -e error/error.%A.err +#SBATCH --mail-type=END,FAIL +#SBATCH --mail-user=james.w.nightingale@durham.ac.uk + +export AP_ROOT=/mnt/ral/jnightin/autolens_profiling +source $AP_ROOT/activate.sh + +export JAX_PLATFORM_NAME=cuda +export JAX_PLATFORMS=cuda,cpu +export XLA_PYTHON_CLIENT_PREALLOCATE=false +export JAX_ENABLE_X64=True +export NUMBA_CACHE_DIR=/tmp/numba_cache +export MPLCONFIGDIR=/tmp/matplotlib + +nvidia-smi +cd $AP_ROOT + +run_probe () { + echo "=== probe: $1 --instrument $2 $3 ===" + date + python3 "$1" --instrument "$2" --vmap-probe $3 || echo "!!! probe FAILED: $1 $2 $3" +} + +# Imaging delaunay — campaign instruments, dense + sparse. +for inst in ao jwst hst; do + run_probe likelihood_runtime/imaging/delaunay.py "$inst" "" + run_probe likelihood_runtime/imaging/delaunay.py "$inst" "--sparse" +done + +# Interferometer delaunay — campaign anchor (alma_high) + the cheap sanity cell. +run_probe likelihood_runtime/interferometer/delaunay.py sma "" +run_probe likelihood_runtime/interferometer/delaunay.py alma_high "" + +echo "Finished." +date diff --git a/hpc/batch_gpu/submit_probe_fast_a100 b/hpc/batch_gpu/submit_probe_fast_a100 new file mode 100644 index 0000000..4a5251e --- /dev/null +++ b/hpc/batch_gpu/submit_probe_fast_a100 @@ -0,0 +1,61 @@ +#!/bin/bash -l +# +# Probe-only sweep — FAST cells (mge / pixelization; ~10 s per XLA compile). +# +# Re-validates the vram/config.py batch-size table for the +# PreOptimizationTimes campaign (autolens_profiling#54): every campaign +# vmap cell is re-probed against current source, dense + sparse imaging +# variants. Each probe writes +# results/runtime///vmap_probe_[_sparse].json +# and exits before any timing loop (--vmap-probe). Runtime cells then +# prefer these fresh JSONs over the static table automatically +# (vram.resolve_vmap_batch), and the table rows get updated from them at +# ingest — see vram/README.md "Probe-only submits". +# +# Pair with submit_probe_delaunay_a100 (slow cells, separate job). + +#SBATCH -J probe_fast_a100 +#SBATCH --partition=gpu +#SBATCH --gres=gpu:1 +#SBATCH --ntasks=1 +#SBATCH --cpus-per-task=4 +#SBATCH --mem=64gb +#SBATCH --time=1:00:00 +#SBATCH -o output/output.%A.out +#SBATCH -e error/error.%A.err +#SBATCH --mail-type=END,FAIL +#SBATCH --mail-user=james.w.nightingale@durham.ac.uk + +export AP_ROOT=/mnt/ral/jnightin/autolens_profiling +source $AP_ROOT/activate.sh + +export JAX_PLATFORM_NAME=cuda +export JAX_PLATFORMS=cuda,cpu +export XLA_PYTHON_CLIENT_PREALLOCATE=false +export JAX_ENABLE_X64=True +export NUMBA_CACHE_DIR=/tmp/numba_cache +export MPLCONFIGDIR=/tmp/matplotlib + +nvidia-smi +cd $AP_ROOT + +run_probe () { + echo "=== probe: $1 --instrument $2 $3 ===" + date + python3 "$1" --instrument "$2" --vmap-probe $3 || echo "!!! probe FAILED: $1 $2 $3" +} + +# Imaging mge / pixelization — campaign instruments, dense + sparse. +for inst in ao jwst hst; do + run_probe likelihood_runtime/imaging/mge.py "$inst" "" + run_probe likelihood_runtime/imaging/mge.py "$inst" "--sparse" + run_probe likelihood_runtime/imaging/pixelization.py "$inst" "" + run_probe likelihood_runtime/imaging/pixelization.py "$inst" "--sparse" +done + +# Interferometer mge / pixelization — sma only (alma+ intentionally blocked). +run_probe likelihood_runtime/interferometer/mge.py sma "" +run_probe likelihood_runtime/interferometer/pixelization.py sma "" + +echo "Finished." +date diff --git a/likelihood_runtime/datacube/delaunay.py b/likelihood_runtime/datacube/delaunay.py index b3061c1..d519edf 100644 --- a/likelihood_runtime/datacube/delaunay.py +++ b/likelihood_runtime/datacube/delaunay.py @@ -127,7 +127,7 @@ resolve_output_paths, ) from simulators.interferometer import INSTRUMENTS # noqa: E402 -from vram import ProbeResult, vmap_batch_for, write_probe_json # noqa: E402 +from vram import ProbeResult, write_probe_json # noqa: E402 _cli = parse_profile_cli() @@ -405,7 +405,7 @@ def _build_transformer(uv_wavelengths, real_space_mask): if _cli.vmap_probe: probe_path = ( _cli.output_dir or (_workspace_root / "results" / "runtime" / "datacube" / "delaunay") - ) / "vmap_probe.json" + ) / "vmap_probe_delaunay.json" probe_path.parent.mkdir(parents=True, exist_ok=True) import json diff --git a/likelihood_runtime/imaging/delaunay.py b/likelihood_runtime/imaging/delaunay.py index 9d78abd..869bcc0 100644 --- a/likelihood_runtime/imaging/delaunay.py +++ b/likelihood_runtime/imaging/delaunay.py @@ -83,7 +83,7 @@ from vram import ( # noqa: E402 probe_vmap_memory, recommend_batch_size, - vmap_batch_for, + resolve_vmap_batch, write_probe_json, ) @@ -470,13 +470,24 @@ def full_pipeline_from_params(params_tree): print("\n--- vmap batched evaluation ---") -batch_size = vmap_batch_for("imaging", "delaunay", instrument) or 3 +_batch_resolved, _batch_source = resolve_vmap_batch( + "imaging", + "delaunay", + instrument, + output_dir=_cli.output_dir + or (_workspace_root / "results" / "runtime" / "imaging" / "delaunay"), + path="sparse" if _cli.use_sparse_operator else "dense", +) +print(f" vmap batch_size: {_batch_resolved} (source: {_batch_source})") +batch_size = _batch_resolved or 3 -# Skip vmap if vmap_batch_for explicitly returns None for this cell. -_vmap_skipped = vmap_batch_for("imaging", "delaunay", instrument) is None +# Skip vmap if batch resolution (probe JSON / table) returned None for this cell. +_vmap_skipped = _batch_resolved is None if _vmap_skipped: - print(" SKIPPED: vmap_batch_for() returned None for this (cell, instrument).") + print( + f" SKIPPED: batch resolution returned None for this (cell, instrument) — source: {_batch_source}." + ) vmap_per_call = None vmap_speedup = None vmap_batch_time = None @@ -585,7 +596,7 @@ def full_pipeline_from_params(params_tree): "inversion_path": "sparse" if _cli.use_sparse_operator else "dense", }, "full_pipeline_single_jit": full_pipeline_per_call, - "vmap": "SKIPPED — vmap_batch_for() returned None for this (cell, instrument)" + "vmap": "SKIPPED — batch resolution returned None for this (cell, instrument)" if _vmap_skipped else { "batch_size": batch_size, diff --git a/likelihood_runtime/imaging/mge.py b/likelihood_runtime/imaging/mge.py index 9d6c519..7f2c9fc 100644 --- a/likelihood_runtime/imaging/mge.py +++ b/likelihood_runtime/imaging/mge.py @@ -90,7 +90,7 @@ from vram import ( # noqa: E402 probe_vmap_memory, recommend_batch_size, - vmap_batch_for, + resolve_vmap_batch, write_probe_json, ) @@ -437,7 +437,15 @@ def full_pipeline_from_params(params_tree): print("\n--- vmap batched evaluation ---") -batch_size = vmap_batch_for("imaging", "mge", instrument) or 3 +_batch_resolved, _batch_source = resolve_vmap_batch( + "imaging", + "mge", + instrument, + output_dir=_cli.output_dir or (_workspace_root / "results" / "runtime" / "imaging" / "mge"), + path="sparse" if _cli.use_sparse_operator else "dense", +) +print(f" vmap batch_size: {_batch_resolved} (source: {_batch_source})") +batch_size = _batch_resolved or 3 # Build the batched pytree: every leaf gets a fresh leading batch axis. No # flat-vector reshaping required — JAX walks the pytree via the registration diff --git a/likelihood_runtime/imaging/pixelization.py b/likelihood_runtime/imaging/pixelization.py index 57715b4..dc9a9ea 100644 --- a/likelihood_runtime/imaging/pixelization.py +++ b/likelihood_runtime/imaging/pixelization.py @@ -76,7 +76,7 @@ from vram import ( # noqa: E402 probe_vmap_memory, recommend_batch_size, - vmap_batch_for, + resolve_vmap_batch, write_probe_json, ) @@ -466,7 +466,16 @@ def full_pipeline_from_params(params_tree): print("\n--- vmap batched evaluation ---") -batch_size = vmap_batch_for("imaging", "pixelization", instrument) or 3 +_batch_resolved, _batch_source = resolve_vmap_batch( + "imaging", + "pixelization", + instrument, + output_dir=_cli.output_dir + or (_workspace_root / "results" / "runtime" / "imaging" / "pixelization"), + path="sparse" if _cli.use_sparse_operator else "dense", +) +print(f" vmap batch_size: {_batch_resolved} (source: {_batch_source})") +batch_size = _batch_resolved or 3 vmap_batch_time = None vmap_per_call = None vmap_speedup = None diff --git a/likelihood_runtime/interferometer/delaunay.py b/likelihood_runtime/interferometer/delaunay.py index fc108e4..9da4c33 100644 --- a/likelihood_runtime/interferometer/delaunay.py +++ b/likelihood_runtime/interferometer/delaunay.py @@ -124,7 +124,7 @@ from vram import ( # noqa: E402 probe_vmap_memory, recommend_batch_size, - vmap_batch_for, + resolve_vmap_batch, write_probe_json, ) @@ -455,7 +455,11 @@ def full_pipeline_from_params(params_tree): recommended = recommend_batch_size(probe) probe_path = ( _cli.output_dir or (_workspace_root / "results" / "runtime" / "interferometer" / "delaunay") - ) / "vmap_probe.json" + ) / ( + "vmap_probe_delaunay_sparse.json" + if _cli.use_sparse_operator + else "vmap_probe_delaunay.json" + ) write_probe_json(probe, recommended, probe_path) print(f"\n vmap_probe samples: {probe.samples}") print(f" per_replica: {probe.per_replica_mb:.1f} MB / replica") @@ -469,10 +473,19 @@ def full_pipeline_from_params(params_tree): print("\n--- vmap batched evaluation ---") -batch_size = vmap_batch_for("interferometer", "delaunay", instrument) or 3 +_batch_resolved, _batch_source = resolve_vmap_batch( + "interferometer", + "delaunay", + instrument, + output_dir=_cli.output_dir + or (_workspace_root / "results" / "runtime" / "interferometer" / "delaunay"), + path="sparse" if _cli.use_sparse_operator else "dense", +) +print(f" vmap batch_size: {_batch_resolved} (source: {_batch_source})") +batch_size = _batch_resolved or 3 -# Skip vmap if vmap_batch_for explicitly returns None for this cell. -_vmap_skipped = vmap_batch_for("interferometer", "delaunay", instrument) is None +# Skip vmap if batch resolution (probe JSON / table) returned None for this cell. +_vmap_skipped = _batch_resolved is None vmap_batch_time = None vmap_per_call = None @@ -483,7 +496,9 @@ def full_pipeline_from_params(params_tree): _n_leaves = len(jax.tree_util.tree_leaves(params_tree)) if _vmap_skipped: - print(" SKIPPED: vmap_batch_for() returned None for this (cell, instrument).") + print( + f" SKIPPED: batch resolution returned None for this (cell, instrument) — source: {_batch_source}." + ) elif _n_leaves == 0: print( " SKIPPED: model has 0 free parameters (all fixed to truth); " @@ -588,7 +603,7 @@ def full_pipeline_from_params(params_tree): if vmap_per_call is None: if _vmap_skipped: - vmap_payload = "SKIPPED — vmap_batch_for() returned None for this (cell, instrument)" + vmap_payload = "SKIPPED — batch resolution returned None for this (cell, instrument)" else: vmap_payload = "SKIPPED — model has 0 free parameters (all fixed to truth)" else: diff --git a/likelihood_runtime/interferometer/mge.py b/likelihood_runtime/interferometer/mge.py index d55e5f7..b3b00a9 100644 --- a/likelihood_runtime/interferometer/mge.py +++ b/likelihood_runtime/interferometer/mge.py @@ -87,7 +87,7 @@ from vram import ( # noqa: E402 probe_vmap_memory, recommend_batch_size, - vmap_batch_for, + resolve_vmap_batch, write_probe_json, ) @@ -352,7 +352,7 @@ def full_pipeline_from_params(params_tree): recommended = recommend_batch_size(probe) probe_path = ( _cli.output_dir or (_workspace_root / "results" / "runtime" / "interferometer" / "mge") - ) / "vmap_probe.json" + ) / ("vmap_probe_mge_sparse.json" if _cli.use_sparse_operator else "vmap_probe_mge.json") write_probe_json(probe, recommended, probe_path) print(f"\n vmap_probe samples: {probe.samples}") print(f" per_replica: {probe.per_replica_mb:.1f} MB / replica") @@ -366,7 +366,16 @@ def full_pipeline_from_params(params_tree): print("\n--- vmap batched evaluation ---") -batch_size = vmap_batch_for("interferometer", "mge", instrument) or 3 +_batch_resolved, _batch_source = resolve_vmap_batch( + "interferometer", + "mge", + instrument, + output_dir=_cli.output_dir + or (_workspace_root / "results" / "runtime" / "interferometer" / "mge"), + path="sparse" if _cli.use_sparse_operator else "dense", +) +print(f" vmap batch_size: {_batch_resolved} (source: {_batch_source})") +batch_size = _batch_resolved or 3 parameters = jax.tree_util.tree_map( lambda leaf: jnp.broadcast_to(leaf, (batch_size, *leaf.shape)), diff --git a/likelihood_runtime/interferometer/pixelization.py b/likelihood_runtime/interferometer/pixelization.py index 4827bd9..cd6055a 100644 --- a/likelihood_runtime/interferometer/pixelization.py +++ b/likelihood_runtime/interferometer/pixelization.py @@ -85,7 +85,7 @@ from vram import ( # noqa: E402 probe_vmap_memory, recommend_batch_size, - vmap_batch_for, + resolve_vmap_batch, write_probe_json, ) @@ -378,7 +378,11 @@ def full_pipeline_from_params(params_tree): probe_path = ( _cli.output_dir or (_workspace_root / "results" / "runtime" / "interferometer" / "pixelization") - ) / "vmap_probe.json" + ) / ( + "vmap_probe_pixelization_sparse.json" + if _cli.use_sparse_operator + else "vmap_probe_pixelization.json" + ) write_probe_json(probe, recommended, probe_path) print(f"\n vmap_probe samples: {probe.samples}") print(f" per_replica: {probe.per_replica_mb:.1f} MB / replica") @@ -396,7 +400,16 @@ def full_pipeline_from_params(params_tree): print("\n--- vmap batched evaluation ---") -batch_size = vmap_batch_for("interferometer", "pixelization", instrument) or 3 +_batch_resolved, _batch_source = resolve_vmap_batch( + "interferometer", + "pixelization", + instrument, + output_dir=_cli.output_dir + or (_workspace_root / "results" / "runtime" / "interferometer" / "pixelization"), + path="sparse" if _cli.use_sparse_operator else "dense", +) +print(f" vmap batch_size: {_batch_resolved} (source: {_batch_source})") +batch_size = _batch_resolved or 3 vmap_batch_time = None vmap_per_call = None vmap_speedup = None diff --git a/likelihood_runtime/point_source/image_plane.py b/likelihood_runtime/point_source/image_plane.py index ef63bd4..0f8ebfe 100644 --- a/likelihood_runtime/point_source/image_plane.py +++ b/likelihood_runtime/point_source/image_plane.py @@ -72,7 +72,7 @@ from vram import ( # noqa: E402 probe_vmap_memory, recommend_batch_size, - vmap_batch_for, + resolve_vmap_batch, write_probe_json, ) @@ -302,7 +302,11 @@ def full_pipeline_from_params(params_tree): probe_path = ( _cli.output_dir or (_workspace_root / "results" / "runtime" / "point_source" / "image_plane") - ) / "vmap_probe.json" + ) / ( + "vmap_probe_image_plane_sparse.json" + if _cli.use_sparse_operator + else "vmap_probe_image_plane.json" + ) write_probe_json(probe, recommended, probe_path) print(f"\n vmap_probe samples: {probe.samples}") print(f" per_replica: {probe.per_replica_mb:.1f} MB / replica") @@ -316,7 +320,16 @@ def full_pipeline_from_params(params_tree): print("\n--- vmap batched evaluation ---") -batch_size = vmap_batch_for("point_source", "image_plane", instrument) or 3 +_batch_resolved, _batch_source = resolve_vmap_batch( + "point_source", + "image_plane", + instrument, + output_dir=_cli.output_dir + or (_workspace_root / "results" / "runtime" / "point_source" / "image_plane"), + path="sparse" if _cli.use_sparse_operator else "dense", +) +print(f" vmap batch_size: {_batch_resolved} (source: {_batch_source})") +batch_size = _batch_resolved or 3 batched_params = jax.tree_util.tree_map( lambda leaf: jnp.broadcast_to(leaf, (batch_size, *leaf.shape)), diff --git a/likelihood_runtime/point_source/source_plane.py b/likelihood_runtime/point_source/source_plane.py index f833627..2f9ccdc 100644 --- a/likelihood_runtime/point_source/source_plane.py +++ b/likelihood_runtime/point_source/source_plane.py @@ -54,7 +54,7 @@ from vram import ( # noqa: E402 probe_vmap_memory, recommend_batch_size, - vmap_batch_for, + resolve_vmap_batch, write_probe_json, ) @@ -304,7 +304,11 @@ def full_pipeline_from_params(params_tree): probe_path = ( _cli.output_dir or (_workspace_root / "results" / "runtime" / "point_source" / "source_plane") - ) / "vmap_probe.json" + ) / ( + "vmap_probe_source_plane_sparse.json" + if _cli.use_sparse_operator + else "vmap_probe_source_plane.json" + ) write_probe_json(probe, recommended, probe_path) print(f"\n vmap_probe samples: {probe.samples}") print(f" per_replica: {probe.per_replica_mb:.1f} MB / replica") @@ -356,7 +360,16 @@ def ray_trace_to_source_plane(params_tree, positions_raw): print("\n--- vmap over ray-trace prefix ---") -batch_size = vmap_batch_for("point_source", "source_plane", instrument) or 3 +_batch_resolved, _batch_source = resolve_vmap_batch( + "point_source", + "source_plane", + instrument, + output_dir=_cli.output_dir + or (_workspace_root / "results" / "runtime" / "point_source" / "source_plane"), + path="sparse" if _cli.use_sparse_operator else "dense", +) +print(f" vmap batch_size: {_batch_resolved} (source: {_batch_source})") +batch_size = _batch_resolved or 3 batched_params = jax.tree_util.tree_map( lambda leaf: jnp.broadcast_to(leaf, (batch_size, *leaf.shape)), diff --git a/test/test_vram_config.py b/test/test_vram_config.py new file mode 100644 index 0000000..0b93814 --- /dev/null +++ b/test/test_vram_config.py @@ -0,0 +1,136 @@ +"""Unit tests for ``vram.config`` batch-size lookup + resolution. + +Covers the sparse-path fallback rule and the probe-JSON-over-table +resolution order added for the PreOptimizationTimes campaign (phase 2, +autolens_profiling#54). No JAX dependency. + +Run:: + + cd autolens_profiling + python -m pytest test/test_vram_config.py +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +# Make `vram` importable without installing the repo. +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +from vram.config import ( # noqa: E402 + VMAP_BATCH, + VMAP_BATCH_SPARSE, + resolve_vmap_batch, + vmap_batch_for, +) + + +def _write_probe( + tmp_path: Path, + *, + model: str = "pixelization", + sparse: bool = False, + dataset: str = "imaging", + instrument: str = "hst", + recommended=24, +) -> Path: + suffix = "_sparse" if sparse else "" + p = tmp_path / f"vmap_probe_{model}{suffix}.json" + p.write_text( + json.dumps( + { + "dataset": dataset, + "model": model, + "instrument": instrument, + "recommended_batch_size": recommended, + } + ) + ) + return p + + +# --------------------------------------------------------------------------- +# vmap_batch_for — path axis +# --------------------------------------------------------------------------- + + +def test_dense_lookup_unchanged(): + assert ( + vmap_batch_for("imaging", "pixelization", "hst") + == VMAP_BATCH[("imaging", "pixelization", "hst")] + ) + + +def test_sparse_falls_back_to_dense_when_unprobed(): + key = ("imaging", "pixelization", "hst") + assert key not in VMAP_BATCH_SPARSE # precondition: sparse row unprobed + assert vmap_batch_for(*key, path="sparse") == VMAP_BATCH[key] + + +def test_sparse_row_wins_when_present(monkeypatch): + key = ("imaging", "pixelization", "hst") + monkeypatch.setitem(VMAP_BATCH_SPARSE, key, 32) + assert vmap_batch_for(*key, path="sparse") == 32 + assert vmap_batch_for(*key) == VMAP_BATCH[key] # dense unaffected + + +def test_intentional_none_preserved(): + assert vmap_batch_for("datacube", "delaunay", "sma") is None + assert vmap_batch_for("interferometer", "mge", "alma") is None + + +# --------------------------------------------------------------------------- +# resolve_vmap_batch — probe JSON over table +# --------------------------------------------------------------------------- + + +def test_resolve_without_output_dir_uses_table(): + batch, source = resolve_vmap_batch("imaging", "pixelization", "hst") + assert batch == VMAP_BATCH[("imaging", "pixelization", "hst")] + assert source == "table" + + +def test_resolve_prefers_matching_probe(tmp_path): + _write_probe(tmp_path, recommended=24) + batch, source = resolve_vmap_batch("imaging", "pixelization", "hst", output_dir=tmp_path) + assert batch == 24 + assert source.startswith("probe") + + +def test_resolve_sparse_probe_selected_by_path(tmp_path): + _write_probe(tmp_path, recommended=24) # dense probe + _write_probe(tmp_path, sparse=True, recommended=40) + batch, source = resolve_vmap_batch( + "imaging", "pixelization", "hst", output_dir=tmp_path, path="sparse" + ) + assert batch == 40 + assert "sparse" in source + + +def test_resolve_ignores_cell_mismatch(tmp_path): + _write_probe(tmp_path, instrument="euclid") # probe for a different instrument + batch, source = resolve_vmap_batch("imaging", "pixelization", "hst", output_dir=tmp_path) + assert batch == VMAP_BATCH[("imaging", "pixelization", "hst")] + assert "mismatch" in source + + +def test_resolve_ignores_invalid_recommendation(tmp_path): + _write_probe(tmp_path, recommended=None) + batch, source = resolve_vmap_batch("imaging", "pixelization", "hst", output_dir=tmp_path) + assert batch == VMAP_BATCH[("imaging", "pixelization", "hst")] + assert "no valid recommendation" in source + + +def test_resolve_survives_corrupt_json(tmp_path): + (tmp_path / "vmap_probe_pixelization.json").write_text("{not json") + batch, source = resolve_vmap_batch("imaging", "pixelization", "hst", output_dir=tmp_path) + assert batch == VMAP_BATCH[("imaging", "pixelization", "hst")] + assert "unreadable" in source + + +def test_resolve_missing_probe_uses_table(tmp_path): + batch, source = resolve_vmap_batch("imaging", "pixelization", "hst", output_dir=tmp_path) + assert batch == VMAP_BATCH[("imaging", "pixelization", "hst")] + assert source == "table" diff --git a/vram/README.md b/vram/README.md index e89397d..4090df3 100644 --- a/vram/README.md +++ b/vram/README.md @@ -30,7 +30,8 @@ From `vram`: | Name | Purpose | |------|---------| -| `vmap_batch_for(dataset, model, instrument)` | Return per-cell batch_size (or `None` if vmap is intentionally skipped or the cell hasn't been probed). | +| `vmap_batch_for(dataset, model, instrument, path="dense")` | Return per-cell batch_size (or `None` if vmap is intentionally skipped or the cell hasn't been probed). `path="sparse"` consults `VMAP_BATCH_SPARSE`, falling back to the dense row while unprobed. | +| `resolve_vmap_batch(dataset, model, instrument, output_dir=None, path="dense")` | The runtime cells' entry point: prefer a fresh, cell-matching `vmap_probe_[_sparse].json` in `output_dir` over the table. Returns `(batch_size, source)`. | | `probe_vmap_memory(func, args, batch_sizes=(2, 4))` | JIT-vmap `func` at each batch, read `compiled.memory_analysis()`, return a `ProbeResult`. | | `recommend_batch_size(probe, vram_budget_gb=70.0, max_batch=64)` | Linear extrapolation → max batch fitting in budget. | | `write_probe_json(probe, recommended, path)` | Serialise probe + recommendation to JSON. | @@ -80,16 +81,45 @@ XLA recompiles for each new batch_size (no cache reuse). Compile cost varies: `batch_sizes=(1,)` and accept linear extrapolation. If the chosen batch OOMs at run time, manually re-probe at a smaller batch. +## Batch-size resolution order (what a runtime cell actually uses) + +1. **Fresh probe JSON** — `results/runtime///vmap_probe_[_sparse].json`, + written by a `--vmap-probe` run (Phase A of the A100 submits). Used only if + its `dataset`/`model`/`instrument` fields match the cell, so a stale table + can never OOM a job whose submit re-probed. +2. **The curated table** — `VMAP_BATCH` (dense) / `VMAP_BATCH_SPARSE` + (w-tilde sparse-operator rows; unprobed sparse cells fall back to the dense + value, which is conservative — the sparse path's per-replica footprint has + been smaller on every cell measured). +3. `None` → the cell script skips vmap with a logged reason. + +Each script logs which source won. Table provenance lives in +`vram.config.PROVENANCE` — check it before a campaign trusts the numbers. + +## Probe-only submits + ingest + +`hpc/batch_gpu/submit_probe_fast_a100` (mge/pixelization, ~minutes) and +`hpc/batch_gpu/submit_probe_delaunay_a100` (delaunay, hours) re-probe every +PreOptimizationTimes campaign vmap cell — dense + sparse imaging variants — +against current source. Ingest after the jobs finish: + +1. Commit the probe JSONs from the HPC checkout (they land under + `results/runtime///`). +2. Copy each `recommended_batch_size` into `VMAP_BATCH` / + `VMAP_BATCH_SPARSE` in `vram/config.py` (keep the per-row MB/replica + comments) and update `PROVENANCE`. +3. Re-run the regular profile to confirm the chosen batch holds at steady + state — the cuFFT-scratch halvings noted in the table comments came from + exactly this step, static analysis alone missed them. + ## Adding a new instrument 1. Add an entry to the appropriate `INSTRUMENTS` dict in `simulators/{imaging,interferometer}.py`. -2. Create a probe SLURM script at - `z_projects/profiling/hpc/batch_gpu/probe_vmap__` - (clone any existing one). -3. Run the probe; pull the resulting JSON to - `output/runtime///vmap_probe.json`. -4. Read `recommended_batch_size` and add the row to `VMAP_BATCH` in - `vram/config.py`. +2. Add the cell to a probe-only submit in `hpc/batch_gpu/` (clone a + `run_probe` line). +3. Run the probe; the JSON lands at + `results/runtime///vmap_probe_[_sparse].json`. +4. Ingest per "Probe-only submits + ingest" above. 5. Re-run the regular profile to confirm the chosen batch holds at steady state (vmap completes, doesn't OOM). diff --git a/vram/__init__.py b/vram/__init__.py index 2724d07..3dbe861 100644 --- a/vram/__init__.py +++ b/vram/__init__.py @@ -14,7 +14,13 @@ See ``vram/README.md`` for methodology and how to extend. """ -from vram.config import VMAP_BATCH, vmap_batch_for +from vram.config import ( + PROVENANCE, + VMAP_BATCH, + VMAP_BATCH_SPARSE, + resolve_vmap_batch, + vmap_batch_for, +) from vram.probe import ( ProbeResult, probe_vmap_memory, @@ -23,7 +29,10 @@ ) __all__ = [ + "PROVENANCE", "VMAP_BATCH", + "VMAP_BATCH_SPARSE", + "resolve_vmap_batch", "vmap_batch_for", "ProbeResult", "probe_vmap_memory", diff --git a/vram/config.py b/vram/config.py index 67fbe19..7107f9c 100644 --- a/vram/config.py +++ b/vram/config.py @@ -123,9 +123,34 @@ } -def vmap_batch_for(dataset: str, model: str, instrument: str) -> int | None: +# Sparse-operator (w-tilde) rows. The sparse inversion path has a different +# per-replica memory profile than dense mapping, so it gets its own table +# rather than a fourth key element (keeps VMAP_BATCH's shape stable for the +# many existing readers). Unprobed sparse cells fall back to the dense row — +# conservative, since the sparse path's per-replica footprint is smaller for +# every cell measured so far. Populated by the probe-only SLURM submits +# (``hpc/batch_gpu/submit_probe_*``) as campaign probes come in. +VMAP_BATCH_SPARSE: dict[tuple[str, str, str], int | None] = {} + + +# Where each table's numbers came from — checked before a campaign trusts +# them. Update whenever probe results are ingested. +PROVENANCE: dict[str, str] = { + "VMAP_BATCH": ( + "probed 2026-05-24 on RAL A100 80GB PCIe (PyAutoLens ~2026.5.x); " + "manual halvings for cuFFT scratch failures noted per row" + ), + "VMAP_BATCH_SPARSE": "unprobed — falls back to dense rows", +} + + +def vmap_batch_for(dataset: str, model: str, instrument: str, path: str = "dense") -> int | None: """Return the per-(dataset, model, instrument) vmap batch_size for A100. + ``path`` selects the inversion path: ``"dense"`` (mapping-matrix, the + default) or ``"sparse"`` (w-tilde sparse operator). Sparse cells without + a probed row fall back to the dense value. + Returns ``None`` when vmap is intentionally skipped (cube cells), blocked (interferometer mge/pix at ALMA+), or when the cell hasn't been probed yet. Callers should default to a small fallback (typically @@ -133,4 +158,60 @@ def vmap_batch_for(dataset: str, model: str, instrument: str) -> int | None: entirely when ``None`` is returned for a known-blocked or known-skipped cell. """ - return VMAP_BATCH.get((dataset, model, instrument)) + key = (dataset, model, instrument) + if path == "sparse" and key in VMAP_BATCH_SPARSE: + return VMAP_BATCH_SPARSE[key] + return VMAP_BATCH.get(key) + + +def resolve_vmap_batch( + dataset: str, + model: str, + instrument: str, + output_dir=None, + path: str = "dense", +) -> tuple[int | None, str]: + """Resolve the vmap batch_size, preferring a fresh probe over the table. + + Resolution order (returns ``(batch_size, source)`` where ``source`` + says which step won, for the run log): + + 1. ``/vmap_probe_[_sparse].json`` — written by a + ``--vmap-probe`` run (Phase A of the A100 submit scripts). Used only + if its ``dataset``/``model``/``instrument`` fields match this cell, + so a stale table can never OOM a job whose submit re-probed. + 2. The curated table (``vmap_batch_for``, including the sparse + fallback-to-dense rule). + + A probe JSON that cannot be read or does not match is ignored (with the + mismatch reported in ``source``), never fatal — the table is the + fallback, not the probe. + """ + import json + from pathlib import Path + + if output_dir is not None: + suffix = "_sparse" if path == "sparse" else "" + probe_path = Path(output_dir) / f"vmap_probe_{model}{suffix}.json" + if probe_path.exists(): + try: + data = json.loads(probe_path.read_text()) + except (OSError, ValueError) as exc: + return ( + vmap_batch_for(dataset, model, instrument, path=path), + f"table (probe JSON unreadable: {exc})", + ) + matches = ( + data.get("dataset") == dataset + and data.get("model") == model + and data.get("instrument") == instrument + ) + recommended = data.get("recommended_batch_size") + if matches and isinstance(recommended, int) and recommended >= 1: + return recommended, f"probe ({probe_path.name})" + return ( + vmap_batch_for(dataset, model, instrument, path=path), + f"table (probe JSON ignored: " + f"{'cell mismatch' if not matches else 'no valid recommendation'})", + ) + return vmap_batch_for(dataset, model, instrument, path=path), "table" From f3ea9e807d4562547128a4b121c4f63c2020dcc8 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Wed, 8 Jul 2026 17:01:54 +0100 Subject: [PATCH 02/10] maint: pinned likelihood checks record-and-flag, never crash (#54) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Design decision (user-directed): autolens_profiling's pinned values guard benchmark comparability, not library correctness — that remit belongs to autolens_workspace_test. A hard assert conflated "computation changed" with "profiling run failed" and could kill a multi-hour A100 job at the final assertion. - _profile_cli.check_pinned(): compare against the pin, warn loudly on drift, return a drift record; record_pinned_check() merges pinned_expected + pinned_drift into the already-written result JSON — the machine surface PyAutoHeart's vitals scan will read (feature/pyautoheart/profiling_drift_check.md filed) - all 9 runtime cells converted; eager ≡ JIT ≡ vmap cross-consistency assertions stay hard (they invalidate the measurement itself) - imaging mge/pixelization/delaunay pins made per-instrument first (the old module-level HST constants were asserted for every --instrument, failing any non-hst run by construction — flushed by this sweep) - vram: non-GPU backends clamp resolved batches to 3 (A100 table applied on laptop CPU OOM-killed the sweep); probes record their backend and resolution ignores probes from a different backend; +12 tests (25/25) - boundary rule recorded in results/notes/design_lock_in.md Co-Authored-By: Claude Fable 5 --- _profile_cli.py | 47 ++++++++ dataset/imaging/euclid/data.fits | Bin 0 -> 46080 bytes dataset/imaging/euclid/noise_map.fits | Bin 0 -> 46080 bytes dataset/imaging/euclid/positions.json | 28 +++++ dataset/imaging/euclid/psf.fits | Bin 0 -> 8640 bytes dataset/imaging/euclid/tracer.json | 110 ++++++++++++++++++ likelihood_runtime/datacube/delaunay.py | 43 +++---- likelihood_runtime/imaging/delaunay.py | 71 ++++++----- likelihood_runtime/imaging/mge.py | 71 ++++++----- likelihood_runtime/imaging/pixelization.py | 71 ++++++----- likelihood_runtime/interferometer/delaunay.py | 51 ++++---- likelihood_runtime/interferometer/mge.py | 50 ++++---- .../interferometer/pixelization.py | 50 ++++---- .../point_source/image_plane.py | 66 +++++------ .../point_source/source_plane.py | 50 ++++---- results/notes/design_lock_in.md | 20 ++++ test/test_vram_config.py | 55 +++++++++ vram/config.py | 36 +++++- vram/probe.py | 3 + 19 files changed, 578 insertions(+), 244 deletions(-) create mode 100644 dataset/imaging/euclid/data.fits create mode 100644 dataset/imaging/euclid/noise_map.fits create mode 100644 dataset/imaging/euclid/positions.json create mode 100644 dataset/imaging/euclid/psf.fits create mode 100644 dataset/imaging/euclid/tracer.json diff --git a/_profile_cli.py b/_profile_cli.py index e23e0bb..934c6bb 100644 --- a/_profile_cli.py +++ b/_profile_cli.py @@ -257,3 +257,50 @@ def auto_simulate_if_missing( ], check=True, ) + + +def check_pinned(got, expected, *, label: str, rtol: float = 1e-4): + """Compare a computed likelihood/evidence against its pinned baseline. + + Profiling runs **record and flag** drift; they never adjudicate library + correctness (that is autolens_workspace_test's remit — see + ``results/notes/design_lock_in.md``). Returns ``None`` when ``got`` is + within ``rtol`` of ``expected``; otherwise prints a loud warning and + returns a drift record for the result JSON, which PyAutoHeart's vitals + scan picks up. Never raises — a changed computation must not kill a + profiling job (the timing numbers are still data; the flag marks them + non-comparable to the pinned baseline). + """ + got_f = float(got) + rel = abs(got_f - expected) / max(abs(expected), 1e-300) + if rel <= rtol: + return None + print( + f" WARNING: PINNED-VALUE DRIFT [{label}] — got {got_f!r}, " + f"pinned {expected!r} (rel diff {rel:.3e} > rtol {rtol:g}). " + f"Timings from this run are NOT comparable to the pinned baseline; " + f"file a bug / check autolens_workspace_test before trusting trends." + ) + return { + "label": label, + "expected": expected, + "got": got_f, + "rel_diff": rel, + "rtol": rtol, + } + + +def record_pinned_check(json_path, expected, drift_records) -> None: + """Merge the pinned-value check outcome into an already-written result JSON. + + Adds ``pinned_expected`` (the baseline value, or ``None`` when the + instrument has no pin) and ``pinned_drift`` (list of drift records — + empty means every compared value matched). The guard blocks run after + the summary JSON is written, so this rewrites the file in place. + """ + import json + + data = json.loads(json_path.read_text()) + data["pinned_expected"] = expected + data["pinned_drift"] = drift_records + json_path.write_text(json.dumps(data, indent=2)) diff --git a/dataset/imaging/euclid/data.fits b/dataset/imaging/euclid/data.fits new file mode 100644 index 0000000000000000000000000000000000000000..474839a8d9a1d76e4fc0263c483ddf20e6a2f687 GIT binary patch literal 46080 zcmeI*Yp|Zxbsum^VBt$BI#@1taIo4L@MWAD+=R?HL-yk~sl`ME`9cRzlZkL_GR_&m z1v*tY8kcnBPBLo86{fUU!DH7om`S4|MWT=ga1K_TFo+z5f5b*4q2oulVf6|9s(s-+ITz%y= z*Isqqyz8%-_s=f=+-K)qcl{MtfANZIzxW32Ctv-Oi$AyU;>+Jq-Y;MHE0glBxc1sB z{%GD8uekn-dDs8ZSHASdQl{Qs^2y6D{_N!A8|wG*NqJZQ>#M%-rEBM1b0x`NzUoU? zU-#u}uD)(c$p7v<=X2hT8a{CP%KlS~&OG>Nn%Cg#Sei^CdLhZ)mx%~H! zJ@|#=x12w|d}#1Dw=Avw7nk-tr~H>KV_*I4v$l{sesjnt_dB)U9QD~g`q9rgWku72 zKBdW9Okes&cHhyD{@zXZ=Dfaj=cjjm?VLMvT4H< zKm51tnes;K`vueN!G1BHadz+7&HTrkUJFxh=&8Q{9vG^-!`e5g2JTR~SaqHmkztXeBbJ`_%k@F3H=KPG0Kj!Bd zzxwvI9<*Cl=Q*ETWv&-`(x4qUX)nx)|2tFeIF;uZ%W+74r+fl6-9Tn4f-4yEe}$ ze`_4=wPPR8O8T_+3(Nn$@V0@6OB_c-FL>)0`muu&zs)ak4ZEZrLqF?D`?N>!MLoNW zuc-Pg%yxZ0V4fjA@ejS}V?57kXY(TmAJ~~*P5wf9l~`A4H+)xGch;qnp6Wd-wKG0_ zRz+OZPd)vuQ+)Wrx3Bud>?gjRaceYi4P5xGv~SST;tKpJe1^V?>+tRsQJ$Qg?-5x4gB#-zuO}}BbVO#YtJ6!X{TPEq`v=Ne|G4% zrw6%8`Bt0XjqC(Ze9QQ5eDgDI*lQZU*qc3-F+O~3 zvv)rZSl8Z@LOHYr?tJQ1Ylbe26J0&~fp?>^^_nzU659Xua*wEl_ z4!m<7{rPFHgdck@4>_JgkMvLMt6lU@yqkWD(w;GX>J#H;`__LU`ROP0>yOWF>`jl+ zSg)R=AO05d*ipMEr?pex_uKdrd4T*U%N13Rh3QYMTlNn7<#=O5gTFcQf?NmM8!zU~ zZt~yEi{Qczp7mF<1AOC|TsUN&6L{wSqCIzo-wYm(H~n_xyy≤F0TQS+&#rqMp9q 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 literal 0 HcmV?d00001 diff --git a/dataset/imaging/euclid/noise_map.fits b/dataset/imaging/euclid/noise_map.fits new file mode 100644 index 0000000000000000000000000000000000000000..c8fa54c4e5b176ba7877d783b305272529336e2c GIT binary patch literal 46080 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 result JSON: profiling records and flags drift, +# never adjudicates library correctness (autolens_workspace_test's remit; +# boundary rule in results/notes/design_lock_in.md). PyAutoHeart's vitals +# scan reads the pinned_drift field. +record_pinned_check(dict_path, _pinned_expected, _pinned_drift) +if _pinned_expected is not None and not _pinned_drift: + print(" Pinned-value check PASSED (recorded in result JSON).") diff --git a/likelihood_runtime/imaging/delaunay.py b/likelihood_runtime/imaging/delaunay.py index 869bcc0..ef762ee 100644 --- a/likelihood_runtime/imaging/delaunay.py +++ b/likelihood_runtime/imaging/delaunay.py @@ -73,10 +73,12 @@ # Sweep-driver CLI args (--config-name / --output-dir / --use-mixed-precision). # Tolerates extra/unknown args via parse_known_args inside the helper. -from _profile_cli import ( # noqa: E402 +from _profile_cli import ( auto_simulate_if_missing, + check_pinned, device_info_dict, parse_profile_cli, + record_pinned_check, # noqa: E402 resolve_output_paths, ) from simulators.imaging import INSTRUMENTS # noqa: E402 @@ -477,6 +479,7 @@ def full_pipeline_from_params(params_tree): output_dir=_cli.output_dir or (_workspace_root / "results" / "runtime" / "imaging" / "delaunay"), path="sparse" if _cli.use_sparse_operator else "dense", + backend=jax.default_backend(), ) print(f" vmap batch_size: {_batch_resolved} (source: {_batch_source})") batch_size = _batch_resolved or 3 @@ -626,31 +629,45 @@ def full_pipeline_from_params(params_tree): # matches imaging/pixelization (adaptive meshes amplify fp drift through # Cholesky / log_det). vmap result asserted only when DELAUNAY_VMAP=1 # (vmap compile takes 20+ min). -EXPECTED_LOG_EVIDENCE_HST = ( - 29110.92085793 # 1500-pixel Hilbert/Delaunay, MGE-60 lens, adapt_image=lensed_source -) +# Pinned empirically per instrument (mirrors interferometer cells); missing +# means "skip the assertion and print the value so it can be pasted in here +# on a clean run". The old module-level EXPECTED_LOG_EVIDENCE_HST constant +# was asserted for every --instrument, which failed any non-hst run by +# construction (phase-2 local validation sweep, autolens_profiling#54). +# The eager ≡ JIT ≡ vmap cross-consistency assertions still guard every +# instrument. +_pinned_drift: list = [] +_pinned_expected = None + +EXPECTED_LOG_EVIDENCE = { + "hst": 29110.92085793 # 1500-pixel Hilbert/Delaunay, MGE-60 lens, adapt_image=lensed_source +} -np.testing.assert_allclose( - log_evidence_ref, - EXPECTED_LOG_EVIDENCE_HST, - rtol=1e-4, - err_msg=( - f"imaging/delaunay[{instrument}]: regression — eager log_evidence drifted " - f"(got {log_evidence_ref}, expected {EXPECTED_LOG_EVIDENCE_HST})" - ), -) -print(f" Eager regression assertion PASSED: log_evidence matches {EXPECTED_LOG_EVIDENCE_HST:.6f}") -np.testing.assert_allclose( - float(full_result), - EXPECTED_LOG_EVIDENCE_HST, - rtol=1e-3, - err_msg=f"imaging/delaunay[{instrument}]: regression — full log_evidence drifted", -) -if not _vmap_skipped: - np.testing.assert_allclose( - np.array(result_vmap), - EXPECTED_LOG_EVIDENCE_HST, - rtol=1e-3, - err_msg=f"imaging/delaunay[{instrument}]: regression — vmap log_evidence drifted", +expected_log_evidence = EXPECTED_LOG_EVIDENCE.get(instrument) +_pinned_expected = expected_log_evidence + +if expected_log_evidence is None: + print( + f" Regression assertion SKIPPED for {instrument} " + f"(no pinned value). Eager log_evidence = {log_evidence_ref}" ) -print(f" Regression assertion PASSED: log_evidence matches {EXPECTED_LOG_EVIDENCE_HST:.6f}") +else: + _rec = check_pinned(log_evidence_ref, _pinned_expected, label="eager", rtol=1e-4) + if _rec is not None: + _pinned_drift.append(_rec) + _rec = check_pinned(float(full_result), _pinned_expected, label="full", rtol=1e-3) + if _rec is not None: + _pinned_drift.append(_rec) + if not _vmap_skipped: + _rec = check_pinned(np.array(result_vmap), _pinned_expected, label="vmap", rtol=1e-3) + if _rec is not None: + _pinned_drift.append(_rec) + + +# Pinned-value outcome -> result JSON: profiling records and flags drift, +# never adjudicates library correctness (autolens_workspace_test's remit; +# boundary rule in results/notes/design_lock_in.md). PyAutoHeart's vitals +# scan reads the pinned_drift field. +record_pinned_check(dict_path, _pinned_expected, _pinned_drift) +if _pinned_expected is not None and not _pinned_drift: + print(" Pinned-value check PASSED (recorded in result JSON).") diff --git a/likelihood_runtime/imaging/mge.py b/likelihood_runtime/imaging/mge.py index 7f2c9fc..9464691 100644 --- a/likelihood_runtime/imaging/mge.py +++ b/likelihood_runtime/imaging/mge.py @@ -80,10 +80,12 @@ # Sweep-driver CLI args (--config-name / --output-dir / --use-mixed-precision). # Tolerates extra/unknown args via parse_known_args inside the helper. sys.path.insert(0, str(Path(__file__).resolve().parents[2])) -from _profile_cli import ( # noqa: E402 +from _profile_cli import ( auto_simulate_if_missing, + check_pinned, device_info_dict, parse_profile_cli, + record_pinned_check, # noqa: E402 resolve_output_paths, ) from simulators.imaging import INSTRUMENTS # noqa: E402 @@ -443,6 +445,7 @@ def full_pipeline_from_params(params_tree): instrument, output_dir=_cli.output_dir or (_workspace_root / "results" / "runtime" / "imaging" / "mge"), path="sparse" if _cli.use_sparse_operator else "dense", + backend=jax.default_backend(), ) print(f" vmap batch_size: {_batch_resolved} (source: {_batch_source})") batch_size = _batch_resolved or 3 @@ -576,30 +579,44 @@ def full_pipeline_from_params(params_tree): # default centre/ell_comps priors) put the evaluation point at the # physically-meaningful truth operating point. Eager, JIT, and vmap all # agree to ~1e-11 precision. -EXPECTED_LOG_LIKELIHOOD_HST = 27379.38890685539 +# Pinned empirically per instrument (mirrors interferometer/mge.py); +# ``None``/missing means "skip the assertion and print the value so it can +# be pasted in here on a clean run". Only hst has a pinned value — the old +# module-level EXPECTED_LOG_LIKELIHOOD_HST constant was asserted for every +# --instrument, which failed any non-hst run by construction (found by the +# phase-2 local validation sweep, autolens_profiling#54). The eager ≡ JIT ≡ +# vmap cross-consistency assertions above still guard every instrument. +_pinned_drift: list = [] +_pinned_expected = None + +EXPECTED_LOG_LIKELIHOOD = { + "hst": 27379.38890685539, +} -np.testing.assert_allclose( - log_likelihood_ref, - EXPECTED_LOG_LIKELIHOOD_HST, - rtol=1e-4, - err_msg=( - f"imaging/mge[{instrument}]: regression — eager log_likelihood drifted " - f"(got {log_likelihood_ref}, expected {EXPECTED_LOG_LIKELIHOOD_HST})" - ), -) -print( - f" Eager regression assertion PASSED: log_likelihood matches {EXPECTED_LOG_LIKELIHOOD_HST:.6f}" -) -np.testing.assert_allclose( - float(full_result), - EXPECTED_LOG_LIKELIHOOD_HST, - rtol=1e-4, - err_msg=f"imaging/mge[{instrument}]: regression — full log_likelihood drifted", -) -np.testing.assert_allclose( - np.array(result_vmap), - EXPECTED_LOG_LIKELIHOOD_HST, - rtol=1e-4, - err_msg=f"imaging/mge[{instrument}]: regression — vmap log_likelihood drifted", -) -print(f" Regression assertion PASSED: log_likelihood matches {EXPECTED_LOG_LIKELIHOOD_HST:.6f}") +expected_log_likelihood = EXPECTED_LOG_LIKELIHOOD.get(instrument) +_pinned_expected = expected_log_likelihood + +if expected_log_likelihood is None: + print( + f" Regression assertion SKIPPED for {instrument} " + f"(no pinned value). Eager log_likelihood = {log_likelihood_ref}" + ) +else: + _rec = check_pinned(log_likelihood_ref, _pinned_expected, label="eager", rtol=1e-4) + if _rec is not None: + _pinned_drift.append(_rec) + _rec = check_pinned(float(full_result), _pinned_expected, label="full", rtol=1e-4) + if _rec is not None: + _pinned_drift.append(_rec) + _rec = check_pinned(np.array(result_vmap), _pinned_expected, label="vmap", rtol=1e-4) + if _rec is not None: + _pinned_drift.append(_rec) + + +# Pinned-value outcome -> result JSON: profiling records and flags drift, +# never adjudicates library correctness (autolens_workspace_test's remit; +# boundary rule in results/notes/design_lock_in.md). PyAutoHeart's vitals +# scan reads the pinned_drift field. +record_pinned_check(dict_path, _pinned_expected, _pinned_drift) +if _pinned_expected is not None and not _pinned_drift: + print(" Pinned-value check PASSED (recorded in result JSON).") diff --git a/likelihood_runtime/imaging/pixelization.py b/likelihood_runtime/imaging/pixelization.py index dc9a9ea..a87d3e7 100644 --- a/likelihood_runtime/imaging/pixelization.py +++ b/likelihood_runtime/imaging/pixelization.py @@ -66,10 +66,12 @@ # Sweep-driver CLI args (--config-name / --output-dir / --use-mixed-precision). # Tolerates extra/unknown args via parse_known_args inside the helper. -from _profile_cli import ( # noqa: E402 +from _profile_cli import ( auto_simulate_if_missing, + check_pinned, device_info_dict, parse_profile_cli, + record_pinned_check, # noqa: E402 resolve_output_paths, ) from simulators.imaging import INSTRUMENTS # noqa: E402 @@ -473,6 +475,7 @@ def full_pipeline_from_params(params_tree): output_dir=_cli.output_dir or (_workspace_root / "results" / "runtime" / "imaging" / "pixelization"), path="sparse" if _cli.use_sparse_operator else "dense", + backend=jax.default_backend(), ) print(f" vmap batch_size: {_batch_resolved} (source: {_batch_source})") batch_size = _batch_resolved or 3 @@ -641,31 +644,45 @@ def full_pipeline_from_params(params_tree): # 1581x1581 mapping matrix relative to the non-adaptive baseline (which # previously matched at 1e-4). The 1e-3 envelope is still tight enough to # catch real numerical regressions while accommodating the adaptive path. -EXPECTED_LOG_EVIDENCE_HST = ( - 28370.27770182 # 39x39 = 1521 source pixels, MGE-60 lens light, adapt_image=lensed_source -) +# Pinned empirically per instrument (mirrors interferometer cells); missing +# means "skip the assertion and print the value so it can be pasted in here +# on a clean run". The old module-level EXPECTED_LOG_EVIDENCE_HST constant +# was asserted for every --instrument, which failed any non-hst run by +# construction (phase-2 local validation sweep, autolens_profiling#54). +# The eager ≡ JIT ≡ vmap cross-consistency assertions still guard every +# instrument. +_pinned_drift: list = [] +_pinned_expected = None + +EXPECTED_LOG_EVIDENCE = { + "hst": 28370.27770182 # 39x39 = 1521 source pixels, MGE-60 lens light, adapt_image=lensed_source +} -np.testing.assert_allclose( - log_evidence_ref, - EXPECTED_LOG_EVIDENCE_HST, - rtol=1e-4, - err_msg=( - f"imaging/pixelization[{instrument}]: regression — eager log_evidence drifted " - f"(got {log_evidence_ref}, expected {EXPECTED_LOG_EVIDENCE_HST})" - ), -) -print(f" Eager regression assertion PASSED: log_evidence matches {EXPECTED_LOG_EVIDENCE_HST:.6f}") -np.testing.assert_allclose( - float(full_result), - EXPECTED_LOG_EVIDENCE_HST, - rtol=1e-3, - err_msg=f"imaging/pixelization[{instrument}]: regression — full log_evidence drifted", -) -if result_vmap is not None: - np.testing.assert_allclose( - np.array(result_vmap), - EXPECTED_LOG_EVIDENCE_HST, - rtol=1e-3, - err_msg=f"imaging/pixelization[{instrument}]: regression — vmap log_evidence drifted", +expected_log_evidence = EXPECTED_LOG_EVIDENCE.get(instrument) +_pinned_expected = expected_log_evidence + +if expected_log_evidence is None: + print( + f" Regression assertion SKIPPED for {instrument} " + f"(no pinned value). Eager log_evidence = {log_evidence_ref}" ) -print(f" Regression assertion PASSED: log_evidence matches {EXPECTED_LOG_EVIDENCE_HST:.6f}") +else: + _rec = check_pinned(log_evidence_ref, _pinned_expected, label="eager", rtol=1e-4) + if _rec is not None: + _pinned_drift.append(_rec) + _rec = check_pinned(float(full_result), _pinned_expected, label="full", rtol=1e-3) + if _rec is not None: + _pinned_drift.append(_rec) + if result_vmap is not None: + _rec = check_pinned(np.array(result_vmap), _pinned_expected, label="vmap", rtol=1e-3) + if _rec is not None: + _pinned_drift.append(_rec) + + +# Pinned-value outcome -> result JSON: profiling records and flags drift, +# never adjudicates library correctness (autolens_workspace_test's remit; +# boundary rule in results/notes/design_lock_in.md). PyAutoHeart's vitals +# scan reads the pinned_drift field. +record_pinned_check(dict_path, _pinned_expected, _pinned_drift) +if _pinned_expected is not None and not _pinned_drift: + print(" Pinned-value check PASSED (recorded in result JSON).") diff --git a/likelihood_runtime/interferometer/delaunay.py b/likelihood_runtime/interferometer/delaunay.py index 9da4c33..bd2a8a0 100644 --- a/likelihood_runtime/interferometer/delaunay.py +++ b/likelihood_runtime/interferometer/delaunay.py @@ -114,10 +114,12 @@ # Sweep-driver CLI args (--config-name / --output-dir / --use-mixed-precision). # Tolerates extra/unknown args via parse_known_args inside the helper. -from _profile_cli import ( # noqa: E402 +from _profile_cli import ( auto_simulate_if_missing, + check_pinned, device_info_dict, parse_profile_cli, + record_pinned_check, # noqa: E402 resolve_output_paths, ) from simulators.interferometer import INSTRUMENTS # noqa: E402 @@ -480,6 +482,7 @@ def full_pipeline_from_params(params_tree): output_dir=_cli.output_dir or (_workspace_root / "results" / "runtime" / "interferometer" / "delaunay"), path="sparse" if _cli.use_sparse_operator else "dense", + backend=jax.default_backend(), ) print(f" vmap batch_size: {_batch_resolved} (source: {_batch_source})") batch_size = _batch_resolved or 3 @@ -662,6 +665,9 @@ def full_pipeline_from_params(params_tree): # print the value so it can be pasted in here on a clean run". sma was # bumped to mask_radius=3.5 in 2026-05-21's INSTRUMENTS refactor — the # old mask_radius=3.0 value no longer applies and needs re-measuring. +_pinned_drift: list = [] +_pinned_expected = None + EXPECTED_LOG_EVIDENCE = { "sma": None, "alma": None, @@ -669,6 +675,7 @@ def full_pipeline_from_params(params_tree): } expected_log_evidence = EXPECTED_LOG_EVIDENCE.get(instrument) +_pinned_expected = expected_log_evidence if expected_log_evidence is None: print( @@ -677,28 +684,22 @@ def full_pipeline_from_params(params_tree): f"and paste it into EXPECTED_LOG_EVIDENCE[{instrument!r}]." ) else: - np.testing.assert_allclose( - figure_of_merit_ref, - expected_log_evidence, - rtol=1e-4, - err_msg=( - f"interferometer/delaunay[{instrument}]: regression — eager log_evidence " - f"drifted (got {figure_of_merit_ref}, expected {expected_log_evidence})" - ), - ) - print(f" Eager regression assertion PASSED: log_evidence matches {expected_log_evidence:.6f}") - np.testing.assert_allclose( - float(full_result), - expected_log_evidence, - rtol=1e-3, - err_msg=f"interferometer/delaunay[{instrument}]: regression — full log_evidence drifted", - ) - print(" Full-pipeline regression assertion PASSED") + _rec = check_pinned(figure_of_merit_ref, _pinned_expected, label="eager", rtol=1e-4) + if _rec is not None: + _pinned_drift.append(_rec) + _rec = check_pinned(float(full_result), _pinned_expected, label="full", rtol=1e-3) + if _rec is not None: + _pinned_drift.append(_rec) if result_vmap is not None: - np.testing.assert_allclose( - np.array(result_vmap), - expected_log_evidence, - rtol=1e-3, - err_msg=f"interferometer/delaunay[{instrument}]: regression — vmap log_evidence drifted", - ) - print(" vmap regression assertion PASSED") + _rec = check_pinned(np.array(result_vmap), _pinned_expected, label="vmap", rtol=1e-3) + if _rec is not None: + _pinned_drift.append(_rec) + + +# Pinned-value outcome -> result JSON: profiling records and flags drift, +# never adjudicates library correctness (autolens_workspace_test's remit; +# boundary rule in results/notes/design_lock_in.md). PyAutoHeart's vitals +# scan reads the pinned_drift field. +record_pinned_check(dict_path, _pinned_expected, _pinned_drift) +if _pinned_expected is not None and not _pinned_drift: + print(" Pinned-value check PASSED (recorded in result JSON).") diff --git a/likelihood_runtime/interferometer/mge.py b/likelihood_runtime/interferometer/mge.py index b3b00a9..a63c3f4 100644 --- a/likelihood_runtime/interferometer/mge.py +++ b/likelihood_runtime/interferometer/mge.py @@ -77,10 +77,12 @@ # Plus this script's own --use-dft override to compare NUFFT against the # historical DFT baseline on SMA. sys.path.insert(0, str(Path(__file__).resolve().parents[2])) -from _profile_cli import ( # noqa: E402 +from _profile_cli import ( auto_simulate_if_missing, + check_pinned, device_info_dict, parse_profile_cli, + record_pinned_check, # noqa: E402 resolve_output_paths, ) from simulators.interferometer import INSTRUMENTS # noqa: E402 @@ -373,6 +375,7 @@ def full_pipeline_from_params(params_tree): output_dir=_cli.output_dir or (_workspace_root / "results" / "runtime" / "interferometer" / "mge"), path="sparse" if _cli.use_sparse_operator else "dense", + backend=jax.default_backend(), ) print(f" vmap batch_size: {_batch_resolved} (source: {_batch_source})") batch_size = _batch_resolved or 3 @@ -566,6 +569,9 @@ def full_pipeline_from_params(params_tree): # print the value so it can be pasted in here on a clean run". sma was # bumped to mask_radius=3.5 in 2026-05-21's INSTRUMENTS refactor — the # old mask_radius=3.0 value no longer applies and needs re-measuring. +_pinned_drift: list = [] +_pinned_expected = None + EXPECTED_LOG_LIKELIHOOD = { "sma": None, "alma": None, @@ -573,6 +579,7 @@ def full_pipeline_from_params(params_tree): } expected_log_likelihood = EXPECTED_LOG_LIKELIHOOD.get(instrument) +_pinned_expected = expected_log_likelihood if expected_log_likelihood is None: print( @@ -585,26 +592,23 @@ def full_pipeline_from_params(params_tree): # the rtol when the user asked for mixed precision. _regression_rtol = 1e-3 if _cli.use_mixed_precision else 1e-4 transformer_label = "DFT" if USE_DFT else "NUFFT" - np.testing.assert_allclose( - log_likelihood_ref, - expected_log_likelihood, - rtol=_regression_rtol, - err_msg=( - f"interferometer/mge[{instrument}, {transformer_label}]: " - f"regression — eager log_likelihood drifted " - f"(got {log_likelihood_ref}, expected {expected_log_likelihood})" - ), - ) - np.testing.assert_allclose( - float(full_result), - expected_log_likelihood, - rtol=_regression_rtol, - err_msg=f"interferometer/mge[{instrument}]: regression — full log_likelihood drifted", - ) - np.testing.assert_allclose( - np.array(result_vmap), - expected_log_likelihood, - rtol=_regression_rtol, - err_msg=f"interferometer/mge[{instrument}]: regression — vmap log_likelihood drifted", + _rec = check_pinned(log_likelihood_ref, _pinned_expected, label="eager", rtol=_regression_rtol) + if _rec is not None: + _pinned_drift.append(_rec) + _rec = check_pinned(float(full_result), _pinned_expected, label="full", rtol=_regression_rtol) + if _rec is not None: + _pinned_drift.append(_rec) + _rec = check_pinned( + np.array(result_vmap), _pinned_expected, label="vmap", rtol=_regression_rtol ) - print(f" Regression assertion PASSED: log_likelihood matches {expected_log_likelihood:.6f}") + if _rec is not None: + _pinned_drift.append(_rec) + + +# Pinned-value outcome -> result JSON: profiling records and flags drift, +# never adjudicates library correctness (autolens_workspace_test's remit; +# boundary rule in results/notes/design_lock_in.md). PyAutoHeart's vitals +# scan reads the pinned_drift field. +record_pinned_check(dict_path, _pinned_expected, _pinned_drift) +if _pinned_expected is not None and not _pinned_drift: + print(" Pinned-value check PASSED (recorded in result JSON).") diff --git a/likelihood_runtime/interferometer/pixelization.py b/likelihood_runtime/interferometer/pixelization.py index cd6055a..22175bf 100644 --- a/likelihood_runtime/interferometer/pixelization.py +++ b/likelihood_runtime/interferometer/pixelization.py @@ -75,10 +75,12 @@ # Sweep-driver CLI args (--config-name / --output-dir / --use-mixed-precision). # Tolerates extra/unknown args via parse_known_args inside the helper. -from _profile_cli import ( # noqa: E402 +from _profile_cli import ( auto_simulate_if_missing, + check_pinned, device_info_dict, parse_profile_cli, + record_pinned_check, # noqa: E402 resolve_output_paths, ) from simulators.interferometer import INSTRUMENTS # noqa: E402 @@ -407,6 +409,7 @@ def full_pipeline_from_params(params_tree): output_dir=_cli.output_dir or (_workspace_root / "results" / "runtime" / "interferometer" / "pixelization"), path="sparse" if _cli.use_sparse_operator else "dense", + backend=jax.default_backend(), ) print(f" vmap batch_size: {_batch_resolved} (source: {_batch_source})") batch_size = _batch_resolved or 3 @@ -630,6 +633,9 @@ def full_pipeline_from_params(params_tree): # print the value so it can be pasted in here on a clean run". sma was # bumped to mask_radius=3.5 in 2026-05-21's INSTRUMENTS refactor — the # old mask_radius=3.0 value no longer applies and needs re-measuring. +_pinned_drift: list = [] +_pinned_expected = None + EXPECTED_LOG_EVIDENCE = { "sma": None, "alma": None, @@ -637,6 +643,7 @@ def full_pipeline_from_params(params_tree): } expected_log_evidence = EXPECTED_LOG_EVIDENCE.get(instrument) +_pinned_expected = expected_log_evidence if expected_log_evidence is None: print( @@ -644,27 +651,22 @@ def full_pipeline_from_params(params_tree): f"(no pinned value). Eager log_evidence = {figure_of_merit_ref}" ) else: - np.testing.assert_allclose( - figure_of_merit_ref, - expected_log_evidence, - rtol=1e-4, - err_msg=( - f"interferometer/pixelization[{instrument}]: regression — " - f"eager log_evidence drifted " - f"(got {figure_of_merit_ref}, expected {expected_log_evidence})" - ), - ) - np.testing.assert_allclose( - float(full_result), - expected_log_evidence, - rtol=1e-3, - err_msg=f"interferometer/pixelization[{instrument}]: regression — full log_evidence drifted", - ) + _rec = check_pinned(figure_of_merit_ref, _pinned_expected, label="eager", rtol=1e-4) + if _rec is not None: + _pinned_drift.append(_rec) + _rec = check_pinned(float(full_result), _pinned_expected, label="full", rtol=1e-3) + if _rec is not None: + _pinned_drift.append(_rec) if result_vmap is not None: - np.testing.assert_allclose( - np.array(result_vmap), - expected_log_evidence, - rtol=1e-3, - err_msg=f"interferometer/pixelization[{instrument}]: regression — vmap log_evidence drifted", - ) - print(f" Regression assertion PASSED: log_evidence matches {expected_log_evidence:.6f}") + _rec = check_pinned(np.array(result_vmap), _pinned_expected, label="vmap", rtol=1e-3) + if _rec is not None: + _pinned_drift.append(_rec) + + +# Pinned-value outcome -> result JSON: profiling records and flags drift, +# never adjudicates library correctness (autolens_workspace_test's remit; +# boundary rule in results/notes/design_lock_in.md). PyAutoHeart's vitals +# scan reads the pinned_drift field. +record_pinned_check(dict_path, _pinned_expected, _pinned_drift) +if _pinned_expected is not None and not _pinned_drift: + print(" Pinned-value check PASSED (recorded in result JSON).") diff --git a/likelihood_runtime/point_source/image_plane.py b/likelihood_runtime/point_source/image_plane.py index 0f8ebfe..2318eac 100644 --- a/likelihood_runtime/point_source/image_plane.py +++ b/likelihood_runtime/point_source/image_plane.py @@ -62,10 +62,12 @@ # Sweep-driver CLI args (--config-name / --output-dir / --use-mixed-precision). # Tolerates extra/unknown args via parse_known_args inside the helper. sys.path.insert(0, str(Path(__file__).resolve().parents[2])) -from _profile_cli import ( # noqa: E402 +from _profile_cli import ( auto_simulate_if_missing, + check_pinned, device_info_dict, parse_profile_cli, + record_pinned_check, # noqa: E402 resolve_output_paths, ) from simulators.point_source import INSTRUMENTS # noqa: E402 @@ -327,6 +329,7 @@ def full_pipeline_from_params(params_tree): output_dir=_cli.output_dir or (_workspace_root / "results" / "runtime" / "point_source" / "image_plane"), path="sparse" if _cli.use_sparse_operator else "dense", + backend=jax.default_backend(), ) print(f" vmap batch_size: {_batch_resolved} (source: {_batch_source})") batch_size = _batch_resolved or 3 @@ -376,7 +379,6 @@ def full_pipeline_from_params(params_tree): f"eager={log_likelihood_ref} vs JIT={float(full_result)}" ), ) -print(" Tier 1: eager ≡ JIT assertion PASSED") np.testing.assert_allclose( np.array(result_vmap), @@ -384,7 +386,6 @@ def full_pipeline_from_params(params_tree): rtol=1e-4, err_msg="point_source/image_plane: JIT vs vmap mismatch", ) -print(" Tier 2: JIT ≡ vmap assertion PASSED") # =================================================================== @@ -510,42 +511,29 @@ def full_pipeline_from_params(params_tree): # regenerated; the new value reflects the current truth-aligned # evaluation against the dataset committed in # autolens_workspace_developer@f8a5cef. -EXPECTED_LOG_LIKELIHOOD_IMAGE_PLANE = 7.196577317761017 +_pinned_drift: list = [] +_pinned_expected = None -np.testing.assert_allclose( - log_likelihood_ref, - EXPECTED_LOG_LIKELIHOOD_IMAGE_PLANE, - rtol=1e-4, - err_msg=( - f"point_source/image_plane: regression — eager log_likelihood drifted " - f"(got {log_likelihood_ref}, expected {EXPECTED_LOG_LIKELIHOOD_IMAGE_PLANE})" - ), -) -print( - f" Eager regression assertion PASSED: log_likelihood matches " - f"{EXPECTED_LOG_LIKELIHOOD_IMAGE_PLANE:.6f}" -) -np.testing.assert_allclose( - float(full_result), - EXPECTED_LOG_LIKELIHOOD_IMAGE_PLANE, - rtol=1e-4, - err_msg=( - f"point_source/image_plane: regression — JIT log_likelihood drifted " - f"(got {float(full_result)}, expected {EXPECTED_LOG_LIKELIHOOD_IMAGE_PLANE})" - ), -) -np.testing.assert_allclose( - np.array(result_vmap), - EXPECTED_LOG_LIKELIHOOD_IMAGE_PLANE, - rtol=1e-4, - err_msg=( - f"point_source/image_plane: regression — vmap log_likelihood drifted " - f"(expected {EXPECTED_LOG_LIKELIHOOD_IMAGE_PLANE})" - ), -) -print( - f" Tier 3: regression assertion PASSED: log_likelihood matches " - f"{EXPECTED_LOG_LIKELIHOOD_IMAGE_PLANE:.6f}" -) +EXPECTED_LOG_LIKELIHOOD_IMAGE_PLANE = 7.196577317761017 +_pinned_expected = EXPECTED_LOG_LIKELIHOOD_IMAGE_PLANE + +_rec = check_pinned(log_likelihood_ref, _pinned_expected, label="eager", rtol=1e-4) +if _rec is not None: + _pinned_drift.append(_rec) +_rec = check_pinned(float(full_result), _pinned_expected, label="JIT", rtol=1e-4) +if _rec is not None: + _pinned_drift.append(_rec) +_rec = check_pinned(np.array(result_vmap), _pinned_expected, label="vmap", rtol=1e-4) +if _rec is not None: + _pinned_drift.append(_rec) timer.summary() + + +# Pinned-value outcome -> result JSON: profiling records and flags drift, +# never adjudicates library correctness (autolens_workspace_test's remit; +# boundary rule in results/notes/design_lock_in.md). PyAutoHeart's vitals +# scan reads the pinned_drift field. +record_pinned_check(dict_path, _pinned_expected, _pinned_drift) +if _pinned_expected is not None and not _pinned_drift: + print(" Pinned-value check PASSED (recorded in result JSON).") diff --git a/likelihood_runtime/point_source/source_plane.py b/likelihood_runtime/point_source/source_plane.py index 2f9ccdc..f323889 100644 --- a/likelihood_runtime/point_source/source_plane.py +++ b/likelihood_runtime/point_source/source_plane.py @@ -44,10 +44,12 @@ # Sweep-driver CLI args (--config-name / --output-dir / --use-mixed-precision). # Tolerates extra/unknown args via parse_known_args inside the helper. sys.path.insert(0, str(Path(__file__).resolve().parents[2])) -from _profile_cli import ( # noqa: E402 +from _profile_cli import ( auto_simulate_if_missing, + check_pinned, device_info_dict, parse_profile_cli, + record_pinned_check, # noqa: E402 resolve_output_paths, ) from simulators.point_source import INSTRUMENTS # noqa: E402 @@ -367,6 +369,7 @@ def ray_trace_to_source_plane(params_tree, positions_raw): output_dir=_cli.output_dir or (_workspace_root / "results" / "runtime" / "point_source" / "source_plane"), path="sparse" if _cli.use_sparse_operator else "dense", + backend=jax.default_backend(), ) print(f" vmap batch_size: {_batch_resolved} (source: {_batch_source})") batch_size = _batch_resolved or 3 @@ -563,35 +566,28 @@ def ray_trace_to_source_plane(params_tree, positions_raw): # log-likelihood. The refreshed value reflects the current truth-aligned # evaluation against the dataset committed in # autolens_workspace_developer@f8a5cef. +_pinned_drift: list = [] +_pinned_expected = None + EXPECTED_LOG_LIKELIHOOD_SOURCE_PLANE = -33788.35731127962 +_pinned_expected = EXPECTED_LOG_LIKELIHOOD_SOURCE_PLANE -np.testing.assert_allclose( - log_likelihood_ref, - EXPECTED_LOG_LIKELIHOOD_SOURCE_PLANE, - rtol=1e-4, - err_msg=( - f"point_source/source_plane: regression — eager log_likelihood drifted " - f"(got {log_likelihood_ref}, expected {EXPECTED_LOG_LIKELIHOOD_SOURCE_PLANE})" - ), -) -print( - f" Eager regression assertion PASSED: log_likelihood matches " - f"{EXPECTED_LOG_LIKELIHOOD_SOURCE_PLANE:.6f}" -) +_rec = check_pinned(log_likelihood_ref, _pinned_expected, label="eager", rtol=1e-4) +if _rec is not None: + _pinned_drift.append(_rec) if full_pipeline_jits: - np.testing.assert_allclose( - float(full_result), - EXPECTED_LOG_LIKELIHOOD_SOURCE_PLANE, - rtol=1e-4, - err_msg=( - f"point_source/source_plane: regression — JIT log_likelihood drifted " - f"(got {float(full_result)}, expected {EXPECTED_LOG_LIKELIHOOD_SOURCE_PLANE})" - ), - ) - print( - f" JIT regression assertion PASSED: log_likelihood matches " - f"{EXPECTED_LOG_LIKELIHOOD_SOURCE_PLANE:.6f}" - ) + _rec = check_pinned(float(full_result), _pinned_expected, label="JIT", rtol=1e-4) + if _rec is not None: + _pinned_drift.append(_rec) timer.summary() + + +# Pinned-value outcome -> result JSON: profiling records and flags drift, +# never adjudicates library correctness (autolens_workspace_test's remit; +# boundary rule in results/notes/design_lock_in.md). PyAutoHeart's vitals +# scan reads the pinned_drift field. +record_pinned_check(dict_path, _pinned_expected, _pinned_drift) +if _pinned_expected is not None and not _pinned_drift: + print(" Pinned-value check PASSED (recorded in result JSON).") diff --git a/results/notes/design_lock_in.md b/results/notes/design_lock_in.md index cbbf580..80c8651 100644 --- a/results/notes/design_lock_in.md +++ b/results/notes/design_lock_in.md @@ -93,6 +93,26 @@ Rule of thumb: **cross-release trend** questions read versioned summaries; - Baselines are **append-only**: never edited after the campaign closes; a post-optimization campaign snapshots a new name next to it. +## Pinned values: record-and-flag, never adjudicate (added phase 2) + +The runtime cells carry pinned log-likelihood/evidence values. Their job here +is **benchmark comparability** — a timing is only comparable to a baseline if +it measures the same computation — *not* library regression testing (that is +`autolens_workspace_test`'s remit). Accordingly: + +- Drift against a pin **never crashes a profiling run**: `check_pinned` + (in `_profile_cli.py`) prints a loud warning and records + `pinned_expected` + `pinned_drift` into the result JSON + (`record_pinned_check`). The numbers still land; the flag marks them + non-comparable to the pinned baseline. +- The **eager ≡ JIT ≡ vmap cross-consistency assertions stay hard** — a + mismatch there invalidates the measurement itself. +- On a drift flag the follow-up is "file a bug / check workspace_test", + never "debug the library inside this repo". +- **PyAutoHeart pairing**: the `pinned_drift` JSON field is the machine + surface Heart's vitals scan reads (tracked as its own Mind task, + `feature/pyautoheart/profiling_drift_check.md`). + ## Deliberately out of scope here Profiling runs (phases 2–4); searches profiling; `point_source` cells (in the diff --git a/test/test_vram_config.py b/test/test_vram_config.py index 0b93814..b7d1a1f 100644 --- a/test/test_vram_config.py +++ b/test/test_vram_config.py @@ -134,3 +134,58 @@ def test_resolve_missing_probe_uses_table(tmp_path): batch, source = resolve_vmap_batch("imaging", "pixelization", "hst", output_dir=tmp_path) assert batch == VMAP_BATCH[("imaging", "pixelization", "hst")] assert source == "table" + + +# --------------------------------------------------------------------------- +# backend awareness (non-GPU clamp + probe backend matching) +# --------------------------------------------------------------------------- + + +def test_non_gpu_backend_clamps_table_value(): + batch, source = resolve_vmap_batch("imaging", "pixelization", "euclid", backend="cpu") + assert batch == 3 + assert "clamped" in source + + +def test_gpu_backend_not_clamped(): + batch, source = resolve_vmap_batch("imaging", "pixelization", "euclid", backend="gpu") + assert batch == VMAP_BATCH[("imaging", "pixelization", "euclid")] + assert "clamped" not in source + + +def test_intentional_none_not_clamped_to_int(): + batch, _ = resolve_vmap_batch("datacube", "delaunay", "sma", backend="cpu") + assert batch is None + + +def test_probe_from_other_backend_ignored(tmp_path): + p = _write_probe(tmp_path, recommended=64) + data = json.loads(p.read_text()) + data["backend"] = "cpu" + p.write_text(json.dumps(data)) + batch, source = resolve_vmap_batch( + "imaging", "pixelization", "hst", output_dir=tmp_path, backend="gpu" + ) + assert batch == VMAP_BATCH[("imaging", "pixelization", "hst")] + assert "mismatch" in source + + +def test_probe_same_backend_accepted_and_clamped_on_cpu(tmp_path): + p = _write_probe(tmp_path, recommended=64) + data = json.loads(p.read_text()) + data["backend"] = "cpu" + p.write_text(json.dumps(data)) + batch, source = resolve_vmap_batch( + "imaging", "pixelization", "hst", output_dir=tmp_path, backend="cpu" + ) + assert batch == 3 # probe wins, then the non-GPU cap applies + assert source.startswith("probe") and "clamped" in source + + +def test_legacy_probe_without_backend_accepted(tmp_path): + _write_probe(tmp_path, recommended=24) # no backend field + batch, source = resolve_vmap_batch( + "imaging", "pixelization", "hst", output_dir=tmp_path, backend="gpu" + ) + assert batch == 24 + assert source.startswith("probe") diff --git a/vram/config.py b/vram/config.py index 7107f9c..bc10c8e 100644 --- a/vram/config.py +++ b/vram/config.py @@ -164,12 +164,20 @@ def vmap_batch_for(dataset: str, model: str, instrument: str, path: str = "dense return VMAP_BATCH.get(key) +# On non-GPU backends the vmap row is a correctness/timing sample, not a +# production configuration — the table and the probe budget are both +# A100-oriented, and batch 64 on a laptop CPU exhausts host RAM (found by +# the phase-2 local validation sweep, autolens_profiling#54). +_NON_GPU_BATCH_CAP = 3 + + def resolve_vmap_batch( dataset: str, model: str, instrument: str, output_dir=None, path: str = "dense", + backend: str | None = None, ) -> tuple[int | None, str]: """Resolve the vmap batch_size, preferring a fresh probe over the table. @@ -183,6 +191,11 @@ def resolve_vmap_batch( 2. The curated table (``vmap_batch_for``, including the sparse fallback-to-dense rule). + ``backend`` is the running JAX backend (``jax.default_backend()``). + The table and the probe budget are A100-oriented, so on any non-``gpu`` + backend the resolved batch is clamped to ``_NON_GPU_BATCH_CAP`` (an + intentional ``None`` stays ``None``). + A probe JSON that cannot be read or does not match is ignored (with the mismatch reported in ``source``), never fatal — the table is the fallback, not the probe. @@ -190,6 +203,16 @@ def resolve_vmap_batch( import json from pathlib import Path + def _clamped(batch: int | None, source: str) -> tuple[int | None, str]: + if ( + backend is not None + and backend != "gpu" + and isinstance(batch, int) + and batch > _NON_GPU_BATCH_CAP + ): + return _NON_GPU_BATCH_CAP, f"{source}, clamped to {_NON_GPU_BATCH_CAP} on {backend}" + return batch, source + if output_dir is not None: suffix = "_sparse" if path == "sparse" else "" probe_path = Path(output_dir) / f"vmap_probe_{model}{suffix}.json" @@ -197,21 +220,26 @@ def resolve_vmap_batch( try: data = json.loads(probe_path.read_text()) except (OSError, ValueError) as exc: - return ( + return _clamped( vmap_batch_for(dataset, model, instrument, path=path), f"table (probe JSON unreadable: {exc})", ) + probe_backend = data.get("backend") matches = ( data.get("dataset") == dataset and data.get("model") == model and data.get("instrument") == instrument + # A probe measured on another backend is not evidence for + # this one (CPU memory analysis says nothing about A100 and + # vice versa). Probes predating the backend field pass. + and (probe_backend is None or backend is None or probe_backend == backend) ) recommended = data.get("recommended_batch_size") if matches and isinstance(recommended, int) and recommended >= 1: - return recommended, f"probe ({probe_path.name})" - return ( + return _clamped(recommended, f"probe ({probe_path.name})") + return _clamped( vmap_batch_for(dataset, model, instrument, path=path), f"table (probe JSON ignored: " f"{'cell mismatch' if not matches else 'no valid recommendation'})", ) - return vmap_batch_for(dataset, model, instrument, path=path), "table" + return _clamped(vmap_batch_for(dataset, model, instrument, path=path), "table") diff --git a/vram/probe.py b/vram/probe.py index 2bf77bb..99c6725 100644 --- a/vram/probe.py +++ b/vram/probe.py @@ -192,8 +192,11 @@ def write_probe_json( extra: dict | None = None, ) -> None: """Serialise probe samples + recommendation to JSON.""" + import jax + output_path.parent.mkdir(parents=True, exist_ok=True) payload = { + "backend": jax.default_backend(), "dataset": probe.dataset, "model": probe.model, "instrument": probe.instrument, From 53c6625b4cf8b8d4fffb37bdf4f9dd86f8642e3d Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Wed, 8 Jul 2026 17:33:43 +0100 Subject: [PATCH 03/10] maint: phase-2 validation sweep results note + dashboard refresh (#54) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All 9 campaign cells green on CPU under the fixed code (results/notes/ vram_validation_2026_07_08.md is the durable record — sweep artifacts were scratch-tier). Simulators dashboard row for the auto-simulated euclid dataset (v2026.7.6.649). Co-Authored-By: Claude Fable 5 --- results/notes/vram_validation_2026_07_08.md | 67 +++++++++++++++++++++ simulators/README.md | 4 +- 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 results/notes/vram_validation_2026_07_08.md diff --git a/results/notes/vram_validation_2026_07_08.md b/results/notes/vram_validation_2026_07_08.md new file mode 100644 index 0000000..e86ace5 --- /dev/null +++ b/results/notes/vram_validation_2026_07_08.md @@ -0,0 +1,67 @@ +# Phase-2 vram-first validation sweep — 2026-07-08 + +Local CPU validation of every PreOptimizationTimes campaign cell **before** +A100 time is spent, per the campaign's vram-first rule. Tracked by +[autolens_profiling#54](https://github.com/PyAutoLabs/autolens_profiling/issues/54); +design context: [`design_lock_in.md`](./design_lock_in.md). PyAutoLens +2026.7.6.649, JAX CPU backend, cheapest instrument per cell; sweep artifacts +were scratch-tier (validation, not campaign data) — this note is the durable +record. + +## Verdict + +**All 9 campaign cells run clean end-to-end on current source** (after the +fixes below). The A100 probe leg (`hpc/batch_gpu/submit_probe_{fast,delaunay}_a100`) +can be dispatched without expecting functional surprises. + +| Cell | Instrument | Path | Wall time (CPU) | +|------|-----------|------|-----------------| +| imaging/mge | euclid | dense | 47 s | +| imaging/pixelization | euclid | dense | 401 s | +| imaging/pixelization | euclid | sparse | 194 s | +| imaging/delaunay | euclid | dense | 381 s | +| imaging/delaunay | euclid | sparse | 213 s | +| interferometer/mge | sma | dense | 23 s | +| interferometer/pixelization | sma | dense | 70 s | +| interferometer/delaunay | sma | dense | 124 s | +| datacube/delaunay | sma | — | 265 s | + +Also validated live: the `--vmap-probe` pipeline end-to-end on CPU, and the +probe-JSON-over-table resolution (a fresh probe in the cell output dir won, +`source: probe (vmap_probe_mge.json)` in the log). + +## Defects flushed (all fixed on the phase-2 branch) + +1. **A100 batch table applied on every backend.** Runtime cells vmapped at + table batch (64) on laptop CPU → OOM-kill (exit 137, pixelization dense at + 97 s); the probe's 65 GB budget default is A100-specific too, so a CPU + probe *also* recommended 64. Fix: `resolve_vmap_batch` takes the running + backend and clamps to 3 on non-GPU; probes record their backend and + resolution ignores probes from a different backend. +2. **Instrument-naive pins.** `imaging/{mge,pixelization,delaunay}` asserted + an HST-only pinned likelihood/evidence for every `--instrument` — any + non-HST run failed by construction (mge at 62 s; pixelization sparse died + at 2499 s *after* 41 min of honest compute, at the final assert). + Converted to the per-instrument dict pattern `interferometer/mge` had. +3. **Pins were hard asserts** (design defect, user-directed redesign): they + guard benchmark comparability, not library correctness, and must never + kill a profiling job — see the record-and-flag section of + [`design_lock_in.md`](./design_lock_in.md). + +## Empirical bonus + +Pixelization **sparse at batch 64 on CPU did not OOM where dense did** (it +survived to the final assert), directly confirming the sparse path's smaller +per-replica footprint — the basis for `VMAP_BATCH_SPARSE`'s +fall-back-to-dense-is-conservative rule. + +## What phase 3 consumes + +- Batch sizes: `vram.resolve_vmap_batch` (probe JSON → table → clamp), sparse + rows via `VMAP_BATCH_SPARSE` once the A100 probes are ingested. +- The dense table's provenance is 2026-05-24 (`vram.config.PROVENANCE`) — + ingest the probe-only A100 submits' JSONs before trusting it for the + campaign (workflow: `vram/README.md` "Probe-only submits + ingest"). +- No pins exist yet for euclid/ao/jwst imaging or any interferometer + instrument — first clean campaign runs should pin them (values are printed + by every unpinned run). diff --git a/simulators/README.md b/simulators/README.md index bd80fed..fc4b507 100644 --- a/simulators/README.md +++ b/simulators/README.md @@ -43,7 +43,9 @@ Old versions are retained alongside new ones so cross-release trends stay visibl Auto-generated by `scripts/build_readme.py` from the latest `*_summary_v.json` artifacts under `results/simulators/`. Cell value is the **total wall time** of the simulator run-to-completion (sum of all `timer.section` phases), not per-likelihood. Hardware tier is CPU only today. -_No data yet — run a simulator under `simulators/` to populate. See section README._ +| Script | Total wall time | PyAutoLens version | +|--------|-----------------|--------------------| +| `imaging_euclid.py` | 2.28 s | v2026.7.6.649 | ## Running a script From 510743d9ef6d9dea34231e53de65fc544422409f Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Wed, 8 Jul 2026 17:44:22 +0100 Subject: [PATCH 04/10] fix: commit simulator artifacts + retire dead likelihood/ link (CI) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two lint-gate failures, one root cause each: - PR gate: build_readme --check diverged because results/simulators/ was still gitignored (a pre-phase-1 "until we decide" rule) — the local dashboard baked a euclid row from an artifact CI never received. The phase-1 design settled that results live in-repo: un-ignore results/simulators/ + results/searches/ (results/likelihood/ stays ignored as the retired legacy section) and commit the euclid artifact pair. - main gate (lychee): simulators/README.md still linked the retired likelihood/README.md; retargeted to likelihood_runtime/breakdown. Repo-wide relative-link scan: no other dead links. Co-Authored-By: Claude Fable 5 --- .gitignore | 4 +- .../imaging_euclid_summary_v2026.7.6.649.json | 45 ++++++++++++++++++ .../imaging_euclid_summary_v2026.7.6.649.png | Bin 0 -> 105748 bytes simulators/README.md | 2 +- 4 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 results/simulators/imaging_euclid_summary_v2026.7.6.649.json create mode 100644 results/simulators/imaging_euclid_summary_v2026.7.6.649.png diff --git a/.gitignore b/.gitignore index 5a9d230..21e43a5 100644 --- a/.gitignore +++ b/.gitignore @@ -46,12 +46,10 @@ dataset/datacube/sim_*/ # that's purely an auto-cache. dataset/**/lensed_source.fits -# Profiling result artifacts — written by scripts under likelihood/, simulators/, searches/. +# Retired legacy results section (pre likelihood_runtime/breakdown split). # Each run produces fresh versioned JSON+PNG; Phase 4's dashboard will explicitly select # which artifacts to commit. Until then, results stay local. results/likelihood/ -results/simulators/ -results/searches/ # Latent profiling sweep outputs — regenerated by latent/sweep.py, not committed. latent/results/ diff --git a/results/simulators/imaging_euclid_summary_v2026.7.6.649.json b/results/simulators/imaging_euclid_summary_v2026.7.6.649.json new file mode 100644 index 0000000..c4a2db1 --- /dev/null +++ b/results/simulators/imaging_euclid_summary_v2026.7.6.649.json @@ -0,0 +1,45 @@ +{ + "autolens_version": "2026.7.6.649", + "type": "imaging", + "instrument": "euclid", + "configuration": { + "grid_shape": [ + 71, + 71 + ], + "pixel_scales": 0.1, + "mask_radius": 3.5, + "psf_shape": [ + 21, + 21 + ], + "psf_sigma": 0.1, + "over_sampling_sub_sizes": [ + 32, + 8, + 2 + ] + }, + "phases": { + "setup_grids": 0.061925699999846984, + "setup_psf_simulator": 0.0005141000001458451, + "setup_galaxies": 8.140000136336312e-05, + "setup_tracer": 1.1600001016631722e-05, + "image_2d_eager": 0.3962143999997352, + "image_2d_jit_lower": 0.47080600000117556, + "image_2d_jit_compile": 0.3903323000013188, + "image_2d_jit_first_call": 0.018089100001816405, + "image_2d_jit_steady_x10": 0.16007420000096317, + "via_tracer_from": 0.6722835999971721, + "solver_build": 8.159999924828298e-05, + "solver_solve_eager": 0.05072459999792045, + "output_fits": 0.01322970000182977, + "output_lensed_source": 0.03972860000067158, + "output_json": 0.002425700000458164 + }, + "key_timings": { + "image_2d_eager_s": 0.3962143999997352, + "via_tracer_from_s": 0.6722835999971721, + "solver_solve_eager_s": 0.05072459999792045 + } +} \ No newline at end of file diff --git a/results/simulators/imaging_euclid_summary_v2026.7.6.649.png b/results/simulators/imaging_euclid_summary_v2026.7.6.649.png new file mode 100644 index 0000000000000000000000000000000000000000..438aeaf1d3c431cbd782f61c86392d7ea43ff478 GIT binary patch literal 105748 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 m$5T_Z%G=b?3d=d4T$r} Date: Wed, 8 Jul 2026 17:44:43 +0100 Subject: [PATCH 05/10] chore: clarify retired-section gitignore comments --- .gitignore | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 21e43a5..6e3c4f2 100644 --- a/.gitignore +++ b/.gitignore @@ -46,10 +46,10 @@ dataset/datacube/sim_*/ # that's purely an auto-cache. dataset/**/lensed_source.fits -# Retired legacy results section (pre likelihood_runtime/breakdown split). -# Each run produces fresh versioned JSON+PNG; Phase 4's dashboard will explicitly select -# which artifacts to commit. Until then, results stay local. +# Retired legacy results section (pre likelihood_runtime/breakdown split); +# nothing writes here since the phase-2 output retarget. Kept ignored so old +# local checkouts don't surface stale artifacts as untracked. results/likelihood/ -# Latent profiling sweep outputs — regenerated by latent/sweep.py, not committed. +# Retired legacy latent output home (latent now writes results/latent/, in-repo). latent/results/ From 25878b7bc2e04f45e450f2c21fdc62e2e710da8e Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Wed, 8 Jul 2026 17:50:54 +0100 Subject: [PATCH 06/10] =?UTF-8?q?fix:=20exclude=20Slack=20invite=20from=20?= =?UTF-8?q?lychee=20(302=E2=86=92403=20to=20non-browser=20clients)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/lint.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3a985a1..3481bcd 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -92,9 +92,11 @@ jobs: working-directory: autolens_profiling run: | # Conservative scope: only README.md files in this repo, exclude - # GitHub UI links that lychee tends to false-positive on. + # GitHub UI links that lychee tends to false-positive on, and the + # Slack invite (302 -> 403 to any non-browser client). lychee \ --exclude '^https://github\.com/.*/(issues|pull|commit|releases)/' \ + --exclude '^https://join\.slack\.com/' \ --no-progress \ --accept '200..=299,429' \ --max-redirects 5 \ From c2466aa662a4219801304aba8dd8222f5b2588a4 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Wed, 8 Jul 2026 17:54:07 +0100 Subject: [PATCH 07/10] fix: retire likelihood/ paths in CI workflows (latent since the runtime/breakdown split; exposed once lychee passed) --- .github/workflows/lint.yml | 7 ++++--- .github/workflows/profile.yml | 8 ++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3481bcd..838c194 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,8 +2,8 @@ # # Cheap, fast (<5 min), CPU-only. Runs on every PR and on push to main. # Does NOT produce real result artifacts — the smoke step short-circuits -# every profile script via AUTOLENS_PROFILING_SMOKE=1 (lands as part of -# Phase 5; see scripts under likelihood/, simulators/, searches/nautilus/). +# every profile script via AUTOLENS_PROFILING_SMOKE=1 (see scripts under +# likelihood_runtime/, likelihood_breakdown/, simulators/, searches/nautilus/). # # Profile re-runs that actually produce results live in `profile.yml`. @@ -113,6 +113,7 @@ jobs: # Each script reads AUTOLENS_PROFILING_SMOKE at module top and # exits 0 immediately after the import + setup section. Catches # import-graph breakage without running the full profile. - python likelihood/imaging/mge.py + python likelihood_runtime/imaging/mge.py + python likelihood_breakdown/imaging/mge.py python simulators/imaging.py python searches/nautilus/simple.py diff --git a/.github/workflows/profile.yml b/.github/workflows/profile.yml index 45b7006..99e1dfa 100644 --- a/.github/workflows/profile.yml +++ b/.github/workflows/profile.yml @@ -92,7 +92,7 @@ jobs: pip install nautilus-sampler==1.0.5 pip install numba - - name: Run likelihood/ scripts + - name: Run likelihood_runtime/ scripts if: ${{ inputs.sections == '' || contains(inputs.sections, 'likelihood') }} working-directory: autolens_profiling continue-on-error: true @@ -102,8 +102,8 @@ jobs: PYTHONPATH: ${{ github.workspace }}/PyAutoConf:${{ github.workspace }}/PyAutoFit:${{ github.workspace }}/PyAutoArray:${{ github.workspace }}/PyAutoGalaxy:${{ github.workspace }}/PyAutoLens run: | set +e # don't abort the whole step on one script's failure - for script in likelihood/imaging/*.py likelihood/interferometer/*.py \ - likelihood/point_source/*.py likelihood/datacube/delaunay.py; do + for script in likelihood_runtime/imaging/*.py likelihood_runtime/interferometer/*.py \ + likelihood_runtime/point_source/*.py likelihood_runtime/datacube/delaunay.py; do echo "::group::$script" python "$script" || echo "::warning::$script failed — see log; dashboard cell will show ERR" echo "::endgroup::" @@ -163,7 +163,7 @@ jobs: # gitignored; future hardware-tier work may flip a subset to # tracked, in which case this `add -f` becomes redundant but # harmless. - git add -f results/ likelihood/README.md likelihood/*/README.md \ + git add -f results/ likelihood_runtime/README.md likelihood_breakdown/README.md \ simulators/README.md searches/README.md \ searches/nautilus/README.md README.md git commit -m "chore: refresh profile artifacts + dashboard tables [skip ci] From b18758b3f44b6e6d64106fe66fd912fe9d9245e2 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Wed, 8 Jul 2026 17:57:06 +0100 Subject: [PATCH 08/10] =?UTF-8?q?fix:=20install=20sibling=20checkouts=20(d?= =?UTF-8?q?ep=20chain)=20for=20the=20smoke=20step=20=E2=80=94=20hand-picke?= =?UTF-8?q?d=20pip=20list=20lacked=20pyyaml=20etc;=20adopt=20the=20lib-tes?= =?UTF-8?q?ts.yml=20pattern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/lint.yml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 838c194..9db2a98 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -60,12 +60,15 @@ jobs: - name: Install runtime dependencies run: | - python -m pip install --upgrade pip - pip install numpy scipy matplotlib jax jaxlib - pip install nautilus-sampler==1.0.5 - pip install numba - # autoconf / autofit / autoarray / autogalaxy / autolens are imported - # from the sibling checkouts via PYTHONPATH below — no install needed. + python -m pip install --upgrade pip setuptools wheel + # Install each sibling checkout in dependency order (the PyAutoHeart + # lib-tests.yml pattern): satisfies the full third-party dep chain + # (pyyaml, astropy, jax, numba, ...) without hand-maintaining a + # package list. PYTHONPATH below still resolves the PyAuto* imports + # from source, so the checkouts stay authoritative. + for r in PyAutoConf PyAutoFit PyAutoArray PyAutoGalaxy PyAutoLens; do + pip install "../$r[optional]" + done - name: Install lint tools run: | From a4a435f11d049788ee9111e8c3e87d3a826c3164 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Wed, 8 Jul 2026 17:57:32 +0100 Subject: [PATCH 09/10] =?UTF-8?q?fix:=20install=20step=20runs=20at=20works?= =?UTF-8?q?pace=20root=20=E2=80=94=20./$r=20not=20../$r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9db2a98..cb5eeac 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -67,7 +67,7 @@ jobs: # package list. PYTHONPATH below still resolves the PyAuto* imports # from source, so the checkouts stay authoritative. for r in PyAutoConf PyAutoFit PyAutoArray PyAutoGalaxy PyAutoLens; do - pip install "../$r[optional]" + pip install "./$r[optional]" done - name: Install lint tools From 43097cf9cecf90efaec6f183f2334583f1277604 Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Wed, 8 Jul 2026 18:00:59 +0100 Subject: [PATCH 10/10] =?UTF-8?q?fix:=20smoke=20step's=20searches=20path?= =?UTF-8?q?=20=E2=80=94=20nautilus/simple.py=20predates=20the=20searches?= =?UTF-8?q?=20redesign?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index cb5eeac..1af30c3 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -119,4 +119,4 @@ jobs: python likelihood_runtime/imaging/mge.py python likelihood_breakdown/imaging/mge.py python simulators/imaging.py - python searches/nautilus/simple.py + python searches/nautilus/imaging/mge.py