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,94 @@
1
+ """Render the batch-size crossover curves from a sweep artifact (docs/DESIGN.md §9 figures).
2
+
3
+ Reads a ``crossover_*.json`` (from ``bench.crossover``) and draws, per (function,
4
+ matrix size), GPU vs CPU per-eval rate against batch size on log-log axes, marking the
5
+ crossover batch. These are the paper figures for the batched-throughput thesis.
6
+
7
+ matplotlib is imported lazily and is **optional** -- without it (or with ``--data-only``)
8
+ the script emits the plot data as CSV so the curves are reproducible regardless. The
9
+ package keeps numpy/mpmath as its only hard deps; matplotlib is a dev/plotting extra.
10
+
11
+ uv run python -m bench.plot_crossover results/throughput/crossover_*.json -o fig/
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import csv
18
+ import json
19
+ import sys
20
+ from pathlib import Path
21
+ from typing import Any
22
+
23
+
24
+ def _load(path: Path) -> dict[str, Any]:
25
+ return json.loads(Path(path).read_text())
26
+
27
+
28
+ def write_csv(artifact: dict[str, Any], out: Path) -> Path:
29
+ out.parent.mkdir(parents=True, exist_ok=True)
30
+ with out.open("w", newline="") as f:
31
+ w = csv.writer(f)
32
+ w.writerow(["func", "matrix_dim", "batch", "gpu_median", "gpu_iqr", "cpu_median", "cpu_iqr",
33
+ "walrus_median", "crossover_batch_vs_cpu", "crossover_batch_vs_walrus",
34
+ "achieved_rel_err_fp64", "precision_tier"])
35
+ for s in artifact["series"]:
36
+ for p in s["points"]:
37
+ w.writerow([s["func"], s["matrix_dim"], p["batch"], p.get("gpu_median"), p.get("gpu_iqr"),
38
+ p.get("cpu_median"), p.get("cpu_iqr"), s.get("walrus_median"),
39
+ s.get("crossover_batch_vs_cpu"), s.get("crossover_batch_vs_walrus"),
40
+ s.get("achieved_rel_err_fp64"), s.get("precision_tier")])
41
+ return out
42
+
43
+
44
+ def plot(artifact: dict[str, Any], out_dir: Path) -> list[Path]:
45
+ try:
46
+ import matplotlib
47
+ matplotlib.use("Agg")
48
+ import matplotlib.pyplot as plt
49
+ except Exception:
50
+ print("# matplotlib not available -- emitting CSV only "
51
+ "(pip install matplotlib, or use the CSV).", file=sys.stderr)
52
+ return [write_csv(artifact, out_dir / "crossover.csv")]
53
+
54
+ out_dir.mkdir(parents=True, exist_ok=True)
55
+ funcs = sorted({s["func"] for s in artifact["series"]})
56
+ written = []
57
+ for func in funcs:
58
+ fig, ax = plt.subplots(figsize=(6, 4))
59
+ for s in (s for s in artifact["series"] if s["func"] == func):
60
+ bs = [p["batch"] for p in s["points"]]
61
+ ax.plot(bs, [p["gpu_median"] for p in s["points"]], "-o", label=f"GPU d={s['matrix_dim']}")
62
+ ax.plot(bs, [p["cpu_median"] for p in s["points"]], "--s", label=f"CPU d={s['matrix_dim']}")
63
+ # the same-instance Walrus baseline is batch-independent -> a horizontal line
64
+ wv = s.get("walrus_median")
65
+ if wv is not None:
66
+ ax.axhline(wv, ls="-.", alpha=0.4, label=f"Walrus d={s['matrix_dim']}")
67
+ if s.get("crossover_batch_vs_cpu"):
68
+ ax.axvline(s["crossover_batch_vs_cpu"], color="grey", ls=":", alpha=0.5)
69
+ ax.set_xscale("log"); ax.set_yscale("log")
70
+ ax.set_xlabel("batch size"); ax.set_ylabel("evals / sec (per eval)")
71
+ ax.set_title(f"{func} -- GPU vs CPU throughput crossover\n"
72
+ f"commit {artifact.get('commit')} | {artifact.get('gpu_backend')}")
73
+ ax.legend(fontsize=7); ax.grid(True, which="both", alpha=0.3)
74
+ p = out_dir / f"crossover_{func}.png"
75
+ fig.tight_layout(); fig.savefig(p, dpi=130); plt.close(fig)
76
+ written.append(p)
77
+ written.append(write_csv(artifact, out_dir / "crossover.csv")) # always emit the data too
78
+ return written
79
+
80
+
81
+ def main() -> None:
82
+ p = argparse.ArgumentParser(description=__doc__)
83
+ p.add_argument("artifact", type=Path, help="a crossover_*.json from bench.crossover")
84
+ p.add_argument("-o", "--out", type=Path, default=Path("fig"))
85
+ p.add_argument("--data-only", action="store_true", help="emit CSV only, skip plotting")
86
+ args = p.parse_args()
87
+ art = _load(args.artifact)
88
+ outs = [write_csv(art, args.out / "crossover.csv")] if args.data_only else plot(art, args.out)
89
+ for o in outs:
90
+ print(f"wrote {o}")
91
+
92
+
93
+ if __name__ == "__main__":
94
+ main()
bench/repeated_ab.py ADDED
@@ -0,0 +1,127 @@
1
+ """R4 device A/B: the repeated-row sieve kernel vs the expanded hafnian kernel.
2
+
3
+ Times, on the SAME device through the public path (both pay H2D/launch/D2H),
4
+ the two ways of evaluating the identical workload — a batch of loop hafnians of
5
+ ``A`` expanded by collision patterns ``(q, ..., q)``:
6
+
7
+ * ``gbskernels.lhaf_repeated(A, 0, reps, backend="gpu")`` — the sieve
8
+ (``core/repeated.cu``), cost ``prod(q+1)`` per element;
9
+ * ``gbskernels.haf_batched(expanded, backend="gpu")`` — the shipped power-trace
10
+ kernel on the ``N = M*q`` expansion, cost ``2^(N/2)`` — only where ``N`` is
11
+ within its cap (the rows above the cap are the capability gap the sieve
12
+ closes, reported as such, not as a timing).
13
+
14
+ Hygiene per docs/benchmark_protocol.md: warm-up before timing, raw repeats with
15
+ median + IQR, a checksum honesty guard (sieve vs expanded values must agree),
16
+ append-only artifact with full provenance. The MEASURED crossover here decides
17
+ whether the sampler's sieve path defaults on for the GPU chain
18
+ (the finite-difference sieve, measured against the expanded-hafnian kernel).
19
+
20
+ python -m bench.repeated_ab --batch 2048 --qs 2,3,4,5,6 --repeats 5
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import argparse
26
+ import json
27
+ import time
28
+ from pathlib import Path
29
+
30
+ import numpy as np
31
+
32
+ import gbskernels
33
+ from bench._provenance import provenance
34
+
35
+ RESULTS = Path(__file__).resolve().parent.parent / "results" / "throughput"
36
+
37
+
38
+ def _median_iqr(ts: list[float]) -> dict[str, float]:
39
+ a = np.sort(np.asarray(ts))
40
+ return {"median_s": float(np.median(a)),
41
+ "iqr_s": float(np.percentile(a, 75) - np.percentile(a, 25)),
42
+ "raw_s": [float(x) for x in a]}
43
+
44
+
45
+ def run(modes: int, qs: list[int], batch: int, repeats: int, warmup: int,
46
+ seed: int) -> dict:
47
+ M = 2 * modes
48
+ g = np.random.default_rng(seed)
49
+ A = g.standard_normal((M, M)) + 1j * g.standard_normal((M, M))
50
+ A = 0.3 * (A + A.T) # modest norm, physical-ish
51
+ gam = np.zeros(M, dtype=np.complex128)
52
+
53
+ rows = []
54
+ for q in qs:
55
+ reps = np.full((batch, M), q, dtype=np.int32)
56
+ N = M * q
57
+ row: dict = {"q": q, "M": M, "N_expanded": N, "batch": batch,
58
+ "sieve_terms": int((q + 1) ** M)}
59
+
60
+ # --- sieve kernel ---
61
+ for _ in range(warmup):
62
+ gbskernels.lhaf_repeated(A, gam, reps[: max(1, batch // 8)], backend="gpu")
63
+ ts = []
64
+ for _ in range(repeats):
65
+ t0 = time.perf_counter()
66
+ v_sieve = gbskernels.lhaf_repeated(A, gam, reps, backend="gpu")
67
+ ts.append(time.perf_counter() - t0)
68
+ chk = complex(np.sum(v_sieve)) # post-timing checksum (honesty guard)
69
+ row["sieve"] = _median_iqr(ts) | {"evals_per_s": batch / float(np.median(ts)),
70
+ "checksum": [chk.real, chk.imag]}
71
+
72
+ # --- expanded power-trace kernel (only within its cap) ---
73
+ cap = 20 # HAF_MAX_N
74
+ if N <= cap:
75
+ idx = [i for i in range(M) for _ in range(q)]
76
+ E = np.ascontiguousarray(A[np.ix_(idx, idx)])
77
+ stack = np.broadcast_to(E, (batch, N, N)).copy()
78
+ for _ in range(warmup):
79
+ gbskernels.haf_batched(stack[: max(1, batch // 8)], backend="gpu")
80
+ ts = []
81
+ for _ in range(repeats):
82
+ t0 = time.perf_counter()
83
+ v_exp = gbskernels.haf_batched(stack, backend="gpu")
84
+ ts.append(time.perf_counter() - t0)
85
+ chk_e = complex(np.sum(v_exp))
86
+ row["expanded"] = _median_iqr(ts) | {
87
+ "evals_per_s": batch / float(np.median(ts)),
88
+ "checksum": [chk_e.real, chk_e.imag]}
89
+ rel = abs(chk - chk_e) / max(abs(chk_e), 1e-300)
90
+ row["values_agree_rel"] = float(rel)
91
+ if not rel <= 1e-8:
92
+ raise SystemExit(f"HONESTY GATE: sieve vs expanded disagree at q={q}: {rel:.2e}")
93
+ row["speedup_sieve_over_expanded"] = (
94
+ row["expanded"]["median_s"] / row["sieve"]["median_s"])
95
+ else:
96
+ row["expanded"] = None # over the expanded kernel's cap:
97
+ row["note"] = f"N={N} > HAF cap {cap}: the sieve evaluates what the expansion cannot"
98
+ rows.append(row)
99
+ print(f"q={q}: sieve {row['sieve']['evals_per_s']:.3g} ev/s"
100
+ + (f", expanded {row['expanded']['evals_per_s']:.3g} ev/s, "
101
+ f"speedup {row['speedup_sieve_over_expanded']:.2f}x"
102
+ if row["expanded"] else f" ({row['note']})"))
103
+ return {"bench": "repeated_ab", "params": {"modes": modes, "batch": batch,
104
+ "repeats": repeats, "warmup": warmup, "seed": seed},
105
+ "gpu_backend_kind": gbskernels.gpu_backend_kind(),
106
+ "provenance": provenance(), "rows": rows}
107
+
108
+
109
+ def main() -> None:
110
+ p = argparse.ArgumentParser(description=__doc__)
111
+ p.add_argument("--modes", type=int, default=3)
112
+ p.add_argument("--qs", type=str, default="2,3,4,5,6")
113
+ p.add_argument("--batch", type=int, default=2048)
114
+ p.add_argument("--repeats", type=int, default=5)
115
+ p.add_argument("--warmup", type=int, default=2)
116
+ p.add_argument("--seed", type=int, default=0)
117
+ args = p.parse_args()
118
+ art = run(args.modes, [int(x) for x in args.qs.split(",")], args.batch,
119
+ args.repeats, args.warmup, args.seed)
120
+ RESULTS.mkdir(parents=True, exist_ok=True)
121
+ out = RESULTS / f"repeated_ab_{time.strftime('%Y%m%dT%H%M%SZ', time.gmtime())}.json"
122
+ out.write_text(json.dumps(art, indent=1))
123
+ print(f"-> {out}")
124
+
125
+
126
+ if __name__ == "__main__":
127
+ main()
@@ -0,0 +1,165 @@
1
+ """End-to-end GBS sampler throughput -- **samples/sec**, not kernel evals/sec.
2
+
3
+ The conditional sampler (`sampling.sampler.sample`) draws photon-number samples by
4
+ the reduced-covariance chain rule, routing each mode's conditional-probability
5
+ hafnian batch through one `gbskernels.Workspace` (ragged bucketing + device-buffer
6
+ residency across the whole chain). This measures the headline product metric --
7
+ samples/sec for a real GBS workload -- on the CPU and GPU backends, with the same
8
+ honest provenance as the kernel benchmarks (commit, GPU/host-shim backend).
9
+
10
+ Runs wherever the GPU extension is importable: a real device times the GPU chain, a
11
+ host-shim build times the CPU emulation (then the GPU number is only indicative, but
12
+ the path and the GPU==CPU sample agreement are validated -- see tests/).
13
+
14
+ uv run python -m bench.sampler_throughput --modes 6 --num-samples 2000 --cutoff 6
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import json
21
+ import os
22
+ import platform
23
+ import subprocess
24
+ from datetime import datetime, timezone
25
+ from importlib.metadata import version
26
+ from pathlib import Path
27
+ from typing import Any
28
+
29
+ import numpy as np
30
+
31
+ import gbskernels
32
+ from bench import _provenance
33
+ from sampling import sampler
34
+
35
+ DEFAULT_OUT = Path(__file__).resolve().parent.parent / "results" / "sampling"
36
+
37
+
38
+ def _random_cov(modes: int, seed: int, hbar: float = 2.0) -> np.ndarray:
39
+ """xxpp covariance of a zero-displacement pure GBS state (squeeze + interferometer),
40
+ built without third-party deps so the benchmark is self-contained on the box."""
41
+ g = np.random.default_rng(seed)
42
+ r = g.uniform(0.3, 0.6, modes)
43
+ z = (g.standard_normal((modes, modes)) + 1j * g.standard_normal((modes, modes))) / np.sqrt(2.0)
44
+ U, rr = np.linalg.qr(z); ph = np.diagonal(rr).copy(); ph /= np.abs(ph); U = U * ph
45
+ sq = np.block([[np.diag(np.exp(-r)), np.zeros((modes, modes))],
46
+ [np.zeros((modes, modes)), np.diag(np.exp(r))]]) # xxpp squeezing
47
+ interf = np.block([[U.real, -U.imag], [U.imag, U.real]]) # xxpp interferometer
48
+ S = interf @ sq
49
+ return (hbar / 2.0) * S @ S.T
50
+
51
+
52
+ def _quantile(xs: list[float], q: float) -> float:
53
+ s = sorted(xs)
54
+ x = q * (len(s) - 1)
55
+ lo = int(x); frac = x - lo
56
+ return s[lo] * (1 - frac) + s[lo + 1] * frac if lo + 1 < len(s) else s[lo]
57
+
58
+
59
+ def run(modes: int, num_samples: int, cutoff: int, repeats: int = 7, seed: int = 0,
60
+ out_dir: Path | None = None, repeated_sieve: bool = False) -> tuple[dict[str, Any], Path]:
61
+ cov = _random_cov(modes, seed)
62
+ backends = ["cpu"] + (["gpu"] if gbskernels.gpu_available() else [])
63
+ rows = []
64
+
65
+ def _time(backend: str, resident: bool = False, sieve: bool | None = None) -> dict[str, Any]:
66
+ # warm-up (untimed) then RAW repetitions -> median + IQR (NOT best-of-N, which hides the
67
+ # spread and biases the headline; the median+IQR matches the kernel/Walrus benches).
68
+ # The warm-up is also a COST PROBE: if one timed draw-set already exceeds
69
+ # SLOW_CELL_S, drop to a single measured repetition rather than `repeats`
70
+ # of them -- the deep-cutoff CPU non-sieve baseline is ~minutes/rep and
71
+ # timing it 7x (twice: default + explicit nosieve) made the sweep
72
+ # intractable (it hung a cell ~2h). A slow baseline still gets ONE honest
73
+ # number; it just does not get the full IQR treatment it cannot afford.
74
+ SLOW_CELL_S = 20.0
75
+ import time as _t
76
+ sampler.sample(cov, max(num_samples // 10, 1), cutoff=cutoff, backend=backend,
77
+ resident=resident, seed=seed, repeated_sieve=sieve)
78
+ _p0 = _t.perf_counter()
79
+ first = sampler.samples_per_second(cov, num_samples, cutoff=cutoff, backend=backend,
80
+ resident=resident, seed=seed, repeated_sieve=sieve)
81
+ n_rep = repeats if (_t.perf_counter() - _p0) < SLOW_CELL_S else 1
82
+ reps = [first] + [sampler.samples_per_second(cov, num_samples, cutoff=cutoff, backend=backend,
83
+ resident=resident, seed=seed + rep,
84
+ repeated_sieve=sieve) for rep in range(1, n_rep)]
85
+ sps = sorted(r["samples_per_sec"] for r in reps)
86
+ secs = [r["seconds"] for r in reps]
87
+ return {"backend": reps[0]["backend"], "num_samples": num_samples, "modes": modes,
88
+ "cutoff": cutoff, "repeats_requested": repeats, "repeats_timed": len(reps),
89
+ "repeated_sieve_effective": reps[0].get("repeated_sieve_effective"),
90
+ "samples_per_sec_median": _quantile(sps, 0.5),
91
+ "samples_per_sec_iqr": _quantile(sps, 0.75) - _quantile(sps, 0.25),
92
+ "seconds_median": _quantile(secs, 0.5),
93
+ "mean_photons": reps[0]["mean_photons"], "raw_samples_per_sec": sps}
94
+
95
+ for b in backends:
96
+ rows.append(_time(b)) # sieve=None: the DEFAULT path, as shipped
97
+ if repeated_sieve: # R4 A/B rows: explicitly pinned paths
98
+ for b in backends:
99
+ rows.append(_time(b, sieve=False))
100
+ rows.append(_time(b, sieve=True))
101
+ # v3 fully on-device (resident) chain -- the before/after vs the hybrid 'gpu' backend -- when
102
+ # the extension exposes it and the config is within the hafnian cap (worst-case 2*modes*cutoff).
103
+ _ext = gbskernels._load_gpu_ext()
104
+ _cap = getattr(gbskernels, "_GPU_MAX_DIM", {}).get("haf", 20)
105
+ if (gbskernels.gpu_available() and _ext is not None and hasattr(_ext, "sample_resident")
106
+ and 2 * modes * cutoff <= _cap):
107
+ rows.append(_time("gpu", resident=True))
108
+
109
+ artifact = {
110
+ "kind": "sampler_throughput",
111
+ "created_utc": datetime.now(timezone.utc).isoformat(),
112
+ **_provenance.provenance(), # commit + container_digest + hostname
113
+ "gpu_backend": gbskernels.gpu_backend_kind(),
114
+ "metric": "end-to-end samples/sec (chain-rule conditional GBS sampler); "
115
+ "median + IQR over raw repeats; warm-up discarded; the 'gpu-resident' row is the "
116
+ "v3 FULLY on-device chain (before/after vs the hybrid 'gpu' row, where in-cap)",
117
+ "params": {"modes": modes, "num_samples": num_samples, "cutoff": cutoff,
118
+ "repeats": repeats, "seed": seed, "warmup": "1 untimed draw/backend"},
119
+ "env": {"platform": platform.platform(), "numpy": version("numpy")},
120
+ "rows": rows,
121
+ }
122
+ out_dir = Path(out_dir) if out_dir is not None else DEFAULT_OUT
123
+ out_dir.mkdir(parents=True, exist_ok=True)
124
+ stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
125
+ path = out_dir / f"sampler_throughput_{stamp}.json"
126
+ path.write_text(json.dumps(artifact, indent=2))
127
+ return artifact, path
128
+
129
+
130
+ def main() -> None:
131
+ p = argparse.ArgumentParser(description=__doc__)
132
+ p.add_argument("--modes", type=int, default=6)
133
+ p.add_argument("--num-samples", type=int, default=2000)
134
+ p.add_argument("--cutoff", type=int, default=6)
135
+ p.add_argument("--repeats", type=int, default=7)
136
+ p.add_argument("--out", type=Path, default=None)
137
+ p.add_argument("--repeated-sieve", action="store_true",
138
+ help="also time the sieve-routed chain (cpu+sieve / gpu+sieve rows)")
139
+ p.add_argument("--sweep", action="store_true",
140
+ help="characterization grid: modes x cutoff cells (each its own artifact) "
141
+ "instead of the single default cell -- the resident/hybrid/sieve "
142
+ "story needs the surface, not one point")
143
+ args = p.parse_args()
144
+ if args.sweep:
145
+ # the characterization surface: shallow->deep collisions, small->mid modes.
146
+ # Each cell is an independent append-only artifact (same hygiene).
147
+ for m_, c_ in [(4, 3), (6, 4), (6, 6), (8, 5), (10, 4), (5, 8)]:
148
+ n_ = max(200, args.num_samples // 4)
149
+ print(f"--- sweep cell: modes={m_} cutoff={c_} samples={n_} ---")
150
+ run(m_, n_, c_, max(3, args.repeats // 2), out_dir=args.out,
151
+ repeated_sieve=args.repeated_sieve)
152
+ return
153
+ artifact, path = run(args.modes, args.num_samples, args.cutoff, args.repeats,
154
+ out_dir=args.out, repeated_sieve=args.repeated_sieve)
155
+ print(f"# GBS sampler throughput ({artifact['gpu_backend']}) -> {path}")
156
+ print(f"# {args.modes} modes, cutoff {args.cutoff}, {args.num_samples} samples, "
157
+ f"{args.repeats} repeats; commit {artifact['commit']}")
158
+ for r in artifact["rows"]:
159
+ print(f" {r['backend']:>4} {r['samples_per_sec_median']:>10.1f} samples/sec median "
160
+ f"(IQR {r['samples_per_sec_iqr']:.1f}, {r['seconds_median']:.2f}s, "
161
+ f"mean photons {r['mean_photons']:.2f})")
162
+
163
+
164
+ if __name__ == "__main__":
165
+ main()
bench/throughput.py ADDED
@@ -0,0 +1,229 @@
1
+ """Throughput benchmark harness (accuracy-normalized, honesty-guarded).
2
+
3
+ The benchmark claim is **throughput at a stated accuracy**, never raw throughput
4
+ (docs/DESIGN.md §9). This harness measures batched-evaluation throughput (evals/sec)
5
+ for the GBSKernels CPU reference and, where available, The Walrus, at matched
6
+ accuracy, with the hygiene that makes the numbers credible:
7
+
8
+ * n >= ``repeats`` timed runs per cell, reported as median + IQR (not mean);
9
+ * **randomized execution order** across (engine, size) cells, so thermal/cache
10
+ drift can't systematically favor one engine;
11
+ * a **benchmark-honesty guard**: every timed call's outputs are reduced to a
12
+ checksum *after* timing, and the checksum is stored, so no async/early-return
13
+ can fake a fast time (trivially true on CPU, essential once the GPU path
14
+ lands);
15
+ * raw per-run data written append-only to ``results/throughput/``; **no composite
16
+ "winner" number**; CPU single-eval latency where The Walrus honestly wins is
17
+ reported, not hidden.
18
+
19
+ This run is a **CPU baseline**. GPU throughput rows are added in a scripted
20
+ rented-GPU session (docs/DESIGN.md §8/sec.10); nothing here runs on a GPU.
21
+
22
+ uv run python -m bench.throughput --sizes 4,8,12 --batch 256 --repeats 7
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import argparse
28
+ import json
29
+ import platform
30
+ import random
31
+ import time
32
+ from datetime import datetime, timezone
33
+ from importlib.metadata import version
34
+ from pathlib import Path
35
+ from typing import Any, Callable
36
+
37
+ import numpy as np
38
+
39
+ import gbskernels
40
+ from bench._inputs import random_complex
41
+
42
+ try:
43
+ import thewalrus
44
+
45
+ _HAVE_WALRUS = True
46
+ except Exception:
47
+ _HAVE_WALRUS = False
48
+
49
+ DEFAULT_OUT = Path(__file__).resolve().parent.parent / "results" / "throughput"
50
+
51
+
52
+ def _checksum(values: np.ndarray) -> str:
53
+ """Order-independent magnitude checksum of a result vector (honesty guard)."""
54
+ v = np.asarray(values, dtype=np.complex128)
55
+ return f"{float(np.sum(np.abs(v))):.12e}|{len(v)}"
56
+
57
+
58
+ def _time_once(fn: Callable[[], np.ndarray]) -> tuple[float, str]:
59
+ """Run ``fn``, return (elapsed_seconds, checksum-of-result-after-sync)."""
60
+ t0 = time.perf_counter()
61
+ out = fn()
62
+ elapsed = time.perf_counter() - t0
63
+ # Post-compute checksum: forces materialization of every result *after*
64
+ # timing so no early-return can fake a fast time (docs/DESIGN.md §8).
65
+ return elapsed, _checksum(out)
66
+
67
+
68
+ def _gbskernels_batched(func: str, batch: list[np.ndarray]) -> Callable[[], np.ndarray]:
69
+ fn = {"perm": gbskernels.perm_batched, "haf": gbskernels.haf_batched}[func]
70
+ return lambda: fn(batch, precision="fp64")
71
+
72
+
73
+ def _walrus_looped(func: str, batch: list[np.ndarray]) -> Callable[[], np.ndarray]:
74
+ wfn = {"perm": thewalrus.perm, "haf": thewalrus.hafnian}[func]
75
+ return lambda: np.array([wfn(A) for A in batch], dtype=np.complex128)
76
+
77
+
78
+ def _make_batch(func: str, n: int, batch_size: int, seed: int) -> list[np.ndarray]:
79
+ out = []
80
+ for b in range(batch_size):
81
+ A = random_complex(n, seed=seed + b)
82
+ if func == "haf":
83
+ A = A + A.T # symmetric for the hafnian
84
+ out.append(A)
85
+ return out
86
+
87
+
88
+ def run(
89
+ func: str,
90
+ sizes: list[int],
91
+ batch_size: int,
92
+ repeats: int,
93
+ seed: int = 0,
94
+ out_dir: Path | None = None,
95
+ ) -> tuple[dict[str, Any], Path]:
96
+ engines = ["gbskernels"]
97
+ if _HAVE_WALRUS:
98
+ engines.append("thewalrus")
99
+
100
+ # Build the randomized work list: (engine, size, repeat) cells, shuffled.
101
+ cells = [
102
+ (engine, n, rep)
103
+ for engine in engines
104
+ for n in sizes
105
+ for rep in range(repeats)
106
+ ]
107
+ random.Random(seed).shuffle(cells)
108
+
109
+ # Fixed input batch per size (same inputs for both engines -> fair, and the
110
+ # checksum must agree across engines, a correctness cross-check for free).
111
+ batches = {n: _make_batch(func, n, batch_size, seed=1000 + n) for n in sizes}
112
+
113
+ raw: list[dict[str, Any]] = []
114
+ for engine, n, rep in cells:
115
+ batch = batches[n]
116
+ if engine == "gbskernels":
117
+ fn = _gbskernels_batched(func, batch)
118
+ else:
119
+ fn = _walrus_looped(func, batch)
120
+ elapsed, checksum = _time_once(fn)
121
+ raw.append(
122
+ {
123
+ "engine": engine,
124
+ "n": n,
125
+ "repeat": rep,
126
+ "seconds": elapsed,
127
+ "evals_per_sec": batch_size / elapsed if elapsed > 0 else float("inf"),
128
+ "checksum": checksum,
129
+ }
130
+ )
131
+
132
+ summary = _summarize(raw, engines, sizes, batch_size)
133
+ artifact = {
134
+ "kind": "throughput",
135
+ "function": func,
136
+ "created_utc": datetime.now(timezone.utc).isoformat(),
137
+ "tier": "cpu-baseline",
138
+ "env": {
139
+ "python": platform.python_version(),
140
+ "platform": platform.platform(),
141
+ "processor": platform.processor(),
142
+ "numpy": version("numpy"),
143
+ "thewalrus": version("thewalrus") if _HAVE_WALRUS else None,
144
+ },
145
+ "params": {
146
+ "batch_size": batch_size,
147
+ "repeats": repeats,
148
+ "sizes": sizes,
149
+ "precision": "fp64",
150
+ "dtype": "complex128",
151
+ "order": "randomized across (engine, size, repeat)",
152
+ },
153
+ "summary": summary,
154
+ "raw": raw,
155
+ }
156
+
157
+ out_dir = Path(out_dir) if out_dir is not None else DEFAULT_OUT
158
+ out_dir.mkdir(parents=True, exist_ok=True)
159
+ stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
160
+ path = out_dir / f"throughput_{func}_{stamp}_cpu.json"
161
+ path.write_text(json.dumps(artifact, indent=2))
162
+ return artifact, path
163
+
164
+
165
+ def _summarize(raw, engines, sizes, batch_size) -> list[dict[str, Any]]:
166
+ rows = []
167
+ for n in sizes:
168
+ row: dict[str, Any] = {"n": n}
169
+ checks = {}
170
+ for engine in engines:
171
+ eps = sorted(r["evals_per_sec"] for r in raw if r["engine"] == engine and r["n"] == n)
172
+ checks[engine] = {r["checksum"] for r in raw if r["engine"] == engine and r["n"] == n}
173
+ row[engine] = {
174
+ "evals_per_sec_median": float(np.median(eps)),
175
+ "evals_per_sec_iqr": float(np.subtract(*np.percentile(eps, [75, 25]))),
176
+ }
177
+ # cross-engine correctness: checksums must match (same inputs, same answer)
178
+ if len(engines) > 1:
179
+ allchecks = set().union(*checks.values())
180
+ row["checksums_agree"] = len(allchecks) == 1
181
+ rows.append(row)
182
+ return rows
183
+
184
+
185
+ def _print_summary(artifact: dict[str, Any], path: Path) -> None:
186
+ print(f"# throughput [{artifact['function']}, {artifact['tier']}] -> {path}")
187
+ e = artifact["env"]
188
+ print(f"# {e['platform']} | py {e['python']} | numpy {e['numpy']} | "
189
+ f"thewalrus {e['thewalrus']}")
190
+ print(f"# batch={artifact['params']['batch_size']} repeats={artifact['params']['repeats']} "
191
+ f"(median evals/sec, IQR); randomized order; no composite score")
192
+ engines = [k for k in ("gbskernels", "thewalrus") if k in artifact["summary"][0]]
193
+ head = f" {'n':>3}"
194
+ for eng in engines:
195
+ head += f" {eng + ' med':>16} {'IQR':>9}"
196
+ if len(engines) > 1:
197
+ head += " checks"
198
+ print(head)
199
+ for row in artifact["summary"]:
200
+ line = f" {row['n']:>3}"
201
+ for eng in engines:
202
+ line += f" {row[eng]['evals_per_sec_median']:>16.1f} {row[eng]['evals_per_sec_iqr']:>9.1f}"
203
+ if len(engines) > 1:
204
+ line += f" {'ok' if row['checksums_agree'] else 'MISMATCH'}"
205
+ print(line)
206
+
207
+
208
+ def _parse_sizes(s: str) -> list[int]:
209
+ if "-" in s and "," not in s:
210
+ lo, hi = s.split("-")
211
+ return list(range(int(lo), int(hi) + 1))
212
+ return [int(x) for x in s.split(",")]
213
+
214
+
215
+ def main() -> None:
216
+ p = argparse.ArgumentParser(description=__doc__)
217
+ p.add_argument("--func", choices=["perm", "haf"], default="perm")
218
+ p.add_argument("--sizes", type=_parse_sizes, default=_parse_sizes("4,6,8,10"))
219
+ p.add_argument("--batch", type=int, default=128)
220
+ p.add_argument("--repeats", type=int, default=7)
221
+ p.add_argument("--seed", type=int, default=0)
222
+ p.add_argument("--out", type=Path, default=None)
223
+ args = p.parse_args()
224
+ artifact, path = run(args.func, args.sizes, args.batch, args.repeats, args.seed, args.out)
225
+ _print_summary(artifact, path)
226
+
227
+
228
+ if __name__ == "__main__":
229
+ main()