Files
giant/giant/analysis/runtime_estimate.py
T
lars 81eb14d75c
CI / Format (ruff format) (push) Successful in 27s
CI / Lint (ruff check) (push) Successful in 28s
CI / Sync project version with tag (push) Has been skipped
CI / Lint (ruff check) (pull_request) Successful in 34s
CI / Type check (ty) (push) Successful in 38s
CI / Format (ruff format) (pull_request) Successful in 52s
CI / Sync project version with tag (pull_request) Has been skipped
CI / Type check (ty) (pull_request) Successful in 51s
CI / Tests (pull_request) Successful in 3m26s
CI / Tests (push) Successful in 3m36s
Move scripts/ to giant/tools/ (issues.md Issue 9)
`scripts` was published as a top-level distribution package, colliding
with one of the most generic names in the Python ecosystem and
shadowable by a stray scripts/ dir on the portal machines' shared
/work/lbogner. Move it under the giant namespace; the dwarf command
name is unchanged, only the Python import path and file location move.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 10:31:47 +02:00

116 lines
5.8 KiB
Python

"""Per-(plot, chunk) HTCondor walltime estimates for `giant analyze submit`.
Each catalog spec's compute cost is close to linear in the number of input
rows a `compute-one` job streams over — every spec is one (or a couple of)
streaming `group_by` pass(es) over the chunk (see `catalog.py`/`reduce.py`).
`_COST_MODEL` below is ``spec_id -> (intercept_s, seconds_per_row)``.
``n_rows`` is the combined rollout+reference row count of the job's input:
the chunk's row count for `chunkable=True` specs, the whole dataset's for the
three `chunkable=False` router specs (they always run as a single job
regardless of chunk count).
Calibrated 2026-07-27 from real HTCondor timings (`condor_history`
``RemoteWallClockTime``) of a production run: prediction ``563f5ee3``
(PbWO4, 50 GeV) analyzed with ``--chunks 4`` against
``giant/analysis/runtime_estimate.py``'s prior (local-synthetic-only) model —
see the ``analysis-rollout-plots`` branch history for the raw data. That run's
4 chunks came out at nearly identical row counts (~63-64M rows each, ~254M
total), so this real data has no genuine row-count spread to fit a slope
against — instead each spec's ``per_row`` here is a single line through the
origin (``intercept=0``) hitting that spec's *median* wall-clock time across
its 4 chunks at that run's row count. A handful of (spec, chunk) pairs showed
3-8x spikes in one chunk only (e.g. ``marginal_edep_by_material``: 88, 88, 90,
722s) — almost certainly shared ``/ceph`` contention from ~130 jobs landing on
the filesystem at once right after submission, not a real per-row cost, so
the median (not the max) was fit to avoid baking that noise into a rate that
would then wrongly scale up with a bigger dataset. `RUNTIME_SAFETY_MARGIN` is
deliberately generous (4x total) specifically to absorb that kind of
contention spike instead. Rerun this calibration (pull fresh
`condor_history`/`run_meta.json`, refit) if the catalog changes or timings
drift — a synthetic local rebaseline via `giant/tools/profile_analysis_costs.py`
is a reasonable fallback when no real cluster data is available yet, but
undershoots real wall time badly (it can't see docker pull / `/ceph` I/O
latency), which is exactly why this file moved off it.
"""
from __future__ import annotations
import math
# Multiplicative pad applied to every job's estimated walltime. The one knob
# this feature was asked to expose. Set generously (4x total, i.e. 3.0 here)
# to absorb the shared-/ceph-contention spikes described above rather than
# encoding them into individual specs' per-row rates.
RUNTIME_SAFETY_MARGIN = 3.00
# Fixed per-job overhead (docker start, `.venv/bin/giant` startup, initial
# `/ceph` read latency) — calibrated as the fastest observed real spec
# (`leakage_fraction`, median 51s) rounded up, since even the cheapest spec
# streams the whole chunk once.
_FIXED_OVERHEAD_S = 60.0
# Router diagnostics run a live torch checkpoint (bounded inference over
# <=200k subsampled rows, independent of chunk size) instead of a row-based
# scan. Calibrated from the 3 real router jobs' observed wall times (119, 66,
# 124s) — max minus _FIXED_OVERHEAD_S, on top of it.
_ROUTER_FIXED_S = 64.0
_ROUTER_IDS = frozenset({"router_gating", "router_share_by_pdg", "router_share_by_process"})
# Conservative fallback for any catalog id not in _COST_MODEL (e.g. a plot
# added after the last calibration run) — the most expensive fitted per-row
# rate observed, plus a small constant pad.
_DEFAULT_COST = (5.0, 3.0e-6)
# spec_id -> (intercept_s, seconds_per_row), fit 2026-07-27 from real
# HTCondor `RemoteWallClockTime` (see module docstring for methodology).
_COST_MODEL: dict[str, tuple[float, float]] = {
"marginal_step_length": (0.0, 5.199e-07),
"marginal_step_length_by_energy": (0.0, 1.678e-06),
"marginal_step_length_by_pdg": (0.0, 4.569e-07),
"marginal_step_length_by_material": (0.0, 2.269e-06),
"marginal_edep": (0.0, 2.804e-06),
"marginal_edep_by_energy": (0.0, 1.386e-06),
"marginal_edep_by_pdg": (0.0, 4.490e-07),
"marginal_edep_by_material": (0.0, 4.333e-07),
"marginal_delta_e": (0.0, 5.042e-07),
"marginal_delta_e_by_energy": (0.0, 1.678e-06),
"marginal_delta_e_by_pdg": (0.0, 4.727e-07),
"marginal_delta_e_by_material": (0.0, 4.490e-07),
"marginal_post_E": (0.0, 4.805e-07),
"marginal_post_E_by_energy": (0.0, 1.284e-06),
"marginal_post_E_by_pdg": (0.0, 4.569e-07),
"marginal_post_E_by_material": (0.0, 4.490e-07),
"marginal_cos_scatter": (0.0, 5.436e-07),
"marginal_cos_scatter_by_energy": (0.0, 1.363e-06),
"marginal_cos_scatter_by_pdg": (0.0, 4.727e-07),
"marginal_cos_scatter_by_material": (0.0, 4.727e-07),
"event_total_edep": (0.0, 4.490e-07),
"event_total_edep_by_energy": (0.0, 4.411e-07),
"event_mean_length": (0.0, 4.333e-07),
"event_n_steps": (0.0, 4.569e-07),
"shower_longitudinal": (0.0, 2.348e-06),
"shower_transverse": (0.0, 2.899e-06),
"species_edep_share": (0.0, 4.333e-07),
"leakage_fraction": (0.0, 0.0),
"sec_count_per_event": (0.0, 4.727e-07),
"sec_count_per_species": (0.0, 4.963e-07),
"sec_energy": (0.0, 4.727e-07),
"sec_cos_angle": (0.0, 2.749e-06),
}
def estimate_runtime_s(spec_id: str, n_rows: int) -> int:
"""Estimated `+RequestWalltime` (seconds) for one (plot, chunk) job.
``n_rows`` is the rollout+reference row count of that job's input slice.
Includes `_FIXED_OVERHEAD_S`/`_ROUTER_FIXED_S` and `RUNTIME_SAFETY_MARGIN`
— callers should pass this straight through to the submit description.
"""
if spec_id in _ROUTER_IDS:
compute_s = _ROUTER_FIXED_S
else:
intercept, per_row = _COST_MODEL.get(spec_id, _DEFAULT_COST)
compute_s = intercept + per_row * n_rows
total = _FIXED_OVERHEAD_S + compute_s
return math.ceil(total * (1 + RUNTIME_SAFETY_MARGIN))