Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,19 @@ overview (vision, latest run-times, roadmap); this file is the operational guide
## Repository Structure

```
likelihood/ Per-instrument likelihood profile scripts (imaging, interferometer,
datacube, point_source) — import the shared _profile_cli helper
likelihood_runtime/ Full-pipeline JIT runtime, driven by sweep.py across CPU/GPU/A100 × fp64/mp
(per-cell scripts under <class>/<model>.py import the shared _profile_cli helper)
likelihood_breakdown/ Per-step JIT decomposition of a single likelihood config
vram/ GPU memory-usage profiling
vram/ GPU memory-usage profiling + the per-cell A100 vmap batch-size table
instruments/ Instrument definitions (pixel scale, shape) used to frame results
searches/ Sampler / search profiling (Nautilus first)
simulators/ Run-time tracking for the PyAutoLens simulators
latent/ Latent-variable profiling
quick_update/ Fast incremental re-profiling helpers
hpc/ SLURM submit scripts for the RAL HPC
results/ Versioned JSON + PNG artifacts (`*_v<YYYY>.<M>.<D>.<PATCH>.{json,png}`)
quick_update/ Fast incremental re-profiling helpers (unversioned scratch tier)
hpc/ SLURM submit scripts for the RAL HPC (A100 rows of the sweep matrix)
scripts/ Repo tooling (build_readme.py dashboard generator)
results/ JSON + PNG artifacts: versioned summaries, sweep comparisons, named
baselines (results/README.md defines the shapes; sweeps default here)
config/ dataset/ output/ Config, input data, runtime output
```

Expand Down
57 changes: 40 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,44 +24,65 @@ Results are framed by **astronomy instrument** (HST, Euclid, JWST, …) rather t

## Latest run-times

