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
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ scaling:
$(GPU_FLAG) \
-e BACKEND=simulator \
-e USE_GPU=$(GPU_AVAILABLE) \
-v "$$(pwd)/results/scaling:/workspace/results/scaling" \
-v "$$(pwd)/results:/workspace/results" \
$(IMAGE_NAME) \
mpirun --allow-run-as-root -np $$p python3 benchmarks/local_test_run.py \
> results/scaling/scaling_p$$p.log 2>&1; \
Expand All @@ -113,7 +113,7 @@ weak-scaling:
$(GPU_FLAG) \
-e BACKEND=simulator \
-e USE_GPU=$(GPU_AVAILABLE) \
-v "$$(pwd)/results/scaling:/workspace/results/scaling" \
-v "$$(pwd)/results:/workspace/results" \
-v "$$(pwd)/checkpoints:/workspace/checkpoints" \
$(IMAGE_NAME) \
mpirun --allow-run-as-root -np $$p python3 -c \
Expand Down
26 changes: 21 additions & 5 deletions benchmarks/aggregate_scaling.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@
from glob import glob


def load_runs(backend: str, since: str | None, seed: int | None) -> list[dict]:
pattern = os.path.join("results", backend, f"{backend}_*.json")
def load_runs(backend: str, since: str | None, seed: int | None, hw: str | None) -> list[dict]:
# results/<hardware-slug>/<backend>/<backend>_*.json -- the hw wildcard/filter
# walks (or targets one of) every hardware folder (a100-sxm4-40gb,
# rtx-6000-ada-generation, cpu-only, ...)
pattern = os.path.join("results", hw or "*", backend, f"{backend}_*.json")
out = []
for path in sorted(glob(pattern)):
try:
Expand All @@ -33,6 +36,7 @@ def load_runs(backend: str, since: str | None, seed: int | None) -> list[dict]:
if since and d.get("timestamp", "") < since:
continue
d["_path"] = path
d["_hw_slug"] = path.split(os.sep)[1]
out.append(d)
return out

Expand All @@ -54,16 +58,28 @@ def main():
help="filter by SEED (default 42 - matches scaling sweep default)")
p.add_argument("--since", default=None,
help="ISO timestamp prefix; ignore runs older than this")
p.add_argument("--hw", default=None,
help="restrict to one results/<hw-slug>/ folder (e.g. a100-sxm4-40gb). "
"Required if runs from more than one hardware slug are found.")
args = p.parse_args()

runs = load_runs(args.backend, args.since, args.seed)
runs = load_runs(args.backend, args.since, args.seed, args.hw)

hw_slugs = {r["_hw_slug"] for r in runs}
if len(hw_slugs) > 1:
print(f"ERROR: runs span multiple hardware folders {sorted(hw_slugs)} -- "
f"a scaling table mixing GPUs is meaningless. Re-run with --hw <slug>.")
sys.exit(1)

by_rank = best_by_rank(runs)

if not by_rank:
print(f"No {args.backend} runs found with seed={args.seed}.")
print(f"No {args.backend} runs found with seed={args.seed}"
+ (f", hw={args.hw}" if args.hw else "") + ".")
sys.exit(1)

print(f"\nStrong scaling table (seed={args.seed}, backend={args.backend})")
print(f"\nStrong scaling table (seed={args.seed}, backend={args.backend}, "
f"hw={hw_slugs.pop() if hw_slugs else 'n/a'})")
for P, r in by_rank.items():
print(f" P={P:<3} {r.get('timestamp', '')[:19]} GPU={r.get('gpu')} "
f"{r['_path']}")
Expand Down
23 changes: 17 additions & 6 deletions benchmarks/aggregate_seeds.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@


