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