concept-collection / turing-sphere-2
turing-sphere-2 / src / bench / runSpec.ts
146 lines · 5.3 KBBlameHistoryRaw
1/**
2 * One run, described in a single object shared by the browser app and the
3 * command-line benchmark. The app formats the run it is currently showing into a
4 * `npm run bench` command; the benchmark parses that command back into the same
5 * object and compiles the same .m with it. Neither side keeps its own copy of
6 * the defaults, so the two runs cannot drift apart — and both execute the same
7 * MATLAB through the same pipeline, so the comparison is like for like.
8 */
9import {
10 mModels,
11 presets,
12 defaultParams,
13 type MModel,
14 type Params,
15 type Preset,
16} from '../mgpu/registry.ts';
17import { gridForLmax, type ShtConfig } from '../sht/layout.ts';
19export interface RunSpec {
20 /** Preset key from the registry; fixes the model, params may still be edited. */
21 preset: string;
22 lmax: number;
23 /** Seed of the initial noise. */
24 seed: number;
25 /** Timed steps (the app runs forever; the benchmark stops here). */
26 steps: number;
27 /** Untimed steps run first, so shader/pipeline warm-up is not measured. */
28 warmup: number;
29 /** Full parameter set of the preset's model, as edited. */
30 params: Params;
33/** The command the app displays and the benchmark answers to. Goes through npm
34 * because the benchmark runs under vite-node, which is what resolves numbl's
35 * compiler sources and the `?raw` model imports. */
36export const BENCH_COMMAND = 'npm run bench --';
37export const DEFAULT_LMAX = 63;
38export const DEFAULT_SEED = 1;
39/** Long enough that clock ramp-up and the occasional scheduling hiccup wash
40 * out: ~10 s of GPU stepping at lmax 63. */
41export const DEFAULT_STEPS = 2000;
42export const DEFAULT_WARMUP = 100;
44/** Model + starting parameters of a preset, for the app's dropdown and the
45 * benchmark's --preset flag. */
46export function resolvePreset(key: string): {
47 preset: Preset;
48 model: MModel;
49 params: Params;
50} {
51 const preset = presets.find((p) => p.key === key);
52 if (!preset) {
53 throw new Error(
54 `unknown preset '${key}' (have: ${presets.map((p) => p.key).join(', ')})`,
55 );
56 }
57 const model = mModels.find((m) => m.key === preset.modelKey);
58 if (!model) throw new Error(`preset '${key}' names unknown model '${preset.modelKey}'`);
59 return { preset, model, params: { ...defaultParams(model), ...preset.params } };
62export function modelForSpec(spec: RunSpec): MModel {
63 return resolvePreset(spec.preset).model;
66/** Transform configuration implied by the spec (same rule as the app). */
67export function configForSpec(spec: RunSpec): ShtConfig {
68 const { nlat, nphi } = gridForLmax(spec.lmax, modelForSpec(spec).pdeg);
69 return { lmax: spec.lmax, mmax: spec.lmax, nlat, nphi };
72/** The command line that reproduces this run. Every knob the app exposes is
73 * written out explicitly, so the command stays valid if a preset changes. */
74export function formatCommand(spec: RunSpec): string {
75 const model = modelForSpec(spec);
76 const parts = [
77 BENCH_COMMAND,
78 `--preset ${spec.preset}`,
79 `--lmax ${spec.lmax}`,
80 `--steps ${spec.steps}`,
81 `--seed ${spec.seed}`,
82 ...model.params.map((p) => `--${p.key} ${String(spec.params[p.key])}`),
83 ];
84 if (spec.warmup !== DEFAULT_WARMUP) parts.push(`--warmup ${spec.warmup}`);
85 return parts.join(' ');
88/** Inverse of formatCommand: `--key value` or `--key=value`, in any order.
89 * Throws with a usable message on anything it does not recognize. */
90export function parseArgs(argv: string[]): RunSpec {
91 const flags = new Map<string, string>();
92 for (let i = 0; i < argv.length; i++) {
93 const arg = argv[i];
94 if (!arg.startsWith('--')) throw new Error(`unexpected argument '${arg}'`);
95 const eq = arg.indexOf('=');
96 const key = eq >= 0 ? arg.slice(2, eq) : arg.slice(2);
97 const value = eq >= 0 ? arg.slice(eq + 1) : argv[++i];
98 if (value === undefined) throw new Error(`--${key} needs a value`);
99 if (!key) throw new Error(`bad option '${arg}'`);
100 flags.set(key, value);
101 }
102 const take = (key: string): string | undefined => {
103 const v = flags.get(key);
104 flags.delete(key);
105 return v;
106 };
107 const number = (key: string, dflt: number): number => {
108 const raw = take(key);
109 if (raw === undefined) return dflt;
110 const v = Number(raw);
111 if (!Number.isFinite(v)) throw new Error(`--${key} must be a number (got '${raw}')`);
112 return v;
113 };
114 const count = (key: string, dflt: number, min: number): number => {
115 const v = number(key, dflt);
116 if (!Number.isInteger(v) || v < min) {
117 throw new Error(`--${key} must be an integer >= ${min} (got '${v}')`);
118 }
119 return v;
120 };
122 const presetKey = take('preset') ?? presets[0].key;
123 const { model, params } = resolvePreset(presetKey);
124 const spec: RunSpec = {
125 preset: presetKey,
126 lmax: count('lmax', DEFAULT_LMAX, 1),
127 seed: number('seed', DEFAULT_SEED),
128 steps: count('steps', DEFAULT_STEPS, 1),
129 warmup: count('warmup', DEFAULT_WARMUP, 0),
130 params,
131 };
132 for (const p of model.params) {
133 const raw = take(p.key);
134 if (raw === undefined) continue;
135 const v = Number(raw);
136 if (!Number.isFinite(v)) throw new Error(`--${p.key} must be a number (got '${raw}')`);
137 params[p.key] = v;
138 }
139 if (flags.size) {
140 throw new Error(
141 `unknown option(s): ${[...flags.keys()].map((k) => `--${k}`).join(', ')}\n` +
142 `parameters of ${model.label}: ${model.params.map((p) => `--${p.key}`).join(' ')}`,
143 );
144 }
145 return spec;