def load_seeded_results(backend: str, since: str | None,
ranks: int | None) -> list[dict]:
ranks: int | None, hw: str | None = None) -> list[dict]:
"""Load seeded JSON result files; optionally filter by mpi_ranks.

Deduplicates by (seed, mpi_ranks), keeping the most recent — guards against
accidental contamination when scaling sweeps reuse SEED=42 default and
produce JSONs at multiple P values.
"""
pattern = os.path.join("results", backend, f"{backend}_*.json")
pattern = os.path.join("results", hw or "*", backend, f"{backend}_*.json")
files = sorted(glob(pattern))
candidates = []
for path in files:
Expand All @@ -42,6 +42,7 @@ def load_seeded_results(backend: str, since: str | None,
if ranks is not None and d.get("mpi_ranks") != ranks:
continue
d["_path"] = path
d["_hw_slug"] = path.split(os.sep)[1]
candidates.append(d)

# Dedup by (seed, mpi_ranks) - keep most recent timestamp.
Expand Down Expand Up @@ -136,22 +137,32 @@ def main():
p.add_argument("--ranks", type=int, default=2,
help="filter by mpi_ranks; default 2 (canonical config). "
"Use 0 to include all (e.g. for ibm backend).")
p.add_argument("--hw", default=None,
help="restrict to one results/<hw-slug>/ folder (e.g. a100-sxm4-40gb). "
"Required if runs from more than one hardware slug are found "
"(wall-clock medians across GPUs are meaningless).")
args = p.parse_args()

ranks_filter = args.ranks if args.ranks > 0 else None
runs = load_seeded_results(args.backend, args.since, ranks_filter)
runs = load_seeded_results(args.backend, args.since, ranks_filter, args.hw)
if not runs:
print(f"No seeded {args.backend} runs found "
f"(looking for JSON files in results/{args.backend}/ with 'seed' field).")
f"(looking for JSON files in results/*/{args.backend}/ with 'seed' field).")
if args.since:
print(f"Filter: timestamp >= {args.since}")
if ranks_filter:
print(f"Filter: mpi_ranks == {ranks_filter}")
sys.exit(1)

hw_slugs = {r["_hw_slug"] for r in runs}
if len(hw_slugs) > 1:
print(f"ERROR: runs span multiple hardware folders {sorted(hw_slugs)} -- "
f"wall-clock medians mixing GPUs are meaningless. Re-run with --hw <slug>.")
sys.exit(1)

rank_label = f"P={ranks_filter}" if ranks_filter else "any P"
print(f"Found {len(runs)} unique {args.backend} run(s) at {rank_label} "
f"(deduped by seed+rank):")
print(f"Found {len(runs)} unique {args.backend} run(s) at {rank_label}, "
f"hw={hw_slugs.pop() if hw_slugs else 'n/a'} (deduped by seed+rank):")
for r in runs:
print(f" seed={r['seed']:<4} {r.get('timestamp', '?')[:19]} {r['_path']}")

Expand Down
11 changes: 7 additions & 4 deletions benchmarks/ibm_test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from src.api.problems import ChemistryProblem, FinanceProblem
from src.api.results import save_results
from src.api.log import init_log, log, close_log
from src.api.hardware import HardwareProfile
import hpc_core
print("hpc_core and interface imported.")
except ImportError as e:
Expand Down Expand Up @@ -165,9 +166,11 @@ def run_scaling_ibm(stack: HPCHybridStack):
check_credentials()

ts = datetime.now().strftime("%Y%m%d_%H%M%S")
os.makedirs("results", exist_ok=True)
os.makedirs("results/ibm", exist_ok=True)
init_log(f"results/ibm/ibm_run_{ts}.log")
# Detected once here purely to route the log file; HPCHybridStack below
# re-detects for its own use (cheap, avoids threading hw through init_log).
_hw_slug = HardwareProfile.detect().results_slug()
os.makedirs(f"results/{_hw_slug}/ibm", exist_ok=True)
init_log(f"results/{_hw_slug}/ibm/ibm_run_{ts}.log")

print(f"[Config] GPU= {'requested' if USE_GPU else 'CPU mode'} ")
print(f"[Config] Backend= {BACKEND} ")
Expand All @@ -192,6 +195,6 @@ def run_scaling_ibm(stack: HPCHybridStack):
"seed": SEED,
"max_iters": MAX_ITERS,
"chemistry": chem_result,
}, backend=BACKEND)
}, backend=BACKEND, hw=stack.hw)

close_log()
21 changes: 13 additions & 8 deletions benchmarks/local_test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from src.api.molecule_resolver import (MoleculeResolver, MoleculeTooBigError, ResolutionError)
from src.api.results import save_results
from src.api.log import init_log, log, close_log
from src.api.hardware import HardwareProfile
import hpc_core
print("hpc_core and interface imported.")
except ImportError as e:
Expand Down Expand Up @@ -165,8 +166,9 @@ def run_scaling_local(stack: HPCHybridStack):
result_line = (f"P={stack.size} T_total={t_total:.4f} final_E={history[-1]:+.6f} tier={getattr(problem, 'ansatz_tier', 'N/A')} qubits={problem.num_qubits} iters={len(history)}")
print(f"[Scaling] {result_line}")

