/ concept-collection / turing-surface
concept-collection / turing-surface
206 lines · 7.6 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 {
18 mGeometries,
19 mGeometryByKey,
20 defaultGeometryParams,
21 DEFAULT_GEOMETRY_KEY,
22 type MGeometry,
23} from '../geom/registry.ts';
24import { gridForLmax, type ShtConfig } from '../sht/layout.ts';
25import { DEFAULT_SOLVER, solverKeys, type SolverKey } from '../mgpu/libs.ts';
27export interface RunSpec {
28 /** Preset key from the registry; fixes the model, params may still be edited. */
29 preset: string;
30 lmax: number;
31 /** Seed of the initial noise. */
32 seed: number;
33 /** Timed steps (the app runs forever; the benchmark stops here). */
34 steps: number;
35 /** Untimed steps run first, so shader/pipeline warm-up is not measured. */
36 warmup: number;
37 /** Full parameter set of the preset's model, as edited. */
38 params: Params;
39 /** Geometry key from the geometry registry. */
40 geometry: string;
41 /** Full parameter set of that geometry, as edited. Written on the command
42 * line with a `g` prefix (`--gwaist`) so a shape parameter can never
43 * collide with a model one. */
44 geometryParams: Params;
45 /** Iterations of the .m's implicit solve. Structural: it is unrolled into
46 * the compiled step, so it belongs to the spec rather than to the params. */
47 niter: number;
48 /** Which solver answers the models' solve(...) call. Structural like
49 * niter: the choice compiles into the step. */
50 solver: SolverKey;
53export const DEFAULT_NITER = 1;
55/** Geometry + starting parameters of a geometry key. */
56export function resolveGeometry(key: string): { geometry: MGeometry; params: Params } {
57 const geometry = mGeometryByKey(key);
58 if (!geometry) {
59 throw new Error(
60 `unknown geometry '${key}' (have: ${mGeometries.map((g) => g.key).join(', ')})`,
61 );
62 }
63 return { geometry, params: defaultGeometryParams(geometry) };
66export function geometryForSpec(spec: RunSpec): MGeometry {
67 return resolveGeometry(spec.geometry).geometry;
70/** The command the app displays and the benchmark answers to. Goes through npm
71 * because the benchmark runs under vite-node, which is what resolves numbl's
72 * compiler sources and the `?raw` model imports. */
73export const BENCH_COMMAND = 'npm run bench --';
74export const DEFAULT_LMAX = 63;
75export const DEFAULT_SEED = 1;
76/** Long enough that clock ramp-up and the occasional scheduling hiccup wash
77 * out: ~10 s of GPU stepping at lmax 63. */
78export const DEFAULT_STEPS = 2000;
79export const DEFAULT_WARMUP = 100;
81/** Model + starting parameters of a preset, for the app's dropdown and the
82 * benchmark's --preset flag. */
83export function resolvePreset(key: string): {
84 preset: Preset;
85 model: MModel;
86 params: Params;
87} {
88 const preset = presets.find((p) => p.key === key);
89 if (!preset) {
90 throw new Error(
91 `unknown preset '${key}' (have: ${presets.map((p) => p.key).join(', ')})`,
92 );
93 }
94 const model = mModels.find((m) => m.key === preset.modelKey);
95 if (!model) throw new Error(`preset '${key}' names unknown model '${preset.modelKey}'`);
96 return { preset, model, params: { ...defaultParams(model), ...preset.params } };
99export function modelForSpec(spec: RunSpec): MModel {
100 return resolvePreset(spec.preset).model;
103/** Transform configuration implied by the spec (same rule as the app). */
104export function configForSpec(spec: RunSpec): ShtConfig {
105 const { nlat, nphi } = gridForLmax(spec.lmax, modelForSpec(spec).pdeg);
106 return { lmax: spec.lmax, mmax: spec.lmax, nlat, nphi };
109/** The command line that reproduces this run. Every knob the app exposes is
110 * written out explicitly, so the command stays valid if a preset changes. */
111export function formatCommand(spec: RunSpec): string {
112 const model = modelForSpec(spec);
113 const geometry = geometryForSpec(spec);
114 const parts = [
115 BENCH_COMMAND,
116 `--preset ${spec.preset}`,
117 `--geometry ${spec.geometry}`,
118 `--lmax ${spec.lmax}`,
119 `--niter ${spec.niter}`,
120 `--solver ${spec.solver}`,
121 `--steps ${spec.steps}`,
122 `--seed ${spec.seed}`,
123 ...model.params.map((p) => `--${p.key} ${String(spec.params[p.key])}`),
124 ...geometry.params.map((p) => `--g${p.key} ${String(spec.geometryParams[p.key])}`),
125 ];
126 if (spec.warmup !== DEFAULT_WARMUP) parts.push(`--warmup ${spec.warmup}`);
127 return parts.join(' ');
130/** Inverse of formatCommand: `--key value` or `--key=value`, in any order.
131 * Throws with a usable message on anything it does not recognize. */
132export function parseArgs(argv: string[]): RunSpec {
133 const flags = new Map<string, string>();
134 for (let i = 0; i < argv.length; i++) {
135 const arg = argv[i];
136 if (!arg.startsWith('--')) throw new Error(`unexpected argument '${arg}'`);
137 const eq = arg.indexOf('=');
138 const key = eq >= 0 ? arg.slice(2, eq) : arg.slice(2);
139 const value = eq >= 0 ? arg.slice(eq + 1) : argv[++i];
140 if (value === undefined) throw new Error(`--${key} needs a value`);
141 if (!key) throw new Error(`bad option '${arg}'`);
142 flags.set(key, value);
143 }
144 const take = (key: string): string | undefined => {
145 const v = flags.get(key);
146 flags.delete(key);
147 return v;
148 };
149 const number = (key: string, dflt: number): number => {
150 const raw = take(key);
151 if (raw === undefined) return dflt;
152 const v = Number(raw);
153 if (!Number.isFinite(v)) throw new Error(`--${key} must be a number (got '${raw}')`);
154 return v;
155 };
156 const count = (key: string, dflt: number, min: number): number => {
157 const v = number(key, dflt);
158 if (!Number.isInteger(v) || v < min) {
159 throw new Error(`--${key} must be an integer >= ${min} (got '${v}')`);
160 }
161 return v;
162 };
164 const presetKey = take('preset') ?? presets[0].key;
165 const { model, params } = resolvePreset(presetKey);
166 const geometryKey = take('geometry') ?? DEFAULT_GEOMETRY_KEY;
167 const { geometry, params: geometryParams } = resolveGeometry(geometryKey);
168 const solverRaw = take('solver') ?? DEFAULT_SOLVER;
169 if (!(solverKeys as string[]).includes(solverRaw)) {
170 throw new Error(
171 `--solver must be one of ${solverKeys.join(', ')} (got '${solverRaw}')`,
172 );
173 }
174 const spec: RunSpec = {
175 preset: presetKey,
176 lmax: count('lmax', DEFAULT_LMAX, 1),
177 seed: number('seed', DEFAULT_SEED),
178 steps: count('steps', DEFAULT_STEPS, 1),
179 warmup: count('warmup', DEFAULT_WARMUP, 0),
180 params,
181 geometry: geometryKey,
182 geometryParams,
183 niter: count('niter', DEFAULT_NITER, 0),
184 solver: solverRaw as SolverKey,
185 };
186 const readInto = (into: Params, key: string, flag: string): void => {
187 const raw = take(flag);
188 if (raw === undefined) return;
189 const v = Number(raw);
190 if (!Number.isFinite(v)) throw new Error(`--${flag} must be a number (got '${raw}')`);
191 into[key] = v;
192 };
193 for (const p of model.params) readInto(params, p.key, p.key);
194 for (const p of geometry.params) readInto(geometryParams, p.key, `g${p.key}`);
195 if (flags.size) {
196 throw new Error(
197 `unknown option(s): ${[...flags.keys()].map((k) => `--${k}`).join(', ')}\n` +
198 `parameters of ${model.label}: ${model.params.map((p) => `--${p.key}`).join(' ')}\n` +
199 `parameters of ${geometry.label}: ` +
200 (geometry.params.length
201 ? geometry.params.map((p) => `--g${p.key}`).join(' ')
202 : '(none)'),
203 );
204 }
205 return spec;