gbskernels 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,169 @@
1
+ """Accuracy characterization for the permanent (FP64 vs mpmath reference).
2
+
3
+ The CPU-runnable, throughput-free half of docs/DESIGN.md §9: relative error vs matrix
4
+ size and vs the Glynn condition number ``kappa``, measured against the
5
+ independent arbitrary-precision reference. This is the "measured boundary" of
6
+ docs/DESIGN.md §6 -- the thing that tells a user exactly when FP64 suffices and when
7
+ the (GPU) double-double tier is required.
8
+
9
+ Writes a timestamped, self-describing JSON artifact to ``results/accuracy/``
10
+ (append-only). **No timing is recorded here** -- throughput numbers come only
11
+ from scripted rented-GPU sessions (docs/DESIGN.md §8 CI policy).
12
+
13
+ uv run python -m bench.accuracy_permanent --sizes 2-12 --seeds 8 --dps 60
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import json
20
+ import platform
21
+ import subprocess
22
+ from datetime import datetime, timezone
23
+ from importlib.metadata import version
24
+ from pathlib import Path
25
+ from typing import Any
26
+
27
+ import mpmath
28
+ import numpy as np
29
+
30
+ from cpu_ref import cancellation_ratio, permanent_glynn
31
+ from cpu_ref.permanent import permanent_glynn_dd
32
+ from highprec_ref import permanent_mp
33
+
34
+ from ._inputs import make_cancellation_matrix, random_complex
35
+
36
+ DEFAULT_OUT = Path(__file__).resolve().parent.parent / "results" / "accuracy"
37
+ CANCELLATION_DELTAS = [1e-1, 1e-2, 1e-4, 1e-6, 1e-8, 1e-10, 1e-12, 1e-14]
38
+
39
+
40
+ def _git_commit() -> str | None:
41
+ try:
42
+ out = subprocess.run(
43
+ ["git", "rev-parse", "--short", "HEAD"],
44
+ capture_output=True, text=True, cwd=Path(__file__).parent,
45
+ )
46
+ return out.stdout.strip() or None
47
+ except Exception:
48
+ return None
49
+
50
+
51
+ def _rel_err_mp(approx: complex, exact_mp: Any, dps: int) -> float:
52
+ """Relative error of an FP64 result vs the mpmath reference, computed in
53
+ mpmath so the reference's full precision is used (not truncated to FP64)."""
54
+ with mpmath.workdps(dps):
55
+ a = mpmath.mpc(approx.real, approx.imag)
56
+ return float(abs(a - exact_mp) / abs(exact_mp))
57
+
58
+
59
+ def run(
60
+ sizes: list[int],
61
+ seeds_per_size: int,
62
+ dps: int = 60,
63
+ out_dir: Path | None = None,
64
+ tag: str = "",
65
+ ) -> tuple[dict[str, Any], Path]:
66
+ """Run the accuracy sweeps and write one append-only artifact. Returns it."""
67
+ size_sweep = []
68
+ for n in sizes:
69
+ rel_errs, kappas = [], []
70
+ for seed in range(seeds_per_size):
71
+ A = random_complex(n, seed=1000 * n + seed)
72
+ exact_mp = permanent_mp(A, dps=dps)
73
+ approx = permanent_glynn(A)
74
+ rel_errs.append(_rel_err_mp(approx, exact_mp, dps))
75
+ kappas.append(cancellation_ratio(A, perm_value=complex(exact_mp)))
76
+ size_sweep.append(
77
+ {
78
+ "n": n,
79
+ "rel_err_median": float(np.median(rel_errs)),
80
+ "rel_err_max": float(np.max(rel_errs)),
81
+ "kappa_median": float(np.median(kappas)),
82
+ "rel_errs": [float(x) for x in rel_errs],
83
+ "kappas": [float(x) for x in kappas],
84
+ }
85
+ )
86
+
87
+ # Conditioning axis: drive kappa with the cancellation knob at fixed n.
88
+ cancellation_sweep = []
89
+ cancel_dps = max(dps, 80)
90
+ for delta in CANCELLATION_DELTAS:
91
+ A = make_cancellation_matrix(6, delta=delta, seed=1)
92
+ exact_mp = permanent_mp(A, dps=cancel_dps)
93
+ cancellation_sweep.append(
94
+ {
95
+ "n": 6,
96
+ "delta": delta,
97
+ "kappa": cancellation_ratio(A, perm_value=complex(exact_mp)),
98
+ "rel_err_fp64": _rel_err_mp(permanent_glynn(A), exact_mp, cancel_dps),
99
+ "rel_err_dd": _rel_err_mp(permanent_glynn_dd(A), exact_mp, cancel_dps),
100
+ }
101
+ )
102
+
103
+ artifact = {
104
+ "kind": "accuracy_permanent",
105
+ "created_utc": datetime.now(timezone.utc).isoformat(),
106
+ "env": {
107
+ "python": platform.python_version(),
108
+ "platform": platform.platform(),
109
+ "numpy": version("numpy"),
110
+ "mpmath": version("mpmath"),
111
+ "git_commit": _git_commit(),
112
+ },
113
+ "params": {
114
+ "sizes": sizes,
115
+ "seeds_per_size": seeds_per_size,
116
+ "dps": dps,
117
+ "dtype": "complex128",
118
+ "input_family": "random_complex (size sweep) / cancellation block (kappa sweep)",
119
+ },
120
+ "size_sweep": size_sweep,
121
+ "cancellation_sweep": cancellation_sweep,
122
+ }
123
+
124
+ out_dir = Path(out_dir) if out_dir is not None else DEFAULT_OUT
125
+ out_dir.mkdir(parents=True, exist_ok=True)
126
+ stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
127
+ suffix = f"_{tag}" if tag else ""
128
+ path = out_dir / f"accuracy_permanent_{stamp}{suffix}.json"
129
+ path.write_text(json.dumps(artifact, indent=2))
130
+ return artifact, path
131
+
132
+
133
+ def _print_summary(artifact: dict[str, Any], path: Path) -> None:
134
+ print(f"# accuracy_permanent -> {path}")
135
+ print(f"# env: py {artifact['env']['python']}, numpy {artifact['env']['numpy']}, "
136
+ f"mpmath {artifact['env']['mpmath']}, commit {artifact['env']['git_commit']}")
137
+ print("\n size sweep (FP64 vs mpmath, random complex):")
138
+ print(f" {'n':>3} {'rel_err median':>15} {'rel_err max':>13} {'kappa median':>13}")
139
+ for row in artifact["size_sweep"]:
140
+ print(f" {row['n']:>3} {row['rel_err_median']:>15.2e} "
141
+ f"{row['rel_err_max']:>13.2e} {row['kappa_median']:>13.2e}")
142
+ print("\n conditioning sweep (the measured FP64<->DD boundary, n=6):")
143
+ print(f" {'delta':>8} {'kappa':>11} {'rel_err FP64':>13} {'rel_err DD':>13}")
144
+ for row in artifact["cancellation_sweep"]:
145
+ print(f" {row['delta']:>8.0e} {row['kappa']:>11.2e} "
146
+ f"{row['rel_err_fp64']:>13.2e} {row['rel_err_dd']:>13.2e}")
147
+
148
+
149
+ def _parse_sizes(s: str) -> list[int]:
150
+ if "-" in s:
151
+ lo, hi = s.split("-")
152
+ return list(range(int(lo), int(hi) + 1))
153
+ return [int(x) for x in s.split(",")]
154
+
155
+
156
+ def main() -> None:
157
+ p = argparse.ArgumentParser(description=__doc__)
158
+ p.add_argument("--sizes", type=_parse_sizes, default=_parse_sizes("2-12"))
159
+ p.add_argument("--seeds", type=int, default=8)
160
+ p.add_argument("--dps", type=int, default=60)
161
+ p.add_argument("--out", type=Path, default=None)
162
+ p.add_argument("--tag", type=str, default="")
163
+ args = p.parse_args()
164
+ artifact, path = run(args.sizes, args.seeds, args.dps, args.out, args.tag)
165
+ _print_summary(artifact, path)
166
+
167
+
168
+ if __name__ == "__main__":
169
+ main()
@@ -0,0 +1,159 @@
1
+ """Calibrate precision="auto": is kappa a trustworthy FP64-risk indicator? (docs/DESIGN.md §6)
2
+
3
+ ``precision="auto"`` trusts the FP64 result when the cancellation indicator
4
+ ``kappa = sum|terms| / |result|`` (``cpu_ref.summation_condition_number``) is below
5
+ ``_AUTO_KAPPA_MAX`` (1e8), and reruns in the high-precision tier otherwise. ``kappa`` is a
6
+ **heuristic**, NOT a rigorous error certificate: the model is ``rel_err_fp64 ~ kappa * eps``,
7
+ but ``kappa`` is itself formed from the (FP64-inexact) computed ``|result|``, so under extreme
8
+ cancellation it can mis-estimate. So we MEASURE the relationship on physical / loss /
9
+ adversarial ensembles and calibrate the threshold rather than assume it:
10
+
11
+ * per ``(func, regime, size)``: ``kappa``, the actual FP64 relative error vs the mpmath
12
+ ground truth, and whether ``auto`` would trust FP64 (``kappa < threshold``);
13
+ * a calibration summary: the **worst FP64 error among the TRUSTED cases** (the accuracy
14
+ ``auto`` actually delivers), the **false-trust count** (``kappa < threshold`` yet the error
15
+ is large), and the kappa-vs-error correlation.
16
+
17
+ The headline a truthful claim can make: on these ensembles, ``kappa < 1e8`` keeps the FP64
18
+ error below ``max_rel_err_when_trusted`` with ``false_trust_count`` exceptions -- a calibrated
19
+ heuristic, stated with its measured failure rate, not a guarantee.
20
+
21
+ uv run python -m bench.calibrate_auto --dps 60
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import argparse
27
+ import json
28
+ import math
29
+ import platform
30
+ from datetime import datetime, timezone
31
+ from pathlib import Path
32
+ from typing import Any
33
+
34
+ import mpmath
35
+ import numpy as np
36
+
37
+ import cpu_ref
38
+ import gbskernels
39
+ import highprec_ref
40
+ from bench import _inputs, _provenance
41
+
42
+ DEFAULT_OUT = Path(__file__).resolve().parent.parent / "results" / "accuracy"
43
+
44
+ _MP = {"perm": highprec_ref.permanent_mp, "haf": highprec_ref.hafnian_mp,
45
+ "lhaf": highprec_ref.loop_hafnian_mp, "tor": highprec_ref.torontonian_mp}
46
+ _FN = {"perm": gbskernels.perm, "haf": gbskernels.haf, "lhaf": gbskernels.lhaf, "tor": gbskernels.tor}
47
+ _EPS = float(np.finfo(np.float64).eps)
48
+ _SIZES = {"perm": [6, 8], "haf": [6, 8], "lhaf": [6, 8], "tor": [6, 8]} # tor dim = 2*modes
49
+
50
+
51
+ def _pearson(pts: list[tuple[float, float]]) -> float | None:
52
+ n = len(pts)
53
+ if n < 2:
54
+ return None
55
+ xs = [p[0] for p in pts]; ys = [p[1] for p in pts]
56
+ mx, my = sum(xs) / n, sum(ys) / n
57
+ num = sum((x - mx) * (y - my) for x, y in pts)
58
+ dx = math.sqrt(sum((x - mx) ** 2 for x in xs)); dy = math.sqrt(sum((y - my) ** 2 for y in ys))
59
+ return float(num / (dx * dy)) if dx > 0 and dy > 0 else None
60
+
61
+
62
+ def _eval(func: str, A, backend: str, threshold: float):
63
+ """(FP64 value, kappa, trusted) for one input on ``backend``. On the GPU backend the kappa
64
+ comes from the on-device ``*_kappa`` kernel and the FP64 value from the GPU FP64 path -- so
65
+ the calibration measures the GPU AUTO decision (not a CPU proxy); ``trusted`` == auto would
66
+ keep FP64 (tier 'fp64'). On CPU it uses cpu_ref + summation_condition_number."""
67
+ if backend == "gpu":
68
+ _val, diag = _FN[func](A, precision="auto", backend="gpu", return_diagnostics=True)
69
+ kappa = float(diag["cancellation"])
70
+ fp = complex(_FN[func](A, precision="fp64", backend="gpu"))
71
+ return fp, kappa, (diag["tier"] == "fp64")
72
+ fp = complex(_FN[func](A, precision="fp64", backend="cpu"))
73
+ kappa = float(cpu_ref.summation_condition_number(func, A, fp))
74
+ return fp, kappa, (kappa < threshold)
75
+
76
+
77
+ def run(dps: int = 60, seeds: int = 5, threshold: float | None = None,
78
+ backend: str | None = None, out_dir: Path | None = None) -> tuple[dict[str, Any], Path]:
79
+ threshold = gbskernels._AUTO_KAPPA_MAX if threshold is None else threshold
80
+ # Calibrate the REAL path the claim is used for: the GPU auto path when the extension is
81
+ # importable (its kappa comes from the device *_kappa kernels), else CPU.
82
+ if backend is None:
83
+ backend = "gpu" if gbskernels.gpu_available() else "cpu"
84
+ rows: list[dict[str, Any]] = []
85
+ for func in ("perm", "haf", "lhaf", "tor"):
86
+ for regime in _inputs.BENCH_REGIMES:
87
+ for dim in _SIZES[func]:
88
+ for s in range(seeds):
89
+ A = _inputs.bench_batch(func, dim, 1, regime, 1000 + dim + s)[0]
90
+ fp, kappa, trusted = _eval(func, A, backend, threshold)
91
+ with mpmath.workdps(dps):
92
+ exact = _MP[func](A, dps=dps)
93
+ rel = float(abs(mpmath.mpc(fp) - exact) / max(abs(exact), mpmath.mpf("1e-300")))
94
+ rows.append({"func": func, "regime": regime, "dim": dim, "seed": s,
95
+ "kappa": kappa, "rel_err_fp64": rel, "trusted": trusted})
96
+
97
+ trusted = [r for r in rows if r["trusted"]]
98
+ per_regime = {}
99
+ for regime in _inputs.BENCH_REGIMES:
100
+ rr = [r for r in rows if r["regime"] == regime]
101
+ tr = [r for r in rr if r["trusted"]]
102
+ per_regime[regime] = {
103
+ "n": len(rr), "n_trusted": len(tr),
104
+ "max_kappa": max(r["kappa"] for r in rr),
105
+ "max_rel_err_when_trusted": max((r["rel_err_fp64"] for r in tr), default=0.0),
106
+ }
107
+ summary = {
108
+ "threshold": threshold, "eps": _EPS,
109
+ "n_total": len(rows), "n_trusted": len(trusted),
110
+ "max_rel_err_when_trusted": max((r["rel_err_fp64"] for r in trusted), default=0.0),
111
+ "false_trust_count": sum(1 for r in trusted if r["rel_err_fp64"] > 1e-6),
112
+ "log_kappa_vs_log_relerr_corr": _pearson(
113
+ [(math.log10(max(r["kappa"], 1.0)), math.log10(max(r["rel_err_fp64"], 1e-300))) for r in rows]),
114
+ "per_regime": per_regime,
115
+ }
116
+ artifact = {
117
+ "kind": "auto_calibration",
118
+ "created_utc": datetime.now(timezone.utc).isoformat(),
119
+ **_provenance.provenance(),
120
+ "indicator": "kappa = sum|terms| / |result| (a-posteriori; a HEURISTIC indicator, "
121
+ "NOT a rigorous error certificate)",
122
+ "model": "rel_err_fp64 ~ kappa * eps",
123
+ "backend": backend, # the path calibrated (gpu kappa kernels or cpu)
124
+ "gpu_backend": gbskernels.gpu_backend_kind(), # real device vs host-shim (honest provenance)
125
+ "params": {"dps": dps, "seeds": seeds, "backend": backend},
126
+ "env": {"platform": platform.platform()},
127
+ "summary": summary,
128
+ "rows": rows,
129
+ }
130
+ out = Path(out_dir) if out_dir is not None else DEFAULT_OUT
131
+ out.mkdir(parents=True, exist_ok=True)
132
+ stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
133
+ path = out / f"auto_calibration_{stamp}.json"
134
+ path.write_text(json.dumps(artifact, indent=2))
135
+ return artifact, path
136
+
137
+
138
+ def main() -> None:
139
+ p = argparse.ArgumentParser(description=__doc__)
140
+ p.add_argument("--dps", type=int, default=60)
141
+ p.add_argument("--seeds", type=int, default=5)
142
+ p.add_argument("--backend", choices=["gpu", "cpu"], default=None,
143
+ help="path to calibrate (default: gpu if the extension is importable)")
144
+ p.add_argument("--out", type=Path, default=None)
145
+ args = p.parse_args()
146
+ art, path = run(dps=args.dps, seeds=args.seeds, backend=args.backend, out_dir=args.out)
147
+ s = art["summary"]
148
+ print(f"# precision='auto' calibration ({art['backend']} path / {art['gpu_backend']}) -> {path}")
149
+ print(f"# commit {art['commit']}; trust when kappa < {s['threshold']:.0e}; eps = {s['eps']:.2e}")
150
+ print(f" trusted {s['n_trusted']}/{s['n_total']}; worst FP64 rel.err when trusted = "
151
+ f"{s['max_rel_err_when_trusted']:.2e}; false-trust (>1e-6) = {s['false_trust_count']}")
152
+ print(f" log(kappa) vs log(rel.err) correlation = {s['log_kappa_vs_log_relerr_corr']}")
153
+ for regime, r in s["per_regime"].items():
154
+ print(f" {regime:>11}: max kappa {r['max_kappa']:.1e}; "
155
+ f"worst rel.err when trusted {r['max_rel_err_when_trusted']:.1e}")
156
+
157
+
158
+ if __name__ == "__main__":
159
+ main()
bench/crossover.py ADDED
@@ -0,0 +1,195 @@
1
+ """Batch-size sweep + accuracy-normalized GPU/CPU/Walrus crossover (docs/DESIGN.md §9).
2
+
3
+ Sweeps the public-path end-to-end throughput over a range of batch sizes and records, per
4
+ (function, matrix size), the GPU per-eval rate at each batch against two ~flat baselines on
5
+ the **same shared workload** (bench._inputs.bench_batch): our CPU reference, and the
6
+ **same-instance The Walrus** (measured once -- Walrus is one-at-a-time, so its per-eval rate
7
+ is batch-independent). At small batch the GPU's fixed H2D/launch/D2H overhead loses; as the
8
+ batch grows the GPU's batched throughput overtakes. The **crossover batch** -- the smallest
9
+ batch where the GPU median overtakes a baseline -- is the headline of the batched-throughput
10
+ thesis, reported separately vs the CPU and vs The Walrus.
11
+
12
+ To find the *actual* crossover (not just "the smallest batch we tried"), the sweep must include
13
+ **low** batches -- the default starts at 1 -- so the GPU genuinely loses at the small end.
14
+
15
+ Evidence discipline (docs/DESIGN.md §9): the artifact retains **median + IQR + the raw repetitions**
16
+ for GPU/CPU at every batch and for the Walrus baseline (not just medians); each series is tagged
17
+ with the **achieved FP64 error** (vs mpmath) and its **precision tier**; and an **official** run
18
+ (``strict=True``) is **fatal** if e2e reports a GPU/CPU public-path checksum disagreement at any
19
+ batch, or if the same-instance Walrus baseline is unavailable -- so no crossover is published
20
+ past a backend mismatch or without its baseline.
21
+
22
+ uv run python -m bench.crossover --batches 1,4,16,64,256,1024,4096,16384 --repeats 7
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import argparse
28
+ import json
29
+ from datetime import datetime, timezone
30
+ from pathlib import Path
31
+ from typing import Any
32
+
33
+ import numpy as np
34
+
35
+ import gbskernels
36
+ from bench import _inputs, _provenance, throughput_end_to_end as e2e, walrus_baseline
37
+
38
+ DEFAULT_OUT = Path(__file__).resolve().parent.parent / "results" / "throughput"
39
+ DEFAULT_BATCHES = [1, 4, 16, 64, 256, 1024, 4096, 16384]
40
+
41
+ _MP = {"perm": "permanent_mp", "haf": "hafnian_mp",
42
+ "lhaf": "loop_hafnian_mp", "tor": "torontonian_mp"}
43
+
44
+
45
+ def _achieved_error(func: str, dim: int, regime: str, n: int, dps: int) -> float | None:
46
+ """Max relative error of the public FP64 GPU path vs the mpmath ground truth on ``n``
47
+ elements of the shared workload -- the accuracy the throughput is bought at. The SAME
48
+ matrices the sweep timed (seed convention ``1000+dim``). ``n<=0`` or ``dps<=0`` disables."""
49
+ if n <= 0 or dps <= 0:
50
+ return None
51
+ import highprec_ref
52
+ stack = _inputs.bench_batch(func, dim, n, regime, 1000 + dim)
53
+ fp = np.asarray(getattr(gbskernels, f"{func}_batched")(stack, backend="gpu", precision="fp64"))
54
+ mp_fn = getattr(highprec_ref, _MP[func])
55
+ rel = 0.0
56
+ for k in range(n):
57
+ ref = complex(mp_fn(stack[k], dps=dps))
58
+ rel = max(rel, abs(complex(fp[k]) - ref) / max(abs(ref), 1e-300))
59
+ return rel
60
+
61
+
62
+ def _pull(summary, raw, func, dim, backend):
63
+ """(median, iqr, raw evals/sec list) for (func, dim, backend) from an e2e artifact --
64
+ so the crossover keeps the dispersion + raw repetitions, not just the median."""
65
+ row = next((r for r in summary if r["func"] == func and r["matrix_dim"] == dim), {})
66
+ sub = row.get(backend, {})
67
+ rawlist = [r["evals_per_sec"] for r in raw
68
+ if r["backend"] == backend and r["func"] == func and r["matrix_dim"] == dim]
69
+ return sub.get("evals_per_sec_median"), sub.get("evals_per_sec_iqr"), rawlist
70
+
71
+
72
+ def run(batches: list[int], repeats: int = 7, seed: int = 0, warmup: int = 2,
73
+ regime: str = "physical", error_n: int = 2, error_dps: int = 30,
74
+ strict: bool = True, out_dir: Path | None = None) -> tuple[dict[str, Any], Path]:
75
+ base = Path(out_dir) if out_dir is not None else DEFAULT_OUT
76
+ tmp = base / "_sweep_tmp"
77
+
78
+ # GPU + CPU per-eval median/IQR/raw at each batch (same shared workload as the Walrus
79
+ # baseline). An e2e GPU/CPU checksum disagreement is FATAL for an official (strict) run.
80
+ curves: dict[tuple[str, int], list[dict[str, Any]]] = {}
81
+ for b in sorted(set(batches)):
82
+ art, _ = e2e.run(batch=b, repeats=repeats, seed=seed, warmup=warmup,
83
+ regime=regime, out_dir=tmp)
84
+ if strict and not art["all_backends_agree"]:
85
+ bad = [f"{r['func']}/d{r['matrix_dim']}" for r in art["summary"] if not r["backends_agree"]]
86
+ raise RuntimeError(
87
+ f"crossover ABORT: e2e GPU/CPU public-path checksum disagreement at batch {b} "
88
+ f"(regime {regime}) on {bad} -- no official crossover from a backend mismatch.")
89
+ for row in art["summary"]:
90
+ func, dim = row["func"], row["matrix_dim"]
91
+ gm, gi, gr = _pull(art["summary"], art["raw"], func, dim, "gpu")
92
+ cm, ci, cr = _pull(art["summary"], art["raw"], func, dim, "cpu")
93
+ curves.setdefault((func, dim), []).append({
94
+ "batch": b, "gpu_median": gm, "gpu_iqr": gi, "gpu_raw": gr,
95
+ "cpu_median": cm, "cpu_iqr": ci, "cpu_raw": cr})
96
+
97
+ # The Walrus per-eval rate is batch-independent (one-at-a-time) -> measure it ONCE on the
98
+ # same workload, keeping median + IQR + raw. Unavailable Walrus is FATAL for a strict run.
99
+ walrus: dict[tuple[str, int], dict[str, Any]] = {}
100
+ walrus_meta: dict[str, Any]
101
+ try:
102
+ wb_batch = max(min(max(batches), 128), 8)
103
+ wart, _ = walrus_baseline.run(batch=wb_batch, repeats=repeats, seed=seed,
104
+ warmup=warmup, regime=regime, out_dir=tmp)
105
+ for r in wart["rows"]:
106
+ rraw = [x["evals_per_sec"] for x in wart["raw"]
107
+ if x["func"] == r["func"] and x["matrix_dim"] == r["matrix_dim"]]
108
+ walrus[(r["func"], r["matrix_dim"])] = {
109
+ "median": r["evals_per_sec_median"], "iqr": r["evals_per_sec_iqr"], "raw": rraw}
110
+ walrus_meta = {"library_version": wart["library_version"], "batch": wb_batch}
111
+ except Exception as e: # pragma: no cover - depends on the box
112
+ walrus_meta = {"skipped": str(e)}
113
+ if strict and "skipped" in walrus_meta:
114
+ raise RuntimeError(
115
+ f"crossover ABORT: the same-instance The Walrus baseline is unavailable "
116
+ f"({walrus_meta['skipped']}) -- an official crossover requires it.")
117
+
118
+ series = []
119
+ for (func, dim), pts in sorted(curves.items()):
120
+ pts.sort(key=lambda p: p["batch"])
121
+ wb = walrus.get((func, dim), {})
122
+ wmed = wb.get("median")
123
+
124
+ def _gt(p, beat):
125
+ return p["gpu_median"] is not None and beat is not None and p["gpu_median"] > beat
126
+
127
+ x_cpu = next((p["batch"] for p in pts if _gt(p, p["cpu_median"])), None)
128
+ x_wal = next((p["batch"] for p in pts if _gt(p, wmed)), None)
129
+ x_best = next((p["batch"] for p in pts
130
+ if p["gpu_median"] is not None
131
+ and any(v is not None for v in (p["cpu_median"], wmed))
132
+ and p["gpu_median"] > max(v for v in (p["cpu_median"], wmed) if v is not None)),
133
+ None)
134
+ series.append({
135
+ "func": func, "matrix_dim": dim, "precision_tier": "fp64",
136
+ "achieved_rel_err_fp64": _achieved_error(func, dim, regime, error_n, error_dps),
137
+ "walrus_median": wmed, "walrus_iqr": wb.get("iqr"), "walrus_raw": wb.get("raw"),
138
+ "crossover_batch_vs_cpu": x_cpu,
139
+ "crossover_batch_vs_walrus": x_wal,
140
+ "crossover_batch_vs_best": x_best,
141
+ "points": pts,
142
+ })
143
+
144
+ artifact = {
145
+ "kind": "crossover_batch_sweep",
146
+ "created_utc": datetime.now(timezone.utc).isoformat(),
147
+ **_provenance.provenance(),
148
+ "gpu_backend": gbskernels.gpu_backend_kind(),
149
+ "metric": "per-eval evals/sec (public path) vs batch; GPU rises, CPU & same-instance "
150
+ "Walrus are ~flat baselines on the same workload; crossover = smallest batch "
151
+ "where GPU median > baseline; median+IQR+raw retained; achieved FP64 error per series",
152
+ "params": {"batches": sorted(set(batches)), "repeats": repeats, "warmup": warmup,
153
+ "regime": regime, "seed": seed, "strict": strict,
154
+ "error_n": error_n, "error_dps": error_dps},
155
+ "walrus": walrus_meta,
156
+ "series": series,
157
+ }
158
+ base.mkdir(parents=True, exist_ok=True)
159
+ stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
160
+ path = base / f"crossover_{stamp}.json"
161
+ path.write_text(json.dumps(artifact, indent=2))
162
+ return artifact, path
163
+
164
+
165
+ def main() -> None:
166
+ p = argparse.ArgumentParser(description=__doc__)
167
+ p.add_argument("--batches", default=",".join(map(str, DEFAULT_BATCHES)),
168
+ help="comma-separated batch sizes to sweep (start low to find the real crossover)")
169
+ p.add_argument("--repeats", type=int, default=7)
170
+ p.add_argument("--warmup", type=int, default=2)
171
+ p.add_argument("--seed", type=int, default=0)
172
+ p.add_argument("--regime", default="physical", choices=list(_inputs.BENCH_REGIMES))
173
+ p.add_argument("--error-n", type=int, default=2, help="elements checked vs mpmath (0=off)")
174
+ p.add_argument("--error-dps", type=int, default=30, help="mpmath precision for the error check")
175
+ p.add_argument("--no-strict", dest="strict", action="store_false",
176
+ help="do NOT fail on e2e disagreement / missing Walrus (non-official run)")
177
+ p.add_argument("--out", type=Path, default=None)
178
+ args = p.parse_args()
179
+ batches = [int(x) for x in args.batches.split(",")]
180
+ artifact, path = run(batches, args.repeats, seed=args.seed, warmup=args.warmup, regime=args.regime,
181
+ error_n=args.error_n, error_dps=args.error_dps, strict=args.strict, out_dir=args.out)
182
+ w = artifact["walrus"]
183
+ print(f"# batch-size sweep / crossover ({artifact['gpu_backend']}) -> {path}")
184
+ print(f"# commit {artifact['commit']}; container {artifact['container_digest']}; "
185
+ f"regime={args.regime}; strict={args.strict}; batches={batches}")
186
+ print(f"# walrus baseline: {w.get('library_version', 'SKIPPED: ' + str(w.get('skipped')))}")
187
+ print(f" {'func':>5} {'dim':>4} {'fp64 rel.err':>12} {'x vs cpu':>9} {'x vs walrus':>11}")
188
+ for s in artifact["series"]:
189
+ err = s["achieved_rel_err_fp64"]
190
+ print(f" {s['func']:>5} {s['matrix_dim']:>4} {('%.2e' % err) if err is not None else 'n/a':>12} "
191
+ f"{str(s['crossover_batch_vs_cpu']):>9} {str(s['crossover_batch_vs_walrus']):>11}")
192
+
193
+
194
+ if __name__ == "__main__":
195
+ main()
@@ -0,0 +1,120 @@
1
+ """Static per-thread local-memory footprint of the one-eval-per-thread kernels.
2
+
3
+ This is the *static* half of "profile register spills / local-memory traffic"
4
+ (perf research item 5): it accounts, from the kernel sources, the per-thread local
5
+ arrays each kernel declares -- the quantity that drives register spilling and the
6
+ local-memory (off-chip) traffic that bounds the hard kernels. The *dynamic* half
7
+ (ncu/nvprof spill counts, achieved occupancy, DRAM throughput) needs a real device
8
+ and is run in a GPU session.
9
+
10
+ Why this matters, and why it is NOT optimization-by-analogy: the measured cooperative
11
+ results map exactly onto the footprint below. The permanent's per-thread state is a
12
+ single length-n vector (sub-KB -> lives in registers, no spill), so its bottleneck is
13
+ the *serial 2^(n-1) Glynn chain* and splitting that across a group wins ~5x. The
14
+ hafnian / loop hafnian / torontonian instead carry several n x n matrices per thread
15
+ (tens of KB -> spills to local/off-chip memory), so they are memory-bound on the
16
+ *per-subset* work; splitting their (short, 2^(N/2)) subset sum cannot help and adds
17
+ launch + global-partials overhead -- which is exactly what was measured (haf 0.6x,
18
+ lhaf 0.5x, tor 1.2x). The lever for the hard kernels is the per-thread footprint.
19
+
20
+ uv run python -m bench.kernel_footprint
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import argparse
26
+ import json
27
+ from pathlib import Path
28
+ from typing import Any
29
+
30
+ CPLX = 16 # bytes per cuDoubleComplex (2 x double)
31
+ DBL = 8
32
+
33
+ # Per-thread local arrays each one-eval-per-thread kernel declares, as
34
+ # (name, element_bytes, size_expr(maxdim)). maxdim is the kernel's compile-time cap
35
+ # (HAF_MAX_N etc.); the arrays are sized for it REGARDLESS of the actual matrix size,
36
+ # which is the core inefficiency for small inputs. Transcribed from core/*.cu.
37
+ _KERNELS: dict[str, dict[str, Any]] = {
38
+ "permanent": {
39
+ "cap": 28, "cap_name": "PERM_MAX_N",
40
+ "buffers": [("rowsum", CPLX, lambda M: M)],
41
+ },
42
+ "hafnian": {
43
+ "cap": 20, "cap_name": "HAF_MAX_N",
44
+ "buffers": [("BX", CPLX, lambda M: M * M), ("P", CPLX, lambda M: M * M),
45
+ ("T", CPLX, lambda M: M * M), ("p", CPLX, lambda M: M + 1),
46
+ ("e", CPLX, lambda M: M + 1), ("pidx", 4, lambda M: M // 2)],
47
+ },
48
+ "loop_hafnian": {
49
+ "cap": 20, "cap_name": "LHAF_MAX_N",
50
+ "buffers": [("C", CPLX, lambda M: M * M), ("P", CPLX, lambda M: M * M),
51
+ ("Q", CPLX, lambda M: M * M), ("T", CPLX, lambda M: M * M),
52
+ ("d", CPLX, lambda M: M), ("w", CPLX, lambda M: M),
53
+ ("kg", CPLX, lambda M: M + 1), ("e", CPLX, lambda M: M + 1),
54
+ ("pidx", 4, lambda M: M // 2)],
55
+ },
56
+ "torontonian": {
57
+ "cap": 24, "cap_name": "TOR_MAX_DIM",
58
+ "buffers": [("sub", CPLX, lambda M: M * M), ("idx", 4, lambda M: M)],
59
+ },
60
+ }
61
+
62
+ # A 64 KB/thread guideline: above this, an SM (with e.g. 64-99 KB usable as registers
63
+ # + L1) cannot hold even a handful of threads' working sets, so the per-thread arrays
64
+ # spill to off-chip local memory. The practical spill cliff for these kernels is well
65
+ # below: the register file is 256 KB/SM = 1 KB / thread at full occupancy, so anything
66
+ # past ~1 KB already starts spilling. We report both the footprint and the implied
67
+ # occupancy ceiling so the GPU session knows what to confirm with ncu.
68
+ _REGFILE_PER_SM = 256 * 1024 # bytes (Ampere/Ada: 64K 32-bit registers)
69
+
70
+
71
+ def footprint(kernel: str, dim: int) -> int:
72
+ """Per-thread local-array bytes for `kernel` if its buffers were sized to `dim`."""
73
+ return sum(eb * sz(dim) for _, eb, sz in _KERNELS[kernel]["buffers"])
74
+
75
+
76
+ def report() -> dict[str, Any]:
77
+ rows = []
78
+ for name, spec in _KERNELS.items():
79
+ cap = spec["cap"]
80
+ at_cap = footprint(name, cap)
81
+ # threads/SM the register file can hold before spilling (rough occupancy ceiling)
82
+ threads = _REGFILE_PER_SM // max(at_cap, 1)
83
+ # for the hard kernels the matrices are usually small; show a small/typical size
84
+ small = 8 if name != "torontonian" else 8 # dim 8 (haf/lhaf N=8; tor 2n=8)
85
+ rows.append({
86
+ "kernel": name, "cap_name": spec["cap_name"], "cap": cap,
87
+ "bytes_at_cap": at_cap, "kb_at_cap": round(at_cap / 1024, 1),
88
+ "bytes_at_dim8": footprint(name, small),
89
+ "kb_at_dim8": round(footprint(name, small) / 1024, 2),
90
+ "occupancy_threads_per_sm_at_cap": threads,
91
+ "n_nxn_buffers": sum(1 for nm, _, sz in spec["buffers"] if sz(10) == 100),
92
+ })
93
+ return {"kind": "kernel_footprint", "regfile_per_sm_bytes": _REGFILE_PER_SM, "rows": rows}
94
+
95
+
96
+ def main() -> None:
97
+ p = argparse.ArgumentParser(description=__doc__)
98
+ p.add_argument("--json", type=Path, default=None, help="also write the report as JSON")
99
+ args = p.parse_args()
100
+ rep = report()
101
+ print("# per-thread local-memory footprint (static; the spill source)")
102
+ print(f"# register file/SM = {_REGFILE_PER_SM // 1024} KB "
103
+ f"(~1 KB/thread at full occupancy -> anything past that spills)")
104
+ print(f" {'kernel':>13} {'cap':>14} {'KB@cap':>7} {'KB@dim8':>8} {'nxn bufs':>9} "
105
+ f"{'occ thr/SM@cap':>15}")
106
+ for r in rep["rows"]:
107
+ print(f" {r['kernel']:>13} {r['cap_name']+'='+str(r['cap']):>14} "
108
+ f"{r['kb_at_cap']:>7} {r['kb_at_dim8']:>8} {r['n_nxn_buffers']:>9} "
109
+ f"{r['occupancy_threads_per_sm_at_cap']:>15}")
110
+ print("\n# Reading: the permanent's <1 KB lives in registers (no spill); the hard")
111
+ print("# kernels carry several n x n matrices (tens of KB) -> spill -> memory-bound")
112
+ print("# on the per-subset work. Sizing buffers to the ACTUAL dim (size specialization)")
113
+ print("# is the lever -- e.g. hafnian at dim 8 is ~6x smaller than at the cap.")
114
+ if args.json:
115
+ args.json.write_text(json.dumps(rep, indent=2))
116
+ print(f"\n# wrote {args.json}")
117
+
118
+
119
+ if __name__ == "__main__":
120
+ main()