@graphty/webgpu-graph-algorithms 0.0.0 → 0.2.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.
- package/README.md +344 -23
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +32 -0
- package/dist/browser.js.map +1 -0
- package/dist/chunks/context-E6iKaeuJ.js +3136 -0
- package/dist/chunks/context-E6iKaeuJ.js.map +1 -0
- package/dist/node.d.ts +1 -0
- package/dist/node.js +131 -0
- package/dist/node.js.map +1 -0
- package/dist/src/accelerator.d.ts +26 -0
- package/dist/src/accelerator.d.ts.map +1 -0
- package/dist/src/accelerator.js +101 -0
- package/dist/src/accelerator.js.map +1 -0
- package/dist/src/algorithms/degree.d.ts +35 -0
- package/dist/src/algorithms/degree.d.ts.map +1 -0
- package/dist/src/algorithms/degree.js +119 -0
- package/dist/src/algorithms/degree.js.map +1 -0
- package/dist/src/browser/index.d.ts +23 -0
- package/dist/src/browser/index.d.ts.map +1 -0
- package/dist/src/browser/index.js +48 -0
- package/dist/src/browser/index.js.map +1 -0
- package/dist/src/constants.d.ts +92 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +92 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/context.d.ts +84 -0
- package/dist/src/context.d.ts.map +1 -0
- package/dist/src/context.js +304 -0
- package/dist/src/context.js.map +1 -0
- package/dist/src/device/acquire.d.ts +57 -0
- package/dist/src/device/acquire.d.ts.map +1 -0
- package/dist/src/device/acquire.js +232 -0
- package/dist/src/device/acquire.js.map +1 -0
- package/dist/src/device/caps.d.ts +43 -0
- package/dist/src/device/caps.d.ts.map +1 -0
- package/dist/src/device/caps.js +104 -0
- package/dist/src/device/caps.js.map +1 -0
- package/dist/src/device/error-scope.d.ts +75 -0
- package/dist/src/device/error-scope.d.ts.map +1 -0
- package/dist/src/device/error-scope.js +152 -0
- package/dist/src/device/error-scope.js.map +1 -0
- package/dist/src/device/lost.d.ts +51 -0
- package/dist/src/device/lost.d.ts.map +1 -0
- package/dist/src/device/lost.js +130 -0
- package/dist/src/device/lost.js.map +1 -0
- package/dist/src/device/webgpu-constants.d.ts +31 -0
- package/dist/src/device/webgpu-constants.d.ts.map +1 -0
- package/dist/src/device/webgpu-constants.js +31 -0
- package/dist/src/device/webgpu-constants.js.map +1 -0
- package/dist/src/errors.d.ts +56 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +57 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/index.d.ts +29 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +27 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/kernel/batch.d.ts +116 -0
- package/dist/src/kernel/batch.d.ts.map +1 -0
- package/dist/src/kernel/batch.js +335 -0
- package/dist/src/kernel/batch.js.map +1 -0
- package/dist/src/kernel/dispatch.d.ts +59 -0
- package/dist/src/kernel/dispatch.d.ts.map +1 -0
- package/dist/src/kernel/dispatch.js +139 -0
- package/dist/src/kernel/dispatch.js.map +1 -0
- package/dist/src/kernel/kernel.d.ts +84 -0
- package/dist/src/kernel/kernel.d.ts.map +1 -0
- package/dist/src/kernel/kernel.js +239 -0
- package/dist/src/kernel/kernel.js.map +1 -0
- package/dist/src/kernel/pipeline-cache.d.ts +90 -0
- package/dist/src/kernel/pipeline-cache.d.ts.map +1 -0
- package/dist/src/kernel/pipeline-cache.js +251 -0
- package/dist/src/kernel/pipeline-cache.js.map +1 -0
- package/dist/src/kernel/prelude.d.ts +35 -0
- package/dist/src/kernel/prelude.d.ts.map +1 -0
- package/dist/src/kernel/prelude.js +211 -0
- package/dist/src/kernel/prelude.js.map +1 -0
- package/dist/src/kernel/profiler.d.ts +64 -0
- package/dist/src/kernel/profiler.d.ts.map +1 -0
- package/dist/src/kernel/profiler.js +120 -0
- package/dist/src/kernel/profiler.js.map +1 -0
- package/dist/src/kernel/struct-block.d.ts +122 -0
- package/dist/src/kernel/struct-block.d.ts.map +1 -0
- package/dist/src/kernel/struct-block.js +353 -0
- package/dist/src/kernel/struct-block.js.map +1 -0
- package/dist/src/kernel/uniform-ring.d.ts +70 -0
- package/dist/src/kernel/uniform-ring.d.ts.map +1 -0
- package/dist/src/kernel/uniform-ring.js +146 -0
- package/dist/src/kernel/uniform-ring.js.map +1 -0
- package/dist/src/kernel/wgsl.d.ts +88 -0
- package/dist/src/kernel/wgsl.d.ts.map +1 -0
- package/dist/src/kernel/wgsl.js +390 -0
- package/dist/src/kernel/wgsl.js.map +1 -0
- package/dist/src/kernels.d.ts +81 -0
- package/dist/src/kernels.d.ts.map +1 -0
- package/dist/src/kernels.js +417 -0
- package/dist/src/kernels.js.map +1 -0
- package/dist/src/layouts/force-simulation.d.ts +498 -0
- package/dist/src/layouts/force-simulation.d.ts.map +1 -0
- package/dist/src/layouts/force-simulation.js +1650 -0
- package/dist/src/layouts/force-simulation.js.map +1 -0
- package/dist/src/layouts/forceatlas2.d.ts +210 -0
- package/dist/src/layouts/forceatlas2.d.ts.map +1 -0
- package/dist/src/layouts/forceatlas2.js +759 -0
- package/dist/src/layouts/forceatlas2.js.map +1 -0
- package/dist/src/layouts/inputs.d.ts +40 -0
- package/dist/src/layouts/inputs.d.ts.map +1 -0
- package/dist/src/layouts/inputs.js +185 -0
- package/dist/src/layouts/inputs.js.map +1 -0
- package/dist/src/layouts/repulsion-exact.d.ts +85 -0
- package/dist/src/layouts/repulsion-exact.d.ts.map +1 -0
- package/dist/src/layouts/repulsion-exact.js +134 -0
- package/dist/src/layouts/repulsion-exact.js.map +1 -0
- package/dist/src/layouts/seed.d.ts +56 -0
- package/dist/src/layouts/seed.d.ts.map +1 -0
- package/dist/src/layouts/seed.js +173 -0
- package/dist/src/layouts/seed.js.map +1 -0
- package/dist/src/memory/buffer-pool.d.ts +73 -0
- package/dist/src/memory/buffer-pool.d.ts.map +1 -0
- package/dist/src/memory/buffer-pool.js +170 -0
- package/dist/src/memory/buffer-pool.js.map +1 -0
- package/dist/src/memory/lease.d.ts +53 -0
- package/dist/src/memory/lease.d.ts.map +1 -0
- package/dist/src/memory/lease.js +85 -0
- package/dist/src/memory/lease.js.map +1 -0
- package/dist/src/memory/readback.d.ts +143 -0
- package/dist/src/memory/readback.d.ts.map +1 -0
- package/dist/src/memory/readback.js +375 -0
- package/dist/src/memory/readback.js.map +1 -0
- package/dist/src/memory/residency.d.ts +83 -0
- package/dist/src/memory/residency.d.ts.map +1 -0
- package/dist/src/memory/residency.js +573 -0
- package/dist/src/memory/residency.js.map +1 -0
- package/dist/src/memory/upload-plan.d.ts +101 -0
- package/dist/src/memory/upload-plan.d.ts.map +1 -0
- package/dist/src/memory/upload-plan.js +265 -0
- package/dist/src/memory/upload-plan.js.map +1 -0
- package/dist/src/node/index.d.ts +64 -0
- package/dist/src/node/index.d.ts.map +1 -0
- package/dist/src/node/index.js +183 -0
- package/dist/src/node/index.js.map +1 -0
- package/dist/src/primitives/reduce.d.ts +57 -0
- package/dist/src/primitives/reduce.d.ts.map +1 -0
- package/dist/src/primitives/reduce.js +161 -0
- package/dist/src/primitives/reduce.js.map +1 -0
- package/dist/src/primitives/segmented-reduce.d.ts +38 -0
- package/dist/src/primitives/segmented-reduce.d.ts.map +1 -0
- package/dist/src/primitives/segmented-reduce.js +211 -0
- package/dist/src/primitives/segmented-reduce.js.map +1 -0
- package/dist/src/types/accelerator.d.ts +209 -0
- package/dist/src/types/accelerator.d.ts.map +1 -0
- package/dist/src/types/accelerator.js +8 -0
- package/dist/src/types/accelerator.js.map +1 -0
- package/dist/src/types/context.d.ts +114 -0
- package/dist/src/types/context.d.ts.map +1 -0
- package/dist/src/types/context.js +7 -0
- package/dist/src/types/context.js.map +1 -0
- package/dist/src/types/layout.d.ts +95 -0
- package/dist/src/types/layout.d.ts.map +1 -0
- package/dist/src/types/layout.js +6 -0
- package/dist/src/types/layout.js.map +1 -0
- package/dist/src/types/memory.d.ts +22 -0
- package/dist/src/types/memory.d.ts.map +1 -0
- package/dist/src/types/memory.js +7 -0
- package/dist/src/types/memory.js.map +1 -0
- package/dist/src/types/options.d.ts +78 -0
- package/dist/src/types/options.d.ts.map +1 -0
- package/dist/src/types/options.js +7 -0
- package/dist/src/types/options.js.map +1 -0
- package/dist/src/types/run.d.ts +13 -0
- package/dist/src/types/run.d.ts.map +1 -0
- package/dist/src/types/run.js +6 -0
- package/dist/src/types/run.js.map +1 -0
- package/dist/src/wgsl/degree.wgsl.d.ts +10 -0
- package/dist/src/wgsl/degree.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/degree.wgsl.js +25 -0
- package/dist/src/wgsl/degree.wgsl.js.map +1 -0
- package/dist/src/wgsl/fa2-attraction.wgsl.d.ts +12 -0
- package/dist/src/wgsl/fa2-attraction.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/fa2-attraction.wgsl.js +37 -0
- package/dist/src/wgsl/fa2-attraction.wgsl.js.map +1 -0
- package/dist/src/wgsl/fa2-integrate.wgsl.d.ts +13 -0
- package/dist/src/wgsl/fa2-integrate.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/fa2-integrate.wgsl.js +69 -0
- package/dist/src/wgsl/fa2-integrate.wgsl.js.map +1 -0
- package/dist/src/wgsl/fa2-repulsion-exact.wgsl.d.ts +12 -0
- package/dist/src/wgsl/fa2-repulsion-exact.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/fa2-repulsion-exact.wgsl.js +79 -0
- package/dist/src/wgsl/fa2-repulsion-exact.wgsl.js.map +1 -0
- package/dist/src/wgsl/fa2-speed-finalize.wgsl.d.ts +15 -0
- package/dist/src/wgsl/fa2-speed-finalize.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/fa2-speed-finalize.wgsl.js +54 -0
- package/dist/src/wgsl/fa2-speed-finalize.wgsl.js.map +1 -0
- package/dist/src/wgsl/fa2-stats-finalize.wgsl.d.ts +14 -0
- package/dist/src/wgsl/fa2-stats-finalize.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/fa2-stats-finalize.wgsl.js +57 -0
- package/dist/src/wgsl/fa2-stats-finalize.wgsl.js.map +1 -0
- package/dist/src/wgsl/fa2-to-scene.wgsl.d.ts +11 -0
- package/dist/src/wgsl/fa2-to-scene.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/fa2-to-scene.wgsl.js +19 -0
- package/dist/src/wgsl/fa2-to-scene.wgsl.js.map +1 -0
- package/dist/src/wgsl/fill.wgsl.d.ts +7 -0
- package/dist/src/wgsl/fill.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/fill.wgsl.js +14 -0
- package/dist/src/wgsl/fill.wgsl.js.map +1 -0
- package/dist/src/wgsl/reduce.wgsl.d.ts +10 -0
- package/dist/src/wgsl/reduce.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/reduce.wgsl.js +63 -0
- package/dist/src/wgsl/reduce.wgsl.js.map +1 -0
- package/dist/src/wgsl/segmented-reduce.wgsl.d.ts +13 -0
- package/dist/src/wgsl/segmented-reduce.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/segmented-reduce.wgsl.js +35 -0
- package/dist/src/wgsl/segmented-reduce.wgsl.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/dist/webgpu-graph-algorithms.d.ts +1 -0
- package/dist/webgpu-graph-algorithms.js +4454 -0
- package/dist/webgpu-graph-algorithms.js.map +1 -0
- package/package.json +108 -17
- package/src/accelerator.ts +117 -0
- package/src/algorithms/degree.ts +142 -0
- package/src/browser/index.ts +57 -0
- package/src/constants.ts +116 -0
- package/src/context.ts +399 -0
- package/src/device/acquire.ts +256 -0
- package/src/device/caps.ts +122 -0
- package/src/device/error-scope.ts +171 -0
- package/src/device/lost.ts +142 -0
- package/src/device/webgpu-constants.ts +44 -0
- package/src/errors.ts +94 -0
- package/src/index.ts +102 -0
- package/src/kernel/batch.ts +427 -0
- package/src/kernel/dispatch.ts +162 -0
- package/src/kernel/kernel.ts +311 -0
- package/src/kernel/pipeline-cache.ts +288 -0
- package/src/kernel/prelude.ts +229 -0
- package/src/kernel/profiler.ts +148 -0
- package/src/kernel/struct-block.ts +439 -0
- package/src/kernel/uniform-ring.ts +184 -0
- package/src/kernel/wgsl.ts +490 -0
- package/src/kernels.ts +511 -0
- package/src/layouts/force-simulation.ts +2111 -0
- package/src/layouts/forceatlas2.ts +942 -0
- package/src/layouts/inputs.ts +252 -0
- package/src/layouts/repulsion-exact.ts +183 -0
- package/src/layouts/seed.ts +198 -0
- package/src/memory/buffer-pool.ts +204 -0
- package/src/memory/lease.ts +93 -0
- package/src/memory/readback.ts +429 -0
- package/src/memory/residency.ts +753 -0
- package/src/memory/upload-plan.ts +350 -0
- package/src/node/index.ts +230 -0
- package/src/primitives/reduce.ts +233 -0
- package/src/primitives/segmented-reduce.ts +270 -0
- package/src/types/accelerator.ts +236 -0
- package/src/types/context.ts +135 -0
- package/src/types/layout.ts +103 -0
- package/src/types/memory.ts +23 -0
- package/src/types/options.ts +84 -0
- package/src/types/run.ts +13 -0
- package/src/wgsl/degree.wgsl.ts +24 -0
- package/src/wgsl/fa2-attraction.wgsl.ts +37 -0
- package/src/wgsl/fa2-integrate.wgsl.ts +69 -0
- package/src/wgsl/fa2-repulsion-exact.wgsl.ts +78 -0
- package/src/wgsl/fa2-speed-finalize.wgsl.ts +53 -0
- package/src/wgsl/fa2-stats-finalize.wgsl.ts +57 -0
- package/src/wgsl/fa2-to-scene.wgsl.ts +19 -0
- package/src/wgsl/fill.wgsl.ts +13 -0
- package/src/wgsl/reduce.wgsl.ts +62 -0
- package/src/wgsl/segmented-reduce.wgsl.ts +35 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `reduce` primitive driver (spec 6 row 1): sum / min / max over f32, u32 or vec4f elements in two or three
|
|
3
|
+
* dispatches of the `reduce` module (3.10.1). Level 1 folds WG elements per workgroup into per-workgroup partials
|
|
4
|
+
* (the prelude's tree helpers, 4.3); a third level folds the partials again when the level-1 grid needed a 2D
|
|
5
|
+
* dispatch (more than MAX_WORKGROUPS_PER_DIM workgroups, i.e. more than MAX_1D_ITEMS elements); the FINAL level --
|
|
6
|
+
* ONE workgroup whose lanes walk the remaining partials sequentially in index order -- writes one element
|
|
7
|
+
* (4 or 16 bytes) at out[outOffset]. Every level reduces in a fixed order, so two runs are bitwise identical on one
|
|
8
|
+
* device (spec 11.9 item 4); the subgroup twin (4.3) agrees to summation-order noise on f32 and bitwise on u32.
|
|
9
|
+
* `count === 0` records level 1 over an empty plan (Kernel.dispatch records nothing, spec 5.6) and the FINAL level
|
|
10
|
+
* over 0 partials, which writes the identity element.
|
|
11
|
+
*
|
|
12
|
+
* The driver owns no device objects: the caller supplies a ReduceScope (P1: the tests build one over a GpuContext and
|
|
13
|
+
* release its scratch in a finally; P2: CommandBatch supplies the same record with a Lease) and the compute pass to
|
|
14
|
+
* record into. `src/primitives/**` never imports `src/context.ts` (the eslint zone of 2.4).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { MAX_WORKGROUPS_PER_DIM } from "../constants.js";
|
|
18
|
+
import { WebGpuGraphError } from "../errors.js";
|
|
19
|
+
import { type DispatchPlan, groupsOf, plan1d } from "../kernel/dispatch.js";
|
|
20
|
+
import { type Kernel } from "../kernel/kernel.js";
|
|
21
|
+
import { type PipelineCache } from "../kernel/pipeline-cache.js";
|
|
22
|
+
import { type UniformBlock, type UniformValues } from "../kernel/struct-block.js";
|
|
23
|
+
import { kernelSpec, REDUCE_PARAMS } from "../kernels.js";
|
|
24
|
+
import { type BufferPool } from "../memory/buffer-pool.js";
|
|
25
|
+
import { type PlanCaps } from "../types/context.js";
|
|
26
|
+
import { type Binding } from "../types/memory.js";
|
|
27
|
+
|
|
28
|
+
/** The reduction operator (4.5 OP override: sum, min, max). */
|
|
29
|
+
export type ReduceOp = "sum" | "min" | "max";
|
|
30
|
+
/** The element type (4.5 DTYPE override: f32, u32, or vec4f lanes of f32). */
|
|
31
|
+
export type ReduceDtype = "f32" | "u32" | "vec4f";
|
|
32
|
+
|
|
33
|
+
/** What reduce() needs of its caller: a pass to record into, scratch, the ring and the cache (P1 has no CommandBatch yet; P2's batch supplies the same record). */
|
|
34
|
+
export interface ReduceScope {
|
|
35
|
+
readonly device: GPUDevice;
|
|
36
|
+
readonly caps: PlanCaps;
|
|
37
|
+
readonly pipelines: PipelineCache;
|
|
38
|
+
readonly pool: BufferPool;
|
|
39
|
+
readonly workgroupSize: number;
|
|
40
|
+
/** Acquires scratch released by the caller's scope (a Lease from P2; P1 releases in a finally). */
|
|
41
|
+
scratch(byteLength: number, label: string): GPUBuffer;
|
|
42
|
+
/** The uniform-slot writer: returns the binding and dynamic offset for a params record. */
|
|
43
|
+
params(block: UniformBlock, values: UniformValues): { readonly binding: Binding; readonly offset: number };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A prepared reduce: records the 2-3 dispatches of spec 6 row 1 into a pass. */
|
|
47
|
+
export interface ReducePlanner {
|
|
48
|
+
readonly op: ReduceOp;
|
|
49
|
+
readonly dtype: ReduceDtype;
|
|
50
|
+
/** Records: level 1 over `count` elements of `src` into partials; a third level when groups > MAX_WORKGROUPS_PER_DIM; the FINAL one-workgroup level writing one element (4 or 16 bytes) at out[outOffset] (element index). Deterministic order. count 0 writes the identity element. */
|
|
51
|
+
record(pass: GPUComputePassEncoder, src: Binding, count: number, out: Binding, outOffset: number): void;
|
|
52
|
+
/** Dispatches the last record() issued (tests bound it: 2 or 3). */
|
|
53
|
+
readonly lastDispatches: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The OP override values of the reduce module (4.5: 0 = sum, 1 = min, 2 = max). */
|
|
57
|
+
const OP_CODE: Readonly<Record<ReduceOp, number>> = Object.freeze({ sum: 0, min: 1, max: 2 });
|
|
58
|
+
/** The DTYPE override values (4.5: 0 = f32, 1 = u32, 2 = vec4f). */
|
|
59
|
+
const DTYPE_CODE: Readonly<Record<ReduceDtype, number>> = Object.freeze({ f32: 0, u32: 1, vec4f: 2 });
|
|
60
|
+
/** Bytes of one element per dtype. */
|
|
61
|
+
const ELEMENT_BYTES: Readonly<Record<ReduceDtype, number>> = Object.freeze({ f32: 4, u32: 4, vec4f: 16 });
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Prepares the reduce pipelines of a scope (compiles once) so record() is synchronous.
|
|
65
|
+
* @param scope - the caller's scope (device, caps, cache, scratch, params)
|
|
66
|
+
* @param op - the operator
|
|
67
|
+
* @param dtype - the element type
|
|
68
|
+
* @returns the planner, with the level and FINAL pipelines resolved
|
|
69
|
+
*/
|
|
70
|
+
export async function prepareReduce(scope: ReduceScope, op: ReduceOp, dtype: ReduceDtype): Promise<ReducePlanner> {
|
|
71
|
+
const overrides = { OP: OP_CODE[op], DTYPE: DTYPE_CODE[dtype] };
|
|
72
|
+
const level = await scope.pipelines.kernel(kernelSpec("reduce", { ...overrides, FINAL: false }));
|
|
73
|
+
const final = await scope.pipelines.kernel(kernelSpec("reduce", { ...overrides, FINAL: true }));
|
|
74
|
+
return new ReducePlannerImpl(scope, op, dtype, level, final);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The argument checks of record() (E_INVALID_ARGUMENT before anything is recorded).
|
|
79
|
+
* @param src - the input binding
|
|
80
|
+
* @param count - the element count
|
|
81
|
+
* @param out - the output binding
|
|
82
|
+
* @param outOffset - the output element index
|
|
83
|
+
* @param elementBytes - bytes per element of the planner's dtype
|
|
84
|
+
*/
|
|
85
|
+
function checkRecordArguments(
|
|
86
|
+
src: Binding,
|
|
87
|
+
count: number,
|
|
88
|
+
out: Binding,
|
|
89
|
+
outOffset: number,
|
|
90
|
+
elementBytes: number,
|
|
91
|
+
): void {
|
|
92
|
+
if (!Number.isSafeInteger(count) || count < 0) {
|
|
93
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", "reduce: count must be a non-negative integer", {
|
|
94
|
+
argument: "count",
|
|
95
|
+
value: count,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
if (!Number.isSafeInteger(outOffset) || outOffset < 0) {
|
|
99
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", "reduce: outOffset must be a non-negative integer", {
|
|
100
|
+
argument: "outOffset",
|
|
101
|
+
value: outOffset,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
if (src.size < count * elementBytes) {
|
|
105
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", "reduce: src is smaller than count x element size", {
|
|
106
|
+
argument: "src",
|
|
107
|
+
value: src.size,
|
|
108
|
+
expected: count * elementBytes,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (out.size < (outOffset + 1) * elementBytes) {
|
|
112
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", "reduce: out cannot hold the element at outOffset", {
|
|
113
|
+
argument: "out",
|
|
114
|
+
value: out.size,
|
|
115
|
+
expected: (outOffset + 1) * elementBytes,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The planner: two resolved kernels (FINAL false / true) over one scope. */
|
|
121
|
+
class ReducePlannerImpl implements ReducePlanner {
|
|
122
|
+
readonly op: ReduceOp;
|
|
123
|
+
readonly dtype: ReduceDtype;
|
|
124
|
+
private readonly scope: ReduceScope;
|
|
125
|
+
private readonly level: Kernel;
|
|
126
|
+
private readonly final: Kernel;
|
|
127
|
+
private dispatches = 0;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Wraps the resolved kernels; use prepareReduce().
|
|
131
|
+
* @param scope - the caller's scope
|
|
132
|
+
* @param op - the operator
|
|
133
|
+
* @param dtype - the element type
|
|
134
|
+
* @param level - the per-workgroup level pipeline (FINAL = false)
|
|
135
|
+
* @param final - the one-workgroup pipeline (FINAL = true)
|
|
136
|
+
*/
|
|
137
|
+
constructor(scope: ReduceScope, op: ReduceOp, dtype: ReduceDtype, level: Kernel, final: Kernel) {
|
|
138
|
+
this.scope = scope;
|
|
139
|
+
this.op = op;
|
|
140
|
+
this.dtype = dtype;
|
|
141
|
+
this.level = level;
|
|
142
|
+
this.final = final;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Dispatches the last record() issued (2, or 3 above MAX_1D_ITEMS elements).
|
|
147
|
+
* @returns the count
|
|
148
|
+
*/
|
|
149
|
+
get lastDispatches(): number {
|
|
150
|
+
return this.dispatches;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Records the levels into the pass (see the interface).
|
|
155
|
+
* @param pass - the compute pass
|
|
156
|
+
* @param src - the elements (count x element bytes at least)
|
|
157
|
+
* @param count - the element count
|
|
158
|
+
* @param out - the output range
|
|
159
|
+
* @param outOffset - the element index written
|
|
160
|
+
*/
|
|
161
|
+
record(pass: GPUComputePassEncoder, src: Binding, count: number, out: Binding, outOffset: number): void {
|
|
162
|
+
const elementBytes = ELEMENT_BYTES[this.dtype];
|
|
163
|
+
checkRecordArguments(src, count, out, outOffset, elementBytes);
|
|
164
|
+
const { scope } = this;
|
|
165
|
+
const wg = scope.workgroupSize;
|
|
166
|
+
let dispatches = 0;
|
|
167
|
+
const plan1 = plan1d(count, wg, scope.caps);
|
|
168
|
+
const groups1 = groupsOf(plan1);
|
|
169
|
+
let partials = this.partialsBinding(Math.max(groups1, 1), "reduce/partials-1");
|
|
170
|
+
this.dispatchLevel(pass, this.level, src, count, partials, 0, plan1, 1);
|
|
171
|
+
dispatches += 1;
|
|
172
|
+
let partialCount = groups1;
|
|
173
|
+
if (groups1 > MAX_WORKGROUPS_PER_DIM) {
|
|
174
|
+
const plan2 = plan1d(groups1, wg, scope.caps);
|
|
175
|
+
const groups2 = groupsOf(plan2);
|
|
176
|
+
const partials2 = this.partialsBinding(groups2, "reduce/partials-2");
|
|
177
|
+
this.dispatchLevel(pass, this.level, partials, groups1, partials2, 0, plan2, 2);
|
|
178
|
+
dispatches += 1;
|
|
179
|
+
partials = partials2;
|
|
180
|
+
partialCount = groups2;
|
|
181
|
+
}
|
|
182
|
+
this.dispatchLevel(
|
|
183
|
+
pass,
|
|
184
|
+
this.final,
|
|
185
|
+
partials,
|
|
186
|
+
partialCount,
|
|
187
|
+
out,
|
|
188
|
+
outOffset,
|
|
189
|
+
plan1d(1, wg, scope.caps),
|
|
190
|
+
dispatches + 1,
|
|
191
|
+
);
|
|
192
|
+
dispatches += 1;
|
|
193
|
+
this.dispatches = dispatches;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* A partials scratch of `elements` elements from the scope, bound whole (never zero-length).
|
|
198
|
+
* @param elements - the partial count (>= 1)
|
|
199
|
+
* @param label - the scratch label
|
|
200
|
+
* @returns the binding
|
|
201
|
+
*/
|
|
202
|
+
private partialsBinding(elements: number, label: string): Binding {
|
|
203
|
+
const size = elements * ELEMENT_BYTES[this.dtype];
|
|
204
|
+
const buffer = this.scope.scratch(size, label);
|
|
205
|
+
return { buffer, offset: 0, size, window: null };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* One level: a params record, the bind groups, one dispatch.
|
|
210
|
+
* @param pass - the compute pass
|
|
211
|
+
* @param kernel - the level or FINAL kernel
|
|
212
|
+
* @param src - the elements of this level
|
|
213
|
+
* @param count - how many
|
|
214
|
+
* @param out - where the partials (or the result) go
|
|
215
|
+
* @param outOffset - the element index (0 for partials)
|
|
216
|
+
* @param plan - the dispatch plan
|
|
217
|
+
* @param level - the level number written into the params (informational)
|
|
218
|
+
*/
|
|
219
|
+
private dispatchLevel(
|
|
220
|
+
pass: GPUComputePassEncoder,
|
|
221
|
+
kernel: Kernel,
|
|
222
|
+
src: Binding,
|
|
223
|
+
count: number,
|
|
224
|
+
out: Binding,
|
|
225
|
+
outOffset: number,
|
|
226
|
+
plan: DispatchPlan,
|
|
227
|
+
level: number,
|
|
228
|
+
): void {
|
|
229
|
+
const params = this.scope.params(REDUCE_PARAMS, { count, outOffset, level });
|
|
230
|
+
const bound = kernel.bind({ src, out, P: params.binding });
|
|
231
|
+
kernel.dispatch(pass, bound, plan, [params.offset]);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The segmented (per-row) reduction primitive of spec 6 row 3 in its thread-per-row tier (P2-P3): one invocation per
|
|
3
|
+
* CSR row folds the caller's VALUE snippet over the row's arcs into `out[row]` (f32), a row with no arcs receiving
|
|
4
|
+
* the identity element. The degree tiers of degreeOrder() (subgroup-per-row for the mid tier, workgroup-per-row for
|
|
5
|
+
* the high tier) land at P4; until then `tiers !== null` is E_UNSUPPORTED and USE_PERM is always false (the perm
|
|
6
|
+
* slot carries the rowPtr dummy of graphBindings). The row and arc counts come from the core's binding sizes: the
|
|
7
|
+
* residency binds every array at its exact byte length (contract 3.8), so rowPtr is 4(n + 1) bytes and colIdx
|
|
8
|
+
* 4 x arcCount.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { WebGpuGraphError } from "../errors.js";
|
|
12
|
+
import { plan1d } from "../kernel/dispatch.js";
|
|
13
|
+
import { type Kernel } from "../kernel/kernel.js";
|
|
14
|
+
import { graphBindings, graphOverrides, kernelSpec, RANGE_PARAMS } from "../kernels.js";
|
|
15
|
+
import { type CoreBinding } from "../memory/residency.js";
|
|
16
|
+
import { type Binding } from "../types/memory.js";
|
|
17
|
+
import { type ReduceOp, type ReduceScope } from "./reduce.js";
|
|
18
|
+
|
|
19
|
+
/** The degree tiers of degreeOrder(): the permutation binding and the CPU-side segmentOffsets [0, hiEnd, midEnd, lowEnd, n]. */
|
|
20
|
+
export interface DegreeTiers {
|
|
21
|
+
readonly perm: Binding;
|
|
22
|
+
readonly segmentOffsets: readonly [number, number, number, number, number];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Options of segmentedReduce. `valueSnippet` is the Gunrock-style functor: WGSL statements assigning `v` from (row, arc, nbr, weight) (4.5; `nbr` because `target` is a WGSL reserved word). */
|
|
26
|
+
export interface SegmentedReduceOptions {
|
|
27
|
+
readonly op: ReduceOp;
|
|
28
|
+
readonly valueSnippet: string;
|
|
29
|
+
readonly tiers: DegreeTiers | null;
|
|
30
|
+
readonly accumulate?: boolean | undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A prepared segmented reduce (P2-P3: the thread-per-row tier only; `tiers !== null` -> E_UNSUPPORTED { feature: "segmentedReduce.tiers" } until P4). */
|
|
34
|
+
export interface SegmentedReducePlanner {
|
|
35
|
+
/** Records one dispatch over rows [0, n) (tiers null) writing out[i] (f32) per row; a row with no arcs gets the identity element. */
|
|
36
|
+
record(pass: GPUComputePassEncoder, core: CoreBinding, out: Binding): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The identifiers a VALUE snippet may name (contract 3.11, 4.5); every other identifier is rejected textually before compose. */
|
|
40
|
+
const VALUE_SNIPPET_VOCABULARY: ReadonlySet<string> = new Set(["row", "arc", "nbr", "weight", "v"]);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The WGSL words a snippet statement may use that are not identifiers: the statement keywords (never the flow
|
|
44
|
+
* keywords `return` / `break` / `continue`, which would leave the fold), the scalar type constructors and the
|
|
45
|
+
* builtin math functions a value expression may call (a bounded list; a binding, a uniform or a module function such
|
|
46
|
+
* as `identity` / `comb` / `linear_id` is NOT in it and is rejected as an identifier).
|
|
47
|
+
*/
|
|
48
|
+
const VALUE_SNIPPET_WGSL_WORDS: ReadonlySet<string> = new Set([
|
|
49
|
+
"if",
|
|
50
|
+
"else",
|
|
51
|
+
"let",
|
|
52
|
+
"var",
|
|
53
|
+
"const",
|
|
54
|
+
"true",
|
|
55
|
+
"false",
|
|
56
|
+
"f32",
|
|
57
|
+
"u32",
|
|
58
|
+
"i32",
|
|
59
|
+
"bool",
|
|
60
|
+
"abs",
|
|
61
|
+
"ceil",
|
|
62
|
+
"clamp",
|
|
63
|
+
"exp",
|
|
64
|
+
"exp2",
|
|
65
|
+
"floor",
|
|
66
|
+
"fract",
|
|
67
|
+
"inverseSqrt",
|
|
68
|
+
"log",
|
|
69
|
+
"log2",
|
|
70
|
+
"max",
|
|
71
|
+
"min",
|
|
72
|
+
"mix",
|
|
73
|
+
"pow",
|
|
74
|
+
"round",
|
|
75
|
+
"select",
|
|
76
|
+
"sign",
|
|
77
|
+
"sqrt",
|
|
78
|
+
"step",
|
|
79
|
+
"trunc",
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The OP override value of an operator (the body's `OP == 1u` / `OP == 2u` tests, contract 4.5).
|
|
84
|
+
* @param op - the operator
|
|
85
|
+
* @returns 0 for sum, 1 for min, 2 for max
|
|
86
|
+
*/
|
|
87
|
+
function opCode(op: ReduceOp): number {
|
|
88
|
+
switch (op) {
|
|
89
|
+
case "sum":
|
|
90
|
+
return 0;
|
|
91
|
+
case "min":
|
|
92
|
+
return 1;
|
|
93
|
+
case "max":
|
|
94
|
+
return 2;
|
|
95
|
+
default:
|
|
96
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `segmentedReduce: unknown op ${String(op)}`, {
|
|
97
|
+
argument: "op",
|
|
98
|
+
value: op,
|
|
99
|
+
expected: "sum | min | max",
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* WGSL comments removed (a comment may mention `target` or any other word).
|
|
106
|
+
* @param text - WGSL text
|
|
107
|
+
* @returns the text with block and line comments replaced by spaces
|
|
108
|
+
*/
|
|
109
|
+
function stripComments(text: string): string {
|
|
110
|
+
return text.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n]*/g, " ");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The textual vocabulary check of contract 3.11: the snippet must assign `v` (`=`, `+=`, `-=`, `*=` or `/=`) and its
|
|
115
|
+
* identifiers may be only row, arc, nbr, weight and v; numeric literals and the WGSL words of
|
|
116
|
+
* VALUE_SNIPPET_WGSL_WORDS are not identifiers and pass; anything else -- a binding, a uniform, a module function,
|
|
117
|
+
* a locally declared name, a flow keyword, a reserved word such as `target` -- is E_SHADER_COMPILE { stage:
|
|
118
|
+
* "compose", slot: "VALUE", identifier } on every device, before any shader is created.
|
|
119
|
+
* @param snippet - the VALUE snippet text
|
|
120
|
+
*/
|
|
121
|
+
function validateValueSnippet(snippet: string): void {
|
|
122
|
+
const code = stripComments(snippet);
|
|
123
|
+
if (!/\bv\s*[-+*/]?=(?!=)/.test(code)) {
|
|
124
|
+
throw new WebGpuGraphError("E_SHADER_COMPILE", "segmentedReduce: the VALUE snippet never assigns v", {
|
|
125
|
+
id: "segmented-reduce",
|
|
126
|
+
stage: "compose",
|
|
127
|
+
slot: "VALUE",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const tokens = code.match(/[A-Za-z_][A-Za-z0-9_]*|[0-9][0-9A-Za-z_.]*/g) ?? [];
|
|
131
|
+
for (const token of tokens) {
|
|
132
|
+
if (/^[0-9]/.test(token) || VALUE_SNIPPET_VOCABULARY.has(token) || VALUE_SNIPPET_WGSL_WORDS.has(token)) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
throw new WebGpuGraphError(
|
|
136
|
+
"E_SHADER_COMPILE",
|
|
137
|
+
`segmentedReduce: the VALUE snippet names "${token}"; the only identifiers allowed are row, arc, nbr, weight and v`,
|
|
138
|
+
{ id: "segmented-reduce", stage: "compose", slot: "VALUE", identifier: token },
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The row count of a core from its rowPtr binding (4(n + 1) bytes).
|
|
145
|
+
* @param core - the core
|
|
146
|
+
* @returns n
|
|
147
|
+
*/
|
|
148
|
+
function rowCountOf(core: CoreBinding): number {
|
|
149
|
+
const bytes = core.rowPtr.size;
|
|
150
|
+
if (bytes < 4 || bytes % 4 !== 0) {
|
|
151
|
+
throw new WebGpuGraphError(
|
|
152
|
+
"E_INVALID_ARGUMENT",
|
|
153
|
+
`segmentedReduce: a rowPtr binding of ${bytes} bytes is not 4(n + 1)`,
|
|
154
|
+
{
|
|
155
|
+
argument: "core.rowPtr",
|
|
156
|
+
value: bytes,
|
|
157
|
+
expected: "a positive multiple of 4",
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
return bytes / 4 - 1;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Rejects a windowed core (executed at P4).
|
|
166
|
+
* @param core - the core
|
|
167
|
+
*/
|
|
168
|
+
function assertNotWindowed(core: CoreBinding): void {
|
|
169
|
+
if (core.plan === "windowed" || core.windows !== null) {
|
|
170
|
+
throw new WebGpuGraphError("E_UNSUPPORTED", "segmentedReduce: windowed cores are executed at P4", {
|
|
171
|
+
feature: "segmentedReduce.windowed",
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** The thread-per-row planner: ONE `segmented-reduce` dispatch with TIER 0 over every row. */
|
|
177
|
+
class ThreadPerRowPlanner implements SegmentedReducePlanner {
|
|
178
|
+
private readonly scope: ReduceScope;
|
|
179
|
+
private readonly kernel: Kernel;
|
|
180
|
+
private readonly hasWeights: boolean;
|
|
181
|
+
private readonly accumulate: boolean;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Wraps a compiled thread-per-row pipeline with the pattern it was compiled for.
|
|
185
|
+
* @param scope - the scope the pipeline was prepared in
|
|
186
|
+
* @param kernel - the compiled kernel
|
|
187
|
+
* @param hasWeights - the HAS_WEIGHTS the pipeline was compiled with
|
|
188
|
+
* @param accumulate - whether record() combines into out instead of overwriting
|
|
189
|
+
*/
|
|
190
|
+
constructor(scope: ReduceScope, kernel: Kernel, hasWeights: boolean, accumulate: boolean) {
|
|
191
|
+
this.scope = scope;
|
|
192
|
+
this.kernel = kernel;
|
|
193
|
+
this.hasWeights = hasWeights;
|
|
194
|
+
this.accumulate = accumulate;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Records the dispatch: rows [0, n), arcs [0, arcCount), plan1d(n); nothing for n = 0 (no zero-length binding is
|
|
199
|
+
* ever created).
|
|
200
|
+
* @param pass - the pass to record into
|
|
201
|
+
* @param core - a core with the SAME weights pattern as the one prepared (any snapshot)
|
|
202
|
+
* @param out - at least 4n bytes of f32
|
|
203
|
+
*/
|
|
204
|
+
record(pass: GPUComputePassEncoder, core: CoreBinding, out: Binding): void {
|
|
205
|
+
assertNotWindowed(core);
|
|
206
|
+
if ((core.weights !== null) !== this.hasWeights) {
|
|
207
|
+
throw new WebGpuGraphError(
|
|
208
|
+
"E_INVALID_ARGUMENT",
|
|
209
|
+
"segmentedReduce: the core's weights pattern differs from the one prepared",
|
|
210
|
+
{
|
|
211
|
+
argument: "core",
|
|
212
|
+
value: core.weights !== null,
|
|
213
|
+
expected: this.hasWeights,
|
|
214
|
+
},
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
const n = rowCountOf(core);
|
|
218
|
+
if (n === 0) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const arcCount = core.colIdx === null ? 0 : core.colIdx.size / 4;
|
|
222
|
+
if (out.size < 4 * n) {
|
|
223
|
+
throw new WebGpuGraphError(
|
|
224
|
+
"E_INVALID_ARGUMENT",
|
|
225
|
+
`segmentedReduce: out holds ${out.size} bytes, ${4 * n} needed`,
|
|
226
|
+
{
|
|
227
|
+
argument: "out",
|
|
228
|
+
value: out.size,
|
|
229
|
+
expected: `>= ${4 * n}`,
|
|
230
|
+
},
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
const params = this.scope.params(RANGE_PARAMS, {
|
|
234
|
+
start: 0,
|
|
235
|
+
end: n,
|
|
236
|
+
arcBase: 0,
|
|
237
|
+
arcEnd: arcCount,
|
|
238
|
+
accumulate: this.accumulate ? 1 : 0,
|
|
239
|
+
n,
|
|
240
|
+
});
|
|
241
|
+
const bound = this.kernel.bind({ ...graphBindings(core, null), out, P: params.binding });
|
|
242
|
+
this.kernel.dispatch(pass, bound, plan1d(n, this.scope.workgroupSize, this.scope.caps), [params.offset]);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Prepares the thread-per-row pipeline for a snapshot's dummy pattern (USE_PERM, HAS_WEIGHTS) and snippet.
|
|
248
|
+
* @param scope - the reduce scope (pipelines, pool, params writer)
|
|
249
|
+
* @param core - the core whose weights pattern selects HAS_WEIGHTS (USE_PERM is false: no tiers at P2)
|
|
250
|
+
* @param options - operator, snippet, tiers (must be null), accumulate
|
|
251
|
+
* @returns the planner
|
|
252
|
+
*/
|
|
253
|
+
export async function prepareSegmentedReduce(
|
|
254
|
+
scope: ReduceScope,
|
|
255
|
+
core: CoreBinding,
|
|
256
|
+
options: SegmentedReduceOptions,
|
|
257
|
+
): Promise<SegmentedReducePlanner> {
|
|
258
|
+
if (options.tiers !== null) {
|
|
259
|
+
throw new WebGpuGraphError("E_UNSUPPORTED", "segmentedReduce: the degree tiers land at P4; pass tiers: null", {
|
|
260
|
+
feature: "segmentedReduce.tiers",
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
assertNotWindowed(core);
|
|
264
|
+
const op = opCode(options.op);
|
|
265
|
+
validateValueSnippet(options.valueSnippet);
|
|
266
|
+
const overrides = { ...graphOverrides(core, null), OP: op, TIER: 0 };
|
|
267
|
+
const spec = kernelSpec("segmented-reduce", overrides, { VALUE: options.valueSnippet });
|
|
268
|
+
const kernel = await scope.pipelines.kernel(spec);
|
|
269
|
+
return new ThreadPerRowPlanner(scope, kernel, core.weights !== null, options.accumulate === true);
|
|
270
|
+
}
|