STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
bench_layer1_numba.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Two-way Layer-1 optimizer benchmark: plain Python vs the numba accelerator.
3
4Serial in-process, one subprocess per condition, warmup discarded. "python" sets
5ED_ACCEL=off so every candidate takes the authoritative Python path; "numba" runs
6the accelerator. The C backend this once compared against was deleted after the
7numba path overtook it.
8
9Timing here is noisy enough that a single run is not a measurement: identical work
10(seed pinned) has varied by 40% on a loaded box. Hence --reps and min/median.
11
12Run: .venv/bin/python scripts/bench_layer1_numba.py --max-iterations 20 --reps 5
13"""
14from __future__ import annotations
15import argparse, copy, json, os, subprocess, sys, time
16from statistics import median
17from pathlib import Path
18
19ROOT = Path(__file__).resolve().parents[1]
20sys.path.insert(0, str(ROOT)); sys.path.insert(0, str(ROOT / "scripts"))
21
22
23class _C:
24 def reset(self):
25 self.native_calls = self.native_none = self.runner_calls = 0
26C = _C(); C.reset()
27
28
30 """Count accelerated evaluates and Python fallbacks."""
31 from engine import accel as ni
32 from engine.core.runner import PintleEngineRunner
33 if getattr(ni.evaluate, "_benched", False):
34 return
35 _on, _or = ni.evaluate, PintleEngineRunner.evaluate
36 def nev(*a, **k):
37 r = _on(*a, **k); C.native_calls += 1
38 if r is None: C.native_none += 1
39 return r
40 def rev(self, *a, **k):
41 C.runner_calls += 1; return _or(self, *a, **k)
42 nev._benched = True
43 ni.evaluate = nev; PintleEngineRunner.evaluate = rev
44
45
46def _one(cfg_path, max_it, restarts, seed):
47 import numpy as np
49 from engine.core.runner import PintleEngineRunner
50 from engine.pipeline.io import load_config
51 L1._get_num_workers = lambda cfg: 1
52 base = load_config(str(cfg_path)); cfg = copy.deepcopy(base)
53 req = cfg.design_requirements.model_dump()
54 pcfg = {"mode": "optimizer_controlled",
55 "max_lox_pressure_psi": float(req["max_lox_tank_pressure_psi"]),
56 "max_fuel_pressure_psi": float(req["max_fuel_tank_pressure_psi"])}
57 # Layer 1 draws its CMA seed base from np.random.SeedSequence().entropy (fresh OS
58 # entropy) unless requirements["layer1_random_seed"] is pinned -- np.random.seed()
59 # only touches the legacy global RandomState and does NOT reach it. Without this pin
60 # every run walks a different candidate trajectory and the modes are not comparable.
61 req["layer1_random_seed"] = int(seed)
62 np.random.seed(seed); t0 = time.perf_counter()
63 L1.run_layer1_optimization(cfg, PintleEngineRunner(copy.deepcopy(base)), req,
64 target_burn_time=float(req.get("target_burn_time", 6.0)),
65 tolerances={"thrust": 0.10, "apogee": 0.15}, pressure_config=pcfg,
66 layer1_smoke=True, layer1_max_iterations=int(max_it), layer1_cma_restarts=int(restarts))
67 return time.perf_counter() - t0
68
69
70def _run_condition(mode, cfg_path, max_it, restarts, seed):
71 os.environ["ED_ACCEL"] = "off" if mode == "python" else "numba"
72 os.environ["ED_LAYER1_NATIVE_EVAL"] = "0" if mode == "python" else "1"
74 _one(cfg_path, 1, 1, seed) # warmup (JIT compile, CEA load) — discarded
75 C.reset()
76 wall = _one(cfg_path, max_it, restarts, seed)
77 cands = C.native_calls if C.native_calls else C.runner_calls
78 out = {"mode": mode, "wall_s": wall, "candidates": cands,
79 "native_calls": C.native_calls, "native_fallbacks": C.native_none,
80 "runner_calls": C.runner_calls,
81 "us_per_candidate": (wall / cands * 1e6) if cands else None}
82 print("BENCH_JSON " + json.dumps(out))
83
84
85def _pc(mode, runs):
86 """Summarise N repetitions of one mode.
87
88 A single wall time is not a measurement here: on a loaded box the SAME work
89 (seed pinned, byte-identical candidate trajectory) was observed to vary
90 6.81s..11.59s, a 1.7x spread that dwarfs the C-vs-Numba difference. Report
91 min (least interference) and median instead.
92 """
93 us = sorted(r["us_per_candidate"] for r in runs)
94 walls = sorted(r["wall_s"] for r in runs)
95 o = runs[0]
96 print(f"\n[{mode.upper()}] reps={len(runs)} candidates={o['candidates']}")
97 print(f" wall_s min={walls[0]:.3f} median={median(walls):.3f} max={walls[-1]:.3f}")
98 print(f" us/candidate min={us[0]:.0f} median={median(us):.0f} max={us[-1]:.0f}")
99 if o["native_calls"]:
100 went = o["native_calls"] - o["native_fallbacks"]
101 print(f" accel path: {went}/{o['native_calls']} ({100.0*went/o['native_calls']:.0f}%)"
102 f" fallback={o['native_fallbacks']} runner.evaluate={o['runner_calls']}")
103 # The seed is pinned, so every rep must do byte-identical work. If it does
104 # not, the trajectory is diverging run-to-run and NO comparison here means
105 # anything -- say so loudly rather than printing a confident ratio.
106 if len({(r["candidates"], r["native_fallbacks"], r["runner_calls"]) for r in runs}) > 1:
107 print(" !! WORK VARIED ACROSS REPS -- seed not reaching the optimizer;"
108 " these numbers are NOT comparable")
109 return {"min": us[0], "median": median(us)}
110
111
112def main():
113 ap = argparse.ArgumentParser()
114 ap.add_argument("--config", default="configs/impinging_lox_ch4_8000N.yaml")
115 ap.add_argument("--max-iterations", type=int, default=8)
116 ap.add_argument("--cma-restarts", type=int, default=1)
117 ap.add_argument("--seed", type=int, default=0)
118 ap.add_argument("--reps", type=int, default=3,
119 help="timed repetitions per accelerated mode (noise control)")
120 ap.add_argument("--python-reps", type=int, default=1,
121 help="reps for the Python baseline (~10x slower, 1 is usually enough)")
122 ap.add_argument("--mode", choices=["python", "numba"], default=None)
123 a = ap.parse_args()
124 cfg_path = (ROOT / a.config) if not os.path.isabs(a.config) else Path(a.config)
125 if a.mode:
126 _run_condition(a.mode, cfg_path, a.max_iterations, a.cma_restarts, a.seed); return
127 print(f"config: {cfg_path}\nbudget: max_iterations={a.max_iterations} restarts={a.cma_restarts} "
128 f"seed={a.seed} reps={a.reps} (serial, warmup discarded)")
129 agg = {}
130 for mode in ("python", "numba"):
131 runs = []
132 for _ in range(a.python_reps if mode == "python" else a.reps):
133 cmd = [sys.executable, str(Path(__file__).resolve()), "--config", a.config,
134 "--max-iterations", str(a.max_iterations), "--cma-restarts", str(a.cma_restarts),
135 "--seed", str(a.seed), "--mode", mode]
136 p = subprocess.run(cmd, cwd=str(ROOT), capture_output=True, text=True)
137 line = next((l for l in p.stdout.splitlines() if l.startswith("BENCH_JSON ")), None)
138 if not line:
139 print(f"!! {mode} produced no result. stderr tail:\n"
140 + "\n".join(p.stderr.splitlines()[-20:])); return
141 runs.append(json.loads(line[len("BENCH_JSON "):]))
142 agg[mode] = _pc(mode, runs)
143 py, nb = agg["python"], agg["numba"]
144 print("\n" + "=" * 68)
145 for stat in ("min", "median"):
146 print(f" [{stat:6s}] per-candidate us: python={py[stat]:.0f} numba={nb[stat]:.0f}"
147 f" speedup={py[stat]/nb[stat]:.1f}x")
148 print("=" * 68)
149
150
151if __name__ == "__main__":
152 main()
_run_condition(mode, cfg_path, max_it, restarts, seed)
_one(cfg_path, max_it, restarts, seed)