aboutsummaryrefslogtreecommitdiffstats
path: root/research/experiments/001-checkpoint-granularity/run.py
diff options
context:
space:
mode:
Diffstat (limited to 'research/experiments/001-checkpoint-granularity/run.py')
-rw-r--r--research/experiments/001-checkpoint-granularity/run.py148
1 files changed, 148 insertions, 0 deletions
diff --git a/research/experiments/001-checkpoint-granularity/run.py b/research/experiments/001-checkpoint-granularity/run.py
new file mode 100644
index 0000000..b2d66f2
--- /dev/null
+++ b/research/experiments/001-checkpoint-granularity/run.py
@@ -0,0 +1,148 @@
+#!/usr/bin/env python3
+"""Build, check, measure, and preserve raw data using only the Python stdlib."""
+import argparse
+import csv
+import hashlib
+import json
+import math
+import os
+from pathlib import Path
+import platform
+import random
+import shlex
+import statistics
+import subprocess
+import tempfile
+from datetime import datetime, timezone
+
+
+ROOT = Path(__file__).resolve().parent
+SOURCE = ROOT / "benchmark.c"
+FLAGS = ["-std=c17", "-O3", "-Wall", "-Wextra", "-Wpedantic", "-Werror"]
+
+
+def command(args):
+ return subprocess.check_output(args, text=True).strip()
+
+
+def write_csv(path, rows):
+ with path.open("w", newline="") as f:
+ writer = csv.DictWriter(f, fieldnames=list(rows[0]))
+ writer.writeheader()
+ writer.writerows(rows)
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--output", type=Path)
+ parser.add_argument("--trials", type=int, default=7)
+ parser.add_argument("--target-ms", type=float, default=20.0)
+ parser.add_argument("--check-only", action="store_true")
+ args = parser.parse_args()
+ if args.trials < 3 or not 1 <= args.target_ms <= 1000:
+ parser.error("use at least 3 trials and a target from 1 to 1000 ms")
+ if not args.check_only and args.output is None:
+ parser.error("--output is required for measurements")
+ cc = shlex.split(os.environ.get("CC", "cc"))
+ environment = {
+ "started_utc": datetime.now(timezone.utc).isoformat(),
+ "platform": platform.platform(), "machine": platform.machine(),
+ "compiler": command(cc + ["--version"]), "flags": FLAGS,
+ "python": platform.python_version(),
+ "source_sha256": hashlib.sha256(SOURCE.read_bytes()).hexdigest(),
+ "runner_sha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
+ "trials": args.trials, "target_ms": args.target_ms,
+ "seed": 20260922, "timing": "batch elapsed time / update ticks; not input latency",
+ "memory": "sizeof engine + requested live array bytes; excludes allocator, harness, code, OS",
+ "power_control": "not controlled; shared host; preliminary measurements",
+ }
+ # One CPU reduces migration noise. This is recorded, not assumed to remove noise.
+ if hasattr(os, "sched_getaffinity"):
+ affinity = sorted(os.sched_getaffinity(0))
+ environment["original_affinity"] = affinity
+ os.sched_setaffinity(0, {affinity[0]})
+ environment["measurement_affinity"] = sorted(os.sched_getaffinity(0))
+ cpuinfo = Path("/proc/cpuinfo")
+ if cpuinfo.exists():
+ allowed = ("model name", "CPU implementer", "CPU architecture", "CPU part", "Model")
+ environment["cpu_description"] = sorted({
+ line for line in cpuinfo.read_text().splitlines()
+ if line.split(":", 1)[0].strip() in allowed
+ })
+ with tempfile.TemporaryDirectory(prefix="pure-checkpoints-") as tmp:
+ binary = Path(tmp) / "benchmark"
+ subprocess.run(cc + FLAGS + [str(SOURCE), "-o", str(binary)], check=True)
+ environment["release_check"] = json.loads(command([str(binary), "--check"]))
+ checked = Path(tmp) / "benchmark-checked"
+ check_flags = ["-std=c17", "-O1", "-g", "-Wall", "-Wextra", "-Wpedantic", "-Werror",
+ "-fsanitize=address,undefined", "-fno-omit-frame-pointer"]
+ subprocess.run(cc + check_flags + [str(SOURCE), "-o", str(checked)], check=True)
+ environment["sanitizer_check"] = json.loads(command([str(checked), "--check"]))
+ print("Differential checks passed in release and ASan/UBSan builds.", flush=True)
+ if args.check_only:
+ print(json.dumps(environment, indent=2))
+ return
+ # Fail before measurements if a previous run would be overwritten.
+ args.output.mkdir(parents=True, exist_ok=False)
+
+ cases = []
+ for chains, length in [(16, 256), (16, 1024)]:
+ for workload in ["roots", "uniform", "tail", "dense"]:
+ for block in [1, 4, 16, 64, length]:
+ cases.append((chains, length, workload, block))
+
+ def sample(case, replays):
+ chains, length, workload, block = case
+ return json.loads(command([str(binary), str(chains), str(length), str(block),
+ workload, "256", str(replays), str(environment["seed"])]))
+
+ calibration = {}
+ for case in cases:
+ pilot = sample(case, 2)
+ wanted = math.ceil(2 * args.target_ms * 1e6 / max(1, pilot["elapsed_ns"]))
+ # Even replay counts leave the same final model in every strategy.
+ calibration[case] = min(2048, max(2, wanted + wanted % 2))
+ environment["calibration_replays"] = [
+ {"chains": c[0], "length": c[1], "workload": c[2], "block": c[3], "replays": r}
+ for c, r in calibration.items()
+ ]
+ rows = []
+ checksums = {}
+ rng = random.Random(81173)
+ for trial in range(args.trials):
+ order = cases.copy()
+ rng.shuffle(order)
+ for case in order:
+ result = sample(case, calibration[case])
+ chains, length, workload, block = case
+ key = (chains, length, workload)
+ if checksums.setdefault(key, result["checksum"]) != result["checksum"]:
+ raise RuntimeError(f"cross-strategy output mismatch: {case}")
+ row = {"trial": trial, "chains": chains, "length": length,
+ "workload": workload, "block": block, "replays": calibration[case],
+ **result}
+ row["ns_per_tick"] = result["elapsed_ns"] / result["ticks"]
+ row["evaluations_per_tick"] = result["node_evaluations"] / result["ticks"]
+ rows.append(row)
+ write_csv(args.output / "raw.csv", rows)
+ print(f"Completed trial {trial + 1}/{args.trials}.", flush=True)
+ summary = []
+ for case in cases:
+ chains, length, workload, block = case
+ group = [r for r in rows if (r["chains"], r["length"], r["workload"], r["block"]) == case]
+ times = [r["ns_per_tick"] for r in group]
+ summary.append({"chains": chains, "length": length, "workload": workload, "block": block,
+ "median_ns_per_tick": statistics.median(times),
+ "min_ns_per_tick": min(times), "max_ns_per_tick": max(times),
+ "core_owned_bytes": group[0]["core_owned_bytes"],
+ "cache_bytes": group[0]["cache_bytes"],
+ "evaluations_per_tick": group[0]["evaluations_per_tick"]})
+ write_csv(args.output / "summary.csv", summary)
+ environment["completed_utc"] = datetime.now(timezone.utc).isoformat()
+ environment["binary_bytes"] = binary.stat().st_size
+ (args.output / "environment.json").write_text(json.dumps(environment, indent=2) + "\n")
+ print(f"Saved raw data, summary, and environment to {args.output}")
+
+
+if __name__ == "__main__":
+ main()