feat: add eval-cost benchmark — Geant4 reference vs surrogate rollout timing
CI / Sync project version with tag (pull_request) Has been skipped
CI / Format (ruff format) (pull_request) Successful in 43s
CI / Lint (ruff check) (pull_request) Successful in 54s
CI / Type check (ty) (pull_request) Successful in 53s
CI / Tests (pull_request) Successful in 2m33s
CI / Bump version, tag, and update changelog on merge to master (pull_request) Has been skipped
CI / Publish package to Gitea package registry (pull_request) Has been skipped

Closes the roadmap's long-standing "no eval-latency number exists for any
configuration" gap. Instruments `giant rollout` to record per-physical-step
wall-clock cost in its YAML sidecar, adds a measured Geant4/miniCaloSim
per-step reference (giant/analysis/geant4_reference.py, from a 3-energy,
4-event-count-per-energy local benchmark), and wires both into a new
eval_cost_per_step PlotSpec in the giant analyze gallery.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-31 11:52:20 +02:00
co-authored by Claude Sonnet 5
parent 50d8368415
commit 2358a75ee1
11 changed files with 354 additions and 7 deletions
+29
View File
@@ -261,3 +261,32 @@ def test_sec_count_per_step_by_species_zero_row_is_per_species(bundle):
for j, _ in enumerate(cols):
if j != g:
assert ref[0][j] == 3 and sum(row[j] for row in ref[1:]) == 0
# ---------------------------------------------------------------------------
# eval_cost_per_step
# ---------------------------------------------------------------------------
def test_eval_cost_per_step_unavailable_without_timing(bundle: Bundle):
# `bundle`'s RolloutSpec carries no `timing` -> no rollout to compare.
spec = get_spec("eval_cost_per_step")
r = spec.finalize([spec.compute_partial(bundle)], bundle.ctx)
assert r.kind == "unavailable"
assert r.payload["note"]
def test_eval_cost_per_step_bar_with_timing(ctx: Context):
spec = get_spec("eval_cost_per_step")
rs = RolloutSpec(
"rollout",
_rollout_frame(),
timing={"us_per_step": 12.5, "write_us_per_step": 2.5},
)
b = Bundle.open([rs], _reference_frame(), ctx)
r = spec.finalize([spec.compute_partial(b)], ctx)
assert r.kind == "bar"
assert r.payload["series"]["rollout"] == [12.5, 2.5, 15.0]
assert len(r.payload["reference"]) == 3
assert r.payload["log_y"] is True
assert "rollout" in r.meta["speedup_vs_geant4_total"]
+42 -1
View File
@@ -8,11 +8,52 @@ from __future__ import annotations
import torch
from typer.testing import CliRunner
from giant.cli import app
from giant.cli import _build_rollout_timing, app
runner = CliRunner()
def test_build_rollout_timing_excludes_synthetic_rows_from_per_step_cost():
# 100 rows total, 30 of them synthetic termination markers (escape) ->
# us_per_step should be normalized over the 70 physical rows only, the
# same unit giant.analysis.geant4_reference measures Geant4 in.
timing = _build_rollout_timing(
setup_s=1.0,
rollout_s=10.0,
write_s=2.0,
n_rows=100,
termination_reason_counts={"escaped": 30, "natural_end": 70},
n_seed_events=5,
device="cpu",
torch_threads=4,
)
assert timing["n_rows"] == 100
assert timing["n_physical_rows"] == 70
assert timing["n_physical_rows"] < timing["n_rows"]
assert timing["sample_s"] == 8.0 # rollout_s - write_s
assert timing["us_per_step"] == 8.0 / 70 * 1e6
assert timing["write_us_per_step"] == 2.0 / 70 * 1e6
assert timing["ms_per_event"] == 10.0 / 5 * 1e3
assert timing["device"] == "cpu" and timing["torch_threads"] == 4
def test_build_rollout_timing_handles_zero_physical_rows_and_events():
timing = _build_rollout_timing(
setup_s=1.0,
rollout_s=1.0,
write_s=0.0,
n_rows=5,
termination_reason_counts={"escaped": 5},
n_seed_events=0,
device="cpu",
torch_threads=1,
)
assert timing["n_physical_rows"] == 0
assert timing["us_per_step"] is None
assert timing["write_us_per_step"] is None
assert timing["ms_per_event"] is None
def test_rollout_exits_1_on_checkpoint_missing_model_config(tmp_path):
checkpoint = tmp_path / "bad.pt"
torch.save({"sec_decoder": {}, "normalizer": {"sec_phys": {}}}, checkpoint)
+16
View File
@@ -252,6 +252,22 @@ def test_compute_one_from_run_dir(tmp_path: Path):
assert list(partial.data["r"]) == ["rollout"]
def test_timing_survives_plot_meta_to_compute_one(tmp_path: Path):
yaml_path = _write_inputs(tmp_path)
d = yaml.safe_load(yaml_path.read_text())
d["timing"] = {"us_per_step": 7.0, "write_us_per_step": 1.0}
yaml_path.write_text(yaml.safe_dump(d))
run_dir = _prep([yaml_path])
meta = RunMeta.load(run_dir / "run_meta.json")
assert meta.rollouts[0]["plot_meta"]["timing"] == {"us_per_step": 7.0, "write_us_per_step": 1.0}
out = compute_one("eval_cost_per_step", run_dir)
reduced = Reduced(**Partial.load(out).data["reduced"])
assert reduced.kind == "bar"
assert reduced.payload["series"]["rollout"] == [7.0, 1.0, 8.0]
def test_compute_reduced_explicit_paths(tmp_path: Path):
run_dir = _prep([_write_inputs(tmp_path)])
meta = RunMeta.load(run_dir / "run_meta.json")
+14
View File
@@ -292,6 +292,20 @@ def test_render_one_of_each_kind(tmp_path: Path):
"ylabel": "frac",
},
),
Reduced(
"cost",
"cost",
"bar",
"Cost",
"phase",
{
"labels": ["sample", "write", "total"],
"series": {"flow": [10.0, 1.0, 11.0]},
"reference": [5.0, 0.5, 5.5],
"ylabel": "us/step",
"log_y": True,
},
),
Reduced(
"s",
"species",