Cell-level full-pipeline numbers live in [`likelihood_runtime/OPTIMIZATION_NOTES.md`](./likelihood_runtime/OPTIMIZATION_NOTES.md), which carries the latest CPU + local-GPU per-call costs together with per-cell "where to optimize next" recommendations and the mp-vs-fp64 verdicts. The detailed multi-config `comparison.json` artifacts are committed under [`autolens_workspace_developer/jax_profiling/results/jit/<class>/<model>/`](https://github.com/PyAutoLabs/autolens_workspace_developer/tree/main/jax_profiling/results/jit) and are re-aggregated by `likelihood_runtime/aggregate.py` whenever a new sweep finishes.
<!-- BEGIN auto-table:headline -->

(The previous auto-generated table in this README was retired when the likelihood profiling was split into `likelihood_breakdown/` + `likelihood_runtime/`; `scripts/build_readme.py` is queued for a path-update follow-up.)
**Likelihood runtime** — full-pipeline per-call cost per cell × config:

_No data yet — run `likelihood_runtime/sweep.py` then `aggregate.py` to populate._

**Likelihood breakdown** — latest per-step decompositions:

| Cell | Instrument | Inversion path | Step-sum total | PyAutoLens version |
|------|------------|----------------|----------------|--------------------|
| `imaging/delaunay` | hst | dense (mapping) | 11.29 s | v2026.5.29.4 |
| `imaging/delaunay` | hst | sparse (w-tilde) | 7.96 s | v2026.5.29.4 |
| `imaging/mge` | hst | dense (mapping) | 178.2 ms | v2026.5.29.4 |
| `imaging/pixelization` | hst | dense (mapping) | 7.79 s | v2026.5.29.4 |
| `imaging/pixelization` | hst | sparse (w-tilde) | 8.94 s | v2026.5.29.4 |
<!-- END auto-table:headline -->

The tables above are auto-generated by `scripts/build_readme.py` from the artifacts under [`results/`](./results/README.md) — never edit them by hand; run `python scripts/build_readme.py` after a profiling run and commit the result (CI checks idempotence via `--check`). Narrative context — per-cell "where to optimize next" recommendations and the mp-vs-fp64 verdicts — lives in [`likelihood_runtime/OPTIMIZATION_NOTES.md`](./likelihood_runtime/OPTIMIZATION_NOTES.md).

**PreOptimizationTimes** is the named baseline the upcoming optimization work is measured against: a frozen snapshot of the full campaign (laptop CPU, HPC CPU, HPC A100 × fp64/mp) under `results/baselines/PreOptimizationTimes/`, rendered as a baseline column in the dashboard once populated. The convention is defined in [`results/notes/design_lock_in.md`](./results/notes/design_lock_in.md).

(Historical multi-config sweeps up to 2026-07 were committed under [`autolens_workspace_developer/jax_profiling/results/jit/`](https://github.com/PyAutoLabs/autolens_workspace_developer/tree/main/jax_profiling/results/jit); sweeps now write in-repo to `results/runtime/` by default.)

## JAX gradients — currently out of scope

Gradient profiling (`jax.grad` of the likelihood, autodiff-based optimisers) is **not yet** part of this repo. It is tracked in [`PyAutoLabs/autolens_workspace_developer/jax_profiling/gradient/`](https://github.com/PyAutoLabs/autolens_workspace_developer/tree/main/jax_profiling/gradient) and will fold into this repo in a future phase once the gradient story stabilises.

## How to read this repo

Each profiling script writes a **versioned artifact pair** under `results/`:
Profiling scripts write two artifact shapes under `results/` (full reference: [`results/README.md`](./results/README.md)):

```
results/<section>/<subfolder>/<profile_name>_summary_v<YYYY>.<M>.<D>.<PATCH>.json
results/<section>/<subfolder>/<profile_name>_summary_v<YYYY>.<M>.<D>.<PATCH>.png
```

The version string matches the PyAutoLens release that produced the numbers (e.g. `v2026.5.1.4`). Older versions are retained alongside newer ones, so trends across releases stay inspectable. The JSON carries structured timings; the PNG is the at-a-glance plot.
# Versioned summaries — standalone runs; history retained side-by-side
results/<section>/<subfolder>/<cell>_<purpose>_<instrument>_v<YYYY>.<M>.<D>.<PATCH>[_sparse].{json,png}

Examples that already exist in the source-of-truth repo:
# Per-config sweeps — sweep.py + aggregate.py; latest sweep per cell
results/runtime/<class>/<model>[/<instrument>]/<config_name>[_sparse].{json,png,log} + comparison.{json,png}
```

- `imaging_summary_v2026.5.1.4.json` / `.png`
- `point_source_summary_v2026.5.1.4.json`
- `delaunay_sparse_cpu_likelihood_summary_hst_v2026.5.1.4.json`
The version string matches the PyAutoLens release that produced the numbers (e.g. `v2026.5.29.4`). The JSON carries structured timings; the PNG is the at-a-glance plot. Cross-release **trend** questions read the versioned summaries; cross-hardware **comparison** questions read `comparison.json`.

## Section index

| Folder | Contents |
|--------|----------|
| [`likelihood_breakdown/`](./likelihood_breakdown/README.md) | Per-step JIT decomposition. Single config. *Where does time go inside the likelihood?* |
| [`likelihood_runtime/`](./likelihood_runtime/README.md) | Full-pipeline JIT only, driven by `sweep.py` across CPU/GPU/A100 × fp64/mp. *How long will this likelihood take on this hardware?* |
| [`likelihood_breakdown/`](./likelihood_breakdown/README.md) | Per-step JIT decomposition. Single config. *Where does time go inside the likelihood?* |
| [`vram/`](./vram/README.md) | GPU memory profiling + the per-cell vmap batch-size table for the A100. |
| [`latent/`](./latent/README.md) | Latent-variable profiling. |
| [`instruments/`](./instruments/README.md) | Instrument presets (pixel scale, shape) that frame every result. |
| [`simulators/`](./simulators/README.md) | Run-time tracking for the PyAutoLens simulators. |
| [`searches/`](./searches/README.md) | Sampler / search profiling, Nautilus first. |
| [`results/`](./results/README.md) | Versioned JSON + PNG artifacts written by the above scripts. |
| [`quick_update/`](./quick_update/README.md) | Fast incremental re-profiling helpers. |
| [`hpc/`](./hpc/README.md) | SLURM submit scripts for the RAL HPC (A100 rows of the sweep matrix). |
| [`results/`](./results/README.md) | JSON + PNG artifacts written by the above scripts; named baselines. |

## Roadmap

This repo is being built in phases. Phase numbers correspond to internal sub-prompts under `PyAutoLabs/PyAutoPrompt/z_features/autolens_profiling.md`.
This repo is being built in phases (bootstrap history now archived in `PyAutoMind`).

| Phase | Title | Status |
|-------|-------|--------|
Expand All @@ -70,7 +91,9 @@ This repo is being built in phases. Phase numbers correspond to internal sub-pro
| 2 | Mirror simulator profiling scripts + run-time tracking | ✓ shipped |
| 3 | Nautilus profiling, design for sampler expansion | ✓ shipped |
| 4 | Top-level + per-section README dashboard with instrument framing | ✓ shipped |
| 5 | GitHub Actions for lint + profile re-runs + README refresh | queued |
| 5 | GitHub Actions for lint + profile re-runs + README refresh | ✓ shipped (`lint.yml` per-PR; `profile.yml` manual/on-release) |
| 6 | Design lock-in + results/dashboard groundwork ([#52](https://github.com/PyAutoLabs/autolens_profiling/issues/52)) | in progress |
| 7 | **PreOptimizationTimes** baseline campaign (vram-first, then runtime + breakdown) | queued |

### Future enhancements (Phase 4 follow-ups)

Expand All @@ -87,7 +110,7 @@ Dashboards can grow in many directions. The list below captures candidate improv

- [`PyAutoLabs/PyAutoLens`](https://github.com/PyAutoLabs/PyAutoLens) — the library being profiled.
- [`PyAutoLabs/autolens_workspace`](https://github.com/PyAutoLabs/autolens_workspace) — user-facing science scripts and tutorials.
- [`PyAutoLabs/autolens_workspace_developer`](https://github.com/PyAutoLabs/autolens_workspace_developer) — the developer workspace; **source of truth during the migration**. Each phase mirrors the relevant subdirectories from here into this repo.
- [`PyAutoLabs/autolens_workspace_developer`](https://github.com/PyAutoLabs/autolens_workspace_developer) — the developer workspace this repo's scripts were migrated from; still hosts the pre-2026-07 sweep history and the gradient-profiling work.
- [`Jammy2211/autolens_colab_profiling`](https://github.com/Jammy2211/autolens_colab_profiling) — sibling repo, Colab-specific scope. Not yet migrated to PyAutoLabs.

## Package vs scripts
Expand Down
4 changes: 2 additions & 2 deletions _adapt_image_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@

def adapt_image_for_dataset(
*,
dataset_path: Union[str, Path],
dataset_path: str | Path,
dataset,
tracer: Optional[al.Tracer] = None,
tracer: al.Tracer | None = None,
) -> aa.Array2D:
"""Return the lensed-source image masked to ``dataset.mask``.

Expand Down
44 changes: 25 additions & 19 deletions _profile_cli.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Shared CLI / JSON / auto-simulate helpers for the likelihood scripts.

Used by every script under ``likelihood/{imaging,interferometer,
datacube,point_source}/`` so the per-script boilerplate stays minimal
Used by every per-cell script under ``likelihood_runtime/`` and
``likelihood_breakdown/`` so the per-script boilerplate stays minimal
and the sweep-driver flags (``--config-name``, ``--output-dir``,
``--use-mixed-precision``) and dataset auto-simulate hook are defined
in one place.
Expand Down Expand Up @@ -30,15 +30,15 @@

@dataclass(frozen=True)
class ProfileCLI:
config_name: Optional[str]
output_dir: Optional[Path]
config_name: str | None
output_dir: Path | None
use_mixed_precision: bool
instrument: Optional[str]
instrument: str | None
vmap_probe: bool
use_sparse_operator: bool


def parse_profile_cli(default_config_name: Optional[str] = None) -> ProfileCLI:
def parse_profile_cli(default_config_name: str | None = None) -> ProfileCLI:
"""Parse the sweep CLI flags accepted by every per-cell profile script.

Returns ``ProfileCLI(config_name, output_dir, use_mixed_precision,
Expand Down Expand Up @@ -69,8 +69,8 @@ def parse_profile_cli(default_config_name: Optional[str] = None) -> ProfileCLI:
"--output-dir",
default=None,
help=(
"Override results dir. Defaults to "
"<autolens_profiling>/results/likelihood/<class>/."
"Override results dir. Each per-cell script defaults to its "
"package's section under <autolens_profiling>/results/."
),
)
parser.add_argument(
Expand Down Expand Up @@ -149,15 +149,19 @@ def device_info_dict() -> dict:
}
if info["backend"] == "gpu":
try:
out = subprocess.check_output(
[
"nvidia-smi",
"--query-gpu=name,memory.used,memory.total",
"--format=csv,noheader",
],
stderr=subprocess.DEVNULL,
timeout=3,
).decode().strip()
out = (
subprocess.check_output(
[
"nvidia-smi",
"--query-gpu=name,memory.used,memory.total",
"--format=csv,noheader",
],
stderr=subprocess.DEVNULL,
timeout=3,
)
.decode()
.strip()
)
info["nvidia_smi"] = out.replace("\n", "; ")
except Exception:
pass
Expand Down Expand Up @@ -246,8 +250,10 @@ def auto_simulate_if_missing(
[
sys.executable,
str(simulator_script),
"--instrument", instrument,
"--output-root", str(workspace_root),
"--instrument",
instrument,
"--output-root",
str(workspace_root),
],
check=True,
)
33 changes: 33 additions & 0 deletions hpc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# hpc

SLURM submit scripts for the RAL HPC — the `hpc_a100_fp64` / `hpc_a100_mp`
rows of the sweep matrix (and HPC-CPU rows) that the local
`likelihood_runtime/sweep.py` driver cannot run itself.

## Layout

```
batch_gpu/
submit_<package>_<class>_<model>_a100_<inst>_<precision>[_sparse] # one submit per cell/config
output/ error/ # SLURM stdout/stderr (gitignored)
```

Submit names follow the same `<class>/<model>` cell grid as the rest of the
repo; `runtime_` prefixed submits drive `likelihood_runtime/` cells, `nss_`
prefixed ones drive `searches/` cells.

## Running

On the HPC login node:

```bash
source activate.sh # repo-root helper: venv + PYTHONPATH at the canonical checkouts
sbatch hpc/batch_gpu/submit_runtime_imaging_mge_a100_hst_fp64
```

Each job writes its per-config JSON into the same `results/` layout as a local
sweep (`--config-name hpc_a100_fp64` etc.), so `likelihood_runtime/aggregate.py`
merges local and A100 rows into one `comparison.json`. Copy/commit the result
JSONs from the HPC checkout back via the normal git flow. The PyAuto*
libraries resolve from sibling source checkouts on `PYTHONPATH` — never
pip-install them into the venv (`HPCPullPyAuto` is the update story).
1 change: 0 additions & 1 deletion instruments/imaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@

from __future__ import annotations


INSTRUMENTS: dict[str, dict] = {
"euclid": {
"pixel_scale": 0.1,
Expand Down
3 changes: 1 addition & 2 deletions instruments/interferometer.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@

from typing import Optional


INSTRUMENTS: dict[str, dict] = {
"sma": {
"pixel_scale": 0.1,
Expand Down Expand Up @@ -92,6 +91,6 @@ def mask_radius_pixels(instrument: str) -> int:
return int(round(cfg["mask_radius"] / cfg["pixel_scale"]))


def transformer_chunk_size_for(instrument: str) -> Optional[int]:
def transformer_chunk_size_for(instrument: str) -> int | None:
"""Per-instrument NUFFT chunk_size (None for one-shot)."""
return INSTRUMENTS[instrument].get("transformer_chunk_size")
70 changes: 35 additions & 35 deletions latent/aggregate.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,8 @@
import matplotlib.pyplot as plt
import numpy as np


_REPO_ROOT = Path(__file__).resolve().parents[1]
_WT_ROOT = _REPO_ROOT.parent
_DEFAULT_OUTPUT_ROOT = _WT_ROOT / "autolens_workspace_developer" / "jax_profiling" / "results" / "latent"
_DEFAULT_OUTPUT_ROOT = _REPO_ROOT / "results" / "latent"

# Canonical ordering of cells — drives auto-discovery sort order.
_CELLS: list[tuple[str, str]] = [
Expand Down Expand Up @@ -134,8 +132,15 @@ def _render_table(comparison: dict, cell_id: str) -> str:
lines = [f"=== {cell_id} ==="]
is_einstein = "effective_einstein_radius" in cell_id
if is_einstein:
header = ("config", "eager_time", "jit_first", "jit_steady", "vmap_per_call",
"cache_first", "cache_second")
header = (
"config",
"eager_time",
"jit_first",
"jit_steady",
"vmap_per_call",
"cache_first",
"cache_second",
)
else:
header = ("config", "eager_time", "jit_first", "jit_steady", "vmap_per_call")
rows = [header]
Expand All @@ -147,23 +152,27 @@ def _render_table(comparison: dict, cell_id: str) -> str:
if is_einstein:
cc_first = cfg.get("closure_cache_first_call_s")
cc_second = cfg.get("closure_cache_second_call_s")
rows.append((
name,
_format_seconds(eager_t),
_format_seconds(jit_first),
_format_seconds(jit_steady),
_format_seconds(vmap),
_format_seconds(cc_first),
_format_seconds(cc_second),
))
rows.append(
(
name,
_format_seconds(eager_t),
_format_seconds(jit_first),
_format_seconds(jit_steady),
_format_seconds(vmap),
_format_seconds(cc_first),
_format_seconds(cc_second),
)
)
else:
rows.append((
name,
_format_seconds(eager_t),
_format_seconds(jit_first),
_format_seconds(jit_steady),
_format_seconds(vmap),
))
rows.append(
(
name,
_format_seconds(eager_t),
_format_seconds(jit_first),
_format_seconds(jit_steady),
_format_seconds(vmap),
)
)
col_w = [max(len(r[i]) for r in rows) for i in range(len(rows[0]))]
for r in rows:
lines.append(" " + " ".join(s.ljust(w) for s, w in zip(r, col_w)))
Expand Down Expand Up @@ -199,18 +208,11 @@ def _render_png(comparison: dict, cell_id: str, png_path: Path) -> None:
series["jit_steady_state"].append(cfg.get("jit_steady_state_s", np.nan))
series["vmap_per_call"].append(cfg.get("vmap_per_call_s", np.nan))
if is_einstein:
series["closure_cache_first"].append(
cfg.get("closure_cache_first_call_s", np.nan)
)
series["closure_cache_second"].append(
cfg.get("closure_cache_second_call_s", np.nan)
)
series["closure_cache_first"].append(cfg.get("closure_cache_first_call_s", np.nan))
series["closure_cache_second"].append(cfg.get("closure_cache_second_call_s", np.nan))

# Drop series that are entirely nan/zero — nothing to plot.
series = {
k: v for k, v in series.items()
if any(np.isfinite(x) and x > 0 for x in v)
}
series = {k: v for k, v in series.items() if any(np.isfinite(x) and x > 0 for x in v)}
if not series:
return

Expand Down Expand Up @@ -262,9 +264,7 @@ def main() -> int:
for spec in args.cell:
parts = spec.split("/")
if len(parts) != 2:
sys.stderr.write(
f"bad --cell argument: {spec!r} (expected class/latent)\n"
)
sys.stderr.write(f"bad --cell argument: {spec!r} (expected class/latent)\n")
return 2
cells.append((parts[0], parts[1]))
else:
Expand All @@ -274,7 +274,7 @@ def main() -> int:
sys.stderr.write(f"no cells found under {args.output_root}\n")
return 1

for (cls, latent) in cells:
for cls, latent in cells:
cell_id = f"{cls}/{latent}"
cell_dir = args.output_root / cls / latent
if not cell_dir.exists():
Expand Down
Loading
Loading