os.makedirs("results/scaling",exist_ok=True)
with open(f"results/scaling/scaling_P{stack.size}.txt", "w") as f:
scaling_dir = f"results/{stack.hw.results_slug()}/scaling"
os.makedirs(scaling_dir, exist_ok=True)
with open(f"{scaling_dir}/scaling_P{stack.size}.txt", "w") as f:
f.write(result_line + "\n")

result = {
Expand Down Expand Up @@ -219,8 +221,9 @@ def run_weak_scaling(stack: HPCHybridStack):
f"T/iter={t_per_iter:.4f} final_E={history[-1]:+.6f}")
print(f"[Weak Scaling] {result_line}")

os.makedirs("results/scaling", exist_ok=True)
with open(f"results/scaling/weak_scaling_P{stack.size}.txt", "w") as f:
scaling_dir = f"results/{stack.hw.results_slug()}/scaling"
os.makedirs(scaling_dir, exist_ok=True)
with open(f"{scaling_dir}/weak_scaling_P{stack.size}.txt", "w") as f:
f.write(result_line + "\n")

result = {
Expand All @@ -241,9 +244,11 @@ def run_weak_scaling(stack: HPCHybridStack):
MOLECULES = sys.argv[1:]

ts = datetime.now().strftime("%Y%m%d_%H%M%S")
os.makedirs("results", exist_ok=True)
os.makedirs("results/simulator", exist_ok=True)
init_log(f"results/simulator/run_{ts}.log")
# Detected once here purely to route the log file; HPCHybridStack below
# re-detects for its own use (cheap, avoids threading hw through init_log).
_hw_slug = HardwareProfile.detect().results_slug()
os.makedirs(f"results/{_hw_slug}/simulator", exist_ok=True)
init_log(f"results/{_hw_slug}/simulator/run_{ts}.log")

print(f"[Config] GPU={'requested' if USE_GPU else 'CPU mode'}")
print(f"[Config] Molecules: {MOLECULES}")
Expand Down Expand Up @@ -305,7 +310,7 @@ def run_weak_scaling(stack: HPCHybridStack):
"molecules": results,
"scaling": scaling_result,
"weak_scaling": weak_scaling_result,
}, backend=BACKEND)
}, backend=BACKEND, hw=stack.hw)

close_log()

Expand Down
15 changes: 11 additions & 4 deletions benchmarks/serial_baseline.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,19 @@ def energy(t):

if __name__ == "__main__":
import json
import socket
from datetime import datetime
from src.api.log import init_log, close_log

# Always CPU-only/single-core by design (no HPCHybridStack, no MPI, no GPU),
# so this never routes by GPU model -- but different hosts (laptop vs.
# cluster vs. cloud CPU) still produce non-comparable wall-clock numbers,
# so the hostname is stamped into the output for traceability.
hostname = socket.gethostname()
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
os.makedirs("results/baseline", exist_ok=True)
init_log(f"results/baseline/serial_baseline_{ts}.log")
out_dir = "results/cpu-only/serial-baseline"
os.makedirs(out_dir, exist_ok=True)
init_log(f"{out_dir}/serial_baseline_{ts}.log")

print("----SERIAL QISKIT AER BASELINE--- ")

Expand All @@ -124,9 +131,9 @@ def energy(t):
for name, d in all_results.items():
print(f"{name:<10} {d['energy']:<16.6f} {d['error']:+.4f} Ha {d['iterations']:<8} {d['wall_time']:<10.2f}")

out_path = f"results/baseline/serial_baseline_{ts}.json"
out_path = f"{out_dir}/serial_baseline_{ts}.json"

with open(out_path, "w") as f:
json.dump(all_results, f, indent=2)
json.dump({"hostname": hostname, **all_results}, f, indent=2)
print(f"\n[Results] Saved to {out_path}")
close_log()
73 changes: 73 additions & 0 deletions results/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Results layout

Results are organized **by hardware first, then by run/date**, so runs from
different GPUs (or CPU-only) never get mixed together in one directory:

