/ concept-collection / turing-sphere
Sign in
concept-collection / turing-sphere
493 lines · 20.2 KBBlameHistoryRaw
1/**
2 * WebGPU spherical harmonic transform plan (scalar transforms, fp32).
3 *
4 * Mirrors the structure of the SHTNS CUDA backend (sht_gpu.cu):
5 * host-side f64 precomputation of grid + recurrence coefficients, shader
6 * source generated with sizes baked in (SHTNS uses NVRTC; WGSL is always
7 * runtime-compiled), then per-transform: Legendre stage + Fourier stage.
8 */
9import { gaussNodesWeights } from './gauss.ts';
10import { legendreCoeffs } from './coeffs.ts';
11import { nlmCalc, validateConfig, isPowerOfTwo, type ShtConfig } from './layout.ts';
12import { legSynthWGSL, legAnalysWGSL } from './wgsl/leg.ts';
13import {
14 fftSynthWGSL,
15 fftAnalysWGSL,
16 fftSynthRealWGSL,
17 fftAnalysRealWGSL,
18 dftSynthWGSL,
19 dftAnalysWGSL,
20 fftThreads,
21} from './wgsl/fourier.ts';
23export type FourierMode = 'auto' | 'fft' | 'dft';
25/** The two bind groups (Legendre stage, Fourier stage) of one transform. */
26export interface ShtBinding {
27 readonly bgLeg: GPUBindGroup;
28 readonly bgFour: GPUBindGroup;
31const bgEntries = (bufs: GPUBuffer[]) =>
32 bufs.map((buffer, binding) => ({ binding, resource: { buffer } }));
34export interface ShtOptions {
35 /** Fourier stage implementation. 'auto' picks fft when nphi is a power of two that fits in workgroup memory. */
36 fourier?: FourierMode;
39const WG_SYNTH = 64;
41/**
42 * Tuning knob, for A/B-ing a change without editing code. Reads globalThis
43 * first (set it before creating a plan, as scripts/_ab.ts does), then the
44 * environment, so `SHT_SUBGROUPS=0 npm run bench:sht` works too. `process` is
45 * absent in the browser, where only the globalThis form applies.
46 */
47function tuning(name: string): unknown {
48 const g = (globalThis as Record<string, unknown>)[name];
49 if (g !== undefined) return g;
50 const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env?.[name];
51 if (env === undefined || env === '') return undefined;
52 if (env === '1' || env === 'true') return true;
53 if (env === '0' || env === 'false') return false;
54 const n = Number(env);
55 return Number.isFinite(n) ? n : env;
58/**
59 * Workgroup size for the analysis Legendre reduction. The right answer differs
60 * between the two reduction strategies, so it is chosen per strategy.
61 *
62 * Measured on an RTX PRO 6000 Blackwell (analysis, us). Shared-memory tree,
63 * where each doubling of wgAnalys costs another barrier per l-pair:
64 *
65 * wgAnalys: 16 32 64 128 256
66 * nlat=128 43.2 40.6 42.6 46.7 52.9 -> 32
67 * nlat=256 104.0 85.6 87.8 89.9 100.0 -> 32
68 * nlat=512 408.8 216.9 184.4 190.8 204.8 -> 64
69 *
70 * i.e. max(32, nlat/8). A flat 32 would be worse than the old default of 256 at
71 * nlat=512, so it cannot be fitted on one grid. With subgroupAdd the barrier
72 * count stops growing with wgAnalys and the picture inverts: threads in flight,
73 * (mmax+1) * wgAnalys, becomes binding, since analysis dispatches only mmax+1
74 * workgroups. 128 then wins at every grid (round trip, us):
75 *
76 * 128x256 37.8 (vs 38.3), 256x512 66.6 (vs 74.7), 512x1024 131.9 (vs 156.6)
77 */
78function defaultWgAnalys(nlat: number, limit: number, subgroups: boolean): number {
79 if (subgroups) {
80 // capped at nlat so small grids do not launch threads with no latitude to own
81 let cap = 1;
82 while (cap < nlat) cap *= 2;
83 return Math.min(128, limit, cap);
84 }
85 const target = Math.max(32, nlat / 8);
86 let wg = 1;
87 while (wg < target) wg *= 2; // the tree reduction halves, so a power of two
88 return Math.min(wg, limit);
91async function makePipeline(
92 device: GPUDevice,
93 code: string,
94 entryPoint: string,
95): Promise<GPUComputePipeline> {
96 device.pushErrorScope('validation');
97 const module = device.createShaderModule({ code, label: entryPoint });
98 const info = await module.getCompilationInfo();
99 const errors = info.messages.filter((m) => m.type === 'error');
100 if (errors.length) {
101 throw new Error(
102 `WGSL compile error in ${entryPoint}:\n` +
103 errors.map((e) => ` ${e.lineNum}:${e.linePos} ${e.message}`).join('\n'),
104 );
105 }
106 const pipeline = await device.createComputePipelineAsync({
107 layout: 'auto',
108 compute: { module, entryPoint },
109 label: entryPoint,
110 });
111 const err = await device.popErrorScope();
112 if (err) throw new Error(`pipeline ${entryPoint}: ${err.message}`);
113 return pipeline;
116export class ShtPlan {
117 readonly cfg: ShtConfig;
118 readonly nlm: number;
119 readonly fourierMode: 'fft' | 'dft';
120 /** Latitudes leg_synth walks: nlat/2 when parity folding. */
121 readonly legLat: number = 0;
122 /** Colatitudes theta_i (f64, increasing: north to south). */
123 readonly theta: Float64Array;
124 readonly cosTheta: Float64Array;
125 readonly gaussWeights: Float64Array;
127 private device: GPUDevice;
128 private bufAb!: GPUBuffer;
129 private bufAmm!: GPUBuffer;
130 private bufCtstw!: GPUBuffer;
131 private bufTrig!: GPUBuffer;
132 /** Spectral input (synthesis) — write with queue.writeBuffer or use synth(). */
133 readonly qlmIn!: GPUBuffer;
134 /** Spectral output (analysis). */
135 readonly qlmOut!: GPUBuffer;
136 /** Fourier-space intermediate [(m)*nlat + ilat], complex f32. COPY_SRC so the
137 * stage boundary is observable: a transform is Legendre-then-Fourier, and
138 * scripts/diagnose-sht.ts tells the two apart by reading this. */
139 readonly fmBuf!: GPUBuffer;
140 /** Spatial field [ilat*nphi + iphi], f32. */
141 readonly spatBuf!: GPUBuffer;
142 private stageSpat!: GPUBuffer;
143 private stageQ!: GPUBuffer;
145 private pipeLegSynth!: GPUComputePipeline;
146 private pipeLegAnalys!: GPUComputePipeline;
147 private pipeFourSynth!: GPUComputePipeline;
148 private pipeFourAnalys!: GPUComputePipeline;
149 private bgLegSynth!: GPUBindGroup;
150 private bgLegAnalys!: GPUBindGroup;
151 private bgFourSynth!: GPUBindGroup;
152 private bgFourAnalys!: GPUBindGroup;
154 private constructor(device: GPUDevice, cfg: ShtConfig, fourierMode: 'fft' | 'dft') {
155 this.device = device;
156 this.cfg = cfg;
157 this.nlm = nlmCalc(cfg.lmax, cfg.mmax);
158 this.fourierMode = fourierMode;
159 const { x, w } = gaussNodesWeights(cfg.nlat);
160 this.cosTheta = x;
161 this.gaussWeights = w;
162 this.theta = new Float64Array(cfg.nlat);
163 for (let i = 0; i < cfg.nlat; i++) this.theta[i] = Math.acos(x[i]);
164 }
166 static async create(device: GPUDevice, cfg: ShtConfig, opts: ShtOptions = {}): Promise<ShtPlan> {
167 validateConfig(cfg);
168 const want = opts.fourier ?? 'auto';
169 const fftFits =
170 isPowerOfTwo(cfg.nphi) &&
171 16 * cfg.nphi <= device.limits.maxComputeWorkgroupStorageSize &&
172 fftThreads(cfg.nphi) <= device.limits.maxComputeInvocationsPerWorkgroup;
173 if (want === 'fft' && !fftFits) {
174 throw new Error(
175 `fourier:'fft' requires power-of-two nphi with 16*nphi <= maxComputeWorkgroupStorageSize ` +
176 `(nphi=${cfg.nphi}, limit=${device.limits.maxComputeWorkgroupStorageSize})`,
177 );
178 }
179 const mode: 'fft' | 'dft' = want === 'dft' ? 'dft' : fftFits ? 'fft' : 'dft';
180 const plan = new ShtPlan(device, cfg, mode);
181 await plan.init();
182 return plan;
183 }
185 private async init(): Promise<void> {
186 const { lmax, mmax, nlat, nphi } = this.cfg;
187 const dev = this.device;
188 const self = this as {
189 -readonly [k in keyof ShtPlan]: ShtPlan[k];
190 };
192 // --- host precomputation (f64), then downcast to f32 for upload ---
193 const { amm, ab } = legendreCoeffs(lmax, mmax);
194 const ctstw = new Float32Array(3 * nlat);
195 for (let i = 0; i < nlat; i++) {
196 ctstw[i] = this.cosTheta[i];
197 ctstw[nlat + i] = Math.sqrt(1 - this.cosTheta[i] * this.cosTheta[i]);
198 ctstw[2 * nlat + i] = this.gaussWeights[i] * ((2 * Math.PI) / nphi);
199 }
200 // twiddle/phase table in f64 (device sin/cos is too inaccurate: ~2^-11 under Vulkan)
201 const trig = new Float32Array(2 * nphi);
202 for (let k = 0; k < nphi; k++) {
203 trig[2 * k] = Math.cos((2 * Math.PI * k) / nphi);
204 trig[2 * k + 1] = Math.sin((2 * Math.PI * k) / nphi);
205 }
207 const mkBuf = (label: string, size: number, usage: GPUBufferUsageFlags) =>
208 dev.createBuffer({ label, size, usage });
209 this.bufAb = mkBuf('sht-ab', 8 * this.nlm, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
210 this.bufAmm = mkBuf('sht-amm', 4 * (mmax + 1), GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
211 this.bufCtstw = mkBuf('sht-ctstw', 4 * 3 * nlat, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
212 this.bufTrig = mkBuf('sht-trig', 8 * nphi, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
213 self.qlmIn = mkBuf('sht-qlm-in', 8 * this.nlm, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC);
214 self.qlmOut = mkBuf('sht-qlm-out', 8 * this.nlm, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC);
215 self.fmBuf = mkBuf('sht-fm', 8 * (mmax + 1) * nlat, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC);
216 self.spatBuf = mkBuf('sht-spat', 4 * nlat * nphi, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC);
217 this.stageSpat = mkBuf('sht-stage-spat', 4 * nlat * nphi, GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST);
218 this.stageQ = mkBuf('sht-stage-q', 8 * this.nlm, GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST);
220 dev.queue.writeBuffer(this.bufAb, 0, new Float32Array(ab));
221 dev.queue.writeBuffer(this.bufAmm, 0, new Float32Array(amm));
222 dev.queue.writeBuffer(this.bufCtstw, 0, ctstw);
223 dev.queue.writeBuffer(this.bufTrig, 0, trig);
225 // --- shaders / pipelines ---
226 const subgroups = tuning('SHT_SUBGROUPS') !== false && dev.features.has('subgroups');
227 // parity folding needs an equator-symmetric grid; Gauss nodes are, if nlat is even
228 const parity = tuning('SHT_PARITY') !== false && nlat % 2 === 0;
229 const wgAnalys =
230 (tuning('SHT_WG_ANALYS') as number | undefined) ??
231 defaultWgAnalys(nlat, dev.limits.maxComputeInvocationsPerWorkgroup, subgroups);
232 const legP = {
233 lmax,
234 mmax,
235 nlat,
236 wgSynth: WG_SYNTH,
237 wgAnalys,
238 subgroups,
239 spanPairs: tuning('SHT_SPAN_PAIRS') as number | undefined,
240 parity,
241 };
242 (this as { legLat: number }).legLat = parity ? nlat / 2 : nlat;
243 const fourP = { mmax, nlat, nphi, radix: (tuning('SHT_RADIX') as number | undefined) ?? 4 };
244 // The spatial field is real (layout.ts stores m >= 0 only), so the Fourier
245 // stage can run an nphi/2-point complex FFT plus a recombination instead of
246 // a full nphi-point one: half the arithmetic and half the workgroup storage.
247 // The complex kernels remain for a future complex-valued field, and are what
248 // SHT_REAL_FFT=0 selects.
249 const realFft =
250 this.fourierMode === 'fft' && nphi % 2 === 0 && tuning('SHT_REAL_FFT') !== false;
251 const fftS = realFft ? fftSynthRealWGSL : fftSynthWGSL;
252 const fftA = realFft ? fftAnalysRealWGSL : fftAnalysWGSL;
253 const [pLegS, pLegA, pFourS, pFourA] = await Promise.all([
254 makePipeline(dev, legSynthWGSL(legP), 'leg_synth'),
255 makePipeline(dev, legAnalysWGSL(legP), 'leg_analys'),
256 makePipeline(
257 dev,
258 this.fourierMode === 'fft' ? fftS(fourP) : dftSynthWGSL(fourP),
259 this.fourierMode === 'fft' ? 'fft_synth' : 'dft_synth',
260 ),
261 makePipeline(
262 dev,
263 this.fourierMode === 'fft' ? fftA(fourP) : dftAnalysWGSL(fourP),
264 this.fourierMode === 'fft' ? 'fft_analys' : 'dft_analys',
265 ),
266 ]);
267 this.pipeLegSynth = pLegS;
268 this.pipeLegAnalys = pLegA;
269 this.pipeFourSynth = pFourS;
270 this.pipeFourAnalys = pFourA;
272 const entries = bgEntries;
273 this.bgLegSynth = dev.createBindGroup({
274 layout: pLegS.getBindGroupLayout(0),
275 entries: entries([this.bufAb, this.bufAmm, this.bufCtstw, this.qlmIn, this.fmBuf]),
276 });
277 this.bgLegAnalys = dev.createBindGroup({
278 layout: pLegA.getBindGroupLayout(0),
279 entries: entries([this.bufAb, this.bufAmm, this.bufCtstw, this.fmBuf, this.qlmOut]),
280 });
281 this.bgFourSynth = dev.createBindGroup({
282 layout: pFourS.getBindGroupLayout(0),
283 entries: entries([this.fmBuf, this.spatBuf, this.bufTrig]),
284 });
285 this.bgFourAnalys = dev.createBindGroup({
286 layout: pFourA.getBindGroupLayout(0),
287 entries: entries([this.spatBuf, this.fmBuf, this.bufTrig]),
288 });
289 }
291 /**
292 * Bind groups for one transform against caller-supplied spectral/spatial
293 * buffers, so a transform can read and write buffers it does not own (the
294 * .m-driven executor keeps a buffer per IR variable). Build these once at
295 * plan time, not per step. `fmBuf` stays internal scratch: passes and
296 * dispatches within a submission execute in order, so sequential transforms
297 * can share it.
298 */
299 createSynthBinding(qlmIn: GPUBuffer, spatOut: GPUBuffer): ShtBinding {
300 return {
301 bgLeg: this.device.createBindGroup({
302 layout: this.pipeLegSynth.getBindGroupLayout(0),
303 entries: bgEntries([this.bufAb, this.bufAmm, this.bufCtstw, qlmIn, this.fmBuf]),
304 }),
305 bgFour: this.device.createBindGroup({
306 layout: this.pipeFourSynth.getBindGroupLayout(0),
307 entries: bgEntries([this.fmBuf, spatOut, this.bufTrig]),
308 }),
309 };
310 }
312 createAnalysBinding(spatIn: GPUBuffer, qlmOut: GPUBuffer): ShtBinding {
313 return {
314 bgFour: this.device.createBindGroup({
315 layout: this.pipeFourAnalys.getBindGroupLayout(0),
316 entries: bgEntries([spatIn, this.fmBuf, this.bufTrig]),
317 }),
318 bgLeg: this.device.createBindGroup({
319 layout: this.pipeLegAnalys.getBindGroupLayout(0),
320 entries: bgEntries([this.bufAb, this.bufAmm, this.bufCtstw, this.fmBuf, qlmOut]),
321 }),
322 };
323 }
325 /** Record synthesis into an existing compute pass. */
326 encodeSynthInto(pass: GPUComputePassEncoder, b: ShtBinding): void {
327 const { mmax, nlat, nphi } = this.cfg;
328 pass.setPipeline(this.pipeLegSynth);
329 pass.setBindGroup(0, b.bgLeg);
330 pass.dispatchWorkgroups(Math.ceil(this.legLat / WG_SYNTH), mmax + 1);
331 pass.setPipeline(this.pipeFourSynth);
332 pass.setBindGroup(0, b.bgFour);
333 if (this.fourierMode === 'fft') {
334 pass.dispatchWorkgroups(nlat);
335 } else {
336 pass.dispatchWorkgroups(Math.ceil(nphi / 64), nlat);
337 }
338 }
340 /** Record analysis into an existing compute pass. */
341 encodeAnalysInto(pass: GPUComputePassEncoder, b: ShtBinding): void {
342 const { mmax, nlat } = this.cfg;
343 pass.setPipeline(this.pipeFourAnalys);
344 pass.setBindGroup(0, b.bgFour);
345 if (this.fourierMode === 'fft') {
346 pass.dispatchWorkgroups(nlat);
347 } else {
348 pass.dispatchWorkgroups(Math.ceil((mmax + 1) / 64), nlat);
349 }
350 pass.setPipeline(this.pipeLegAnalys);
351 pass.setBindGroup(0, b.bgLeg);
352 pass.dispatchWorkgroups(mmax + 1);
353 }
355 /** Record the synthesis (spectral qlmIn -> spatial spatBuf) into an encoder. */
356 encodeSynth(encoder: GPUCommandEncoder): void {
357 const pass = encoder.beginComputePass({ label: 'sht-synth' });
358 this.encodeSynthInto(pass, { bgLeg: this.bgLegSynth, bgFour: this.bgFourSynth });
359 pass.end();
360 }
362 /**
363 * Diagnostics: encode one stage alone, in its own pass, so a timestamp query
364 * can measure just that kernel. The solver wants both stages in a shared pass
365 * and should use encodeSynthInto/encodeAnalysInto; this exists because
366 * inferring per-kernel cost by subtracting trivially-sized runs is unreliable.
367 */
368 encodeStage(
369 encoder: GPUCommandEncoder,
370 stage: 'legSynth' | 'fourSynth' | 'fourAnalys' | 'legAnalys',
371 timestampWrites?: GPUComputePassTimestampWrites,
372 ): void {
373 const { mmax, nlat, nphi } = this.cfg;
374 const fft = this.fourierMode === 'fft';
375 const pass = encoder.beginComputePass({ label: `sht-${stage}`, timestampWrites });
376 switch (stage) {
377 case 'legSynth':
378 pass.setPipeline(this.pipeLegSynth);
379 pass.setBindGroup(0, this.bgLegSynth);
380 pass.dispatchWorkgroups(Math.ceil(this.legLat / WG_SYNTH), mmax + 1);
381 break;
382 case 'fourSynth':
383 pass.setPipeline(this.pipeFourSynth);
384 pass.setBindGroup(0, this.bgFourSynth);
385 if (fft) pass.dispatchWorkgroups(nlat);
386 else pass.dispatchWorkgroups(Math.ceil(nphi / 64), nlat);
387 break;
388 case 'fourAnalys':
389 pass.setPipeline(this.pipeFourAnalys);
390 pass.setBindGroup(0, this.bgFourAnalys);
391 if (fft) pass.dispatchWorkgroups(nlat);
392 else pass.dispatchWorkgroups(Math.ceil((mmax + 1) / 64), nlat);
393 break;
394 case 'legAnalys':
395 pass.setPipeline(this.pipeLegAnalys);
396 pass.setBindGroup(0, this.bgLegAnalys);
397 pass.dispatchWorkgroups(mmax + 1);
398 break;
399 }
400 pass.end();
401 }
403 /** Record the analysis (spatial spatBuf -> spectral qlmOut) into an encoder. */
404 encodeAnalys(encoder: GPUCommandEncoder): void {
405 const pass = encoder.beginComputePass({ label: 'sht-analys' });
406 this.encodeAnalysInto(pass, { bgLeg: this.bgLegAnalys, bgFour: this.bgFourAnalys });
407 pass.end();
408 }
410 /**
411 * Spectral -> spatial. qlm: interleaved [re, im], SHTNS LM ordering,
412 * length 2*nlm. Returns the spatial field, length nlat*nphi.
413 */
414 async synth(qlm: Float32Array): Promise<Float32Array> {
415 const { nlat, nphi } = this.cfg;
416 if (qlm.length !== 2 * this.nlm) throw new Error(`qlm must have length ${2 * this.nlm}`);
417 this.device.queue.writeBuffer(this.qlmIn, 0, qlm as Float32Array<ArrayBuffer>);
418 const enc = this.device.createCommandEncoder();
419 this.encodeSynth(enc);
420 enc.copyBufferToBuffer(this.spatBuf, 0, this.stageSpat, 0, 4 * nlat * nphi);
421 this.device.queue.submit([enc.finish()]);
422 await this.stageSpat.mapAsync(GPUMapMode.READ);
423 const out = new Float32Array(this.stageSpat.getMappedRange().slice(0));
424 this.stageSpat.unmap();
425 return out;
426 }
428 /** Spatial -> spectral. spat: length nlat*nphi. Returns interleaved qlm, length 2*nlm. */
429 async analys(spat: Float32Array): Promise<Float32Array> {
430 const { nlat, nphi } = this.cfg;
431 if (spat.length !== nlat * nphi) throw new Error(`spat must have length ${nlat * nphi}`);
432 this.device.queue.writeBuffer(this.spatBuf, 0, spat as Float32Array<ArrayBuffer>);
433 const enc = this.device.createCommandEncoder();
434 this.encodeAnalys(enc);
435 enc.copyBufferToBuffer(this.qlmOut, 0, this.stageQ, 0, 8 * this.nlm);
436 this.device.queue.submit([enc.finish()]);
437 await this.stageQ.mapAsync(GPUMapMode.READ);
438 const out = new Float32Array(this.stageQ.getMappedRange().slice(0));
439 this.stageQ.unmap();
440 return out;
441 }
443 destroy(): void {
444 for (const b of [
445 this.bufAb, this.bufAmm, this.bufCtstw, this.bufTrig, this.qlmIn, this.qlmOut,
446 this.fmBuf, this.spatBuf, this.stageSpat, this.stageQ,
447 ]) b?.destroy();
448 }
451/** Best-effort human-readable adapter name, so it is clear which GPU (or
452 * software rasterizer) is actually running the transforms. */
453export async function describeAdapter(device: GPUDevice): Promise<string> {
454 const fmt = (info: GPUAdapterInfo | undefined): string => {
455 if (!info) return '';
456 const parts = [info.description, info.device, info.vendor].filter(
457 (s): s is string => !!s && s.length > 0,
458 );
459 const name = parts[0] ?? '';
460 return info.architecture && !name.includes(info.architecture)
461 ? `${name} (${info.architecture})`.trim()
462 : name;
463 };
464 const own = fmt((device as GPUDevice & { adapterInfo?: GPUAdapterInfo }).adapterInfo);
465 if (own) return own;
466 try {
467 const adapter = await navigator.gpu.requestAdapter();
468 return fmt(adapter?.info);
469 } catch {
470 return '';
471 }
474/** Request an adapter/device suitable for the transforms. */
475export async function requestShtDevice(): Promise<GPUDevice> {
476 if (!navigator.gpu) throw new Error('WebGPU is not available in this browser');
477 const adapter = await navigator.gpu.requestAdapter();
478 if (!adapter) throw new Error('No WebGPU adapter available');
479 // ask for a larger workgroup storage if the adapter offers it (bigger FFTs)
480 const wgStorage = Math.min(adapter.limits.maxComputeWorkgroupStorageSize, 32768);
481 // `subgroups` lets the analysis reduction use subgroupAdd instead of a
482 // shared-memory tree (2 barriers per l-pair instead of 1 + log2(wgAnalys)).
483 // Optional: ShtPlan falls back to the tree when it is not available.
484 const features: GPUFeatureName[] = [];
485 if (adapter.features.has('subgroups')) features.push('subgroups');
486 // timestamp-query is only used by the profiling scripts, but it has to be
487 // requested at device creation, and asking costs nothing when unused.
488 if (adapter.features.has('timestamp-query')) features.push('timestamp-query');
489 return adapter.requestDevice({
490 requiredFeatures: features,
491 requiredLimits: { maxComputeWorkgroupStorageSize: wgStorage },
492 });
moveopenescclose