@danielsimonjr/mathts-functions 0.17.2 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,52 +2,65 @@
2
2
  * WebGPU fused element-wise chain.
3
3
  *
4
4
  * Why a *chain* and not a single op: a lone element-wise op on the GPU is pure
5
- * transfer tax — upload n floats, do one flop each, read n floats back. The
6
- * same memory-bound economics retired element-wise ops from the WASM backend.
7
- * A **fused chain** uploads once, runs every op in the chain on-device by
8
- * ping-ponging two storage buffers, and reads back once, so the transfer cost
9
- * is amortized across the whole chain.
5
+ * transfer tax — upload n floats, do one flop each, read n floats back. A
6
+ * **fused chain** uploads once, runs every op on-device by ping-ponging two
7
+ * storage buffers, and reads back once, so the transfer is amortized across the
8
+ * whole chain.
10
9
  *
11
- * Contract (mirrors the WASM `elementwiseChainDispatch`): this is a
12
- * **never-throw** best-effort fast path. It returns `null` never rejects
13
- * whenever the GPU is unavailable, not opted in, the input is too small, or the
14
- * chain contains an op with no GPU kernel. The caller then falls through to the
15
- * existing WASM/JS tiers.
10
+ * **Read this before reaching for the GPU:** for element-wise chains the GPU is
11
+ * the *fastest* tier (3.2–8.3× over WASMsee the table on
12
+ * `fuseUnaryChainAsync`), but it computes in **f32** where every CPU tier is
13
+ * f64-exact. That is the whole trade, and `enableGpu()` is how a caller consents
14
+ * to it. `fuseUnaryChainAsync` therefore tries the GPU first, but only when the
15
+ * flag is on; with it off (the default) the GPU never runs.
16
16
  *
17
- * Precision: the GPU path computes in **f32** (WGSL has no f64). This is why it
18
- * is gated behind the explicit `enableGpu()` opt-in.
19
- */
20
- import { type GPUContextOptions } from '@danielsimonjr/mathts-gpu';
21
- /**
22
- * WGSL expressions for each supported op, as a function of `x`.
17
+ * An earlier revision of this comment claimed the GPU was ~1. *slower* than
18
+ * WASM. That was an artifact of a `Float32Array.from()` in this very file — the
19
+ * generic `Array.from` path, which cost 433 ms at n=2²⁰ where the constructor
20
+ * costs 5.9 ms. Do not re-derive a tier ranking from a single tier's number; see
21
+ * `gpu-vs-wasm.browser.test.ts`, which measures all three in one run.
23
22
  *
24
- * Deliberately a SUBSET of the WASM op set. `erfc` is absent: WGSL has no
25
- * `erfc` builtin, and hand-rolling a polynomial approximation would silently
26
- * change the accuracy contract. A chain containing an unsupported op returns
27
- * `null` and falls back, rather than quietly computing something else.
23
+ * Contract (mirrors the WASM `elementwiseChainDispatch`): a **never-throw**
24
+ * best-effort fast path. It returns `null` never rejects whenever the GPU is
25
+ * unavailable, not opted in, the input is too small, or the chain contains an op
26
+ * with no GPU kernel. The caller then falls through to the CPU tiers.
28
27
  */
28
+ import { type GPUContextOptions } from '@danielsimonjr/mathts-gpu';
29
29
  declare const WGSL_OP_BODY: {
30
30
  readonly abs: "return abs(x);";
31
31
  readonly sin: "return sin(x);";
32
32
  readonly cos: "return cos(x);";
33
33
  readonly tan: "return tan(x);";
34
34
  readonly exp: "return exp(x);";
35
- readonly log: "return log(x);";
36
35
  readonly atan: "return atan(x);";
37
36
  readonly sinh: "return sinh(x);";
38
37
  readonly tanh: "return tanh(x);";
39
- readonly atanh: "return atanh(x);";
40
- readonly log2: "return log2(x);";
41
- readonly log10: "return log(x) * 0.4342944819032518;";
42
- readonly sec: "return 1.0 / cos(x);";
43
- readonly csc: "return 1.0 / sin(x);";
44
- readonly cot: "return 1.0 / tan(x);";
38
+ readonly log: "return safe_log(x);";
39
+ readonly log2: "return safe_log(x) * 1.4426950408889634;";
40
+ readonly log10: "return safe_log(x) * 0.4342944819032518;";
41
+ readonly atanh: "return safe_atanh(x);";
42
+ readonly sec: "return safe_recip(cos(x));";
43
+ readonly csc: "return safe_recip(sin(x));";
44
+ readonly cot: "return safe_recip(tan(x));";
45
45
  };
