@graphty/webgpu-graph-algorithms 0.5.0 → 0.6.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 +104 -52
- package/dist/browser.js +1 -1
- package/dist/chunks/{context-CRbw2Wyo.js → context-BXqgCifx.js} +225 -33
- package/dist/chunks/context-BXqgCifx.js.map +1 -0
- package/dist/node.js +1 -1
- package/dist/src/accelerator.d.ts +12 -10
- package/dist/src/accelerator.d.ts.map +1 -1
- package/dist/src/accelerator.js +32 -10
- package/dist/src/accelerator.js.map +1 -1
- package/dist/src/algorithms/components.d.ts.map +1 -1
- package/dist/src/algorithms/components.js +12 -13
- package/dist/src/algorithms/components.js.map +1 -1
- package/dist/src/algorithms/degree.d.ts +6 -8
- package/dist/src/algorithms/degree.d.ts.map +1 -1
- package/dist/src/algorithms/degree.js +58 -35
- package/dist/src/algorithms/degree.js.map +1 -1
- package/dist/src/algorithms/pagerank.d.ts.map +1 -1
- package/dist/src/algorithms/pagerank.js +16 -14
- package/dist/src/algorithms/pagerank.js.map +1 -1
- package/dist/src/algorithms/power-iteration.d.ts +2 -2
- package/dist/src/algorithms/power-iteration.d.ts.map +1 -1
- package/dist/src/algorithms/power-iteration.js +17 -14
- package/dist/src/algorithms/power-iteration.js.map +1 -1
- package/dist/src/constants.d.ts +85 -8
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +85 -8
- package/dist/src/constants.js.map +1 -1
- package/dist/src/errors.d.ts +3 -2
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/errors.js +2 -1
- package/dist/src/errors.js.map +1 -1
- package/dist/src/index.d.ts +10 -5
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +14 -5
- package/dist/src/index.js.map +1 -1
- package/dist/src/kernel/dispatch.d.ts +8 -3
- package/dist/src/kernel/dispatch.d.ts.map +1 -1
- package/dist/src/kernel/dispatch.js +18 -7
- package/dist/src/kernel/dispatch.js.map +1 -1
- package/dist/src/kernel/kernel.d.ts +30 -1
- package/dist/src/kernel/kernel.d.ts.map +1 -1
- package/dist/src/kernel/kernel.js +49 -5
- package/dist/src/kernel/kernel.js.map +1 -1
- package/dist/src/kernel/prelude.d.ts.map +1 -1
- package/dist/src/kernel/prelude.js +9 -1
- package/dist/src/kernel/prelude.js.map +1 -1
- package/dist/src/kernel/profiler.d.ts +15 -3
- package/dist/src/kernel/profiler.d.ts.map +1 -1
- package/dist/src/kernel/profiler.js +27 -4
- package/dist/src/kernel/profiler.js.map +1 -1
- package/dist/src/kernels.d.ts +18 -8
- package/dist/src/kernels.d.ts.map +1 -1
- package/dist/src/kernels.js +345 -22
- package/dist/src/kernels.js.map +1 -1
- package/dist/src/layouts/calibrate.d.ts +51 -0
- package/dist/src/layouts/calibrate.d.ts.map +1 -0
- package/dist/src/layouts/calibrate.js +172 -0
- package/dist/src/layouts/calibrate.js.map +1 -0
- package/dist/src/layouts/force-simulation.d.ts +42 -5
- package/dist/src/layouts/force-simulation.d.ts.map +1 -1
- package/dist/src/layouts/force-simulation.js +84 -22
- package/dist/src/layouts/force-simulation.js.map +1 -1
- package/dist/src/layouts/forceatlas2.d.ts +107 -38
- package/dist/src/layouts/forceatlas2.d.ts.map +1 -1
- package/dist/src/layouts/forceatlas2.js +297 -290
- package/dist/src/layouts/forceatlas2.js.map +1 -1
- package/dist/src/layouts/fruchterman-reingold.d.ts +241 -0
- package/dist/src/layouts/fruchterman-reingold.d.ts.map +1 -0
- package/dist/src/layouts/fruchterman-reingold.js +739 -0
- package/dist/src/layouts/fruchterman-reingold.js.map +1 -0
- package/dist/src/layouts/model-common.d.ts +140 -0
- package/dist/src/layouts/model-common.d.ts.map +1 -0
- package/dist/src/layouts/model-common.js +269 -0
- package/dist/src/layouts/model-common.js.map +1 -0
- package/dist/src/layouts/repulsion-grid.d.ts +152 -0
- package/dist/src/layouts/repulsion-grid.d.ts.map +1 -0
- package/dist/src/layouts/repulsion-grid.js +318 -0
- package/dist/src/layouts/repulsion-grid.js.map +1 -0
- package/dist/src/layouts/spring-electrical.d.ts +224 -0
- package/dist/src/layouts/spring-electrical.d.ts.map +1 -0
- package/dist/src/layouts/spring-electrical.js +665 -0
- package/dist/src/layouts/spring-electrical.js.map +1 -0
- package/dist/src/memory/residency.d.ts +6 -2
- package/dist/src/memory/residency.d.ts.map +1 -1
- package/dist/src/memory/residency.js +84 -14
- package/dist/src/memory/residency.js.map +1 -1
- package/dist/src/primitives/core-shape.d.ts +38 -2
- package/dist/src/primitives/core-shape.d.ts.map +1 -1
- package/dist/src/primitives/core-shape.js +71 -3
- package/dist/src/primitives/core-shape.js.map +1 -1
- package/dist/src/primitives/grid-pyramid.d.ts +71 -0
- package/dist/src/primitives/grid-pyramid.d.ts.map +1 -0
- package/dist/src/primitives/grid-pyramid.js +143 -0
- package/dist/src/primitives/grid-pyramid.js.map +1 -0
- package/dist/src/primitives/grid.d.ts +118 -0
- package/dist/src/primitives/grid.d.ts.map +1 -0
- package/dist/src/primitives/grid.js +225 -0
- package/dist/src/primitives/grid.js.map +1 -0
- package/dist/src/primitives/histogram.d.ts +67 -0
- package/dist/src/primitives/histogram.d.ts.map +1 -0
- package/dist/src/primitives/histogram.js +190 -0
- package/dist/src/primitives/histogram.js.map +1 -0
- package/dist/src/primitives/radix-sort.d.ts +75 -0
- package/dist/src/primitives/radix-sort.d.ts.map +1 -0
- package/dist/src/primitives/radix-sort.js +168 -0
- package/dist/src/primitives/radix-sort.js.map +1 -0
- package/dist/src/primitives/scan.d.ts +44 -0
- package/dist/src/primitives/scan.d.ts.map +1 -0
- package/dist/src/primitives/scan.js +151 -0
- package/dist/src/primitives/scan.js.map +1 -0
- package/dist/src/primitives/segmented-reduce.d.ts +25 -17
- package/dist/src/primitives/segmented-reduce.d.ts.map +1 -1
- package/dist/src/primitives/segmented-reduce.js +166 -47
- package/dist/src/primitives/segmented-reduce.js.map +1 -1
- package/dist/src/primitives/spmv.d.ts +18 -14
- package/dist/src/primitives/spmv.d.ts.map +1 -1
- package/dist/src/primitives/spmv.js +94 -58
- package/dist/src/primitives/spmv.js.map +1 -1
- package/dist/src/primitives/verify.d.ts +49 -0
- package/dist/src/primitives/verify.d.ts.map +1 -0
- package/dist/src/primitives/verify.js +229 -0
- package/dist/src/primitives/verify.js.map +1 -0
- package/dist/src/types/accelerator.d.ts +7 -3
- package/dist/src/types/accelerator.d.ts.map +1 -1
- package/dist/src/types/context.d.ts +53 -0
- package/dist/src/types/context.d.ts.map +1 -1
- package/dist/src/types/layout.d.ts +52 -0
- package/dist/src/types/layout.d.ts.map +1 -1
- package/dist/src/types/options.d.ts +43 -1
- package/dist/src/types/options.d.ts.map +1 -1
- package/dist/src/wgsl/counting-scatter.wgsl.d.ts +8 -0
- package/dist/src/wgsl/counting-scatter.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/counting-scatter.wgsl.js +17 -0
- package/dist/src/wgsl/counting-scatter.wgsl.js.map +1 -0
- package/dist/src/wgsl/fa2-attraction.wgsl.d.ts +23 -8
- package/dist/src/wgsl/fa2-attraction.wgsl.d.ts.map +1 -1
- package/dist/src/wgsl/fa2-attraction.wgsl.js +100 -17
- package/dist/src/wgsl/fa2-attraction.wgsl.js.map +1 -1
- package/dist/src/wgsl/fa2-integrate.wgsl.d.ts +7 -2
- package/dist/src/wgsl/fa2-integrate.wgsl.d.ts.map +1 -1
- package/dist/src/wgsl/fa2-integrate.wgsl.js +28 -2
- package/dist/src/wgsl/fa2-integrate.wgsl.js.map +1 -1
- package/dist/src/wgsl/fa2-repulsion-exact.wgsl.d.ts +4 -2
- package/dist/src/wgsl/fa2-repulsion-exact.wgsl.d.ts.map +1 -1
- package/dist/src/wgsl/fa2-repulsion-exact.wgsl.js +14 -5
- package/dist/src/wgsl/fa2-repulsion-exact.wgsl.js.map +1 -1
- package/dist/src/wgsl/fa2-stats-finalize.wgsl.d.ts +12 -1
- package/dist/src/wgsl/fa2-stats-finalize.wgsl.d.ts.map +1 -1
- package/dist/src/wgsl/fa2-stats-finalize.wgsl.js +54 -0
- package/dist/src/wgsl/fa2-stats-finalize.wgsl.js.map +1 -1
- package/dist/src/wgsl/grid-cell-key.wgsl.d.ts +8 -0
- package/dist/src/wgsl/grid-cell-key.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/grid-cell-key.wgsl.js +30 -0
- package/dist/src/wgsl/grid-cell-key.wgsl.js.map +1 -0
- package/dist/src/wgsl/grid-centroid-hub.wgsl.d.ts +8 -0
- package/dist/src/wgsl/grid-centroid-hub.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/grid-centroid-hub.wgsl.js +29 -0
- package/dist/src/wgsl/grid-centroid-hub.wgsl.js.map +1 -0
- package/dist/src/wgsl/grid-centroid.wgsl.d.ts +8 -0
- package/dist/src/wgsl/grid-centroid.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/grid-centroid.wgsl.js +29 -0
- package/dist/src/wgsl/grid-centroid.wgsl.js.map +1 -0
- package/dist/src/wgsl/grid-downsample.wgsl.d.ts +7 -0
- package/dist/src/wgsl/grid-downsample.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/grid-downsample.wgsl.js +28 -0
- package/dist/src/wgsl/grid-downsample.wgsl.js.map +1 -0
- package/dist/src/wgsl/grid-far-field.wgsl.d.ts +13 -0
- package/dist/src/wgsl/grid-far-field.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/grid-far-field.wgsl.js +98 -0
- package/dist/src/wgsl/grid-far-field.wgsl.js.map +1 -0
- package/dist/src/wgsl/grid-near-field.wgsl.d.ts +19 -0
- package/dist/src/wgsl/grid-near-field.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/grid-near-field.wgsl.js +129 -0
- package/dist/src/wgsl/grid-near-field.wgsl.js.map +1 -0
- package/dist/src/wgsl/histogram.wgsl.d.ts +7 -0
- package/dist/src/wgsl/histogram.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/histogram.wgsl.js +15 -0
- package/dist/src/wgsl/histogram.wgsl.js.map +1 -0
- package/dist/src/wgsl/indirect-finalize.wgsl.d.ts +8 -0
- package/dist/src/wgsl/indirect-finalize.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/indirect-finalize.wgsl.js +26 -0
- package/dist/src/wgsl/indirect-finalize.wgsl.js.map +1 -0
- package/dist/src/wgsl/radix-hist.wgsl.d.ts +9 -0
- package/dist/src/wgsl/radix-hist.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/radix-hist.wgsl.js +31 -0
- package/dist/src/wgsl/radix-hist.wgsl.js.map +1 -0
- package/dist/src/wgsl/radix-scatter.wgsl.d.ts +9 -0
- package/dist/src/wgsl/radix-scatter.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/radix-scatter.wgsl.js +40 -0
- package/dist/src/wgsl/radix-scatter.wgsl.js.map +1 -0
- package/dist/src/wgsl/scan-add.wgsl.d.ts +6 -0
- package/dist/src/wgsl/scan-add.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/scan-add.wgsl.js +14 -0
- package/dist/src/wgsl/scan-add.wgsl.js.map +1 -0
- package/dist/src/wgsl/scan-block.wgsl.d.ts +8 -0
- package/dist/src/wgsl/scan-block.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/scan-block.wgsl.js +30 -0
- package/dist/src/wgsl/scan-block.wgsl.js.map +1 -0
- package/dist/src/wgsl/segmented-reduce.wgsl.d.ts +22 -8
- package/dist/src/wgsl/segmented-reduce.wgsl.d.ts.map +1 -1
- package/dist/src/wgsl/segmented-reduce.wgsl.js +84 -15
- package/dist/src/wgsl/segmented-reduce.wgsl.js.map +1 -1
- package/dist/src/wgsl/spmv-pull.wgsl.d.ts +22 -11
- package/dist/src/wgsl/spmv-pull.wgsl.d.ts.map +1 -1
- package/dist/src/wgsl/spmv-pull.wgsl.js +110 -36
- package/dist/src/wgsl/spmv-pull.wgsl.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/webgpu-graph-algorithms.js +5016 -1130
- package/dist/webgpu-graph-algorithms.js.map +1 -1
- package/package.json +10 -7
- package/src/accelerator.ts +46 -12
- package/src/algorithms/components.ts +12 -16
- package/src/algorithms/degree.ts +58 -43
- package/src/algorithms/pagerank.ts +20 -18
- package/src/algorithms/power-iteration.ts +19 -18
- package/src/constants.ts +108 -8
- package/src/errors.ts +3 -1
- package/src/index.ts +25 -5
- package/src/kernel/dispatch.ts +18 -7
- package/src/kernel/kernel.ts +59 -5
- package/src/kernel/prelude.ts +15 -0
- package/src/kernel/profiler.ts +28 -4
- package/src/kernels.ts +378 -24
- package/src/layouts/calibrate.ts +187 -0
- package/src/layouts/force-simulation.ts +111 -26
- package/src/layouts/forceatlas2.ts +346 -324
- package/src/layouts/fruchterman-reingold.ts +918 -0
- package/src/layouts/model-common.ts +323 -0
- package/src/layouts/repulsion-grid.ts +451 -0
- package/src/layouts/spring-electrical.ts +845 -0
- package/src/memory/residency.ts +126 -20
- package/src/primitives/core-shape.ts +91 -4
- package/src/primitives/grid-pyramid.ts +221 -0
- package/src/primitives/grid.ts +349 -0
- package/src/primitives/histogram.ts +273 -0
- package/src/primitives/radix-sort.ts +246 -0
- package/src/primitives/scan.ts +197 -0
- package/src/primitives/segmented-reduce.ts +214 -56
- package/src/primitives/spmv.ts +125 -65
- package/src/primitives/verify.ts +249 -0
- package/src/types/accelerator.ts +15 -3
- package/src/types/context.ts +56 -0
- package/src/types/layout.ts +58 -0
- package/src/types/options.ts +45 -1
- package/src/wgsl/counting-scatter.wgsl.ts +16 -0
- package/src/wgsl/fa2-attraction.wgsl.ts +100 -17
- package/src/wgsl/fa2-integrate.wgsl.ts +28 -2
- package/src/wgsl/fa2-repulsion-exact.wgsl.ts +14 -5
- package/src/wgsl/fa2-stats-finalize.wgsl.ts +54 -0
- package/src/wgsl/grid-cell-key.wgsl.ts +29 -0
- package/src/wgsl/grid-centroid-hub.wgsl.ts +28 -0
- package/src/wgsl/grid-centroid.wgsl.ts +28 -0
- package/src/wgsl/grid-downsample.wgsl.ts +27 -0
- package/src/wgsl/grid-far-field.wgsl.ts +97 -0
- package/src/wgsl/grid-near-field.wgsl.ts +128 -0
- package/src/wgsl/histogram.wgsl.ts +14 -0
- package/src/wgsl/indirect-finalize.wgsl.ts +25 -0
- package/src/wgsl/radix-hist.wgsl.ts +30 -0
- package/src/wgsl/radix-scatter.wgsl.ts +39 -0
- package/src/wgsl/scan-add.wgsl.ts +13 -0
- package/src/wgsl/scan-block.wgsl.ts +29 -0
- package/src/wgsl/segmented-reduce.wgsl.ts +84 -15
- package/src/wgsl/spmv-pull.wgsl.ts +110 -36
- package/dist/chunks/context-CRbw2Wyo.js.map +0 -1
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The device self-check: one real exclusive scan of known numbers, run once per device before this package
|
|
3
|
+
* computes anything on it, so a device that returns wrong numbers is refused instead of believed.
|
|
4
|
+
*
|
|
5
|
+
* WHY IT EXISTS. On the Windows host lane -- Dawn's D3D12 backend over the Microsoft Basic Render Driver, the
|
|
6
|
+
* software renderer built into Windows -- every exclusive scan of more than one workgroup came back wrong: a
|
|
7
|
+
* block total of 1 where 256 belonged, or a second block nothing wrote at all, differing between two runs of
|
|
8
|
+
* identical code in one job. Everything built on the scan was wrong with it (the histogram, the radix sort, the
|
|
9
|
+
* grid build and every layout above them, eighteen test files), while every single-block scan and every earlier
|
|
10
|
+
* primitive stayed right. Three shader repairs failed; copying Microsoft's current redistributable renderer
|
|
11
|
+
* beside the runner's Node executable made all of it pass untouched. The defect is the driver's, and the danger
|
|
12
|
+
* to a caller is not a crash but silence: a graph laid out from wrong numbers with no error anywhere.
|
|
13
|
+
*
|
|
14
|
+
* WHY IT SCANS RATHER THAN PROBING A CONSTRUCT. An earlier hand-written probe matrix of workgroup constructs
|
|
15
|
+
* (test/primitives/workgroup-id-probe.test.ts) was measured PASSING on that renderer in the same job in which the
|
|
16
|
+
* real scan returned the wrong answer, so a copy of the shader's shape is not evidence about the shader. This
|
|
17
|
+
* check therefore drives the shipped `prepareScan` through the shipped composer and pipeline cache: it tests the
|
|
18
|
+
* code that runs, and it cannot drift away from it. One scan covers both recorded failures -- a value crossing
|
|
19
|
+
* `workgroupBarrier` inside a block, and a block total crossing from one dispatch of a compute pass to the next
|
|
20
|
+
* dispatch that reads it, which is the shape of every driver in this package.
|
|
21
|
+
*
|
|
22
|
+
* WHY IT REFUSES RATHER THAN ROUTING AROUND THE DEFECT. See docs/decisions/device-self-check.md: a barrier-free second
|
|
23
|
+
* implementation was designed and costed, and rejected because it would ship unverified on the only platform it
|
|
24
|
+
* exists for.
|
|
25
|
+
*
|
|
26
|
+
* WHERE IT RUNS. Lazily, at the first compute entry point a caller reaches, memoised per GPUDevice: the
|
|
27
|
+
* algorithms (degree, connectedComponents, the power-iteration family) and calibrateLayout await it on entry, and
|
|
28
|
+
* a layout simulation awaits it in the compile-and-bind promise `load()` starts -- the promise every batch and
|
|
29
|
+
* every debug run already awaits before it may submit. So a caller is covered whether the context was created,
|
|
30
|
+
* adopted through `create({ device })` or adopted through `GpuContext.from`, and a simulation refuses before its
|
|
31
|
+
* first iteration without the check sitting on the per-batch path, where an extra await would move the moment a
|
|
32
|
+
* submission becomes visible to a step() issued in the same tick and so change how the frame loop coalesces.
|
|
33
|
+
* It deliberately does NOT run in GpuContext.create:
|
|
34
|
+
* a context that only compiles -- Dawn's null backend, which computes nothing by design -- must stay usable, and
|
|
35
|
+
* `src/context.ts` may not import a primitive (the layer rule of spec 3.2).
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { WebGpuGraphError } from "../errors.js";
|
|
39
|
+
import { UniformRing } from "../kernel/uniform-ring.js";
|
|
40
|
+
import { type CheckedContext, type DeviceCheck, type DeviceCheckMismatch } from "../types/context.js";
|
|
41
|
+
import { type Binding } from "../types/memory.js";
|
|
42
|
+
import { type ReduceScope } from "./reduce.js";
|
|
43
|
+
import { prepareScan } from "./scan.js";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The word written into every output slot before the dispatch, so a word NOTHING wrote is distinguishable from
|
|
47
|
+
* a word written wrongly. The Windows lane returned this value from the second block, which is how the failure
|
|
48
|
+
* was read as "the block was never written" rather than "the block was miscomputed".
|
|
49
|
+
*/
|
|
50
|
+
const POISON = 0xdeadbeef;
|
|
51
|
+
|
|
52
|
+
/** Full workgroups the check scans; one partial workgroup of a single word follows them. */
|
|
53
|
+
const CHECK_BLOCKS = 32;
|
|
54
|
+
|
|
55
|
+
/** Params slots the scan's levels need (two levels of block scan plus one add-back, with room to spare). */
|
|
56
|
+
const RING_SLOTS = 8;
|
|
57
|
+
|
|
58
|
+
/** The settled check per device: the one work a second caller never repeats. A rejection is dropped, so a check that failed to RUN is retried. */
|
|
59
|
+
const checked = new WeakMap<GPUDevice, Promise<DeviceCheck>>();
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The input word at `i`: 1, 2, 3, ... so no two workgroups have the same block total (all-ones would give every
|
|
63
|
+
* block the same one, and a block total stored at the wrong index would still read correct -- which is exactly
|
|
64
|
+
* the Windows symptom) and every output word is a different number.
|
|
65
|
+
* @param i - the word index
|
|
66
|
+
* @returns the input value
|
|
67
|
+
*/
|
|
68
|
+
function inputAt(i: number): number {
|
|
69
|
+
return i + 1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The verdict: the first output word or total that disagrees with the arithmetic answer, or null when nothing
|
|
74
|
+
* does. Pure, so test/primitives/verify.test.ts can drive it past a fabricated wrong result and exercise the
|
|
75
|
+
* refusal without a broken device.
|
|
76
|
+
* @param words - the `count` output words read back from the device
|
|
77
|
+
* @param total - the total word read back from the device
|
|
78
|
+
* @param count - the words scanned
|
|
79
|
+
* @returns the first disagreement, or null
|
|
80
|
+
* @internal
|
|
81
|
+
*/
|
|
82
|
+
export function checkScanWords(words: Uint32Array, total: number, count: number): DeviceCheckMismatch | null {
|
|
83
|
+
let running = 0;
|
|
84
|
+
for (let i = 0; i < count; i++) {
|
|
85
|
+
const got = words[i];
|
|
86
|
+
if (got !== running) {
|
|
87
|
+
return { where: `out[${String(i)}]`, expected: running, actual: got, poison: got === POISON };
|
|
88
|
+
}
|
|
89
|
+
// u32 addition wraps in WGSL, so the host oracle wraps too: at WORKGROUP_SIZE 256 the total cannot
|
|
90
|
+
// reach 2^32, but a false refusal would deny every device on earth, so it is not left to that constant
|
|
91
|
+
running = (running + inputAt(i)) >>> 0;
|
|
92
|
+
}
|
|
93
|
+
if (total !== running) {
|
|
94
|
+
return { where: "total", expected: running, actual: total, poison: total === POISON };
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Binds a whole scratch buffer.
|
|
101
|
+
* @param buffer - the buffer
|
|
102
|
+
* @param size - the bytes bound
|
|
103
|
+
* @returns the binding
|
|
104
|
+
*/
|
|
105
|
+
function whole(buffer: GPUBuffer, size: number): Binding {
|
|
106
|
+
return { buffer, offset: 0, size, window: null };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Runs the check once: one scan of `CHECK_BLOCKS * workgroupSize + 1` words recorded into one compute pass, one
|
|
111
|
+
* submit, the output and the total read back and verified on the host.
|
|
112
|
+
*
|
|
113
|
+
* It never throws for a wrong ANSWER -- that is the returned record's job. A failure of the machinery (a lost
|
|
114
|
+
* device, a validation error, out of memory) propagates with its own code, so "the driver computes incorrectly"
|
|
115
|
+
* is never said about a device that merely died.
|
|
116
|
+
* @param ctx - the context to check
|
|
117
|
+
* @returns what the check found
|
|
118
|
+
*/
|
|
119
|
+
async function runCheck(ctx: CheckedContext): Promise<DeviceCheck> {
|
|
120
|
+
const started = performance.now();
|
|
121
|
+
const wg = ctx.workgroupSize;
|
|
122
|
+
const count = CHECK_BLOCKS * wg + 1;
|
|
123
|
+
const bytes = 4 * count;
|
|
124
|
+
const lease = ctx.pool.lease();
|
|
125
|
+
const ring = new UniformRing(ctx.device, ctx.allocator, RING_SLOTS, "device-check/ring");
|
|
126
|
+
try {
|
|
127
|
+
const scope: ReduceScope = {
|
|
128
|
+
device: ctx.device,
|
|
129
|
+
caps: ctx.caps,
|
|
130
|
+
pipelines: ctx.pipelines,
|
|
131
|
+
pool: ctx.pool,
|
|
132
|
+
workgroupSize: wg,
|
|
133
|
+
scratch: (byteLength, label) => lease.storage(byteLength, `device-check/${label}`),
|
|
134
|
+
params(block, values) {
|
|
135
|
+
const slot = ring.reserve(1);
|
|
136
|
+
ring.write(slot, block, values);
|
|
137
|
+
return { binding: ring.binding(block), offset: ring.offsetOf(slot) };
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
const planner = await prepareScan(scope);
|
|
141
|
+
const src = lease.storage(bytes, "device-check/src");
|
|
142
|
+
const out = lease.storage(bytes, "device-check/out");
|
|
143
|
+
const input = new Uint32Array(count);
|
|
144
|
+
for (let i = 0; i < count; i++) {
|
|
145
|
+
input[i] = inputAt(i);
|
|
146
|
+
}
|
|
147
|
+
ctx.device.queue.writeBuffer(src, 0, input);
|
|
148
|
+
ctx.device.queue.writeBuffer(out, 0, new Uint32Array(count).fill(POISON));
|
|
149
|
+
const encoder = ctx.device.createCommandEncoder({ label: "device-check" });
|
|
150
|
+
const pass = encoder.beginComputePass({ label: "device-check" });
|
|
151
|
+
const total = planner.record(pass, whole(src, bytes), count, whole(out, bytes));
|
|
152
|
+
pass.end();
|
|
153
|
+
ring.flush();
|
|
154
|
+
ctx.device.queue.submit([encoder.finish()]);
|
|
155
|
+
const words = new Uint32Array(await ctx.readback.read(out, bytes));
|
|
156
|
+
const totalWord = new Uint32Array(
|
|
157
|
+
await ctx.readback.read(total.binding.buffer, 4, undefined, total.binding.offset + 4 * total.index),
|
|
158
|
+
);
|
|
159
|
+
const mismatch = checkScanWords(words, totalWord[0], count);
|
|
160
|
+
return {
|
|
161
|
+
check: "exclusive-scan",
|
|
162
|
+
ok: mismatch === null,
|
|
163
|
+
workgroupSize: wg,
|
|
164
|
+
count,
|
|
165
|
+
blocks: CHECK_BLOCKS,
|
|
166
|
+
ms: performance.now() - started,
|
|
167
|
+
vendor: ctx.caps.vendor,
|
|
168
|
+
architecture: ctx.caps.architecture,
|
|
169
|
+
description: ctx.caps.description,
|
|
170
|
+
mismatch,
|
|
171
|
+
};
|
|
172
|
+
} finally {
|
|
173
|
+
ring.destroy();
|
|
174
|
+
lease.release();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* What this device computed when it was asked for an answer this package already knows, run once per device and
|
|
180
|
+
* remembered. A caller may await it before committing work to a context; every compute entry point of the
|
|
181
|
+
* package awaits it too, so the cost is paid once whoever asks first.
|
|
182
|
+
*
|
|
183
|
+
* It reports rather than throws, so a caller can ask a device a question without having to catch the answer. The
|
|
184
|
+
* entry points use `assertDeviceComputes`, which turns a failing report into E_DEVICE_INCORRECT.
|
|
185
|
+
* @param ctx - the context whose device is checked
|
|
186
|
+
* @returns the capability record; `ok` false means this device returns wrong numbers
|
|
187
|
+
*/
|
|
188
|
+
export function verifyDevice(ctx: CheckedContext): Promise<DeviceCheck> {
|
|
189
|
+
const cached = checked.get(ctx.device);
|
|
190
|
+
if (cached !== undefined) {
|
|
191
|
+
return cached;
|
|
192
|
+
}
|
|
193
|
+
const running = runCheck(ctx).catch((err: unknown) => {
|
|
194
|
+
// only a settled verdict is worth keeping: a check that could not RUN (a lost device, a transient
|
|
195
|
+
// out-of-memory) must not answer for the device forever
|
|
196
|
+
checked.delete(ctx.device);
|
|
197
|
+
throw err;
|
|
198
|
+
});
|
|
199
|
+
checked.set(ctx.device, running);
|
|
200
|
+
return running;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The guard every compute entry point awaits: resolves on a device that computed the check correctly, throws
|
|
205
|
+
* E_DEVICE_INCORRECT on one that did not, carrying the first wrong word, what was required there, and the
|
|
206
|
+
* adapter strings that separate a broken driver from a fixed one.
|
|
207
|
+
* @param ctx - the context about to be computed on
|
|
208
|
+
* @internal
|
|
209
|
+
*/
|
|
210
|
+
export async function assertDeviceComputes(ctx: CheckedContext): Promise<void> {
|
|
211
|
+
const check = await verifyDevice(ctx);
|
|
212
|
+
const { mismatch } = check;
|
|
213
|
+
if (mismatch === null) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const adapter = {
|
|
217
|
+
vendor: check.vendor,
|
|
218
|
+
architecture: check.architecture,
|
|
219
|
+
device: ctx.caps.device,
|
|
220
|
+
description: check.description,
|
|
221
|
+
};
|
|
222
|
+
const hint =
|
|
223
|
+
check.architecture === "warp"
|
|
224
|
+
? "this is the Microsoft Basic Render Driver; the Microsoft.Direct3D.WARP redistributable (1.0.21 or newer) beside the host executable computes it correctly"
|
|
225
|
+
: undefined;
|
|
226
|
+
const observed = mismatch.poison
|
|
227
|
+
? `${mismatch.where} was never written (the ${String(POISON)} the check wrote beforehand survived the dispatch)`
|
|
228
|
+
: `${mismatch.where} came back as ${String(mismatch.actual)} where ${String(mismatch.expected)} was required`;
|
|
229
|
+
const message =
|
|
230
|
+
`this device computes multi-workgroup shaders incorrectly: an exclusive scan of ${String(check.count)} ` +
|
|
231
|
+
`known numbers is wrong -- ${observed}. Refusing to run: every number this package computed here would ` +
|
|
232
|
+
`be unreliable. adapter: ${adapter.vendor}/${adapter.architecture} "${adapter.description}"`;
|
|
233
|
+
throw new WebGpuGraphError(
|
|
234
|
+
"E_DEVICE_INCORRECT",
|
|
235
|
+
hint === undefined ? message : `${message}. ${hint}`,
|
|
236
|
+
{
|
|
237
|
+
check: check.check,
|
|
238
|
+
where: mismatch.where,
|
|
239
|
+
expected: mismatch.expected,
|
|
240
|
+
actual: mismatch.actual,
|
|
241
|
+
poison: mismatch.poison,
|
|
242
|
+
count: check.count,
|
|
243
|
+
blocks: check.blocks,
|
|
244
|
+
workgroupSize: check.workgroupSize,
|
|
245
|
+
adapter,
|
|
246
|
+
...(hint === undefined ? {} : { hint }),
|
|
247
|
+
},
|
|
248
|
+
);
|
|
249
|
+
}
|
package/src/types/accelerator.ts
CHANGED
|
@@ -37,8 +37,14 @@ import type {
|
|
|
37
37
|
KatzOptions,
|
|
38
38
|
PageRankOptions,
|
|
39
39
|
} from "./algorithms.js";
|
|
40
|
-
import type {
|
|
41
|
-
|
|
40
|
+
import type {
|
|
41
|
+
ForceAtlas2Stats,
|
|
42
|
+
FruchtermanReingoldStats,
|
|
43
|
+
GpuLayoutSimulation,
|
|
44
|
+
GpuLayoutTuning,
|
|
45
|
+
SpringElectricalStats,
|
|
46
|
+
} from "./layout.js";
|
|
47
|
+
import type { ForceAtlas2Options, FruchtermanReingoldOptions, SpringElectricalOptions } from "./options.js";
|
|
42
48
|
|
|
43
49
|
// ---- the real @graphty/layout interfaces (spec 9.3, D27): imported at W1b, re-exported so the package's public
|
|
44
50
|
// surface is unchanged and src/types/layout.ts keeps resolving them from here. `export type`, never a bare
|
|
@@ -86,7 +92,9 @@ export interface AcceleratorOptions {
|
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
/**
|
|
89
|
-
* The injectable object (spec 3.3): P3's forceAtlas2, release and dispose,
|
|
95
|
+
* The injectable object (spec 3.3): P3's forceAtlas2, release and dispose, P5's fruchtermanReingold and
|
|
96
|
+
* springElectrical (the two other optional members of the real LayoutAccelerator, spec 9.3; the CPU option types in,
|
|
97
|
+
* the GPU simulations out), plus P7's seven algorithm members
|
|
90
98
|
* (spec 8.2, 8.3; M8b-T8), non-optional here and returning the `Gpu*Result` shapes, which satisfy the `*ResultLike`
|
|
91
99
|
* mirrors (spec 9.7: `precision` is an extra field, `F32` is a `NumericVector`). `connectedComponents` and
|
|
92
100
|
* `weaklyConnectedComponents` are the same algorithm (spec 3.3: WCC semantics on directed input) under both names
|
|
@@ -100,6 +108,10 @@ export interface GpuAccelerator extends AlgorithmAccelerator, LayoutAccelerator
|
|
|
100
108
|
readonly ctx: GpuContext;
|
|
101
109
|
readonly options: Readonly<AcceleratorOptions>;
|
|
102
110
|
forceAtlas2(options?: ForceAtlas2Options): GpuLayoutSimulation<ForceAtlas2Options, ForceAtlas2Stats>;
|
|
111
|
+
fruchtermanReingold(
|
|
112
|
+
options?: FruchtermanReingoldOptions,
|
|
113
|
+
): GpuLayoutSimulation<FruchtermanReingoldOptions, FruchtermanReingoldStats>;
|
|
114
|
+
springElectrical(options?: SpringElectricalOptions): GpuLayoutSimulation<SpringElectricalOptions, SpringElectricalStats>;
|
|
103
115
|
pageRank(s: GraphSnapshot, options?: PageRankOptions): Promise<GpuPageRankResult>;
|
|
104
116
|
personalizedPageRank(
|
|
105
117
|
s: GraphSnapshot,
|
package/src/types/context.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* package eslint config). P0-T3 wrote AdapterInfoLike and AdapterSummary; P1-T1 completes the file.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import type { GpuContext } from "../context.js";
|
|
7
8
|
import type { WebGpuGraphError } from "../errors.js";
|
|
8
9
|
|
|
9
10
|
/** The limits `"raise"` takes from the adapter (spec 2.2); maxComputeWorkgroupsPerDimension is deliberately absent. */
|
|
@@ -133,3 +134,58 @@ export interface PlanCaps {
|
|
|
133
134
|
export interface GpuDebugFlags {
|
|
134
135
|
inspect: boolean;
|
|
135
136
|
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The context the device self-check runs on. `src/primitives/**` may not name GpuContext (the layer rule of
|
|
140
|
+
* spec 3.2), and this file may, so the alias is how src/primitives/verify.ts types its argument. It is
|
|
141
|
+
* GpuContext and nothing else: pass the context you were going to compute on.
|
|
142
|
+
*/
|
|
143
|
+
export type CheckedContext = GpuContext;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The first word a device got wrong in the self-check. `poison` is true when the word still held the
|
|
147
|
+
* 0xdeadbeef the check wrote before the dispatch, which means NOTHING wrote it -- a different failure from a
|
|
148
|
+
* word written with the wrong value, and the distinction that made the Windows diagnosis possible.
|
|
149
|
+
*/
|
|
150
|
+
export interface DeviceCheckMismatch {
|
|
151
|
+
/** Where it was found: `out[<index>]`, or `total`. */
|
|
152
|
+
readonly where: string;
|
|
153
|
+
/** The value arithmetic requires there. */
|
|
154
|
+
readonly expected: number;
|
|
155
|
+
/** The value the device returned. */
|
|
156
|
+
readonly actual: number;
|
|
157
|
+
/** True when `actual` is the poison word the check wrote beforehand: nothing wrote this word. */
|
|
158
|
+
readonly poison: boolean;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* What one device self-check found (spec 3.3): the capability record a caller may read BEFORE it commits work
|
|
163
|
+
* to a device, through `verifyDevice`. `mismatch === null` (equivalently `ok`) means every one of `count`
|
|
164
|
+
* output words and the total matched the arithmetic answer; a non-null `mismatch` is the device returning
|
|
165
|
+
* wrong numbers, which every compute entry point of this package turns into E_DEVICE_INCORRECT.
|
|
166
|
+
*
|
|
167
|
+
* It reports one property -- that values crossing a workgroup barrier and block totals crossing dispatches of
|
|
168
|
+
* one compute pass survive -- and is therefore a refusal mechanism, not a certificate of correctness.
|
|
169
|
+
*/
|
|
170
|
+
export interface DeviceCheck {
|
|
171
|
+
/** The check that ran; one today, named so a second can be added without changing the shape. */
|
|
172
|
+
readonly check: "exclusive-scan";
|
|
173
|
+
/** True when nothing disagreed. */
|
|
174
|
+
readonly ok: boolean;
|
|
175
|
+
/** The device's workgroup size (spec 5.1). */
|
|
176
|
+
readonly workgroupSize: number;
|
|
177
|
+
/** The words scanned: `blocks * workgroupSize + 1`, so the last workgroup is a partial one. */
|
|
178
|
+
readonly count: number;
|
|
179
|
+
/** The full workgroups of the scan (one partial one follows). */
|
|
180
|
+
readonly blocks: number;
|
|
181
|
+
/** Wall time of this check in milliseconds, pipeline compile included on the first call. */
|
|
182
|
+
readonly ms: number;
|
|
183
|
+
/** caps.vendor of the device that ran it. */
|
|
184
|
+
readonly vendor: string;
|
|
185
|
+
/** caps.architecture of the device that ran it. */
|
|
186
|
+
readonly architecture: string;
|
|
187
|
+
/** caps.description: the string that separates a broken driver from a fixed one. */
|
|
188
|
+
readonly description: string;
|
|
189
|
+
/** The first disagreement, or null when there was none. */
|
|
190
|
+
readonly mismatch: DeviceCheckMismatch | null;
|
|
191
|
+
}
|
package/src/types/layout.ts
CHANGED
|
@@ -43,6 +43,42 @@ export interface ForceAtlas2Stats extends LayoutStatsBase {
|
|
|
43
43
|
readonly trace: ReadonlyArray<ForceAtlas2TraceRecord>;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* One per-iteration trace record of the last completed batch of a Fruchterman-Reingold simulation (spec 3.3
|
|
48
|
+
* FruchtermanReingoldStats.trace element): K1 writes `temperature` from the iteration's uniform slot (spec 7.20).
|
|
49
|
+
* @public
|
|
50
|
+
*/
|
|
51
|
+
export interface FruchtermanReingoldTraceRecord {
|
|
52
|
+
readonly temperature: number;
|
|
53
|
+
readonly meanDisplacement: number;
|
|
54
|
+
readonly settledCount: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Spec 3.3 FruchtermanReingoldStats, verbatim: the cooling schedule's value replaces the controller fields. */
|
|
58
|
+
export interface FruchtermanReingoldStats extends LayoutStatsBase {
|
|
59
|
+
readonly temperature: number;
|
|
60
|
+
readonly trace: ReadonlyArray<FruchtermanReingoldTraceRecord>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* One per-iteration trace record of the last completed batch of a spring-electrical simulation (spec 3.3
|
|
65
|
+
* SpringElectricalStats.trace element): `kineticEnergy` is `0.5 * sum m |v|^2` over the free nodes after the
|
|
66
|
+
* PREVIOUS iteration's integrate: K5 writes it into partials B and the NEXT iteration's K1 folds it (PD-4), so the
|
|
67
|
+
* first record after load() carries 0 and record i carries the energy of iteration i - 1.
|
|
68
|
+
* @public
|
|
69
|
+
*/
|
|
70
|
+
export interface SpringElectricalTraceRecord {
|
|
71
|
+
readonly kineticEnergy: number;
|
|
72
|
+
readonly meanDisplacement: number;
|
|
73
|
+
readonly settledCount: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Spec 3.3 SpringElectricalStats, verbatim; `kineticEnergy` is the last folded value, one iteration behind the last integrate (PD-4). */
|
|
77
|
+
export interface SpringElectricalStats extends LayoutStatsBase {
|
|
78
|
+
readonly kineticEnergy: number;
|
|
79
|
+
readonly trace: ReadonlyArray<SpringElectricalTraceRecord>;
|
|
80
|
+
}
|
|
81
|
+
|
|
46
82
|
/** Options of GpuLayoutSimulation.run (spec 3.3). */
|
|
47
83
|
export interface RunOptions {
|
|
48
84
|
readonly maxIter?: number | undefined;
|
|
@@ -101,3 +137,25 @@ export interface ResolvedLayoutTuning {
|
|
|
101
137
|
readonly extentFactor: number;
|
|
102
138
|
readonly compat: "paper" | "networkx";
|
|
103
139
|
}
|
|
140
|
+
|
|
141
|
+
/** Spec 2.2 CalibrateOptions, verbatim: the probe sizes of calibrateLayout (default 8k / 16k / 32k / 65k). */
|
|
142
|
+
export interface CalibrateOptions {
|
|
143
|
+
readonly sizes?: readonly number[] | undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Spec 2.2 GpuCalibration, verbatim: the per-size ms per iteration of both repulsion tiers on the actual device,
|
|
148
|
+
* `pairsPerSecond` of the exact tier at the largest probed size, `suggestedExactMaxNodes` by the spec 7.8 rule
|
|
149
|
+
* (the largest probed n with exactMs(n) <= min(4 ms, gridMs(n)), rounded down to a power of two; when NO probed size
|
|
150
|
+
* qualifies -- the grid tier faster at every probe, or every probe over the budget -- the largest power of two strictly
|
|
151
|
+
* below the smallest probe, so the exact tier runs at no probed size and a value below floorPow2(min(sizes)) tells the
|
|
152
|
+
* caller the rule found nothing in the range: src/layouts/calibrate.ts) and the wall time of the whole call (pipeline
|
|
153
|
+
* compilation included on the first call).
|
|
154
|
+
*/
|
|
155
|
+
export interface GpuCalibration {
|
|
156
|
+
readonly pairsPerSecond: number;
|
|
157
|
+
readonly exactMsPerIter: Readonly<Record<number, number>>;
|
|
158
|
+
readonly gridMsPerIter: Readonly<Record<number, number>>;
|
|
159
|
+
readonly suggestedExactMaxNodes: number;
|
|
160
|
+
readonly firstCallMs: number;
|
|
161
|
+
}
|
package/src/types/options.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* import.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type { F32, NodeId } from "@graphty/graph-format";
|
|
9
|
+
import type { F32, NodeId, NodeMask } from "@graphty/graph-format";
|
|
10
10
|
import type {
|
|
11
11
|
CommonLayoutOptions,
|
|
12
12
|
ForceAtlas2Options,
|
|
@@ -51,3 +51,47 @@ export interface ResolvedForceAtlas2Options {
|
|
|
51
51
|
readonly iterationsPerStep: number;
|
|
52
52
|
readonly maxInFlight: number;
|
|
53
53
|
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The resolved (defaults applied) Fruchterman-Reingold option record (spec 7.20, 9.3): `k` null means `1 / sqrt(n)`
|
|
57
|
+
* at load; `fixed` is applied at load through ModelInputs.fixed (PD-6). Exported for src/layouts/fruchterman-reingold.ts
|
|
58
|
+
* and the option tests.
|
|
59
|
+
* @public
|
|
60
|
+
*/
|
|
61
|
+
export interface ResolvedFruchtermanReingoldOptions {
|
|
62
|
+
readonly k: number | null;
|
|
63
|
+
readonly iterations: number;
|
|
64
|
+
readonly cooling: "linear" | "adaptive";
|
|
65
|
+
readonly fixed: NodeMask | string | null;
|
|
66
|
+
readonly dim: 2 | 3;
|
|
67
|
+
readonly scale: number;
|
|
68
|
+
readonly center: readonly [number, number, number];
|
|
69
|
+
readonly seed: number | null;
|
|
70
|
+
readonly settleThreshold: number;
|
|
71
|
+
readonly settleWindow: number;
|
|
72
|
+
readonly iterationsPerStep: number;
|
|
73
|
+
readonly maxInFlight: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The resolved spring-electrical option record (spec 7.20, 9.3; ngraph's names): `gravity` is the Coulomb constant
|
|
78
|
+
* (negative repels), never FA2's centre gravity. `gravity` and `springCoefficient` are null when left to their
|
|
79
|
+
* defaults: ngraph's constant scaled by min(1, SE_SCALE_REFERENCE_NODES / n) once n is known at load. Exported for
|
|
80
|
+
* src/layouts/spring-electrical.ts and the option tests.
|
|
81
|
+
* @public
|
|
82
|
+
*/
|
|
83
|
+
export interface ResolvedSpringElectricalOptions {
|
|
84
|
+
readonly springLength: number;
|
|
85
|
+
readonly springCoefficient: number | null;
|
|
86
|
+
readonly gravity: number | null;
|
|
87
|
+
readonly dragCoefficient: number;
|
|
88
|
+
readonly timeStep: number;
|
|
89
|
+
readonly dim: 2 | 3;
|
|
90
|
+
readonly scale: number;
|
|
91
|
+
readonly center: readonly [number, number, number];
|
|
92
|
+
readonly seed: number | null;
|
|
93
|
+
readonly settleThreshold: number;
|
|
94
|
+
readonly settleWindow: number;
|
|
95
|
+
readonly iterationsPerStep: number;
|
|
96
|
+
readonly maxInFlight: number;
|
|
97
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `counting-scatter` kernel body (spec 6 row 5; P4-T3): the scatter of a counting sort. `start[k]` is the
|
|
3
|
+
* exclusive scan of the histogram, `cursor[k]` a zeroed per-bin atomic; an element takes the slot `start[k] +
|
|
4
|
+
* atomicAdd(&cursor[k], 1u)`. The order inside a bin depends on the schedule (set-deterministic, design 6). Body
|
|
5
|
+
* only; normative text.
|
|
6
|
+
*/
|
|
7
|
+
export const countingScatterWgsl = /* wgsl */ `
|
|
8
|
+
@compute @workgroup_size(WG)
|
|
9
|
+
fn counting_scatter(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
|
|
10
|
+
let i = linear_id(wid, lid.x);
|
|
11
|
+
if (i >= P.count) { return; } // no barrier follows
|
|
12
|
+
let k = keys[i];
|
|
13
|
+
let slot = atomicAdd(&cursor[k], 1u); // the per-bin cursor (6 row 5)
|
|
14
|
+
outIndex[start[k] + slot] = i;
|
|
15
|
+
}
|
|
16
|
+
`;
|
|
@@ -1,37 +1,120 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* K2 of the ForceAtlas2 iteration, `fa2-attraction` (spec 7.5; contract 4.5): the
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* K2 of the ForceAtlas2 iteration, `fa2-attraction` (spec 7.5; contract 4.5): the gather of the attraction force
|
|
3
|
+
* over the undirected CSR rows (both arcs present, so the sum is symmetric with no atomics), the linear or linlog
|
|
4
|
+
* law, the optional weights, the distributed-action division by the mass in `pos.w`, written as the FIRST writer of
|
|
5
|
+
* `force` each iteration (or combined into it under `P.accumulate`, the windowed pattern of 4.2). The arcs are read
|
|
6
|
+
* inside the bound window [P.arcBase, P.arcEnd). P4-T5 (PD-6, PD-7) gives it the three degree tiers: TIER 0 is one
|
|
7
|
+
* row per thread over `[P.tierStart, P.tierEnd)`; TIER 1 is 32 lanes per row over `[P.hiEnd, P.midEnd)` with a
|
|
8
|
+
* five-step tree in workgroup memory; TIER 2 is one workgroup per row over `[0, P.hiEnd)` through `wg_reduce_vec4`
|
|
9
|
+
* (hence `needs: ["subgroups"]`); the row is `perm[row]` under USE_PERM. `LAW` (P5, spec 7.20) picks the pair law:
|
|
10
|
+
* 0 = the FA2 text, 1 = Fruchterman-Reingold `d^2 / k` (unfloored), 2 = ngraph's Hooke spring `k_s (d - L)`; under
|
|
11
|
+
* 1 / 2 the models compile `LINLOG = false` and `HAS_WEIGHTS = false`, and the law overwrites `w` so weights are
|
|
12
|
+
* ignored either way.
|
|
7
13
|
*
|
|
8
|
-
* Body only (spec 3.5, D9); normative text (contract 4.5); the K2 sabotage mutations (P3-T5
|
|
14
|
+
* Body only (spec 3.5, D9); normative text (contract 4.5); the K2 sabotage mutations (P3-T5, P5-T7, P4-T5 / T6) are
|
|
15
|
+
* textual edits of it.
|
|
16
|
+
*
|
|
17
|
+
* TIER 0 folds its row through `row_force_dense`, a stride-one copy of `row_force`, because the shader compiler
|
|
18
|
+
* emits `row_force(i, 0u, 1u)` as a call and leaves the stride in a parameter: the loop then walks the row with a
|
|
19
|
+
* runtime step, which costs the strength-reduced addressing into colIdx / weights and the unrolling that keeps
|
|
20
|
+
* several loads in flight per thread. TIER 0 runs on every load -- alone when no row reaches degree 32, and over
|
|
21
|
+
* the low-degree rows, which are most of them, when the tiers are bound. The two folds spell their locals apart
|
|
22
|
+
* (`arc` / `nbr` / `weight` / `total` against `a` / `j` / `w` / `f`) so that each sabotage row names exactly one
|
|
23
|
+
* of them.
|
|
9
24
|
*/
|
|
10
25
|
|
|
11
|
-
/** The K2 body: entry point `attraction`;
|
|
26
|
+
/** The K2 body: entry point `attraction`; the tier bodies are functions called under the uniform `TIER` override, so the barriers of `tiered` are reached in uniform control flow and `tier0`'s early return is legal (spec 3.5 rule 1). */
|
|
12
27
|
export const fa2AttractionWgsl = /* wgsl */ `fn store_force(i: u32, f: vec3f) {
|
|
13
28
|
force[3u * i] = f.x;
|
|
14
29
|
force[3u * i + 1u] = f.y;
|
|
15
30
|
force[3u * i + 2u] = f.z;
|
|
16
31
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
fn
|
|
20
|
-
let row = linear_id(wid, lid.x) + P.tierStart;
|
|
21
|
-
if (row >= P.tierEnd) { return; } // no barrier follows in this tier (3.5 rule 1)
|
|
22
|
-
let i = select(row, perm[row], USE_PERM);
|
|
32
|
+
fn load_force(i: u32) -> vec3f { return vec3f(force[3u * i], force[3u * i + 1u], force[3u * i + 2u]); }
|
|
33
|
+
fn row_node(row: u32) -> u32 { return select(row, perm[row], USE_PERM); }
|
|
34
|
+
fn row_force(i: u32, lane: u32, step: u32) -> vec3f { // the arcs of row i this lane walks inside the bound window [P.arcBase, P.arcEnd) (4.2)
|
|
23
35
|
let pi = pos[i]; // xyz + mass in one load (D23)
|
|
36
|
+
let a0 = max(rowPtr[i], P.arcBase);
|
|
37
|
+
let a1 = min(rowPtr[i + 1u], P.arcEnd);
|
|
24
38
|
var f = vec3f(0.0);
|
|
25
|
-
for (var a =
|
|
26
|
-
let j = colIdx[a];
|
|
39
|
+
for (var a = a0 + lane; a < a1; a = a + step) {
|
|
40
|
+
let j = colIdx[a - P.arcBase];
|
|
27
41
|
if (j == i) { continue; } // a self-loop exerts no force
|
|
28
42
|
var w = 1.0;
|
|
29
|
-
if (HAS_WEIGHTS) { w = weights[a]; }
|
|
43
|
+
if (HAS_WEIGHTS) { w = weights[a - P.arcBase]; }
|
|
30
44
|
let d = pos[j].xyz - pi.xyz; // toward j
|
|
31
45
|
let len = max(length(d), FA2_DIST_FLOOR);
|
|
46
|
+
if (LAW == 1u) { w = length(d) / P.frK; } // LAW 1 (FR, 7.20): |F| = d^2 / k along d / d, unfloored; the linear select below applies w as is
|
|
47
|
+
if (LAW == 2u) { w = P.springCoefficient * (len - P.springLength) / len; } // LAW 2 (spring, ngraph generateCreateSpringForce.js:33-36): Hooke k_s (d - L) toward j
|
|
32
48
|
let mag = select(w, w * log(1.0 + len) / len, LINLOG); // linear: |F| = w len; linlog: |F| = w log(1 + len)
|
|
33
49
|
f = f + d * mag;
|
|
34
50
|
}
|
|
35
|
-
|
|
51
|
+
return f;
|
|
52
|
+
}
|
|
53
|
+
fn row_force_dense(i: u32) -> vec3f { // TIER 0's stride-one twin of row_force (see the header)
|
|
54
|
+
let pi = pos[i];
|
|
55
|
+
let lo = max(rowPtr[i], P.arcBase);
|
|
56
|
+
let hi = min(rowPtr[i + 1u], P.arcEnd);
|
|
57
|
+
var total = vec3f(0.0);
|
|
58
|
+
for (var arc = lo; arc < hi; arc = arc + 1u) {
|
|
59
|
+
let k = arc - P.arcBase; // the window-local index; this walk is contiguous
|
|
60
|
+
let nbr = colIdx[k];
|
|
61
|
+
if (nbr == i) { continue; } // a self-loop exerts no force
|
|
62
|
+
var weight = 1.0;
|
|
63
|
+
if (HAS_WEIGHTS) { weight = weights[k]; }
|
|
64
|
+
let d = pos[nbr].xyz - pi.xyz; // toward the neighbour
|
|
65
|
+
let len = max(length(d), FA2_DIST_FLOOR);
|
|
66
|
+
if (LAW == 1u) { weight = length(d) / P.frK; } // LAW 1 (FR, 7.20), as in row_force
|
|
67
|
+
if (LAW == 2u) { weight = P.springCoefficient * (len - P.springLength) / len; } // LAW 2 (spring), as in row_force
|
|
68
|
+
let mag = select(weight, weight * log(1.0 + len) / len, LINLOG);
|
|
69
|
+
total = total + d * mag;
|
|
70
|
+
}
|
|
71
|
+
return total;
|
|
72
|
+
}
|
|
73
|
+
fn finish(i: u32, f0: vec3f) {
|
|
74
|
+
var f = f0;
|
|
75
|
+
if (DISTRIBUTED) { f = f / pos[i].w; }
|
|
76
|
+
if (P.accumulate == 1u) { f = f + load_force(i); } // the windowed loop of 4.2 (arcBase != 0 dispatches after the first)
|
|
36
77
|
store_force(i, f); // overwrites: attraction is the first writer of force each iteration
|
|
78
|
+
}
|
|
79
|
+
fn tier0(wid: vec3<u32>, lane: u32) { // TIER 0: one row per thread over [tierStart, tierEnd); no barrier, so the early return is legal (3.5 rule 1)
|
|
80
|
+
let row = linear_id(wid, lane) + P.tierStart;
|
|
81
|
+
if (row >= P.tierEnd) { return; }
|
|
82
|
+
let i = row_node(row);
|
|
83
|
+
finish(i, row_force_dense(i));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
var<workgroup> sh: array<vec3f, WG>;
|
|
87
|
+
|
|
88
|
+
fn tiered(wid: vec3<u32>, lid: u32) { // TIER 1: 32 lanes per row over [hiEnd, midEnd); TIER 2: WG lanes per row over [0, hiEnd) (PD-6, PD-7)
|
|
89
|
+
let g = group_id(wid);
|
|
90
|
+
var row = g;
|
|
91
|
+
var end = P.hiEnd;
|
|
92
|
+
var lane = lid;
|
|
93
|
+
var step = WG;
|
|
94
|
+
if (TIER == 1u) { row = P.hiEnd + g * (WG / 32u) + lid / 32u; end = P.midEnd; lane = lid % 32u; step = 32u; }
|
|
95
|
+
let valid = row < end;
|
|
96
|
+
var i = 0u;
|
|
97
|
+
var f = vec3f(0.0);
|
|
98
|
+
if (valid) { i = row_node(row); f = row_force(i, lane, step); }
|
|
99
|
+
if (TIER == 1u) {
|
|
100
|
+
sh[lid] = f;
|
|
101
|
+
workgroupBarrier();
|
|
102
|
+
for (var s = 16u; s >= 1u; s = s / 2u) { // the five-step tree over each 32-lane group; every lane runs every step
|
|
103
|
+
var t = vec3f(0.0);
|
|
104
|
+
if (lane < s) { t = sh[lid + s]; }
|
|
105
|
+
workgroupBarrier();
|
|
106
|
+
sh[lid] = sh[lid] + t;
|
|
107
|
+
workgroupBarrier();
|
|
108
|
+
}
|
|
109
|
+
if (valid && lane == 0u) { finish(i, sh[lid]); }
|
|
110
|
+
}
|
|
111
|
+
if (TIER == 2u) {
|
|
112
|
+
let t = wg_reduce_vec4(vec4f(f, 0.0), lid, 0u);
|
|
113
|
+
if (valid && lid == 0u) { finish(i, t.xyz); }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@compute @workgroup_size(WG)
|
|
118
|
+
fn attraction(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
|
|
119
|
+
if (TIER == 0u) { tier0(wid, lid.x); } else { tiered(wid, lid.x); }
|
|
37
120
|
}`;
|