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
bench/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Accuracy-vs-throughput harness (accuracy half is CPU-runnable; see docs/DESIGN.md §9).
|
|
2
|
+
|
|
3
|
+
Throughput benchmarking belongs to scripted rented-GPU sessions and never runs
|
|
4
|
+
here. The *accuracy* characterization (relative error vs size and conditioning
|
|
5
|
+
against the mpmath reference, docs/DESIGN.md §6) is pure CPU work and lives in this
|
|
6
|
+
package.
|
|
7
|
+
"""
|
bench/_inputs.py
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""Input families for accuracy characterization and stress testing.
|
|
2
|
+
|
|
3
|
+
Single source of truth shared by the harness and the test suite, so the
|
|
4
|
+
cancellation family used to *measure* the FP64 boundary is exactly the one the
|
|
5
|
+
tests assert against.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"random_complex",
|
|
14
|
+
"unit_modulus_complex",
|
|
15
|
+
"pm1_matrix",
|
|
16
|
+
"make_cancellation_matrix",
|
|
17
|
+
"cancellation_hafnian",
|
|
18
|
+
"cancellation_loop_hafnian",
|
|
19
|
+
"cancellation_torontonian",
|
|
20
|
+
"physical_permanent",
|
|
21
|
+
"physical_hafnian",
|
|
22
|
+
"physical_loop_hafnian",
|
|
23
|
+
"physical_torontonian",
|
|
24
|
+
"loss_permanent",
|
|
25
|
+
"loss_hafnian",
|
|
26
|
+
"loss_loop_hafnian",
|
|
27
|
+
"loss_torontonian",
|
|
28
|
+
"adversarial_permanent",
|
|
29
|
+
"adversarial_hafnian",
|
|
30
|
+
"adversarial_loop_hafnian",
|
|
31
|
+
"adversarial_torontonian",
|
|
32
|
+
"haar_unitary",
|
|
33
|
+
"bench_batch",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def random_complex(n: int, seed: int, scale: float = 1.0) -> np.ndarray:
|
|
38
|
+
g = np.random.default_rng(seed)
|
|
39
|
+
return scale * (g.uniform(-1, 1, (n, n)) + 1j * g.uniform(-1, 1, (n, n)))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def unit_modulus_complex(n: int, seed: int) -> np.ndarray:
|
|
43
|
+
"""Entries ``e^{i theta}``, theta ~ U(0, 2pi).
|
|
44
|
+
|
|
45
|
+
A naturally cancellation-heavy family: every entry has modulus 1, so the
|
|
46
|
+
Glynn terms grow like ``n^{n/2}`` while the permanent stays near
|
|
47
|
+
``sqrt(n!)`` -- the summation condition number rises ~exponentially in ``n``.
|
|
48
|
+
"""
|
|
49
|
+
g = np.random.default_rng(seed)
|
|
50
|
+
theta = g.uniform(0.0, 2.0 * np.pi, (n, n))
|
|
51
|
+
return np.exp(1j * theta)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def pm1_matrix(n: int, seed: int) -> np.ndarray:
|
|
55
|
+
"""+/-1 matrix. Integer-valued permanent -> exact ground truth at small n."""
|
|
56
|
+
g = np.random.default_rng(seed)
|
|
57
|
+
return np.where(g.random((n, n)) < 0.5, -1.0, 1.0)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def make_cancellation_matrix(n: int, delta: float, seed: int) -> np.ndarray:
|
|
61
|
+
"""A matrix with a *tunable* Glynn cancellation, kappa ~ O(1/delta).
|
|
62
|
+
|
|
63
|
+
Direct-sum of a 2x2 near-singular-permanent block with a well-conditioned
|
|
64
|
+
random remainder. The block ``B = [[2, 1], [-1, 0.5 + delta]]`` has
|
|
65
|
+
``perm(B) = 2*delta`` while its Glynn terms are O(1) (they cancel ~1.5 down
|
|
66
|
+
to ``2*delta``), so the whole matrix has ``perm = 2*delta * perm(R)`` with a
|
|
67
|
+
condition number that scales as ``1/delta`` -- the knob that drives FP64 off
|
|
68
|
+
the accuracy cliff while the mpmath reference stays exact (docs/DESIGN.md §6/sec.8).
|
|
69
|
+
Requires ``n >= 2``.
|
|
70
|
+
"""
|
|
71
|
+
if n < 2:
|
|
72
|
+
raise ValueError("cancellation matrix needs n >= 2")
|
|
73
|
+
B = np.array([[2.0, 1.0], [-1.0, 0.5 + delta]], dtype=np.complex128)
|
|
74
|
+
A = np.zeros((n, n), dtype=np.complex128)
|
|
75
|
+
A[:2, :2] = B
|
|
76
|
+
if n > 2:
|
|
77
|
+
A[2:, 2:] = random_complex(n - 2, seed=seed)
|
|
78
|
+
return A
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def cancellation_hafnian(delta: float, seed: int) -> np.ndarray:
|
|
82
|
+
"""Symmetric matrix whose hafnian nearly cancels to ``delta`` (8x8).
|
|
83
|
+
|
|
84
|
+
A 4x4 block with ``haf = a*f + b*e + c*d = 2 + (delta-2) = delta`` (three O(1)
|
|
85
|
+
matchings cancel) direct-summed with a well-conditioned remainder, so
|
|
86
|
+
``haf = delta * haf(R)`` is tiny while the power-trace terms are O(1). Same
|
|
87
|
+
family as ``core/check_hafnian_dd.cu``.
|
|
88
|
+
"""
|
|
89
|
+
a = f = b = e = c = 1.0
|
|
90
|
+
d = delta - 2.0
|
|
91
|
+
B = np.array([[0, a, b, c], [a, 0, d, e], [b, d, 0, f], [c, e, f, 0]], dtype=np.complex128)
|
|
92
|
+
g = np.random.default_rng(seed)
|
|
93
|
+
G = g.standard_normal((4, 4))
|
|
94
|
+
R = G + G.T
|
|
95
|
+
np.fill_diagonal(R, 0.0) # ordinary hafnian ignores the diagonal
|
|
96
|
+
A = np.zeros((8, 8), dtype=np.complex128)
|
|
97
|
+
A[:4, :4] = B
|
|
98
|
+
A[4:, 4:] = R
|
|
99
|
+
return A
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def cancellation_loop_hafnian(delta: float, seed: int) -> np.ndarray:
|
|
103
|
+
"""Symmetric matrix whose loop hafnian nearly cancels to ``delta`` (6x6).
|
|
104
|
+
|
|
105
|
+
A 2x2 block ``[[1,2],[2,delta-2]]`` has ``lhaf = h + g*k = 2 + (delta-2) =
|
|
106
|
+
delta``, direct-summed with a well-conditioned remainder (nonzero diagonal =
|
|
107
|
+
loops). Same family as ``core/check_loop_hafnian_dd.cu``.
|
|
108
|
+
"""
|
|
109
|
+
B = np.array([[1.0, 2.0], [2.0, delta - 2.0]], dtype=np.complex128)
|
|
110
|
+
g = np.random.default_rng(seed)
|
|
111
|
+
G = g.standard_normal((4, 4))
|
|
112
|
+
R = (G + G.T).astype(np.complex128) # diagonal kept (loop weights)
|
|
113
|
+
A = np.zeros((6, 6), dtype=np.complex128)
|
|
114
|
+
A[:2, :2] = B
|
|
115
|
+
A[2:, 2:] = R
|
|
116
|
+
return A
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def cancellation_torontonian(a: float) -> np.ndarray:
|
|
120
|
+
"""Single-mode O = diag(a, a): ``tor = a/(1-a)``, but the kernel computes
|
|
121
|
+
``1/sqrt(det(I-O)) - 1`` with a catastrophic ``(1+a+...) - 1`` cancellation as
|
|
122
|
+
``a -> 0``. Same family as ``core/check_torontonian_dd.cu``. Real-domain."""
|
|
123
|
+
return np.array([[a, 0.0], [0.0, a]], dtype=np.complex128)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# --- physical (realistic, well-conditioned) inputs -------------------------
|
|
127
|
+
# Representative of how each function is actually used in photonic sampling, and
|
|
128
|
+
# well-conditioned (FP64 is accurate here) -- the complement of the adversarial
|
|
129
|
+
# cancellation families above.
|
|
130
|
+
|
|
131
|
+
def physical_permanent(n: int, seed: int) -> np.ndarray:
|
|
132
|
+
"""A Haar-random interferometer ``U`` (n x n): ``perm(U)`` is a standard
|
|
133
|
+
boson-sampling amplitude. Well-conditioned (unit-modulus singular values)."""
|
|
134
|
+
return haar_unitary(n, seed)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def physical_hafnian(n: int, seed: int) -> np.ndarray:
|
|
138
|
+
"""A Gaussian-boson-sampling kernel ``B = U diag(tanh r) U^T`` (n x n complex
|
|
139
|
+
symmetric, modest squeezing) -- the matrix whose hafnian gives a GBS
|
|
140
|
+
amplitude. ``n`` should be even (haf of odd size is 0)."""
|
|
141
|
+
U = haar_unitary(n, seed)
|
|
142
|
+
g = np.random.default_rng(seed + 99)
|
|
143
|
+
r = g.uniform(0.1, 0.5, n)
|
|
144
|
+
return (U @ np.diag(np.tanh(r)) @ U.T).astype(np.complex128)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def physical_loop_hafnian(n: int, seed: int) -> np.ndarray:
|
|
148
|
+
"""A GBS kernel with a displacement-like diagonal (the loop weights) -- the
|
|
149
|
+
loop-hafnian (displaced GBS) input. ``n`` even."""
|
|
150
|
+
B = physical_hafnian(n, seed).copy()
|
|
151
|
+
g = np.random.default_rng(seed + 7)
|
|
152
|
+
np.fill_diagonal(B, g.uniform(-0.3, 0.3, n) + 1j * g.uniform(-0.3, 0.3, n))
|
|
153
|
+
return B
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def physical_torontonian(n_modes: int, seed: int, scale: float = 0.1) -> np.ndarray:
|
|
157
|
+
"""A small-norm real symmetric ``O`` (2n x 2n) -- a physical threshold-detector
|
|
158
|
+
matrix (``I - O_S`` stays positive definite). Real domain."""
|
|
159
|
+
g = np.random.default_rng(seed)
|
|
160
|
+
M = g.standard_normal((2 * n_modes, 2 * n_modes)) * scale
|
|
161
|
+
return ((M + M.T) / 2).astype(np.complex128)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# --- loss / mixed-state inputs ---------------------------------------------
|
|
165
|
+
# A pure squeezed + interferometer state passed through a uniform loss channel
|
|
166
|
+
# (transmission eta < 1) becomes MIXED: its Husimi covariance has det(Q) > 1, and the
|
|
167
|
+
# matrices the kernels consume (the A-matrix block for the (loop) hafnian, O = I - Q^-1
|
|
168
|
+
# for the torontonian) are structurally different from the pure case -- the regime a
|
|
169
|
+
# real lossy GBS experiment produces. This is the third input regime alongside
|
|
170
|
+
# "physical" (pure, well-conditioned) and "adversarial" (tunable cancellation).
|
|
171
|
+
|
|
172
|
+
def _qmat(cov: np.ndarray, hbar: float = 2.0) -> np.ndarray:
|
|
173
|
+
"""Husimi Q covariance from an xxpp Wigner covariance (matches sampling.gbs._qmat)."""
|
|
174
|
+
N = len(cov) // 2
|
|
175
|
+
x = cov[:N, :N] * 2 / hbar
|
|
176
|
+
xp = cov[:N, N:] * 2 / hbar
|
|
177
|
+
p = cov[N:, N:] * 2 / hbar
|
|
178
|
+
aidaj = (x + p + 1j * (xp - xp.T) - 2 * np.eye(N)) / 4
|
|
179
|
+
aiaj = (x - p + 1j * (xp + xp.T)) / 4
|
|
180
|
+
return np.block([[aidaj, aiaj.conj()], [aiaj, aidaj.conj()]]) + np.identity(2 * N)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _lossy_cov(modes: int, seed: int, eta: float = 0.6, hbar: float = 2.0) -> np.ndarray:
|
|
184
|
+
"""xxpp Wigner covariance of a pure squeezed + interferometer state after a uniform
|
|
185
|
+
loss channel of transmission ``eta`` (eta=1 is the pure state; eta<1 is mixed)."""
|
|
186
|
+
g = np.random.default_rng(seed)
|
|
187
|
+
r = g.uniform(0.3, 0.6, modes)
|
|
188
|
+
z = (g.standard_normal((modes, modes)) + 1j * g.standard_normal((modes, modes))) / np.sqrt(2.0)
|
|
189
|
+
U, rr = np.linalg.qr(z); ph = np.diagonal(rr).copy(); ph /= np.abs(ph); U = U * ph
|
|
190
|
+
sq = np.block([[np.diag(np.exp(-r)), np.zeros((modes, modes))],
|
|
191
|
+
[np.zeros((modes, modes)), np.diag(np.exp(r))]])
|
|
192
|
+
interf = np.block([[U.real, -U.imag], [U.imag, U.real]])
|
|
193
|
+
S = interf @ sq
|
|
194
|
+
pure = (hbar / 2.0) * S @ S.T
|
|
195
|
+
return eta * pure + (1.0 - eta) * (hbar / 2.0) * np.eye(2 * modes) # loss -> mixed
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _amat(cov: np.ndarray, hbar: float = 2.0) -> np.ndarray:
|
|
199
|
+
"""A-matrix ``X (I - Q^-1)`` of a Gaussian state (the (loop) hafnian's matrix)."""
|
|
200
|
+
m = cov.shape[0] // 2
|
|
201
|
+
Q = _qmat(cov, hbar)
|
|
202
|
+
X = np.block([[np.zeros((m, m)), np.eye(m)], [np.eye(m), np.zeros((m, m))]])
|
|
203
|
+
return X @ (np.eye(2 * m) - np.linalg.inv(Q))
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def loss_hafnian(n: int, seed: int, eta: float = 0.6) -> np.ndarray:
|
|
207
|
+
"""The A-matrix block (n x n complex symmetric) of a lossy/mixed GBS state -- the
|
|
208
|
+
matrix the hafnian consumes in the mixed regime. ``n`` even."""
|
|
209
|
+
A = _amat(_lossy_cov(n, seed, eta))
|
|
210
|
+
B = A[:n, :n]
|
|
211
|
+
return np.ascontiguousarray(0.5 * (B + B.T)).astype(np.complex128) # enforce symmetry
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def loss_loop_hafnian(n: int, seed: int, eta: float = 0.6) -> np.ndarray:
|
|
215
|
+
"""The lossy A-block with displacement-derived loop weights on the diagonal -- the
|
|
216
|
+
loop hafnian's input for a lossy displaced state. ``n`` even."""
|
|
217
|
+
B = loss_hafnian(n, seed, eta).copy()
|
|
218
|
+
g = np.random.default_rng(seed + 3)
|
|
219
|
+
np.fill_diagonal(B, g.uniform(-0.3, 0.3, n) + 1j * g.uniform(-0.3, 0.3, n))
|
|
220
|
+
return B
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def loss_torontonian(n_modes: int, seed: int, eta: float = 0.6) -> np.ndarray:
|
|
224
|
+
"""``O = I - Q^-1`` (2n x 2n, real) of a lossy/mixed Gaussian state -- the threshold-
|
|
225
|
+
detector matrix a real lossy GBS experiment produces."""
|
|
226
|
+
Q = _qmat(_lossy_cov(n_modes, seed, eta))
|
|
227
|
+
O = np.eye(2 * n_modes) - np.linalg.inv(Q)
|
|
228
|
+
return np.ascontiguousarray(np.real(O)).astype(np.complex128) # physical real domain
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def haar_unitary(m: int, seed: int) -> np.ndarray:
|
|
232
|
+
"""Haar-random m x m unitary (QR of a complex Ginibre matrix, phase-fixed)."""
|
|
233
|
+
g = np.random.default_rng(seed)
|
|
234
|
+
z = (g.standard_normal((m, m)) + 1j * g.standard_normal((m, m))) / np.sqrt(2.0)
|
|
235
|
+
q, r = np.linalg.qr(z)
|
|
236
|
+
ph = np.diagonal(r).copy()
|
|
237
|
+
ph /= np.abs(ph)
|
|
238
|
+
return q * ph # columns scaled by the phases of diag(R)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def loss_permanent(n: int, seed: int) -> np.ndarray:
|
|
242
|
+
"""The top-left ``n x n`` block of a Haar-random ``2n x 2n`` unitary -- a *sub-unitary*
|
|
243
|
+
(a contraction, singular values <= 1): the linear-optical map of a LOSSY interferometer,
|
|
244
|
+
the permanent's mixed-state analog (boson sampling with loss). Well-conditioned."""
|
|
245
|
+
U = haar_unitary(2 * n, seed)
|
|
246
|
+
return np.ascontiguousarray(U[:n, :n])
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# --- adversarial (tunable cancellation) inputs, generalized to any dim -------
|
|
250
|
+
# The canonical near-cancelling block (+) a well-conditioned remainder. perm/haf/tor all
|
|
251
|
+
# factorize over a direct sum (perm(A(+)B)=perm A * perm B; same for haf; tor(O1(+)O2)=
|
|
252
|
+
# tor O1 * tor O2), so the small block sets the whole value's cancellation while the
|
|
253
|
+
# remainder fills out the requested size. A MODERATE strength (kappa ~ 1e6) -- enough to
|
|
254
|
+
# show FP64 shedding digits in the achieved-error column, but not so ill-conditioned that
|
|
255
|
+
# the GPU and CPU determinant/linear-algebra routines legitimately diverge (which would
|
|
256
|
+
# false-trip the public-path checksum gate). The accuracy STUDY (bench.accuracy) sweeps the
|
|
257
|
+
# strength all the way to the FP64 cliff; here we want a representative stressed throughput
|
|
258
|
+
# input on which GPU and CPU still agree.
|
|
259
|
+
_ADV_DELTA = 1e-6
|
|
260
|
+
_ADV_TOR_A = 1e-6
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def adversarial_permanent(n: int, seed: int) -> np.ndarray:
|
|
264
|
+
"""``make_cancellation_matrix`` at the benchmark strength (2x2 block (+) remainder)."""
|
|
265
|
+
return make_cancellation_matrix(n, _ADV_DELTA, seed)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def adversarial_hafnian(n: int, seed: int) -> np.ndarray:
|
|
269
|
+
"""4x4 near-cancelling hafnian block (+) a well-conditioned ``(n-4)`` symmetric
|
|
270
|
+
remainder. ``n`` even, ``>= 4``. Generalizes ``cancellation_hafnian`` to any dim."""
|
|
271
|
+
if n < 4 or n % 2:
|
|
272
|
+
raise ValueError("adversarial hafnian needs even n >= 4")
|
|
273
|
+
a = f = b = e = c = 1.0
|
|
274
|
+
d = _ADV_DELTA - 2.0
|
|
275
|
+
A = np.zeros((n, n), dtype=np.complex128)
|
|
276
|
+
A[:4, :4] = np.array([[0, a, b, c], [a, 0, d, e], [b, d, 0, f], [c, e, f, 0]],
|
|
277
|
+
dtype=np.complex128)
|
|
278
|
+
if n > 4:
|
|
279
|
+
g = np.random.default_rng(seed)
|
|
280
|
+
G = g.standard_normal((n - 4, n - 4))
|
|
281
|
+
R = G + G.T
|
|
282
|
+
np.fill_diagonal(R, 0.0) # ordinary hafnian ignores the diagonal
|
|
283
|
+
A[4:, 4:] = R
|
|
284
|
+
return A
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def adversarial_loop_hafnian(n: int, seed: int) -> np.ndarray:
|
|
288
|
+
"""2x2 near-cancelling loop-hafnian block (+) a ``(n-2)`` symmetric remainder (diagonal
|
|
289
|
+
kept = loops). ``n`` even, ``>= 2``. Generalizes ``cancellation_loop_hafnian``."""
|
|
290
|
+
if n < 2 or n % 2:
|
|
291
|
+
raise ValueError("adversarial loop hafnian needs even n >= 2")
|
|
292
|
+
A = np.zeros((n, n), dtype=np.complex128)
|
|
293
|
+
A[:2, :2] = np.array([[1.0, 2.0], [2.0, _ADV_DELTA - 2.0]], dtype=np.complex128)
|
|
294
|
+
if n > 2:
|
|
295
|
+
g = np.random.default_rng(seed)
|
|
296
|
+
G = g.standard_normal((n - 2, n - 2))
|
|
297
|
+
A[2:, 2:] = (G + G.T).astype(np.complex128)
|
|
298
|
+
return A
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def adversarial_torontonian(n_modes: int, seed: int) -> np.ndarray:
|
|
302
|
+
"""One catastrophic-cancellation mode ``diag(a, a)`` (``tor_1 = 1/sqrt(1-a) - 1`` with a
|
|
303
|
+
``(1 + a/2 + ...) - 1`` cancellation as ``a -> 0``) (+) ``n_modes-1`` physical small-norm
|
|
304
|
+
modes. Real, ``I - O_S`` positive definite for every S; ``dim = 2*n_modes``."""
|
|
305
|
+
O = np.zeros((2 * n_modes, 2 * n_modes))
|
|
306
|
+
if n_modes > 1:
|
|
307
|
+
P = np.real(physical_torontonian(n_modes - 1, seed)) # 2(n-1) x 2(n-1)
|
|
308
|
+
idx = list(range(1, n_modes)) + list(range(n_modes + 1, 2 * n_modes))
|
|
309
|
+
O[np.ix_(idx, idx)] = P
|
|
310
|
+
O[0, 0] = _ADV_TOR_A
|
|
311
|
+
O[n_modes, n_modes] = _ADV_TOR_A # mode 0's a / a-dagger entries
|
|
312
|
+
return np.ascontiguousarray(O).astype(np.complex128)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# --- the single shared benchmark workload generator ------------------------
|
|
316
|
+
|
|
317
|
+
_BENCH_FAMILIES = {
|
|
318
|
+
"physical": {"perm": lambda d, s: physical_permanent(d, s),
|
|
319
|
+
"haf": lambda d, s: physical_hafnian(d, s),
|
|
320
|
+
"lhaf": lambda d, s: physical_loop_hafnian(d, s),
|
|
321
|
+
"tor": lambda d, s: physical_torontonian(d // 2, s)},
|
|
322
|
+
"loss": {"perm": lambda d, s: loss_permanent(d, s),
|
|
323
|
+
"haf": lambda d, s: loss_hafnian(d, s),
|
|
324
|
+
"lhaf": lambda d, s: loss_loop_hafnian(d, s),
|
|
325
|
+
"tor": lambda d, s: loss_torontonian(d // 2, s)},
|
|
326
|
+
"adversarial": {"perm": lambda d, s: adversarial_permanent(d, s),
|
|
327
|
+
"haf": lambda d, s: adversarial_hafnian(d, s),
|
|
328
|
+
"lhaf": lambda d, s: adversarial_loop_hafnian(d, s),
|
|
329
|
+
"tor": lambda d, s: adversarial_torontonian(d // 2, s)},
|
|
330
|
+
}
|
|
331
|
+
BENCH_REGIMES = tuple(_BENCH_FAMILIES)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def bench_batch(func: str, dim: int, batch: int, regime: str = "physical",
|
|
335
|
+
seed: int = 0) -> np.ndarray:
|
|
336
|
+
"""A uniform ``(batch, dim, dim)`` complex128 stack for ``func`` in ``regime`` -- THE
|
|
337
|
+
single workload generator shared by the GPU, CPU, and The Walrus benchmarks, so a
|
|
338
|
+
throughput comparison is *same-input* (docs/DESIGN.md §9). ``regime`` in
|
|
339
|
+
``{physical, loss, adversarial}``; for the torontonian ``dim = 2*modes``. Element ``k``
|
|
340
|
+
uses ``seed + k`` so the batch is varied yet reproducible: the SAME
|
|
341
|
+
``(func, dim, regime, seed)`` yields the SAME matrices for every engine."""
|
|
342
|
+
if regime not in _BENCH_FAMILIES:
|
|
343
|
+
raise ValueError(f"unknown regime {regime!r}; expected one of {BENCH_REGIMES}")
|
|
344
|
+
fam = _BENCH_FAMILIES[regime]
|
|
345
|
+
if func not in fam:
|
|
346
|
+
raise ValueError(f"unknown func {func!r}; expected one of {tuple(fam)}")
|
|
347
|
+
gen = fam[func]
|
|
348
|
+
return np.ascontiguousarray(np.stack([gen(dim, seed + k) for k in range(batch)]))
|
bench/_provenance.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Shared provenance for every benchmark artifact (the frozen-experiment record).
|
|
2
|
+
|
|
3
|
+
Anchor §9 / the reproducibility contract: *every* artifact -- throughput, end-to-end,
|
|
4
|
+
accuracy, sampler -- carries the same provenance block so a result can be reproduced
|
|
5
|
+
exactly: the code **commit**, the pinned **container digest**, the GPU, and when it ran.
|
|
6
|
+
|
|
7
|
+
The rented box has no ``.git`` (rsync excludes it) and its image is pinned by digest,
|
|
8
|
+
so both are passed in by the session script as environment variables captured on the
|
|
9
|
+
host / from the container before the run:
|
|
10
|
+
|
|
11
|
+
* ``GBS_COMMIT`` -- ``git rev-parse --short HEAD`` of the uploaded tree.
|
|
12
|
+
* ``GBS_CONTAINER_DIGEST`` -- the image digest the instance was launched from
|
|
13
|
+
(e.g. ``nvidia/cuda:12.4.1-devel-ubuntu22.04@sha256:...``); pins the toolchain.
|
|
14
|
+
|
|
15
|
+
Both fall back to a local probe (git; ``/etc/gbs_container_digest`` if present) and to
|
|
16
|
+
``None`` -- recorded honestly rather than faked.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import platform
|
|
23
|
+
import re
|
|
24
|
+
import subprocess
|
|
25
|
+
from datetime import datetime, timezone
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _gpu_info() -> dict[str, Any] | None:
|
|
30
|
+
"""GPU model / compute capability / driver / memory via nvidia-smi (None off-device)."""
|
|
31
|
+
try:
|
|
32
|
+
out = subprocess.run(
|
|
33
|
+
["nvidia-smi", "--query-gpu=name,compute_cap,driver_version,memory.total",
|
|
34
|
+
"--format=csv,noheader"], capture_output=True, text=True, timeout=10).stdout.strip()
|
|
35
|
+
if out:
|
|
36
|
+
name, cc, drv, mem = (x.strip() for x in out.splitlines()[0].split(","))
|
|
37
|
+
return {"name": name, "compute_cap": cc, "driver": drv, "memory_total": mem}
|
|
38
|
+
except Exception:
|
|
39
|
+
pass
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _cuda_version() -> str | None:
|
|
44
|
+
try:
|
|
45
|
+
out = subprocess.run(["nvcc", "--version"], capture_output=True, text=True, timeout=10).stdout
|
|
46
|
+
m = re.search(r"release (\d+\.\d+)", out)
|
|
47
|
+
return m.group(1) if m else None
|
|
48
|
+
except Exception:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def environment() -> dict[str, Any]:
|
|
53
|
+
"""The machine the artifact was produced on, so a throughput/accuracy number is
|
|
54
|
+
self-describing: the GPU (model/driver/CUDA), the CPU + logical cores, and the BLAS/
|
|
55
|
+
OpenMP thread caps (which materially affect the CPU-side timings; see the OpenBLAS
|
|
56
|
+
many-core-host segfault mitigation in gpu_session.sh)."""
|
|
57
|
+
return {
|
|
58
|
+
"gpu": _gpu_info(),
|
|
59
|
+
"cuda": _cuda_version(),
|
|
60
|
+
"cpu": {"processor": platform.processor() or None,
|
|
61
|
+
"machine": platform.machine(), "logical_cores": os.cpu_count()},
|
|
62
|
+
"blas_threads": {v: os.environ.get(v) for v in
|
|
63
|
+
("OPENBLAS_NUM_THREADS", "OMP_NUM_THREADS", "MKL_NUM_THREADS", "GOTO_NUM_THREADS")},
|
|
64
|
+
"platform": platform.platform(),
|
|
65
|
+
"python": platform.python_version(),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def commit() -> str | None:
|
|
70
|
+
env = os.environ.get("GBS_COMMIT", "").strip()
|
|
71
|
+
if env:
|
|
72
|
+
return env
|
|
73
|
+
try:
|
|
74
|
+
return subprocess.run(["git", "rev-parse", "--short", "HEAD"],
|
|
75
|
+
capture_output=True, text=True).stdout.strip() or None
|
|
76
|
+
except Exception:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def container_digest() -> str | None:
|
|
81
|
+
"""The pinned container image digest the run was launched from (or None)."""
|
|
82
|
+
env = os.environ.get("GBS_CONTAINER_DIGEST", "").strip()
|
|
83
|
+
if env:
|
|
84
|
+
return env
|
|
85
|
+
try: # a session may drop the digest here on the box
|
|
86
|
+
p = "/etc/gbs_container_digest"
|
|
87
|
+
if os.path.exists(p):
|
|
88
|
+
with open(p) as f:
|
|
89
|
+
return f.read().strip() or None
|
|
90
|
+
except Exception:
|
|
91
|
+
pass
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def provenance() -> dict[str, Any]:
|
|
96
|
+
"""The common provenance block embedded in every artifact (commit + container digest +
|
|
97
|
+
machine environment), so a result reproduces from the file alone (docs/DESIGN.md §9)."""
|
|
98
|
+
return {
|
|
99
|
+
"commit": commit(),
|
|
100
|
+
"container_digest": container_digest(),
|
|
101
|
+
"hostname": platform.node() or None,
|
|
102
|
+
"captured_utc": datetime.now(timezone.utc).isoformat(),
|
|
103
|
+
"environment": environment(),
|
|
104
|
+
}
|
bench/accuracy.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Accuracy characterization for ALL FOUR functions (the measured boundary, §6).
|
|
2
|
+
|
|
3
|
+
For each of permanent / hafnian / loop hafnian / torontonian, sweep a tunable
|
|
4
|
+
cancellation family and record, against the independent ``mpmath`` reference:
|
|
5
|
+
|
|
6
|
+
* ``rel_err_fp64`` -- the native double-precision relative error (the boundary:
|
|
7
|
+
it grows as the result cancels);
|
|
8
|
+
* ``rel_err_dd`` -- the double-double relative error, when the compiled GPU
|
|
9
|
+
extension is available (the *real* DD kernels, host-emulated through the
|
|
10
|
+
host-shim build or run on a GPU). DD arithmetic is **internal**: the kernels
|
|
11
|
+
collapse the double-double result back to ``complex128`` on output, so this
|
|
12
|
+
measures DD's ability to recover a correct FP64 answer after cancellation, not
|
|
13
|
+
31-digit output.
|
|
14
|
+
|
|
15
|
+
Writes a self-describing JSON artifact to ``results/accuracy/`` (append-only).
|
|
16
|
+
This generalizes ``accuracy_permanent.py`` (which keeps the detailed
|
|
17
|
+
condition-number sweep for the permanent) to the whole library.
|
|
18
|
+
|
|
19
|
+
uv run python -m bench.accuracy --dps 60
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import json
|
|
26
|
+
import platform
|
|
27
|
+
from datetime import datetime, timezone
|
|
28
|
+
from importlib.metadata import version
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Any, Callable
|
|
31
|
+
|
|
32
|
+
import mpmath
|
|
33
|
+
import numpy as np
|
|
34
|
+
|
|
35
|
+
import gbskernels
|
|
36
|
+
import highprec_ref
|
|
37
|
+
from bench import _inputs, _provenance
|
|
38
|
+
|
|
39
|
+
DEFAULT_OUT = Path(__file__).resolve().parent.parent / "results" / "accuracy"
|
|
40
|
+
|
|
41
|
+
# mpmath ground-truth reference per function.
|
|
42
|
+
_REF = {
|
|
43
|
+
"perm": highprec_ref.permanent_mp, "haf": highprec_ref.hafnian_mp,
|
|
44
|
+
"lhaf": highprec_ref.loop_hafnian_mp, "tor": highprec_ref.torontonian_mp,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# ADVERSARIAL: a tunable cancellation family (knob -> matrix) + the knob grid.
|
|
48
|
+
_ADVERSARIAL: dict[str, tuple[Callable[[float], np.ndarray], list[float]]] = {
|
|
49
|
+
"perm": (lambda d: _inputs.make_cancellation_matrix(6, d, seed=1),
|
|
50
|
+
[1e-1, 1e-2, 1e-4, 1e-6, 1e-8, 1e-10, 1e-12, 1e-14]),
|
|
51
|
+
"haf": (lambda d: _inputs.cancellation_hafnian(d, seed=1), [1e-2, 1e-4, 1e-6, 1e-8, 1e-10]),
|
|
52
|
+
"lhaf": (lambda d: _inputs.cancellation_loop_hafnian(d, seed=1), [1e-2, 1e-4, 1e-6, 1e-8, 1e-10]),
|
|
53
|
+
"tor": (lambda d: _inputs.cancellation_torontonian(d), [1e-2, 1e-4, 1e-6, 1e-8, 1e-10]),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# PHYSICAL: realistic well-conditioned inputs (size -> matrix) + the size grid
|
|
57
|
+
# (capped so the DD kernels and the mpmath reference stay feasible).
|
|
58
|
+
_PHYSICAL: dict[str, tuple[Callable[[int, int], np.ndarray], list[int]]] = {
|
|
59
|
+
"perm": (_inputs.physical_permanent, [4, 8, 12]),
|
|
60
|
+
"haf": (_inputs.physical_hafnian, [4, 8, 12]),
|
|
61
|
+
"lhaf": (_inputs.physical_loop_hafnian, [4, 8, 10]),
|
|
62
|
+
"tor": (_inputs.physical_torontonian, [2, 4, 6]), # n = modes (matrix 2n)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# LOSS / mixed-state regime: matrices from a lossy (mixed) Gaussian state -- the third
|
|
66
|
+
# realistic regime (the permanent is a unitary boson-sampling amplitude, with no
|
|
67
|
+
# Gaussian loss analog, so it has no loss section).
|
|
68
|
+
_LOSS: dict[str, tuple[Callable[[int, int], np.ndarray], list[int]]] = {
|
|
69
|
+
"haf": (_inputs.loss_hafnian, [4, 8]),
|
|
70
|
+
"lhaf": (_inputs.loss_loop_hafnian, [4, 8]),
|
|
71
|
+
"tor": (_inputs.loss_torontonian, [2, 4]), # n = modes (matrix 2n)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _rel_err_mp(approx: complex, exact_mp: Any, dps: int) -> float:
|
|
76
|
+
with mpmath.workdps(dps):
|
|
77
|
+
a = mpmath.mpc(complex(approx).real, complex(approx).imag)
|
|
78
|
+
return float(abs(a - exact_mp) / abs(exact_mp))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _errors(func: str, A: np.ndarray, dps: int, have_dd: bool) -> dict[str, float]:
|
|
82
|
+
"""FP64 and (when the GPU backend is present) DD relative error vs mpmath.
|
|
83
|
+
|
|
84
|
+
FP64 is evaluated on the SAME backend as DD when the extension is present, so
|
|
85
|
+
the artifact records the FP64<->DD crossover on the device (not a CPU FP64 vs
|
|
86
|
+
GPU DD comparison). With no extension, FP64 falls back to the CPU reference."""
|
|
87
|
+
backend = "gpu" if have_dd else "cpu"
|
|
88
|
+
fp = getattr(gbskernels, func)(A, backend=backend)
|
|
89
|
+
with mpmath.workdps(dps):
|
|
90
|
+
exact = _REF[func](A, dps=dps)
|
|
91
|
+
out = {"rel_err_fp64": _rel_err_mp(fp, exact, dps)}
|
|
92
|
+
if have_dd:
|
|
93
|
+
dd = getattr(gbskernels, func)(A, precision="dd", backend="gpu")
|
|
94
|
+
out["rel_err_dd"] = _rel_err_mp(dd, exact, dps)
|
|
95
|
+
return out
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def run(dps: int = 60, out_dir: Path | None = None) -> tuple[dict[str, Any], Path]:
|
|
99
|
+
have_dd = gbskernels.gpu_available()
|
|
100
|
+
dps = max(dps, 80)
|
|
101
|
+
sweeps: dict[str, Any] = {}
|
|
102
|
+
for func in _REF:
|
|
103
|
+
family, deltas = _ADVERSARIAL[func]
|
|
104
|
+
adversarial = [dict(delta=d, **_errors(func, family(d), dps, have_dd)) for d in deltas]
|
|
105
|
+
pfamily, sizes = _PHYSICAL[func]
|
|
106
|
+
physical = [dict(n=n, **_errors(func, pfamily(n, seed=1), dps, have_dd)) for n in sizes]
|
|
107
|
+
sweeps[func] = {"adversarial": adversarial, "physical": physical}
|
|
108
|
+
if func in _LOSS: # third regime: lossy / mixed-state inputs (Gaussian only)
|
|
109
|
+
lfamily, lsizes = _LOSS[func]
|
|
110
|
+
sweeps[func]["loss"] = [dict(n=n, **_errors(func, lfamily(n, seed=1), dps, have_dd))
|
|
111
|
+
for n in lsizes]
|
|
112
|
+
|
|
113
|
+
# Whether the DD numbers were produced on a real GPU or host-emulated (the
|
|
114
|
+
# same kernel source either way; the distinction matters for the "GPU-vs-mpmath"
|
|
115
|
+
# claim, so it is recorded honestly). The signal is the extension's own
|
|
116
|
+
# compile-time build flag -- NOT the host OS -- so a no-GPU CI runner is
|
|
117
|
+
# correctly "host-shim", never mislabelled "gpu".
|
|
118
|
+
dd_device = gbskernels.gpu_backend_kind()
|
|
119
|
+
# FP64 was measured on the same backend as DD when the extension is present
|
|
120
|
+
# (so the FP64<->DD crossover is established on-device), else the CPU reference.
|
|
121
|
+
fp64_device = dd_device if have_dd else "cpu"
|
|
122
|
+
|
|
123
|
+
artifact = {
|
|
124
|
+
"kind": "accuracy_all_functions",
|
|
125
|
+
"created_utc": datetime.now(timezone.utc).isoformat(),
|
|
126
|
+
**_provenance.provenance(), # commit + container_digest + hostname (every artifact)
|
|
127
|
+
"dd_measured": have_dd,
|
|
128
|
+
"fp64_backend": fp64_device, # where the FP64 numbers came from
|
|
129
|
+
"dd_backend": dd_device, # "gpu" (real device) | "host-shim" (CPU emulation)
|
|
130
|
+
"note": ("Work-precision study for all four functions across THREE input regimes vs an "
|
|
131
|
+
"independent mpmath reference: PHYSICAL (pure, well-conditioned), ADVERSARIAL "
|
|
132
|
+
"(tunable cancellation), and LOSS (lossy/mixed-state matrices; Gaussian "
|
|
133
|
+
"functions only). DD is an INTERNAL precision tier: kernels collapse the "
|
|
134
|
+
"double-double result to complex128 on output, recovering a correct FP64 "
|
|
135
|
+
"answer under cancellation (not exposing 31-digit results)."),
|
|
136
|
+
"env": {"python": platform.python_version(), "numpy": version("numpy"),
|
|
137
|
+
"mpmath": version("mpmath"), "platform": platform.platform()},
|
|
138
|
+
"params": {"dps": dps, "dtype": "complex128",
|
|
139
|
+
"sections": "physical (size sweep) + adversarial (cancellation sweep) "
|
|
140
|
+
"+ loss (mixed-state, haf/lhaf/tor)"},
|
|
141
|
+
"sweeps": sweeps,
|
|
142
|
+
}
|
|
143
|
+
out_dir = Path(out_dir) if out_dir is not None else DEFAULT_OUT
|
|
144
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
145
|
+
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
146
|
+
path = out_dir / f"accuracy_all_{stamp}.json"
|
|
147
|
+
path.write_text(json.dumps(artifact, indent=2))
|
|
148
|
+
return artifact, path
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _print(artifact: dict[str, Any], path: Path) -> None:
|
|
152
|
+
dd = artifact["dd_measured"]
|
|
153
|
+
print(f"# accuracy: all four functions, physical + adversarial -> {path}")
|
|
154
|
+
print(f"# DD measured: {dd} (backend: {artifact.get('dd_backend')}) | the work-precision study, §6")
|
|
155
|
+
for func, sec in artifact["sweeps"].items():
|
|
156
|
+
print(f"\n {func} -- physical (well-conditioned): {'n':>3} {'rel_err_fp64':>13}" +
|
|
157
|
+
(" rel_err_dd" if dd else ""))
|
|
158
|
+
for r in sec["physical"]:
|
|
159
|
+
line = f" {r['n']:>3} {r['rel_err_fp64']:>13.2e}"
|
|
160
|
+
if "rel_err_dd" in r:
|
|
161
|
+
line += f" {r['rel_err_dd']:>10.2e}"
|
|
162
|
+
print(line)
|
|
163
|
+
print(f" {func} -- adversarial (cancellation): {'delta':>8} {'rel_err_fp64':>13}" +
|
|
164
|
+
(" rel_err_dd" if dd else ""))
|
|
165
|
+
for r in sec["adversarial"]:
|
|
166
|
+
line = f" {r['delta']:>8.0e} {r['rel_err_fp64']:>13.2e}"
|
|
167
|
+
if "rel_err_dd" in r:
|
|
168
|
+
line += f" {r['rel_err_dd']:>10.2e}"
|
|
169
|
+
print(line)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def main() -> None:
|
|
173
|
+
p = argparse.ArgumentParser(description=__doc__)
|
|
174
|
+
p.add_argument("--dps", type=int, default=60)
|
|
175
|
+
p.add_argument("--out", type=Path, default=None)
|
|
176
|
+
args = p.parse_args()
|
|
177
|
+
artifact, path = run(args.dps, args.out)
|
|
178
|
+
_print(artifact, path)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
if __name__ == "__main__":
|
|
182
|
+
main()
|