concept-collection / shtns-webgpu
shtns-webgpu / README.md
5.1 KBPreviewCodeBlameHistoryRaw

shtns-webgpu#

Spherical harmonic transforms on WebGPU (browser, fp32), modeled on SHTNS. This is a from-scratch TypeScript + WGSL implementation of the scalar transforms, structured after the SHTNS CUDA backend (sht_gpu.cu / SHT/cuda_legendre.gen.cu).

Live demo: https://concept-collection.github.io/shtns-webgpu/ (validation suite: test.html)

Scope (v0.1)#

Conventions (= SHTNS defaults)#

Usage#

import { ShtPlan, requestShtDevice, lmIndex } from 'shtns-webgpu';

const device = await requestShtDevice();           // or your own GPUDevice
const plan = await ShtPlan.create(device, { lmax: 127, mmax: 127, nlat: 128, nphi: 256 });

const qlm = new Float32Array(2 * plan.nlm);
qlm[2 * lmIndex(127, 8, 5)] = 1.0;                 // Y_8^5

const spat = await plan.synth(qlm);                // nlat*nphi field
const qBack = await plan.analys(spat);             // back to spectral
plan.destroy();

For GPU-resident pipelines (no readback), use plan.encodeSynth(encoder) / plan.encodeAnalys(encoder) with the exposed qlmIn / qlmOut / spatBuf buffers.

Constraints checked at plan creation: nlat > lmax (Gauss quadrature exactness), nphi >= 2*mmax + 1 (no aliasing).

How it works#

Same two-stage split as SHTNS:

  1. Legendre stage (src/wgsl/leg.ts): associated Legendre functions are generated on the fly inside the shader by the standard 3-term recurrence over l (coefficients from legendre_precomp()-equivalent host code, src/coeffs.ts). Underflow of sin^m(theta) — fatal in fp32 beyond m ≈ 75 — is handled with the SHTNS extended-range scheme (SHT_SCALE_FACTOR = 2^56, SHT_ACCURACY = 1e-15, per-thread integer exponent), ported from the HI_LLIM path of SHT/cuda_legendre.gen.cu. Synthesis runs one thread per latitude and one workgroup row per m; analysis runs one workgroup per m with a shared-memory tree reduction over latitudes (the portable equivalent of SHTNS's warp shuffles).
  2. Fourier stage (src/wgsl/fourier.ts): batched radix-2 Stockham FFT in workgroup memory (one workgroup per latitude row) when nphi is a power of two that fits (16*nphi <= maxComputeWorkgroupStorageSize); otherwise a direct band-limited DFT. Twiddles come from a host-computed f64 table — device sin/cos is only guaranteed to ~2^-11 under Vulkan, which would otherwise dominate the error budget.

All problem sizes are baked into the WGSL at plan creation (the WGSL equivalent of SHTNS's NVRTC runtime compilation).

Accuracy (fp32)#

Relative L2 errors vs the double-precision reference (src/reference.ts), random spectra, measured on SwiftShader (results on hardware GPUs are the same to within noise since the arithmetic is IEEE fp32):

lmax synthesis analysis round trip
15 6e-7 3e-7 6e-7
63 5e-6 2e-6 3e-6
127 7e-6 3e-6 5e-6
255 2e-5 6e-6 1e-5
399 9e-5 1e-5 2e-5

SHTNS itself switches its fp32 GPU recurrence to f64 above lmax = 128 (SHT_L_RESCALE_FLY_FLOAT); WGSL has no f64, so past that point accuracy degrades gracefully as above. Fine for visualization; for scientific use keep lmax ≲ 128 or wait for the float-float recurrence (planned).

Develop / test#

npm install
npm run dev        # demo at http://localhost:5173
npm run test:node  # f64 math tests (no GPU needed)
npm run test:gpu   # builds, then runs the browser suite in headless Chrome
                   # (falls back to SwiftShader software WebGPU; CHROME_PATH to override)

Roadmap#