```
results/
<hardware-slug>/
simulator/ run_<ts>.log + simulator_<ts>.json (per-run VQE data)
scaling/ scaling_P<N>.txt, weak_scaling_P<N>.txt
ibm/ ibm_run_<ts>.log + ibm_cloud_<ts>.json
baseline/ serial_baseline_<ts>.json (serial-baseline backend only)
plots/ aggregate figures generated from the above (hardware-agnostic)
```

## Current hardware folders

- **`rtx-6000-ada-generation/`** — NVIDIA RTX 6000 Ada (48 GB), university IE
cluster (`haskell`), full P∈{1,2,4,8} strong+weak scaling sweep, April 2026.
No seed sweep (predates that feature), base 4-molecule benchmark only.
Also has `slurm/` (raw Slurm job logs) and `trial/` (7-layer diagnostic logs)
since this ran via `make slurm-trial`/`make slurm-gpu`, not locally.
This name is exactly what `results_slug()` derives from `nvidia-smi`, so a
future rerun on the same cluster hardware auto-appends here.

- **`a100-lambda-jul2026-archive/`** — NVIDIA A100, Lambda Cloud instance,
July 2026. Seeds 42-46, adds NH3 + N2 to the benchmark set (CO2 attempted,
cancelled after running >1hr — no result). **Only P=2 exists** — no
strong/weak scaling sweep has been run on this hardware yet.
`session-archives/` holds a termination snapshot (exact code + git log)
from the first Lambda session, kept for provenance.
**This folder name was chosen by hand, not by `results_slug()`** — the
exact A100 variant (SXM4/PCIe, 40GB/80GB) wasn't captured at the time, so
the auto-slug for a *new* A100 run will likely create a differently-named
folder (e.g. `a100-sxm4-40gb/`). After the next A100 sweep, check which
folder shows up and either treat it as the new live A100 folder (if the
exact same instance type) or rename this archive to line up — don't just
assume they match.

- **`cpu-only/`** — anything with no GPU: the `serial-baseline/` single-core
reference (run on Anna's laptop, i7-1065G7 — see `hostname` field in each
JSON to confirm which machine, since re-running this benchmark on a
different CPU produces non-comparable wall-clock numbers), plus early
CPU-only distributed-MPI dev runs and IBM QPU runs from March 2026
(`distributed-mpi/`, `ibm/`).

- **`gtx1650-reference/`** — placeholder. The raw GTX 1650 result files were
never committed to this repo and are presumed lost. The only surviving
numbers are the ones published in the thesis PDF
(`sections/methodology.tex` §4.4.6, `sections/results.tex` §4.6.4/§4.7) —
treat those as a fixed reference, not reproducible raw data. This GPU is
kept out of the published GPU-vs-GPU comparison going forward; it was the
preliminary/exploratory hardware only.

## How new runs get routed here automatically

`HardwareProfile.results_slug()` (`src/api/hardware.py`) derives the slug
from `nvidia-smi`'s reported GPU name (e.g. `NVIDIA A100-SXM4-40GB` ->
`a100-sxm4-40gb`), or `cpu-only` if no GPU is detected. `template.py` and
`benchmarks/{local_test_run,ibm_test_run}.py` use this to build their output
paths, and `src/api/results.py:save_results()` also stamps `gpu_name`,
`gpu_class`, and `hostname` into every JSON payload — so even if a file gets
copied out of its folder later, it's still self-identifying.

`serial_baseline.py` is the one exception: it never touches the GPU by
design, so it always writes to `results/cpu-only/serial-baseline/`
regardless of what hardware it's run on, but still stamps `hostname` since
different host CPUs aren't comparable to each other either.

When aggregating across runs (`benchmarks/aggregate_scaling.py`,
`benchmarks/aggregate_seeds.py`), pass `--hw <slug>` to target one hardware
folder — both scripts refuse to silently average/compare wall-clock times
across different hardware slugs.
1 change: 1 addition & 0 deletions results/a100-lambda-jul2026-archive/scaling/scaling_P2.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
P=2 T_total=0.5522 final_E=-5.001826 tier=hwe_adaptive qubits=12 iters=10
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
P=2 mol=LiH terms=631 terms/rank=316 T_total=0.5004 T/iter=0.0500 final_E=-3.546561
Loading