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.
- bench/__init__.py +7 -0
- bench/_inputs.py +348 -0
- bench/_provenance.py +104 -0
- bench/accuracy.py +182 -0
- bench/accuracy_permanent.py +169 -0
- bench/calibrate_auto.py +159 -0
- bench/crossover.py +195 -0
- bench/kernel_footprint.py +120 -0
- bench/plot_crossover.py +94 -0
- bench/repeated_ab.py +127 -0
- bench/sampler_throughput.py +165 -0
- bench/throughput.py +229 -0
- bench/throughput_end_to_end.py +204 -0
- bench/throughput_gpu.py +126 -0
- bench/tightness.py +174 -0
- bench/walrus_baseline.py +148 -0
- cpu_ref/__init__.py +50 -0
- cpu_ref/certified.py +499 -0
- cpu_ref/dd.py +114 -0
- cpu_ref/diagnostics.py +170 -0
- cpu_ref/hafnian.py +134 -0
- cpu_ref/loop_hafnian.py +120 -0
- cpu_ref/permanent.py +194 -0
- cpu_ref/repeated.py +228 -0
- cpu_ref/tor_recursive.py +101 -0
- cpu_ref/torontonian.py +69 -0
- gbskernels/__init__.py +846 -0
- gbskernels-0.2.0.dist-info/METADATA +255 -0
- gbskernels-0.2.0.dist-info/RECORD +41 -0
- gbskernels-0.2.0.dist-info/WHEEL +5 -0
- gbskernels-0.2.0.dist-info/licenses/LICENSE +202 -0
- gbskernels-0.2.0.dist-info/top_level.txt +5 -0
- highprec_ref/__init__.py +16 -0
- highprec_ref/hafnian.py +46 -0
- highprec_ref/loop_hafnian.py +38 -0
- highprec_ref/permanent.py +85 -0
- highprec_ref/torontonian.py +46 -0
- sampling/__init__.py +18 -0
- sampling/boson_sampling.py +90 -0
- sampling/gbs.py +301 -0
- sampling/sampler.py +446 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""End-to-end (public-API) throughput: the GPU binding path vs the CPU backend.
|
|
2
|
+
|
|
3
|
+
This measures the **public** call path -- `gbskernels.X_batched(stack,
|
|
4
|
+
backend=...)` -- which for the GPU backend includes the numpy -> host_api ->
|
|
5
|
+
H2D -> launch -> sync -> D2H -> numpy round trip on *every* call (the binding has
|
|
6
|
+
no device-resident workspace; see the README "Known limitations"). It is
|
|
7
|
+
deliberately separate from the kernel-only timing in `core/bench_kernels.cu`:
|
|
8
|
+
together they bound how much of the cost is data movement vs compute.
|
|
9
|
+
|
|
10
|
+
Benchmark hygiene (docs/DESIGN.md §9): n>=repeats timed runs per cell reported as
|
|
11
|
+
median + IQR; **randomized execution order** across (backend, func, size) cells;
|
|
12
|
+
a post-call **checksum honesty guard** that also cross-checks the GPU and CPU
|
|
13
|
+
backends agree; raw data appended to `results/throughput/`; no composite score.
|
|
14
|
+
|
|
15
|
+
Runs wherever the GPU extension is importable -- on a CUDA box it times the real
|
|
16
|
+
device, on a host-shim build it times the CPU emulation (the timing is then only
|
|
17
|
+
indicative, but the harness and the GPU/CPU agreement are validated). The
|
|
18
|
+
accuracy-normalized vs-The-Walrus comparison lives in `bench/throughput.py`.
|
|
19
|
+
|
|
20
|
+
uv run python -m bench.throughput_end_to_end --batch 1024 --repeats 7
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import argparse
|
|
26
|
+
import json
|
|
27
|
+
import platform
|
|
28
|
+
import random
|
|
29
|
+
import sys
|
|
30
|
+
import time
|
|
31
|
+
from datetime import datetime, timezone
|
|
32
|
+
from importlib.metadata import version
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Any
|
|
35
|
+
|
|
36
|
+
import numpy as np
|
|
37
|
+
|
|
38
|
+
import gbskernels
|
|
39
|
+
from bench import _inputs, _provenance
|
|
40
|
+
|
|
41
|
+
DEFAULT_OUT = Path(__file__).resolve().parent.parent / "results" / "throughput"
|
|
42
|
+
|
|
43
|
+
# func -> (batched fn, matrix sizes within the GPU kernels' caps). The matrices come from
|
|
44
|
+
# the SINGLE shared workload generator (bench._inputs.bench_batch) so the GPU, CPU, and the
|
|
45
|
+
# same-instance The Walrus baseline are all timed on the *same input* per (func, dim, regime)
|
|
46
|
+
# -- an apples-to-apples, accuracy-normalizable comparison (docs/DESIGN.md §9). For the
|
|
47
|
+
# torontonian the matrix dim is 2*modes.
|
|
48
|
+
_SPECS = {
|
|
49
|
+
"perm": (gbskernels.perm_batched, [8, 12, 16]),
|
|
50
|
+
"haf": (gbskernels.haf_batched, [8, 12, 16]),
|
|
51
|
+
"lhaf": (gbskernels.lhaf_batched, [8, 12]),
|
|
52
|
+
"tor": (gbskernels.tor_batched, [8, 12, 16]),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _checksum(v: np.ndarray) -> float:
|
|
57
|
+
"""Order-independent magnitude checksum (the post-sync honesty guard)."""
|
|
58
|
+
return float(np.sum(np.abs(np.asarray(v, dtype=np.complex128))))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _quantile(sorted_vals: list[float], q: float) -> float:
|
|
62
|
+
x = q * (len(sorted_vals) - 1)
|
|
63
|
+
lo = int(x)
|
|
64
|
+
frac = x - lo
|
|
65
|
+
return (sorted_vals[lo] * (1 - frac) + sorted_vals[lo + 1] * frac
|
|
66
|
+
if lo + 1 < len(sorted_vals) else sorted_vals[lo])
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def run(batch: int, repeats: int, seed: int = 0, warmup: int = 2,
|
|
72
|
+
regime: str = "physical", out_dir: Path | None = None) -> tuple[dict[str, Any], Path]:
|
|
73
|
+
if not gbskernels.gpu_available():
|
|
74
|
+
raise RuntimeError(
|
|
75
|
+
"GPU extension not importable; build bindings/ (or the host-shim) first."
|
|
76
|
+
)
|
|
77
|
+
backends = ["gpu", "cpu"]
|
|
78
|
+
cells = [(eng, func, d, rep)
|
|
79
|
+
for eng in backends for func, (_, sizes) in _SPECS.items()
|
|
80
|
+
for d in sizes for rep in range(repeats)]
|
|
81
|
+
random.Random(seed).shuffle(cells)
|
|
82
|
+
|
|
83
|
+
# one shared workload per (func, dim) from bench._inputs (regime-aware): GPU, CPU, and
|
|
84
|
+
# the Walrus baseline all consume the SAME matrices -> same-input comparison.
|
|
85
|
+
stacks = {(func, d): _inputs.bench_batch(func, d, batch, regime, 1000 + d)
|
|
86
|
+
for func, (_, sizes) in _SPECS.items() for d in sizes}
|
|
87
|
+
|
|
88
|
+
# The CPU backend loops the Python reference, so it is timed on a capped
|
|
89
|
+
# sub-batch; evals/sec is per-eval and stays comparable to the GPU full batch.
|
|
90
|
+
cpu_cap = 128
|
|
91
|
+
|
|
92
|
+
# Warm-up policy (docs/DESIGN.md §9): each (backend, func, size) cell is run `warmup` times
|
|
93
|
+
# UNTIMED before any timed run, to reach steady-state GPU clocks and prime caches /
|
|
94
|
+
# the kernel JIT. The cold first run is never a headline number -- this is what
|
|
95
|
+
# tames the large cold-start IQRs. Warm-up runs are discarded.
|
|
96
|
+
for eng, func, d in {(e, f, dd) for e, f, dd, _ in cells}:
|
|
97
|
+
fn, n = _SPECS[func][0], (batch if eng == "gpu" else min(batch, cpu_cap))
|
|
98
|
+
for _ in range(warmup):
|
|
99
|
+
fn(stacks[(func, d)][:n], backend=eng)
|
|
100
|
+
|
|
101
|
+
raw: list[dict[str, Any]] = []
|
|
102
|
+
for eng, func, d, rep in cells:
|
|
103
|
+
fn = _SPECS[func][0]
|
|
104
|
+
n = batch if eng == "gpu" else min(batch, cpu_cap)
|
|
105
|
+
A = stacks[(func, d)][:n]
|
|
106
|
+
t0 = time.perf_counter()
|
|
107
|
+
out = fn(A, backend=eng)
|
|
108
|
+
elapsed = time.perf_counter() - t0 # GPU path syncs inside host_api
|
|
109
|
+
# checksum over the first cpu_cap results (the same matrices both backends
|
|
110
|
+
# compute) so GPU and CPU are comparable despite different timed batch sizes.
|
|
111
|
+
raw.append({"backend": eng, "func": func, "matrix_dim": d, "repeat": rep,
|
|
112
|
+
"batch_timed": n, "seconds": elapsed,
|
|
113
|
+
"evals_per_sec": n / elapsed if elapsed else float("inf"),
|
|
114
|
+
"checksum": _checksum(out[:min(batch, cpu_cap)])})
|
|
115
|
+
|
|
116
|
+
summary = _summarize(raw)
|
|
117
|
+
# The public-path honesty gate: on a real device the GPU and CPU backends must agree to
|
|
118
|
+
# FP64 tolerance on every well-conditioned (physical) cell. A single disagreement means a
|
|
119
|
+
# kernel/binding bug, and NO timing from this run is publishable -- the caller (main / the
|
|
120
|
+
# GPU session) turns this into a non-zero exit. (Adversarial inputs may legitimately
|
|
121
|
+
# diverge; gate on the physical regime.)
|
|
122
|
+
all_agree = all(r["backends_agree"] for r in summary)
|
|
123
|
+
artifact = {
|
|
124
|
+
"kind": "throughput_end_to_end",
|
|
125
|
+
"created_utc": datetime.now(timezone.utc).isoformat(),
|
|
126
|
+
**_provenance.provenance(), # commit + container_digest + hostname
|
|
127
|
+
"path": "public gbskernels.*_batched(backend=...) incl. H2D/launch/sync/D2H",
|
|
128
|
+
# build-flag provenance, not host OS: a no-GPU runner is "host-shim".
|
|
129
|
+
"gpu_backend": gbskernels.gpu_backend_kind(),
|
|
130
|
+
"env": {"platform": platform.platform(), "numpy": version("numpy")},
|
|
131
|
+
"params": {"batch": batch, "repeats": repeats, "warmup": warmup, "precision": "fp64",
|
|
132
|
+
"regime": regime, "seed": seed,
|
|
133
|
+
"metric": "evals/sec median + IQR; randomized cell order; "
|
|
134
|
+
"warm-up discarded; checksum guard"},
|
|
135
|
+
"all_backends_agree": all_agree,
|
|
136
|
+
"summary": summary,
|
|
137
|
+
"raw": raw,
|
|
138
|
+
}
|
|
139
|
+
out_dir = Path(out_dir) if out_dir is not None else DEFAULT_OUT
|
|
140
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
141
|
+
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
142
|
+
path = out_dir / f"throughput_e2e_{stamp}.json"
|
|
143
|
+
path.write_text(json.dumps(artifact, indent=2))
|
|
144
|
+
return artifact, path
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _summarize(raw: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
148
|
+
rows = []
|
|
149
|
+
keys = sorted({(r["func"], r["matrix_dim"]) for r in raw})
|
|
150
|
+
for func, d in keys:
|
|
151
|
+
row: dict[str, Any] = {"func": func, "matrix_dim": d}
|
|
152
|
+
checks = {}
|
|
153
|
+
for eng in ("gpu", "cpu"):
|
|
154
|
+
eps = sorted(r["evals_per_sec"] for r in raw
|
|
155
|
+
if r["backend"] == eng and r["func"] == func and r["matrix_dim"] == d)
|
|
156
|
+
cs = [r["checksum"] for r in raw
|
|
157
|
+
if r["backend"] == eng and r["func"] == func and r["matrix_dim"] == d]
|
|
158
|
+
checks[eng] = cs[0] if cs else None
|
|
159
|
+
if eps:
|
|
160
|
+
row[eng] = {"evals_per_sec_median": _quantile(eps, 0.5),
|
|
161
|
+
"evals_per_sec_iqr": _quantile(eps, 0.75) - _quantile(eps, 0.25)}
|
|
162
|
+
# GPU vs CPU agree to FP64 tolerance -- NOT bit-exact: the two backends use
|
|
163
|
+
# different determinant/linear-algebra routines, so mildly-conditioned
|
|
164
|
+
# inputs legitimately differ at ~1e-11 (a strict string check would flag it).
|
|
165
|
+
g, c = checks.get("gpu"), checks.get("cpu")
|
|
166
|
+
row["backends_agree"] = (g is not None and c is not None
|
|
167
|
+
and abs(g - c) <= 1e-7 * max(abs(c), 1e-300))
|
|
168
|
+
row["checksum_gpu"], row["checksum_cpu"] = g, c
|
|
169
|
+
rows.append(row)
|
|
170
|
+
return rows
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def main() -> None:
|
|
174
|
+
p = argparse.ArgumentParser(description=__doc__)
|
|
175
|
+
p.add_argument("--batch", type=int, default=1024)
|
|
176
|
+
p.add_argument("--repeats", type=int, default=7)
|
|
177
|
+
p.add_argument("--seed", type=int, default=0)
|
|
178
|
+
p.add_argument("--out", type=Path, default=None)
|
|
179
|
+
p.add_argument("--warmup", type=int, default=2, help="untimed warm-up runs per cell")
|
|
180
|
+
p.add_argument("--regime", default="physical", choices=list(_inputs.BENCH_REGIMES),
|
|
181
|
+
help="shared input family for every (func,dim) cell")
|
|
182
|
+
args = p.parse_args()
|
|
183
|
+
artifact, path = run(args.batch, args.repeats, args.seed, args.warmup,
|
|
184
|
+
regime=args.regime, out_dir=args.out)
|
|
185
|
+
print(f"# end-to-end throughput ({artifact['gpu_backend']}) -> {path}")
|
|
186
|
+
print(f"# public binding path; commit {artifact['commit']}; digest "
|
|
187
|
+
f"{artifact['container_digest']}; batch={args.batch} repeats={args.repeats} "
|
|
188
|
+
f"warmup={args.warmup} regime={args.regime}")
|
|
189
|
+
print(f" {'func':>5} {'dim':>4} {'gpu med':>12} {'cpu med':>12} agree")
|
|
190
|
+
for r in artifact["summary"]:
|
|
191
|
+
gm = r.get("gpu", {}).get("evals_per_sec_median", float("nan"))
|
|
192
|
+
cm = r.get("cpu", {}).get("evals_per_sec_median", float("nan"))
|
|
193
|
+
print(f" {r['func']:>5} {r['matrix_dim']:>4} {gm:>12.3e} {cm:>12.3e} "
|
|
194
|
+
f"{'ok' if r['backends_agree'] else 'MISMATCH'}")
|
|
195
|
+
# P0.8: a public-path checksum disagreement fails the run (non-zero exit), so the GPU
|
|
196
|
+
# session aborts instead of publishing a number from a backend mismatch.
|
|
197
|
+
if not artifact["all_backends_agree"]:
|
|
198
|
+
print("\n[FAIL] GPU and CPU disagree on a public-path cell -- no timing from this run "
|
|
199
|
+
"is publishable. Investigate the kernel; rerun on the physical regime.")
|
|
200
|
+
sys.exit(2)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
if __name__ == "__main__":
|
|
204
|
+
main()
|
bench/throughput_gpu.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""GPU throughput driver — runs the compiled CUDA timing harness with provenance.
|
|
2
|
+
|
|
3
|
+
Thin wrapper (docs/DESIGN.md §9/sec.10) around ``core/build/bench_kernels`` (built by
|
|
4
|
+
CMake under nvcc in a rented-GPU session). It runs the harness, captures the
|
|
5
|
+
per-(func, size) evals/sec + honesty checksums it emits, attaches GPU/driver/clock
|
|
6
|
+
provenance from ``nvidia-smi``, and writes an append-only artifact to
|
|
7
|
+
``results/throughput/``. No composite "winner" number; raw data retained.
|
|
8
|
+
|
|
9
|
+
This module does **not** compile or run anything on a GPU by itself — it requires
|
|
10
|
+
that ``core/build/bench_kernels`` already exists (the session's manifest builds
|
|
11
|
+
it). Invoked as ``python -m bench.throughput_gpu`` from ``scripts/session.py``.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import platform
|
|
19
|
+
import subprocess
|
|
20
|
+
from datetime import datetime, timezone
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from bench import _provenance
|
|
25
|
+
|
|
26
|
+
REPO = Path(__file__).resolve().parent.parent
|
|
27
|
+
DEFAULT_BIN = REPO / "core" / "build" / "bench_kernels"
|
|
28
|
+
DEFAULT_OUT = REPO / "results" / "throughput"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _nvidia_smi() -> dict[str, Any]:
|
|
32
|
+
"""GPU name, driver, and clocks for provenance (empty dict if unavailable)."""
|
|
33
|
+
q = ("name,driver_version,memory.total,memory.used,temperature.gpu,"
|
|
34
|
+
"clocks.sm,clocks.mem,clocks.max.sm,power.draw")
|
|
35
|
+
try:
|
|
36
|
+
out = subprocess.run(
|
|
37
|
+
["nvidia-smi", f"--query-gpu={q}", "--format=csv,noheader"],
|
|
38
|
+
capture_output=True, text=True, check=True,
|
|
39
|
+
).stdout.strip()
|
|
40
|
+
fields = [f.strip() for f in out.split(",")]
|
|
41
|
+
keys = ["name", "driver_version", "memory_total", "memory_used", "temperature_c",
|
|
42
|
+
"clock_sm", "clock_mem", "clock_sm_max", "power_draw"]
|
|
43
|
+
return dict(zip(keys, fields))
|
|
44
|
+
except Exception:
|
|
45
|
+
return {}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def run(bench_bin: Path, batch: int, repeats: int, out_dir: Path) -> tuple[dict[str, Any], Path]:
|
|
49
|
+
if not bench_bin.exists():
|
|
50
|
+
raise FileNotFoundError(
|
|
51
|
+
f"{bench_bin} not found — build it first (cmake --build core/build) in the "
|
|
52
|
+
"rented-GPU session; this driver only runs the compiled harness."
|
|
53
|
+
)
|
|
54
|
+
proc = subprocess.run(
|
|
55
|
+
[str(bench_bin), str(batch), str(repeats)],
|
|
56
|
+
capture_output=True, text=True, check=True,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def _parse(line: str) -> dict[str, Any]:
|
|
60
|
+
# C printf emits bare nan/inf; rewrite to the JSON tokens json accepts so
|
|
61
|
+
# an anomalous row is recorded (as NaN/inf) rather than crashing the run.
|
|
62
|
+
import re
|
|
63
|
+
line = re.sub(r":\s*-inf\b", ": -Infinity", line)
|
|
64
|
+
line = re.sub(r":\s*inf\b", ": Infinity", line)
|
|
65
|
+
line = re.sub(r":\s*nan\b", ": NaN", line)
|
|
66
|
+
return json.loads(line)
|
|
67
|
+
|
|
68
|
+
rows = [_parse(line) for line in proc.stdout.splitlines() if line.strip().startswith("{")]
|
|
69
|
+
|
|
70
|
+
artifact = {
|
|
71
|
+
"kind": "throughput",
|
|
72
|
+
"tier": "gpu",
|
|
73
|
+
"created_utc": datetime.now(timezone.utc).isoformat(),
|
|
74
|
+
**_provenance.provenance(), # commit + container_digest + hostname
|
|
75
|
+
"gpu": _nvidia_smi(), # name/driver/memory + temperature & live clocks at run time
|
|
76
|
+
"host": {"platform": platform.platform()},
|
|
77
|
+
"params": {"batch": batch, "repeats": repeats, "precision": "fp64+dd",
|
|
78
|
+
"metric": "evals_per_sec median + IQR over repeats; bench_kernels.cu "
|
|
79
|
+
"warms each cell before timing; post-sync checksum/row"},
|
|
80
|
+
"methodology_caveats": (
|
|
81
|
+
"Toward docs/DESIGN.md §9 bar: the container digest + commit are now recorded and "
|
|
82
|
+
"the kernel timing is warmed up. Still single-instance per run (sweep the 4090 "
|
|
83
|
+
"AND a datacenter GPU across runs); the on-the-same-instance The Walrus "
|
|
84
|
+
"comparison runs in the GPU session (bench.walrus_baseline)."
|
|
85
|
+
),
|
|
86
|
+
"rows": rows,
|
|
87
|
+
}
|
|
88
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
89
|
+
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
90
|
+
gpu_tag = (artifact["gpu"].get("name", "gpu") or "gpu").replace(" ", "_")
|
|
91
|
+
path = out_dir / f"throughput_{gpu_tag}_{stamp}.json"
|
|
92
|
+
path.write_text(json.dumps(artifact, indent=2))
|
|
93
|
+
return artifact, path
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def main() -> None:
|
|
97
|
+
p = argparse.ArgumentParser(description=__doc__)
|
|
98
|
+
p.add_argument("--bin", type=Path, default=DEFAULT_BIN)
|
|
99
|
+
p.add_argument("--batch", type=int, default=4096)
|
|
100
|
+
p.add_argument("--repeats", type=int, default=7)
|
|
101
|
+
p.add_argument("--out", type=Path, default=DEFAULT_OUT)
|
|
102
|
+
# accepted for manifest-compatibility with the CPU harness; the compiled
|
|
103
|
+
# binary fixes its own size grid, so these are informational here.
|
|
104
|
+
p.add_argument("--func", default=None)
|
|
105
|
+
p.add_argument("--sizes", default=None)
|
|
106
|
+
args = p.parse_args()
|
|
107
|
+
|
|
108
|
+
artifact, path = run(args.bin, args.batch, args.repeats, args.out)
|
|
109
|
+
print(f"# gpu throughput -> {path}")
|
|
110
|
+
g = artifact["gpu"]
|
|
111
|
+
print(f"# GPU: {g.get('name','?')} | driver {g.get('driver_version','?')} | "
|
|
112
|
+
f"{g.get('temperature_c','?')}C | clk {g.get('clock_sm','?')}")
|
|
113
|
+
print(f"# commit {artifact.get('commit')} | container {artifact.get('container_digest')} | "
|
|
114
|
+
f"batch={args.batch} repeats={args.repeats}")
|
|
115
|
+
print(f" {'func':>9} {'matrix_dim':>10} {'evals/sec median':>17} {'IQR':>11}")
|
|
116
|
+
for r in artifact["rows"]:
|
|
117
|
+
med = r.get("evals_per_sec_median", r.get("evals_per_sec_best", float("nan")))
|
|
118
|
+
iqr = r.get("evals_per_sec_iqr", float("nan"))
|
|
119
|
+
# the cooperative permanent rows carry a "groups" width; surface it so the
|
|
120
|
+
# perm-vs-perm_coop crossover is legible (raw rows keep the field regardless).
|
|
121
|
+
label = r["func"] + (f"/g{r['groups']}" if "groups" in r else "")
|
|
122
|
+
print(f" {label:>9} {r['matrix_dim']:>10} {med:>17.3e} {iqr:>11.2e}")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
if __name__ == "__main__":
|
|
126
|
+
main()
|
bench/tightness.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Certified-bound tightness distributions -- the numerics figure.
|
|
2
|
+
|
|
3
|
+
For a CS/numerics reviewer, "the enclosure is always safe" is table stakes; the
|
|
4
|
+
question is *how tight*. This measures, across ensembles rather than single
|
|
5
|
+
worst cases, the distribution of three quantities for each certified function:
|
|
6
|
+
|
|
7
|
+
rel_bound = bound / |value| -- what the certificate PROMISES (≈ κ·u)
|
|
8
|
+
rel_err = |value − exact| / |value| -- what actually happened (vs mpmath)
|
|
9
|
+
tightness = bound / max(|value−exact|, u·|value|)
|
|
10
|
+
-- the bound's slack over the true
|
|
11
|
+
error, floored at the fp64 ulp
|
|
12
|
+
(you cannot resolve error below u·|v|)
|
|
13
|
+
|
|
14
|
+
The hard invariant (`rel_err ≤ rel_bound` on every sample -- the enclosure) is
|
|
15
|
+
checked here too and must never fail. The story the distribution tells: on the
|
|
16
|
+
physical ensemble the bound is tight (small, bounded tightness) and on the
|
|
17
|
+
adversarial cancellation families it widens *correctly* -- because those inputs
|
|
18
|
+
are genuinely ill-conditioned, and the certificate reports that honestly rather
|
|
19
|
+
than hiding it. That is the difference between a rigorous certificate and a
|
|
20
|
+
heuristic κ.
|
|
21
|
+
|
|
22
|
+
uv run python -m bench.tightness --samples 150 [--plot]
|
|
23
|
+
|
|
24
|
+
Writes results/tightness/tightness_<utc>.json (per function×regime distribution
|
|
25
|
+
stats + enclosure check) and, with --plot, results/tightness/tightness.png.
|
|
26
|
+
CPU-only; references are mpmath. Sizes are kept modest so the reference is
|
|
27
|
+
affordable -- the point is the distribution shape, not large N.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import argparse
|
|
33
|
+
import json
|
|
34
|
+
import time
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import Any, Callable
|
|
37
|
+
|
|
38
|
+
import numpy as np
|
|
39
|
+
|
|
40
|
+
import cpu_ref
|
|
41
|
+
import highprec_ref as href
|
|
42
|
+
from bench import _inputs as inp
|
|
43
|
+
from bench._provenance import provenance
|
|
44
|
+
|
|
45
|
+
RESULTS = Path(__file__).resolve().parent.parent / "results" / "tightness"
|
|
46
|
+
U = 2.0 ** -53
|
|
47
|
+
|
|
48
|
+
# func -> (mpmath reference, {regime: sampler(seed) -> matrix})
|
|
49
|
+
_CASES: dict[str, tuple[Callable, dict[str, Callable]]] = {
|
|
50
|
+
"perm": (href.permanent_mp, {
|
|
51
|
+
"physical": lambda s: inp.physical_permanent(10, s),
|
|
52
|
+
"adversarial": lambda s: inp.pm1_matrix(10, s), # ±1: heavy cancellation
|
|
53
|
+
}),
|
|
54
|
+
"haf": (href.hafnian_mp, {
|
|
55
|
+
"physical": lambda s: inp.physical_hafnian(10, s),
|
|
56
|
+
"adversarial": lambda s: inp.cancellation_hafnian(10.0 ** -(3 + s % 9), s),
|
|
57
|
+
}),
|
|
58
|
+
"lhaf": (href.loop_hafnian_mp, {
|
|
59
|
+
"physical": lambda s: inp.physical_loop_hafnian(10, s),
|
|
60
|
+
"adversarial": lambda s: inp.cancellation_loop_hafnian(10.0 ** -(3 + s % 9), s),
|
|
61
|
+
}),
|
|
62
|
+
"tor": (href.torontonian_mp, {
|
|
63
|
+
"physical": lambda s: np.real(inp.physical_torontonian(4, s)),
|
|
64
|
+
"adversarial": lambda s: inp.cancellation_torontonian(10.0 ** -(1 + s % 6)),
|
|
65
|
+
}),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _quantiles(x: np.ndarray) -> dict[str, float]:
|
|
70
|
+
q = np.quantile(x, [0.05, 0.25, 0.5, 0.75, 0.95])
|
|
71
|
+
return {"q05": float(q[0]), "q25": float(q[1]), "median": float(q[2]),
|
|
72
|
+
"q75": float(q[3]), "q95": float(q[4]), "max": float(x.max()),
|
|
73
|
+
"min": float(x.min())}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _collect(func: str, regime: str, gen: Callable, ref: Callable,
|
|
77
|
+
samples: int) -> dict[str, Any]:
|
|
78
|
+
rel_bound, rel_err, tight = [], [], []
|
|
79
|
+
enclosure_fails = 0
|
|
80
|
+
for s in range(samples):
|
|
81
|
+
A = gen(s)
|
|
82
|
+
value, bound = cpu_ref.certified(func, A)
|
|
83
|
+
av = abs(value)
|
|
84
|
+
if av == 0.0 or not np.isfinite(bound):
|
|
85
|
+
continue
|
|
86
|
+
exact = complex(ref(A, dps=50))
|
|
87
|
+
err = abs(complex(value) - exact)
|
|
88
|
+
rb, re = bound / av, err / av
|
|
89
|
+
if re > rb * (1 + 1e-9): # the enclosure invariant -- must hold
|
|
90
|
+
enclosure_fails += 1
|
|
91
|
+
rel_bound.append(rb)
|
|
92
|
+
rel_err.append(re)
|
|
93
|
+
tight.append(bound / max(err, U * av))
|
|
94
|
+
return {"n": len(tight), "enclosure_fails": enclosure_fails,
|
|
95
|
+
"rel_bound": _quantiles(np.array(rel_bound)),
|
|
96
|
+
"rel_err": _quantiles(np.array(rel_err)),
|
|
97
|
+
"tightness": _quantiles(np.array(tight)),
|
|
98
|
+
"_raw_tightness": [float(x) for x in tight],
|
|
99
|
+
"_raw_rel_bound": [float(x) for x in rel_bound]}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def run(samples: int) -> dict[str, Any]:
|
|
103
|
+
out: dict[str, Any] = {}
|
|
104
|
+
print(f"{'func/regime':>18} {'N':>4} {'encl':>5} "
|
|
105
|
+
f"{'rel_bound med':>14} {'rel_err med':>12} {'tightness med (q95)':>22}")
|
|
106
|
+
for func, (ref, regimes) in _CASES.items():
|
|
107
|
+
out[func] = {}
|
|
108
|
+
for regime, gen in regimes.items():
|
|
109
|
+
d = _collect(func, regime, gen, ref, samples)
|
|
110
|
+
out[func][regime] = d
|
|
111
|
+
t = d["tightness"]
|
|
112
|
+
print(f"{func + '/' + regime:>18} {d['n']:>4} {d['enclosure_fails']:>5} "
|
|
113
|
+
f"{d['rel_bound']['median']:>14.2e} {d['rel_err']['median']:>12.2e} "
|
|
114
|
+
f"{t['median']:>10.1f} ({t['q95']:.1f})")
|
|
115
|
+
total_fails = sum(out[f][r]["enclosure_fails"]
|
|
116
|
+
for f in out for r in out[f])
|
|
117
|
+
print(f"\nenclosure violations across all ensembles: {total_fails} "
|
|
118
|
+
f"(MUST be 0 -- the certificate is only meaningful if it never under-claims)")
|
|
119
|
+
return {"kind": "certified_tightness", **provenance(),
|
|
120
|
+
"params": {"samples": samples, "sizes": "perm/haf/lhaf n=10, tor 2n=8"},
|
|
121
|
+
"enclosure_violations_total": total_fails, "by_function": out}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def plot(art: dict[str, Any], path: Path) -> None:
|
|
125
|
+
import matplotlib
|
|
126
|
+
matplotlib.use("Agg")
|
|
127
|
+
import matplotlib.pyplot as plt
|
|
128
|
+
|
|
129
|
+
funcs = list(art["by_function"])
|
|
130
|
+
fig, ax = plt.subplots(figsize=(9, 5))
|
|
131
|
+
positions, data, colors, labels = [], [], [], []
|
|
132
|
+
pos = 0
|
|
133
|
+
for func in funcs:
|
|
134
|
+
for regime, col in (("physical", "#2a7"), ("adversarial", "#d63")):
|
|
135
|
+
raw = art["by_function"][func][regime].get("_raw_tightness", [])
|
|
136
|
+
if raw:
|
|
137
|
+
positions.append(pos)
|
|
138
|
+
data.append(np.log10(np.maximum(raw, 1.0)))
|
|
139
|
+
colors.append(col)
|
|
140
|
+
labels.append(f"{func}\n{regime}")
|
|
141
|
+
pos += 1
|
|
142
|
+
pos += 0.6
|
|
143
|
+
parts = ax.violinplot(data, positions=positions, showmedians=True, widths=0.8)
|
|
144
|
+
for pc, c in zip(parts["bodies"], colors):
|
|
145
|
+
pc.set_facecolor(c)
|
|
146
|
+
pc.set_alpha(0.6)
|
|
147
|
+
ax.set_xticks(positions)
|
|
148
|
+
ax.set_xticklabels(labels, fontsize=8)
|
|
149
|
+
ax.set_ylabel(r"$\log_{10}$ tightness = bound / max(actual error, $u\,|v|$)")
|
|
150
|
+
ax.set_title("Certified-bound tightness distribution (rigorous, never < 1)\n"
|
|
151
|
+
"physical ensembles tight; adversarial families widen honestly")
|
|
152
|
+
ax.axhline(0, color="k", lw=0.5, ls=":")
|
|
153
|
+
fig.tight_layout()
|
|
154
|
+
fig.savefig(path, dpi=140)
|
|
155
|
+
print(f"-> {path}")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def main() -> None:
|
|
159
|
+
ap = argparse.ArgumentParser(description=__doc__)
|
|
160
|
+
ap.add_argument("--samples", type=int, default=150)
|
|
161
|
+
ap.add_argument("--plot", action="store_true")
|
|
162
|
+
args = ap.parse_args()
|
|
163
|
+
art = run(args.samples)
|
|
164
|
+
RESULTS.mkdir(parents=True, exist_ok=True)
|
|
165
|
+
stamp = time.strftime("%Y%m%dT%H%M%SZ", time.gmtime())
|
|
166
|
+
out = RESULTS / f"tightness_{stamp}.json"
|
|
167
|
+
out.write_text(json.dumps(art, indent=1))
|
|
168
|
+
print(f"-> {out}")
|
|
169
|
+
if args.plot:
|
|
170
|
+
plot(art, RESULTS / "tightness.png")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
if __name__ == "__main__":
|
|
174
|
+
main()
|
bench/walrus_baseline.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Same-instance The Walrus baseline (docs/DESIGN.md §9 / the reproducibility contract).
|
|
2
|
+
|
|
3
|
+
Times Xanadu's The Walrus (the canonical CPU reference) for the four functions on the
|
|
4
|
+
**same GPU instance** that ran our kernels, with the same hygiene -- warm-up, median +
|
|
5
|
+
IQR over repeats, randomized cell order, the shared provenance block -- so the
|
|
6
|
+
GPU-vs-The-Walrus comparison is apples-to-apples on one machine (not our GPU on a
|
|
7
|
+
rented box vs The Walrus on a laptop). The crossover analysis (`bench.crossover`) pairs
|
|
8
|
+
this artifact with the kernel throughput artifact from the same session.
|
|
9
|
+
|
|
10
|
+
The Walrus is one-evaluation-at-a-time, so each cell loops a small batch and reports
|
|
11
|
+
per-eval evals/sec, directly comparable to our batched per-eval rate. Runs wherever
|
|
12
|
+
``thewalrus`` is importable (the GPU session installs it); skips with a clear error
|
|
13
|
+
otherwise.
|
|
14
|
+
|
|
15
|
+
uv run python -m bench.walrus_baseline --batch 64 --repeats 7
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import json
|
|
22
|
+
import platform
|
|
23
|
+
import random
|
|
24
|
+
import time
|
|
25
|
+
from datetime import datetime, timezone
|
|
26
|
+
from importlib.metadata import version
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
import numpy as np
|
|
31
|
+
|
|
32
|
+
from bench import _inputs, _provenance
|
|
33
|
+
|
|
34
|
+
# The Walrus's high-level paths use np.find_common_type (removed in NumPy 2.0); the
|
|
35
|
+
# low-level perm/hafnian/tor used here do not, but apply the NumPy-2.0 compat shim
|
|
36
|
+
# defensively so the import + any internal use is safe.
|
|
37
|
+
if not hasattr(np, "find_common_type"):
|
|
38
|
+
np.find_common_type = lambda a, s: np.result_type(*a, *s)
|
|
39
|
+
|
|
40
|
+
DEFAULT_OUT = Path(__file__).resolve().parent.parent / "results" / "throughput"
|
|
41
|
+
|
|
42
|
+
# func -> (The Walrus call, matrix sizes). The matrices come from the SAME shared generator
|
|
43
|
+
# (bench._inputs.bench_batch) and the SAME seed convention as bench.throughput_end_to_end, so
|
|
44
|
+
# The Walrus is timed on the *identical inputs* as our GPU/CPU path -- a same-input baseline
|
|
45
|
+
# (docs/DESIGN.md §9). For the torontonian the matrix dim is 2*modes (so [8,12,16] = 4..8 modes).
|
|
46
|
+
_SPECS: dict[str, Any] = {
|
|
47
|
+
"perm": (lambda tw, M: tw.perm(M), [8, 12, 16]),
|
|
48
|
+
"haf": (lambda tw, M: tw.hafnian(M), [8, 12, 16]),
|
|
49
|
+
"lhaf": (lambda tw, M: tw.hafnian(M, loop=True), [8, 12]),
|
|
50
|
+
"tor": (lambda tw, M: tw.tor(np.real(M)), [8, 12, 16]),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _quantile(xs: list[float], q: float) -> float:
|
|
55
|
+
s = sorted(xs)
|
|
56
|
+
x = q * (len(s) - 1)
|
|
57
|
+
lo = int(x); frac = x - lo
|
|
58
|
+
return s[lo] * (1 - frac) + s[lo + 1] * frac if lo + 1 < len(s) else s[lo]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def run(batch: int, repeats: int, seed: int = 0, warmup: int = 2,
|
|
62
|
+
regime: str = "physical", out_dir: Path | None = None) -> tuple[dict[str, Any], Path]:
|
|
63
|
+
try:
|
|
64
|
+
import thewalrus as tw
|
|
65
|
+
except Exception as e: # pragma: no cover - depends on the box
|
|
66
|
+
raise RuntimeError(
|
|
67
|
+
"thewalrus is not importable; install it on the instance to record the "
|
|
68
|
+
"same-instance baseline (the GPU session does this)."
|
|
69
|
+
) from e
|
|
70
|
+
|
|
71
|
+
cells = [(func, d, rep) for func, (_, sizes) in _SPECS.items()
|
|
72
|
+
for d in sizes for rep in range(repeats)]
|
|
73
|
+
random.Random(seed).shuffle(cells)
|
|
74
|
+
|
|
75
|
+
# the SAME shared workload (and seed convention 1000+d) as throughput_end_to_end, so the
|
|
76
|
+
# Walrus baseline is timed on the identical matrices our GPU/CPU path is.
|
|
77
|
+
mats = {(func, d): list(_inputs.bench_batch(func, d, batch, regime, 1000 + d))
|
|
78
|
+
for func, (_, sizes) in _SPECS.items() for d in sizes}
|
|
79
|
+
|
|
80
|
+
call = {func: _SPECS[func][0] for func in _SPECS}
|
|
81
|
+
# warm-up (untimed) per distinct cell -- steady state before timing
|
|
82
|
+
for func, d in {(f, dd) for f, dd, _ in cells}:
|
|
83
|
+
for _ in range(warmup):
|
|
84
|
+
for M in mats[(func, d)][: min(batch, 8)]:
|
|
85
|
+
call[func](tw, M)
|
|
86
|
+
|
|
87
|
+
raw: list[dict[str, Any]] = []
|
|
88
|
+
for func, d, rep in cells:
|
|
89
|
+
batch_mats = mats[(func, d)]
|
|
90
|
+
t0 = time.perf_counter()
|
|
91
|
+
s = 0.0
|
|
92
|
+
for M in batch_mats:
|
|
93
|
+
s += abs(complex(call[func](tw, M)))
|
|
94
|
+
elapsed = time.perf_counter() - t0
|
|
95
|
+
raw.append({"func": func, "matrix_dim": d, "repeat": rep, "batch": batch,
|
|
96
|
+
"seconds": elapsed, "evals_per_sec": batch / elapsed if elapsed else float("inf"),
|
|
97
|
+
"checksum": s})
|
|
98
|
+
|
|
99
|
+
rows = []
|
|
100
|
+
for func, (_, sizes) in _SPECS.items():
|
|
101
|
+
for d in sizes:
|
|
102
|
+
eps = [r["evals_per_sec"] for r in raw if r["func"] == func and r["matrix_dim"] == d]
|
|
103
|
+
rows.append({"func": func, "matrix_dim": d,
|
|
104
|
+
"evals_per_sec_median": _quantile(eps, 0.5),
|
|
105
|
+
"evals_per_sec_iqr": _quantile(eps, 0.75) - _quantile(eps, 0.25)})
|
|
106
|
+
|
|
107
|
+
artifact = {
|
|
108
|
+
"kind": "walrus_baseline",
|
|
109
|
+
"created_utc": datetime.now(timezone.utc).isoformat(),
|
|
110
|
+
**_provenance.provenance(),
|
|
111
|
+
"library": "thewalrus", "library_version": version("thewalrus"),
|
|
112
|
+
"metric": "per-eval evals/sec (The Walrus, one-at-a-time, looped over a batch); "
|
|
113
|
+
"median + IQR; randomized cell order; warm-up discarded",
|
|
114
|
+
"params": {"batch": batch, "repeats": repeats, "warmup": warmup, "precision": "fp64",
|
|
115
|
+
"regime": regime, "seed": seed},
|
|
116
|
+
"env": {"platform": platform.platform(), "numpy": version("numpy")},
|
|
117
|
+
"rows": rows, "raw": raw,
|
|
118
|
+
}
|
|
119
|
+
out_dir = Path(out_dir) if out_dir is not None else DEFAULT_OUT
|
|
120
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
121
|
+
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
122
|
+
path = out_dir / f"walrus_baseline_{stamp}.json"
|
|
123
|
+
path.write_text(json.dumps(artifact, indent=2))
|
|
124
|
+
return artifact, path
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def main() -> None:
|
|
128
|
+
p = argparse.ArgumentParser(description=__doc__)
|
|
129
|
+
p.add_argument("--batch", type=int, default=64)
|
|
130
|
+
p.add_argument("--repeats", type=int, default=7)
|
|
131
|
+
p.add_argument("--warmup", type=int, default=2)
|
|
132
|
+
p.add_argument("--regime", default="physical", choices=list(_inputs.BENCH_REGIMES),
|
|
133
|
+
help="shared input family (same generator as throughput_end_to_end)")
|
|
134
|
+
p.add_argument("--out", type=Path, default=None)
|
|
135
|
+
args = p.parse_args()
|
|
136
|
+
artifact, path = run(args.batch, args.repeats, warmup=args.warmup,
|
|
137
|
+
regime=args.regime, out_dir=args.out)
|
|
138
|
+
print(f"# The Walrus baseline (same instance) -> {path}")
|
|
139
|
+
print(f"# thewalrus {artifact['library_version']}; commit {artifact['commit']}; "
|
|
140
|
+
f"container {artifact['container_digest']}")
|
|
141
|
+
print(f" {'func':>5} {'dim':>4} {'evals/sec median':>17} {'IQR':>11}")
|
|
142
|
+
for r in artifact["rows"]:
|
|
143
|
+
print(f" {r['func']:>5} {r['matrix_dim']:>4} {r['evals_per_sec_median']:>17.3e} "
|
|
144
|
+
f"{r['evals_per_sec_iqr']:>11.2e}")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if __name__ == "__main__":
|
|
148
|
+
main()
|