46
46
  /** Ops that have a GPU kernel. A chain outside this set falls back. */
47
47
  export type GpuElementwiseOp = keyof typeof WGSL_OP_BODY;
48
48
  export declare const GPU_ELEMENTWISE_OPS: GpuElementwiseOp[];
49
49
  /** Whether every op in the chain has a GPU kernel. */
50
50
  export declare function isGpuChainSupported(ops: readonly string[]): ops is readonly GpuElementwiseOp[];
51
+ /** Drop the cached shaders/buffers (device loss, or between tests). */
52
+ export declare function resetGpuElementwise(): void;
53
+ /** Options for a GPU element-wise dispatch. */
54
+ export interface GpuChainOptions extends GPUContextOptions {
55
+ /**
56
+ * Per-call override of the global `enableGpu()` flag.
57
+ *
58
+ * The global flag is process-wide mutable state: any dependency that calls
59
+ * `enableGpu()` would otherwise change *your* call's behaviour. Passing `gpu`
60
+ * explicitly makes a call self-describing and immune to that.
61
+ */
62
+ gpu?: boolean;
63
+ }
51
64
  /**
52
65
  * Run a fused element-wise chain on the GPU.
53
66
  *
@@ -55,6 +68,6 @@ export declare function isGpuChainSupported(ops: readonly string[]): ops is read
55
68
  * @param xs - input samples
56
69
  * @returns the f32 results, or `null` to signal "fall back to another tier"
57
70
  */
58
- export declare function elementwiseChainGpuDispatch(ops: readonly string[], xs: Float64Array | Float32Array, options?: GPUContextOptions): Promise<Float32Array | null>;
71
+ export declare function elementwiseChainGpuDispatch(ops: readonly string[], xs: Float64Array | Float32Array, options?: GpuChainOptions): Promise<Float32Array | null>;
59
72
  export {};
60
73
  //# sourceMappingURL=elementwise-gpu.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"elementwise-gpu.d.ts","sourceRoot":"","sources":["../../src/gpu/elementwise-gpu.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,2BAA2B,CAAC;AAEnC;;;;;;;GAOG;AACH,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;CAkCR,CAAC;AAEX,uEAAuE;AACvE,MAAM,MAAM,gBAAgB,GAAG,MAAM,OAAO,YAAY,CAAC;AAEzD,eAAO,MAAM,mBAAmB,EAAgC,gBAAgB,EAAE,CAAC;AAEnF,sDAAsD;AACtD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,GAAG,IAAI,SAAS,gBAAgB,EAAE,CAE9F;AA8CD;;;;;;GAMG;AACH,wBAAsB,2BAA2B,CAC/C,GAAG,EAAE,SAAS,MAAM,EAAE,EACtB,EAAE,EAAE,YAAY,GAAG,YAAY,EAC/B,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAuH9B"}
