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