1/**
2 * A run's final state, in a form two different machines can be compared on.
3 *
4 * The pipeline is deterministic given (model source, parameters, lmax, seed,
5 * steps): the perturbation comes from a seeded PRNG, and everything after it is
6 * fixed arithmetic. So the same spec run anywhere should land on the same state —
7 * not bit for bit, since GPUs differ in fused-multiply-add and other latitude the
8 * fp32 rules allow, but far closer than any real difference in what is being
9 * computed would be.
10 *
11 * That makes a cross-environment comparison a genuine check that the browser and
12 * the desktop are running the same computation, rather than something that merely
13 * looks similar.
14 */
16export interface StateDigest {
17 /** Element count, so a shape mismatch is caught before the values are read. */
18 n: number;
19 min: number;
20 max: number;
21 mean: number;
22 /** Root mean square — sensitive to every element, unlike min/max. */
23 rms: number;
24 /** Which Fourier stage the transform plan chose. FFT and DFT are different
25 * algorithms and round differently, so a mismatch here explains a difference
26 * in the values rather than being a symptom of one. */
27 fourier: 'fft' | 'dft';
28 /** Informational: the GPU the numbers came from. */
29 adapter: string;
30}
32export function digestOf(
33 values: ArrayLike<number>,
34 fourier: 'fft' | 'dft',
35 adapter: string,
36): StateDigest {
37 let min = Infinity;
38 let max = -Infinity;
39 let sum = 0;
40 let sumsq = 0;
41 for (let i = 0; i < values.length; i++) {
42 const v = values[i];
43 if (v < min) min = v;
44 if (v > max) max = v;
45 sum += v;
46 sumsq += v * v;
47 }
48 const n = values.length;
49 return {
50 n,
51 min,
52 max,
53 mean: sum / n,
54 rms: Math.sqrt(sumsq / n),
55 fourier,
56 adapter,
57 };
58}
60/** Relative L2 difference of two states of equal length. */
61export function relL2(a: ArrayLike<number>, b: ArrayLike<number>): number {
62 let num = 0;
63 let den = 0;
64 for (let i = 0; i < a.length; i++) {
65 const d = a[i] - b[i];
66 num += d * d;
67 den += b[i] * b[i];
68 }
69 return Math.sqrt(num / Math.max(den, 1e-300));
70}
72/** Relative L-infinity (max-norm) difference of two states of equal length. */
73export function relLinf(a: ArrayLike<number>, b: ArrayLike<number>): number {
74 let num = 0;
75 let den = 0;
76 for (let i = 0; i < a.length; i++) {
77 const d = Math.abs(a[i] - b[i]);
78 if (d > num) num = d;
79 const bd = Math.abs(b[i]);
80 if (bd > den) den = bd;
81 }
82 return num / Math.max(den, 1e-300);
83}
85export function formatDigest(d: StateDigest): string {
86 const g = (v: number): string => v.toPrecision(9);
87 return (
88 `n=${d.n} min=${g(d.min)} max=${g(d.max)} mean=${g(d.mean)} rms=${g(d.rms)} ` +
89 `fourier=${d.fourier}`
90 );
91}
93/** Worst relative disagreement between two digests' scalar summaries. */
94export function digestDrift(a: StateDigest, b: StateDigest): number {
95 const rel = (x: number, y: number): number =>
96 Math.abs(x - y) / Math.max(Math.abs(x), Math.abs(y), 1e-30);
97 return Math.max(
98 rel(a.min, b.min),
99 rel(a.max, b.max),
100 rel(a.mean, b.mean),
101 rel(a.rms, b.rms),
102 );
103}