1
+ {"version":3,"file":"elementwise-gpu.d.ts","sourceRoot":"","sources":["../../src/gpu/elementwise-gpu.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAOL,KAAK,iBAAiB,EACvB,MAAM,2BAA2B,CAAC;AA+CnC,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;CAgCR,CAAC;AAEX,uEAAuE;AACvE,MAAM,MAAM,gBAAgB,GAAG,MAAM,OAAO,YAAY,CAAC;AAEzD,eAAO,MAAM,mBAAmB,EAAgC,gBAAgB,EAAE,CAAC;AAEnF,sDAAsD;AACtD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,GAAG,IAAI,SAAS,gBAAgB,EAAE,CAE9F;AAsED,uEAAuE;AACvE,wBAAgB,mBAAmB,IAAI,IAAI,CAG1C;AAED,+CAA+C;AAC/C,MAAM,WAAW,eAAgB,SAAQ,iBAAiB;IACxD;;;;;;OAMG;IACH,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAED;;;;;;GAMG;AACH,wBAAsB,2BAA2B,CAC/C,GAAG,EAAE,SAAS,MAAM,EAAE,EACtB,EAAE,EAAE,YAAY,GAAG,YAAY,EAC/B,OAAO,CAAC,EAAE,eAAe,GACxB,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CA6H9B"}
package/dist/index.js CHANGED
@@ -396,6 +396,7 @@ __export(typed_exports, {
396
396
  reduce: () => reduce,
397
397
  reflectVector: () => reflectVector,
398
398
  resample: () => resample,
399
+ resetGpuElementwise: () => resetGpuElementwise,
399
400
  residue: () => residue,
400
401
  resultant: () => resultant,
401
402
  rightArithShift: () => rightArithShift,
@@ -7341,41 +7342,76 @@ var typedSpecial = {
7341
7342
  // src/gpu/elementwise-gpu.ts
7342
7343
  import {
7343
7344
  getGpuDevice,
7345
+ getGlobalGPUContext,
7344
7346
  isGpuEnabled,
7345
- GPU_MIN_ELEMENTS
7347
+ GPU_MIN_ELEMENTS,
7348
+ ShaderManager,
7349
+ BufferPool
7346
7350
  } from "@danielsimonjr/mathts-gpu";
7351
+ var WGSL_IEEE = `
7352
+ // NaN / \xB1Inf are supplied through the UNIFORM, not written as literals.
7353
+ //
7354
+ // WGSL const-folds \`bitcast<f32>(0x7fc00000u)\` even inside a function body and
7355
+ // then rejects the result: "value nan cannot be represented as 'f32'" \u2014 a const
7356
+ // expression may not BE NaN or Inf. Reading the bit pattern from \`params\` (a
7357
+ // runtime uniform) is not const-foldable, so the bitcast survives to runtime.
7358
+ // params = (n, nanBits, posInfBits, negInfBits).
7359
+ fn nan_f32() -> f32 { return bitcast<f32>(params.y); }
7360
+ fn pos_inf() -> f32 { return bitcast<f32>(params.z); }
7361
+ fn neg_inf() -> f32 { return bitcast<f32>(params.w); }
7362
+
7363
+ fn safe_log(x: f32) -> f32 {
7364
+ if (x < 0.0) { return nan_f32(); }
7365
+ if (x == 0.0) { return neg_inf(); }
7366
+ return log(x);
7367
+ }
7368
+ fn safe_atanh(x: f32) -> f32 {
7369
+ if (x > 1.0 || x < -1.0) { return nan_f32(); }
7370
+ if (x == 1.0) { return pos_inf(); }
7371
+ if (x == -1.0) { return neg_inf(); }
7372
+ return atanh(x);
7373
+ }
7374
+ // 1/d with IEEE division-by-zero semantics (JS: 1/0 = +Inf, 1/-0 = -Inf).
7375
+ fn safe_recip(d: f32) -> f32 {
7376
+ if (d == 0.0) {
7377
+ // Distinguish +0 from -0 by its sign bit, as IEEE division does.
7378
+ if ((bitcast<u32>(d) & 0x80000000u) != 0u) { return neg_inf(); }
7379
+ return pos_inf();
7380
+ }
7381
+ return 1.0 / d;
7382
+ }
7383
+ `;
7347
7384
  var WGSL_OP_BODY = {
7348
7385
  abs: "return abs(x);",
7349
7386
  sin: "return sin(x);",
7350
7387
  cos: "return cos(x);",
7351
7388
  tan: "return tan(x);",
7352
7389
  exp: "return exp(x);",
7353
- log: "return log(x);",
7354
7390
  atan: "return atan(x);",
7355
7391
  sinh: "return sinh(x);",
7356
7392
  tanh: "return tanh(x);",
7357
- atanh: "return atanh(x);",
7358
- log2: "return log2(x);",
7359
- // WGSL has no log10 builtin; log(x) * 1/ln(10).
7360
- log10: "return log(x) * 0.4342944819032518;",
7361
- // NOTE `expm1` and `log1p` are DELIBERATELY ABSENT, like `erfc`.
7362
- //
7363
- // WGSL has no builtin for either. The naive identities (`exp(x)-1`,
7364
- // `log(1+x)`) are catastrophically wrong near zero in f32: `1.0 + 1e-8`
7365
- // rounds to exactly 1.0f, so `log(1+x)` returns 0 where the true value is
7366
- // 1e-8 — a 100% relative error.
7393
+ // Domain-guarded: WGSL says indeterminate outside the domain; JS does not.
7394
+ log: "return safe_log(x);",
7395
+ log2: "return safe_log(x) * 1.4426950408889634;",
7396
+ // ln(x) / ln(2)
7397
+ log10: "return safe_log(x) * 0.4342944819032518;",
7398
+ // ln(x) / ln(10)
7399
+ atanh: "return safe_atanh(x);",
7400
+ sec: "return safe_recip(cos(x));",
7401
+ csc: "return safe_recip(sin(x));",
7402
+ cot: "return safe_recip(tan(x));"
7403
+ // NOTE — `expm1`, `log1p` and `erfc` are DELIBERATELY ABSENT.
7367
7404
  //
7368
- // The standard Kahan-compensated forms were implemented and MEASURED on real
7369
- // hardware (NVIDIA Pascal). They still scored 38% (expm1) and 62% (log1p) max
7370
- // relative error over x in [1e-9, 1e-3], because the compensation relies on
7371
- // `log()` being accurate for arguments near 1.0, and the GPU's fast-math
7372
- // `log()` is not.
7405
+ // WGSL has no builtin for any of them. For expm1/log1p the naive identities
7406
+ // (`exp(x)-1`, `log(1+x)`) are catastrophically wrong near zero in f32
7407
+ // (`1.0 + 1e-8` rounds to exactly 1.0f, so `log(1+x)` returns 0 where the true
7408
+ // value is 1e-8 100% relative error), and the standard Kahan-compensated
7409
+ // forms were implemented and MEASURED on real hardware: still 38% (expm1) and
7410
+ // 62% (log1p) max relative error, because the compensation needs an accurate
7411
+ // `log()` near 1.0 and the GPU's fast-math `log()` is not.
7373
7412
  //
7374
- // An f32 fast path is allowed to be less precise. It is not allowed to be
7375
- // WRONG. Chains containing these fall back to the exact WASM/JS tiers.
7376
- sec: "return 1.0 / cos(x);",
7377
- csc: "return 1.0 / sin(x);",
7378
- cot: "return 1.0 / tan(x);"
7413
+ // An f32 fast path may be less precise. It may not be WRONG. Chains containing
7414
+ // these fall back to the exact WASM/JS tiers.
7379
7415
  };
7380
7416
  var GPU_ELEMENTWISE_OPS = Object.keys(WGSL_OP_BODY);
7381
7417
  function isGpuChainSupported(ops) {
@@ -7386,6 +7422,9 @@ function wgslFor(op) {
7386
7422
  return `
7387
7423
  @group(0) @binding(0) var<storage, read> inp: array<f32>;
7388
7424
  @group(0) @binding(1) var<storage, read_write> outp: array<f32>;
7425
+ @group(0) @binding(2) var<uniform> params: vec4<u32>; // n, nanBits, +infBits, -infBits
7426
+
7427
+ ${WGSL_IEEE}
7389
7428
 
7390
7429
  fn apply(x: f32) -> f32 {
7391
7430
  ${WGSL_OP_BODY[op]}
@@ -7394,80 +7433,72 @@ function wgslFor(op) {
7394
7433
  @compute @workgroup_size(${WORKGROUP_SIZE})
7395
7434
  fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
7396
7435
  let i = gid.x;
7397
- if (i >= arrayLength(&inp)) { return; }
7436
+ if (i >= params.x) { return; }
7398
7437
  outp[i] = apply(inp[i]);
7399
7438
  }
7400
7439
  `;
7401
7440
  }
7402
- var pipelineCache = /* @__PURE__ */ new WeakMap();
7403
- function getPipeline(device, op) {
7404
- let byOp = pipelineCache.get(device);
7405
- if (!byOp) {
7406
- byOp = /* @__PURE__ */ new Map();
7407
- pipelineCache.set(device, byOp);
7408
- }
7409
- const cached = byOp.get(op);
7410
- if (cached) return cached;
7411
- const pipeline = device.createComputePipeline({
7412
- label: `elementwise:${op}`,
7413
- layout: "auto",
7414
- compute: {
7415
- module: device.createShaderModule({ code: wgslFor(op), label: `elementwise:${op}` }),
7416
- entryPoint: "main"
7417
- }
7418
- });
7419
- byOp.set(op, pipeline);
7420
- return pipeline;
7441
+ var resources = null;
7442
+ async function getResources(options) {
7443
+ const device = await getGpuDevice(options);
7444
+ if (!device) return null;
7445
+ if (resources && resources.device === device) return resources;
7446
+ const ctx = getGlobalGPUContext();
7447
+ const shaders = new ShaderManager(ctx);
7448
+ for (const op of GPU_ELEMENTWISE_OPS) {
7449
+ shaders.registerShader(op, wgslFor(op));
7450
+ }
7451
+ shaders.precompileRegistered();
7452
+ resources = { device, shaders, pool: new BufferPool(ctx) };
7453
+ return resources;
7454
+ }
7455
+ function resetGpuElementwise() {
7456
+ resources?.pool.destroy();
7457
+ resources = null;
7421
7458
  }
7422
7459
  async function elementwiseChainGpuDispatch(ops, xs, options) {
7423
7460
  const n = xs.length;
7424
- if (!isGpuEnabled()) return null;
7461
+ const enabled = options?.gpu ?? isGpuEnabled();
7462
+ if (!enabled) return null;
7425
7463
  if (ops.length === 0) return null;
7426
7464
  if (n < GPU_MIN_ELEMENTS) return null;
7427
7465
  if (!isGpuChainSupported(ops)) return null;
7428
7466
  const bytes = n * 4;
7429
7467
  const workgroups = Math.ceil(n / WORKGROUP_SIZE);
7468
+ let res;
7430
7469
  let bufA;
7431
7470
  let bufB;
7432
7471
  let staging;
7472
+ let params;
7433
7473
  let scopePopped = false;
7434
- let device;
7435
7474
  try {
7436
- const d = await getGpuDevice(options);
7437
- if (!d) return null;
7438
- device = d;
7475
+ const r = await getResources(options);
7476
+ if (!r) return null;
7477
+ res = r;
7478
+ const { device, shaders, pool } = r;
7439
7479
  const limits = device.limits;
7440
7480
  if (workgroups > limits.maxComputeWorkgroupsPerDimension) return null;
7441
7481
  if (bytes > limits.maxStorageBufferBindingSize) return null;
7442
7482
  if (bytes > limits.maxBufferSize) return null;
7443
- const input = xs instanceof Float32Array ? xs : Float32Array.from(xs);
7483
+ const input = xs instanceof Float32Array ? xs : new Float32Array(xs);
7444
7484
  device.pushErrorScope("validation");
7445
- bufA = device.createBuffer({
7446
- size: bytes,
7447
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
7448
- label: "chain-a"
7449
- });
7450
- bufB = device.createBuffer({
7451
- size: bytes,
7452
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
7453
- label: "chain-b"
7454
- });
7455
- staging = device.createBuffer({
7456
- size: bytes,
7457
- usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
7458
- label: "chain-staging"
7459
- });
7485
+ bufA = pool.acquireStorageBuffer(bytes, "chain-a", true, true);
7486
+ bufB = pool.acquireStorageBuffer(bytes, "chain-b", true, true);
7487
+ staging = pool.acquireStagingBuffer(bytes, "chain-staging");
7488
+ params = pool.acquireUniformBuffer(16, "chain-params");
7460
7489
  device.queue.writeBuffer(bufA, 0, input);
7490
+ device.queue.writeBuffer(params, 0, new Uint32Array([n, 2143289344, 2139095040, 4286578688]));
7461
7491
  const encoder = device.createCommandEncoder({ label: "elementwise-chain" });
7462
7492
  let src = bufA;
7463
7493
  let dst2 = bufB;
7464
7494
  for (const op of ops) {
7465
- const pipeline = getPipeline(device, op);
7495
+ const pipeline = shaders.getRegisteredPipeline(op);
7466
7496
  const bindGroup = device.createBindGroup({
7467
7497
  layout: pipeline.getBindGroupLayout(0),
7468
7498
  entries: [
7469
7499
  { binding: 0, resource: { buffer: src } },
7470
- { binding: 1, resource: { buffer: dst2 } }
7500
+ { binding: 1, resource: { buffer: dst2 } },
7501
+ { binding: 2, resource: { buffer: params } }
7471
7502
  ]
7472
7503
  });
7473
7504
  const pass = encoder.beginComputePass({ label: `chain:${op}` });
@@ -7482,22 +7513,25 @@ async function elementwiseChainGpuDispatch(ops, xs, options) {
7482
7513
  const validationError = await device.popErrorScope();
7483
7514
  scopePopped = true;
7484
7515
  if (validationError) return null;
7485
- await staging.mapAsync(GPUMapMode.READ);
7486
- const out = new Float32Array(staging.getMappedRange().slice(0));
7516
+ await staging.mapAsync(GPUMapMode.READ, 0, bytes);
7517
+ const out = new Float32Array(staging.getMappedRange(0, bytes).slice(0));
7487
7518
  staging.unmap();
7488
7519
  return out;
7489
7520
  } catch {
7490
7521
  return null;
7491
7522
  } finally {
7492
- if (device && !scopePopped) {
7523
+ if (res && !scopePopped) {
7493
7524
  try {
7494
- await device.popErrorScope();
7525
+ await res.device.popErrorScope();
7495
7526
  } catch {
7496
7527
  }
7497
7528
  }
7498
- bufA?.destroy();
7499
- bufB?.destroy();
7500
- staging?.destroy();
7529
+ if (res) {
7530
+ if (bufA) res.pool.release(bufA);
7531
+ if (bufB) res.pool.release(bufB);
7532
+ if (staging) res.pool.release(staging);
7533
+ if (params) res.pool.release(params);
7534
+ }
7501
7535
  }
7502
7536
  }
7503
7537
 
@@ -7528,18 +7562,18 @@ function fuseUnaryChain(ops, xs) {
7528
7562
  return jsChain(ops, xs);
7529
7563
  }
7530
7564
  function jsChain(ops, xs) {
7531
- const out = Float64Array.from(xs);
7565
+ const out = new Float64Array(xs);
7532
7566
  for (const op of ops) {
7533
7567
  const f = SCALAR[op];
7534
7568
  for (let i = 0; i < out.length; i++) out[i] = f(out[i]);
7535
7569
  }
7536
7570
  return out;
7537
7571
  }
7538
- async function fuseUnaryChainAsync(ops, xs) {
7572
+ async function fuseUnaryChainAsync(ops, xs, options) {
7573
+ const gpu = await elementwiseChainGpuDispatch(ops, xs, options);
7574
+ if (gpu) return new Float64Array(gpu);
7539
7575
  const wasm = elementwiseChainDispatch(ops, xs);
7540
7576
  if (wasm) return wasm;
7541
- const gpu = await elementwiseChainGpuDispatch(ops, xs);
7542
- if (gpu) return gpu;
7543
7577
  return jsChain(ops, xs);
7544
7578
  }
7545
7579
 
@@ -45593,6 +45627,7 @@ export {
45593
45627
  reflectVector,
45594
45628
  replacer,
45595
45629
  resample,
45630
+ resetGpuElementwise,
45596
45631
  reshape2 as reshape,
45597
45632
  residue,
45598
45633
  resize2 as resize,
@@ -13,6 +13,7 @@
13
13
  * array is below threshold, so the result is always correct.
14
14
  */
15
15
  import { type WasmElementwiseOp } from '../wasm/elementwise/wasm-bridge.js';
16
+ import { type GpuChainOptions } from '../gpu/elementwise-gpu.js';
16
17
  /**
17
18
  * Apply `ops` left-to-right over `xs` (i.e. `ops[last](…ops[0](x))`), fused in
18
19
  * WASM when possible. Returns a new `Float64Array`; never mutates `xs`.
@@ -25,35 +26,61 @@ export declare function fuseUnaryChain(ops: WasmElementwiseOp[], xs: Float64Arra
25
26
  * `fuseUnaryChain`, because a GPU dispatch is inherently asynchronous and
26
27
  * `fuseUnaryChain`'s synchronous signature is public API.
27
28
  *
28
- * Tiers, in order: **WASM (f64) → GPU (f32) → JS (f64)**.
29
+ * Tiers, in order: **GPU (f32, opt-in) → WASM (f64) → JS (f64)**.
29
30
  *
30
- * ### Why WASM is tried BEFORE the GPU
31
+ * ### Why the GPU is tried first but ONLY when explicitly enabled
31
32
  *
32
- * Measured in Chrome on an NVIDIA Pascal adapter, chain `sin→exp→tanh→cos`:
33
+ * Chain `sin→exp→tanh→cos`, 5 reps. ONE run, 2026-07-13, Chrome on an NVIDIA
34
+ * Pascal adapter — the same run quoted in the CHANGELOG and the reference docs, so
35
+ * the three tables agree. Pinned by `gpu-vs-wasm.browser.test.ts`:
33
36
  *
34
- * | n | JS | WASM | GPU | GPU vs WASM |
35
- * | --------- | ------- | ------- | ------- | ----------- |
36
- * | 65,536 | 90 ms | 16 ms | 28 ms | **0.57×** |
37
- * | 262,144 | 400 ms | 60 ms | 103 ms | **0.59×** |
38
- * | 1,048,576 | 1613 ms | 250 ms | 457 ms | **0.55×** |
37
+ * | n | JS | WASM | GPU | GPU vs WASM |
38
+ * | --------- | ------ | ------ | ---------- | ----------- |
39
+ * | 65,536 | 44 ms | 17 ms | **5.2 ms** | **3.2×** |
40
+ * | 262,144 | 185 ms | 63 ms | **7.5 ms** | **8.3×** |
41
+ * | 1,048,576 | 711 ms | 256 ms | **35 ms** | **7.2×** |
39
42
  *
40
- * **WASM is ~1.8× faster than the GPU and it is f64-exact while the GPU is
41
- * f32.** For element-wise chains the GPU is therefore both slower *and* less
42
- * precise, so it must never pre-empt WASM. (An earlier revision had GPU first;
43
- * that only looked like a win because a separate bug meant WASM never loaded in
44
- * browsers, making the baseline pure JS.)
43
+ * The GPU is the fastest tier by a wide margin. It is nevertheless **last-resort
44
+ * by default**, because it computes in f32 while every other tier is f64-exact.
45
+ * `enableGpu()` is how a caller consents to that trade: precision for speed. With
46
+ * the flag off the default this function is exactly WASM JS and returns
47
+ * bit-identical f64 results, so opting out costs nothing.
45
48
  *
46
- * The GPU still earns its place where WASM is **unavailable** — there it beats
47
- * the JS scalar pass ~2–2.5×. It engages only when the caller opted in via
48
- * `enableGpu()`, WASM declined, a device exists, the array clears
49
- * `GPU_MIN_ELEMENTS`, and every op has a GPU kernel.
49
+ * ### Provenance of these numbers (read before changing the order)
50
50
  *
51
- * (The GPU *does* win decisively for compute-bound work like a large matmul —
52
- * see `gpuMatmul`. This ordering is specific to memory-bound element-wise work.)
51
+ * This ordering has been wrong twice, both times from a benchmark measuring
52
+ * something other than what it claimed:
53
53
  *
54
- * **Precision:** the result is a `Float32Array` only when the GPU tier ran
55
- * (~7 significant digits); otherwise it is an exact-f64 `Float64Array`. The
56
- * return type tells you which path ran.
54
+ * 1. GPU-first was first adopted on a "2.3-2.9x faster than JS" result. That
55
+ * baseline was pure JS only because a *separate* bug meant WASM never loaded
56
+ * in browsers. Fixing WASM revealed it beat the GPU, so the order was flipped
57
+ * to WASM-first.
58
+ * 2. That flip was also wrong. The GPU figure it rested on was inflated by
59
+ * `Float32Array.from(f64array)` in the dispatch — the generic `Array.from`
60
+ * path, which runs ToNumber per element. Naming the denominators, because they
61
+ * differ: the *conversion* alone was 73x slower (433 ms vs 5.9 ms at n=2^20),
62
+ * which made the *end-to-end dispatch* 12.2x slower (439.80 ms -> 36.06 ms).
63
+ * With that fixed, the GPU wins outright, as above.
64
+ *
65
+ * The lesson both times: a tier's number is only as good as the tier it is
66
+ * compared against. Re-measure ALL THREE tiers in one run before touching this
67
+ * order — `gpu-vs-wasm.browser.test.ts` does exactly that and fails loudly if the
68
+ * ranking changes.
69
+ *
70
+ * (The GPU also wins decisively for compute-bound work like a large matmul — see
71
+ * `gpuMatmul`. It is not merely a memory-bound-work story.)
72
+ *
73
+ * **Precision.** Always returns a `Float64Array`. When the GPU tier runs, the
74
+ * *values* carry f32 precision (~7 significant digits) even though the container
75
+ * is f64 — the GPU cannot compute in f64 at all.
76
+ *
77
+ * It previously returned `Float64Array | Float32Array` to encode which path ran.
78
+ * That union was a footgun: `.map` / `.filter` / `.set` on it are TS2349 errors,
79
+ * so **every** caller had to narrow with `instanceof` before touching the result,
80
+ * and `new Float64Array(r.buffer)` silently produced garbage when the f32 branch
81
+ * hit. A narrowing tax on 100% of callers, for a branch most never take, is a bad
82
+ * trade. Callers who specifically want the raw f32 buffer can call
83
+ * `elementwiseChainGpuDispatch` directly — it is exported for exactly that.
57
84
  */
58
- export declare function fuseUnaryChainAsync(ops: WasmElementwiseOp[], xs: Float64Array): Promise<Float64Array | Float32Array>;
85
+ export declare function fuseUnaryChainAsync(ops: WasmElementwiseOp[], xs: Float64Array, options?: GpuChainOptions): Promise<Float64Array>;
59
86
  //# sourceMappingURL=fused.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fused.d.ts","sourceRoot":"","sources":["../../src/typed/fused.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,oCAAoC,CAAC;AA0B5C;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,iBAAiB,EAAE,EAAE,EAAE,EAAE,YAAY,GAAG,YAAY,CAIvF;AAYD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,iBAAiB,EAAE,EACxB,EAAE,EAAE,YAAY,GACf,OAAO,CAAC,YAAY,GAAG,YAAY,CAAC,CAUtC"}
1
+ {"version":3,"file":"fused.d.ts","sourceRoot":"","sources":["../../src/typed/fused.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAA+B,KAAK,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAyB9F;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,iBAAiB,EAAE,EAAE,EAAE,EAAE,YAAY,GAAG,YAAY,CAIvF;AAcD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;AACH,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,iBAAiB,EAAE,EACxB,EAAE,EAAE,YAAY,EAChB,OAAO,CAAC,EAAE,eAAe,GACxB,OAAO,CAAC,YAAY,CAAC,CAgBvB"}
@@ -47,7 +47,7 @@ export * from './matrix-ops.js';
47
47
  export { cond } from './matrix-ops.js';
48
48
  export * from './gpu.js';
49
49
  export { enableGpu, disableGpu, isGpuEnabled, GPU_MIN_ELEMENTS } from '@danielsimonjr/mathts-gpu';
50
- export { elementwiseChainGpuDispatch, isGpuChainSupported, GPU_ELEMENTWISE_OPS, type GpuElementwiseOp, } from '../gpu/elementwise-gpu.js';
50
+ export { elementwiseChainGpuDispatch, isGpuChainSupported, resetGpuElementwise, GPU_ELEMENTWISE_OPS, type GpuElementwiseOp, type GpuChainOptions, } from '../gpu/elementwise-gpu.js';
51
51
  export * from './relational.js';
52
52
  export { typedRelational } from './relational.js';
53
53
  export * from './string.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/typed/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAOH,cAAc,iBAAiB,CAAC;AAChC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAGlD,cAAc,mBAAmB,CAAC;AAClC,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAGtD,cAAc,iBAAiB,CAAC;AAChC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAGlD,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG1C,cAAc,cAAc,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAG5C,cAAc,cAAc,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAG5C,cAAc,cAAc,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAK5C,cAAc,UAAU,CAAC;AACzB,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAGpC,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAG5C,cAAc,oBAAoB,CAAC;AACnC,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAGxD,cAAc,eAAe,CAAC;AAG9B,cAAc,cAAc,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAG5C,cAAc,kBAAkB,CAAC;AAGjC,cAAc,oBAAoB,CAAC;AAGnC,cAAc,cAAc,CAAC;AAG7B,cAAc,oBAAoB,CAAC;AAGnC,cAAc,YAAY,CAAC;AAG3B,cAAc,mBAAmB,CAAC;AAGlC,cAAc,iBAAiB,CAAC;AAWhC,cAAc,iBAAiB,CAAC;AAGhC,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAGvC,cAAc,UAAU,CAAC;AAIzB,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAClG,OAAO,EACL,2BAA2B,EAC3B,mBAAmB,EACnB,mBAAmB,EACnB,KAAK,gBAAgB,GACtB,MAAM,2BAA2B,CAAC;AAInC,cAAc,iBAAiB,CAAC;AAChC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAGlD,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAI1C,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGpD,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAsCtC;;;;;;GAMG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8B1B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/typed/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAOH,cAAc,iBAAiB,CAAC;AAChC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAGlD,cAAc,mBAAmB,CAAC;AAClC,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAGtD,cAAc,iBAAiB,CAAC;AAChC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAGlD,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG1C,cAAc,cAAc,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAG5C,cAAc,cAAc,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAG5C,cAAc,cAAc,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAK5C,cAAc,UAAU,CAAC;AACzB,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAGpC,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAG5C,cAAc,oBAAoB,CAAC;AACnC,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAGxD,cAAc,eAAe,CAAC;AAG9B,cAAc,cAAc,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAG5C,cAAc,kBAAkB,CAAC;AAGjC,cAAc,oBAAoB,CAAC;AAGnC,cAAc,cAAc,CAAC;AAG7B,cAAc,oBAAoB,CAAC;AAGnC,cAAc,YAAY,CAAC;AAG3B,cAAc,mBAAmB,CAAC;AAGlC,cAAc,iBAAiB,CAAC;AAWhC,cAAc,iBAAiB,CAAC;AAGhC,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAGvC,cAAc,UAAU,CAAC;AAIzB,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAClG,OAAO,EACL,2BAA2B,EAC3B,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,KAAK,gBAAgB,EACrB,KAAK,eAAe,GACrB,MAAM,2BAA2B,CAAC;AAInC,cAAc,iBAAiB,CAAC;AAChC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAGlD,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAI1C,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGpD,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAsCtC;;;;;;GAMG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8B1B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danielsimonjr/mathts-functions",
3
- "version": "0.17.2",
3
+ "version": "0.19.0",
4
4
  "description": "Mathematical functions for MathTS - arithmetic, algebra, trigonometry, statistics, and more",
5
5
  "author": "Daniel Simon Jr.",
6
6
  "license": "MIT",
@@ -37,7 +37,7 @@
37
37
  "@danielsimonjr/mathts-core": "^0.6.0",
38
38
  "@danielsimonjr/mathts-expression": "^0.6.0",
39
39
  "@danielsimonjr/mathts-gpu": "^0.1.1",
40
- "@danielsimonjr/mathts-matrix": "^0.3.2",
40
+ "@danielsimonjr/mathts-matrix": "^0.4.0",
41
41
  "@danielsimonjr/mathts-parallel": "^0.3.4",
42
42
  "bignumber.js": "^9.1.2",
43
43
  "complex.js": "^2.2.5",