/ concept-collection / turing-sphere
Sign in
concept-collection / turing-sphere
156 lines · 5.6 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.mjs ...` 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. It names the
37 * .mjs wrapper rather than bench.ts, so that it also runs on a Node that
38 * needs to be told to strip types (see scripts/bench.mjs). */
39export const BENCH_COMMAND = 'node scripts/bench.mjs';
40export const DEFAULT_LMAX = 63;
41export const DEFAULT_SEED = 1;
42/** Long enough that clock ramp-up and the occasional scheduling hiccup wash
43 * out: ~10 s of GPU stepping at lmax 63. */
44export const DEFAULT_STEPS = 2000;
45export const DEFAULT_WARMUP = 100;
46export const DEFAULT_BACKEND: BackendKind = 'webgpu';
48/** Model + starting parameters of a preset, for the app's dropdown and the
49 * benchmark's --preset flag. */
50export function resolvePreset(key: string): {
51 preset: Preset;
52 model: ModelSpec;
53 params: Params;
54} {
55 const preset = presets.find((p) => p.key === key);
56 if (!preset) {
57 throw new Error(
58 `unknown preset '${key}' (have: ${presets.map((p) => p.key).join(', ')})`,
59 );
60 }
61 const model = models.find((m) => m.key === preset.modelKey);
62 if (!model) throw new Error(`preset '${key}' names unknown model '${preset.modelKey}'`);
63 return { preset, model, params: { ...defaultParams(model), ...preset.params } };
66export function modelForSpec(spec: RunSpec): ModelSpec {
67 return resolvePreset(spec.preset).model;
70/** Transform configuration implied by the spec (same rule as the app). */
71export function configForSpec(spec: RunSpec): ShtConfig {
72 const { nlat, nphi } = gridForLmax(spec.lmax, modelForSpec(spec).pdeg);
73 return { lmax: spec.lmax, mmax: spec.lmax, nlat, nphi };
76/** The command line that reproduces this run. Every knob the app exposes is
77 * written out explicitly, so the command stays valid if a preset changes. */
78export function formatCommand(spec: RunSpec): string {
79 const model = modelForSpec(spec);
80 const parts = [
81 BENCH_COMMAND,
82 `--preset ${spec.preset}`,
83 `--lmax ${spec.lmax}`,
84 `--backend ${spec.backend}`,
85 `--steps ${spec.steps}`,
86 `--seed ${spec.seed}`,
87 ...model.params.map((p) => `--${p.key} ${String(spec.params[p.key])}`),
88 ];
89 if (spec.warmup !== DEFAULT_WARMUP) parts.push(`--warmup ${spec.warmup}`);
90 return parts.join(' ');
93/** Inverse of formatCommand: `--key value` or `--key=value`, in any order.
94 * Throws with a usable message on anything it does not recognize. */
95export function parseArgs(argv: string[]): RunSpec {
96 const flags = new Map<string, string>();
97 for (let i = 0; i < argv.length; i++) {
98 const arg = argv[i];
99 if (!arg.startsWith('--')) throw new Error(`unexpected argument '${arg}'`);
100 const eq = arg.indexOf('=');
101 const key = eq >= 0 ? arg.slice(2, eq) : arg.slice(2);
102 const value = eq >= 0 ? arg.slice(eq + 1) : argv[++i];
103 if (value === undefined) throw new Error(`--${key} needs a value`);
104 if (!key) throw new Error(`bad option '${arg}'`);
105 flags.set(key, value);
106 }
107 const take = (key: string): string | undefined => {
108 const v = flags.get(key);
109 flags.delete(key);
110 return v;
111 };
112 const number = (key: string, dflt: number): number => {
113 const raw = take(key);
114 if (raw === undefined) return dflt;
115 const v = Number(raw);
116 if (!Number.isFinite(v)) throw new Error(`--${key} must be a number (got '${raw}')`);
117 return v;
118 };
119 const count = (key: string, dflt: number, min: number): number => {
120 const v = number(key, dflt);
121 if (!Number.isInteger(v) || v < min) {
122 throw new Error(`--${key} must be an integer >= ${min} (got '${v}')`);
123 }
124 return v;
125 };
127 const presetKey = take('preset') ?? presets[0].key;
128 const { model, params } = resolvePreset(presetKey);
129 const backend = take('backend') ?? DEFAULT_BACKEND;
130 if (backend !== 'webgpu' && backend !== 'cpu') {
131 throw new Error(`--backend must be 'webgpu' or 'cpu' (got '${backend}')`);
132 }
133 const spec: RunSpec = {
134 preset: presetKey,
135 lmax: count('lmax', DEFAULT_LMAX, 1),
136 backend,
137 seed: number('seed', DEFAULT_SEED),
138 steps: count('steps', DEFAULT_STEPS, 1),
139 warmup: count('warmup', DEFAULT_WARMUP, 0),
140 params,
141 };
142 for (const p of model.params) {
143 const raw = take(p.key);
144 if (raw === undefined) continue;
145 const v = Number(raw);
146 if (!Number.isFinite(v)) throw new Error(`--${p.key} must be a number (got '${raw}')`);
147 params[p.key] = v;
148 }
149 if (flags.size) {
150 throw new Error(
151 `unknown option(s): ${[...flags.keys()].map((k) => `--${k}`).join(', ')}\n` +
152 `parameters of ${model.label}: ${model.params.map((p) => `--${p.key}`).join(' ')}`,
153 );
154 }
155 return spec;
moveopenescclose