Skip to content

Commit c06a7e3

Browse files
committed
add performance scripts for DTypeFront
1 parent 13539af commit c06a7e3

2 files changed

Lines changed: 252 additions & 0 deletions

File tree

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
#!/usr/bin/env python3
2+
"""Generate a DTypeFront performance table from benchmark logs."""
3+
4+
from __future__ import annotations
5+
6+
import argparse
7+
import re
8+
from dataclasses import dataclass
9+
from pathlib import Path
10+
11+
12+
METHOD_ORDER = {
13+
"ROS2S": 0,
14+
"Rodas3P": 1,
15+
"Rodas4P": 2,
16+
"VODE": 3,
17+
"Rodas5P": 4,
18+
}
19+
20+
21+
@dataclass
22+
class Metrics:
23+
method: str
24+
fom_us: float | None = None
25+
mupdates: float | None = None
26+
tp_wall_s: float | None = None
27+
chem_s: float | None = None
28+
chem_pct: float | None = None
29+
adv_s: float | None = None
30+
adv_pct: float | None = None
31+
hydro_s: float | None = None
32+
hydro_pct: float | None = None
33+
subcycles: float | None = None
34+
failed: str | None = None
35+
36+
@property
37+
def rad_no_ode_pct(self) -> float | None:
38+
if self.adv_pct is None or self.hydro_pct is None or self.chem_pct is None:
39+
return None
40+
return self.adv_pct - self.hydro_pct - self.chem_pct
41+
42+
@property
43+
def hydro_only_speedup(self) -> float | None:
44+
if self.hydro_pct is None or self.hydro_pct == 0.0:
45+
return None
46+
return 100.0 / self.hydro_pct
47+
48+
@property
49+
def rad_substep_cost(self) -> float | None:
50+
if self.adv_s is None or self.hydro_s is None or self.subcycles is None or self.hydro_s == 0.0:
51+
return None
52+
return (self.adv_s - self.hydro_s) / (self.hydro_s * self.subcycles)
53+
54+
55+
def discover_logs(paths: list[Path]) -> list[Path]:
56+
logs: list[Path] = []
57+
for path in paths:
58+
if path.is_dir():
59+
logs.extend(sorted(path.glob("*.log")))
60+
else:
61+
logs.append(path)
62+
return logs
63+
64+
65+
def method_from_log(text: str, path: Path) -> str:
66+
match = re.search(r"DTypeFront microphysics integrator: VODE", text)
67+
if match:
68+
return "VODE"
69+
70+
match = re.search(r"DTypeFront microphysics integrator: Rosenbrock \(Rosenbrock tableau \d+: ([^)]+)\)", text)
71+
if match:
72+
return match.group(1)
73+
74+
name_map = {
75+
"ros2s": "ROS2S",
76+
"rodas3p": "Rodas3P",
77+
"rodas4p": "Rodas4P",
78+
"rodas5p": "Rodas5P",
79+
"vode": "VODE",
80+
}
81+
return name_map.get(path.stem.lower(), path.stem)
82+
83+
84+
def parse_profiler_line(text: str, name: str) -> tuple[float, float] | None:
85+
# Example:
86+
# PhotoChemistry::computePhotoChemistry() 7734 6.474 6.474 6.474 33.65%
87+
pattern = re.compile(rf"^{re.escape(name)}\s+\d+\s+([0-9.eE+-]+)\s+([0-9.eE+-]+)\s+([0-9.eE+-]+)\s+([0-9.eE+-]+)%", re.MULTILINE)
88+
matches = [(float(match.group(3)), float(match.group(4))) for match in pattern.finditer(text)]
89+
if not matches:
90+
return None
91+
# The same profiler name can appear as exclusive and inclusive entries. The
92+
# inclusive entry is the one with the largest walltime/percentage.
93+
return max(matches, key=lambda value: value[1])
94+
95+
96+
def parse_log(path: Path) -> Metrics:
97+
text = path.read_text(errors="replace")
98+
metrics = Metrics(method=method_from_log(text, path))
99+
100+
match = re.search(r"Performance figure-of-merit:\s+([0-9.eE+-]+)\s+.*?\[([0-9.eE+-]+)\s+Mupdates/s\]", text)
101+
if match:
102+
metrics.fom_us = float(match.group(1))
103+
metrics.mupdates = float(match.group(2))
104+
105+
match = re.search(r"TinyProfiler total time across processes \[min\.\.\.avg\.\.\.max\]:\s+([0-9.eE+-]+)\s+\.\.\.\s+([0-9.eE+-]+)\s+\.\.\.\s+([0-9.eE+-]+)", text)
106+
if match:
107+
metrics.tp_wall_s = float(match.group(3))
108+
109+
match = re.search(r"avg\. num\. of radiation subcycles\s*=\s*([0-9.eE+-]+)", text)
110+
if match:
111+
metrics.subcycles = float(match.group(1))
112+
113+
chem = parse_profiler_line(text, "PhotoChemistry::computePhotoChemistry()")
114+
if chem is not None:
115+
metrics.chem_s, metrics.chem_pct = chem
116+
117+
advance = parse_profiler_line(text, "QuokkaSimulation::advanceSingleTimestepAtLevel()")
118+
if advance is not None:
119+
metrics.adv_s, metrics.adv_pct = advance
120+
121+
hydro = parse_profiler_line(text, "REG::HydroSolver")
122+
if hydro is not None:
123+
metrics.hydro_s, metrics.hydro_pct = hydro
124+
125+
match = re.search(r"Photochemistry burn failed.*", text)
126+
if match:
127+
metrics.failed = match.group(0)
128+
129+
return metrics
130+
131+
132+
def fmt(value: float | None, precision: int = 2) -> str:
133+
if value is None:
134+
return "n/a"
135+
return f"{value:.{precision}f}"
136+
137+
138+
def fmt_fom(value: float | None) -> str:
139+
if value is None:
140+
return "n/a"
141+
return f"{value:.4f}"
142+
143+
144+
def make_table(metrics: list[Metrics]) -> str:
145+
rows = sorted(metrics, key=lambda row: (METHOD_ORDER.get(row.method, 99), row.method))
146+
lines = [
147+
"| Method | FoM us/zone | Mupdate/s | TP wall s | Chem s | Chem % | Rad-noODE % | Hydro % | Hydro-only x | RadSub/HydroStep | Subcyc |",
148+
"|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|",
149+
]
150+
for row in rows:
151+
lines.append(
152+
"| "
153+
+ " | ".join(
154+
[
155+
row.method,
156+
fmt_fom(row.fom_us),
157+
fmt(row.mupdates, 2),
158+
fmt(row.tp_wall_s, 2),
159+
fmt(row.chem_s, 2),
160+
fmt(row.chem_pct, 2),
161+
fmt(row.rad_no_ode_pct, 2),
162+
fmt(row.hydro_pct, 2),
163+
fmt(row.hydro_only_speedup, 2),
164+
fmt(row.rad_substep_cost, 2),
165+
fmt(row.subcycles, 2),
166+
]
167+
)
168+
+ " |"
169+
)
170+
return "\n".join(lines)
171+
172+
173+
def main() -> None:
174+
parser = argparse.ArgumentParser(description=__doc__)
175+
parser.add_argument("logs", nargs="+", type=Path, help="DTypeFront log files or directories containing *.log files")
176+
parser.add_argument("--show-failures", action="store_true", help="print failed-run diagnostics after the table")
177+
args = parser.parse_args()
178+
179+
logs = discover_logs(args.logs)
180+
if not logs:
181+
raise SystemExit("no log files found")
182+
183+
metrics = [parse_log(path) for path in logs]
184+
print(make_table(metrics))
185+
186+
if args.show_failures:
187+
failures = [(path, row.failed) for path, row in zip(logs, metrics, strict=True) if row.failed is not None]
188+
if failures:
189+
print("\nFailures:")
190+
for path, failure in failures:
191+
print(f"- {path}: {failure}")
192+
193+
194+
if __name__ == "__main__":
195+
main()
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/bin/bash --login
2+
#SBATCH --job-name=dtypefront_gpu
3+
#SBATCH --output=%x-%j.out
4+
#SBATCH --error=%x-%j.err
5+
#SBATCH --time=00:20:00
6+
#SBATCH --nodes=1
7+
#SBATCH --ntasks=1
8+
#SBATCH --cpus-per-task=16
9+
#SBATCH --gpus=h200:1
10+
#SBATCH --gpu-bind=closest
11+
#SBATCH --mem=64G
12+
13+
set -euo pipefail
14+
15+
ROOT_DIR="${SLURM_SUBMIT_DIR:-$(pwd)}"
16+
INPUT_FILE="${ROOT_DIR}/inputs/DTypeFront.toml"
17+
LOG_DIR="${ROOT_DIR}/logs/dtypefront-${SLURM_JOB_ID:-manual}"
18+
ROS_BUILD_DIR="${ROOT_DIR}/build/slurm-dtypefront-rosenbrock"
19+
VODE_BUILD_DIR="${ROOT_DIR}/build/slurm-dtypefront-vode"
20+
BUILD_JOBS="${SLURM_CPUS_PER_TASK:-16}"
21+
22+
mkdir -p "${LOG_DIR}"
23+
24+
run_case() {
25+
local exe="$1"
26+
local tag="$2"
27+
shift 2
28+
29+
echo "=== $(date -Is) ${tag} ==="
30+
echo "CUDA_VISIBLE_DEVICES=${CUDA_VISIBLE_DEVICES:-unset}"
31+
/usr/bin/time -p "${exe}" "${INPUT_FILE}" dtype_front.plot_radii=0 "$@" \
32+
> "${LOG_DIR}/${tag}.log" 2>&1
33+
}
34+
35+
configure_build() {
36+
local build_dir="$1"
37+
local integrator="$2"
38+
39+
cmake -S "${ROOT_DIR}" -B "${build_dir}" -DDTypeFront_INTEGRATOR="${integrator}" -DAMReX_GPU_BACKEND=CUDA -DAMReX_GPU_ARCH=9.0 -DAMReX_SPACEDIM=3
40+
cmake --build "${build_dir}" --target DTypeFront -j "${BUILD_JOBS}"
41+
}
42+
43+
echo "Running on ${HOSTNAME}"
44+
echo "Submit dir: ${ROOT_DIR}"
45+
echo "Log dir: ${LOG_DIR}"
46+
echo "Build jobs: ${BUILD_JOBS}"
47+
48+
configure_build "${ROS_BUILD_DIR}" Rosenbrock
49+
run_case "${ROS_BUILD_DIR}/src/problems/DTypeFront/DTypeFront" ros2s integrator.rosenbrock_tableau=3
50+
run_case "${ROS_BUILD_DIR}/src/problems/DTypeFront/DTypeFront" rodas3p integrator.rosenbrock_tableau=2
51+
run_case "${ROS_BUILD_DIR}/src/problems/DTypeFront/DTypeFront" rodas4p integrator.rosenbrock_tableau=1
52+
run_case "${ROS_BUILD_DIR}/src/problems/DTypeFront/DTypeFront" rodas5p integrator.rosenbrock_tableau=0
53+
54+
configure_build "${VODE_BUILD_DIR}" VODE
55+
run_case "${VODE_BUILD_DIR}/src/problems/DTypeFront/DTypeFront" vode
56+
57+
echo "Done."

0 commit comments

Comments
 (0)