@graphty/webgpu-graph-algorithms 0.2.0 → 0.3.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 +68 -25
- package/dist/browser.js +1 -1
- package/dist/chunks/{context-E6iKaeuJ.js → context-CRbw2Wyo.js} +178 -19
- package/dist/chunks/{context-E6iKaeuJ.js.map → context-CRbw2Wyo.js.map} +1 -1
- package/dist/node.js +1 -1
- package/dist/src/accelerator.d.ts +9 -6
- package/dist/src/accelerator.d.ts.map +1 -1
- package/dist/src/accelerator.js +85 -6
- package/dist/src/accelerator.js.map +1 -1
- package/dist/src/algorithms/components.d.ts +30 -0
- package/dist/src/algorithms/components.d.ts.map +1 -0
- package/dist/src/algorithms/components.js +300 -0
- package/dist/src/algorithms/components.js.map +1 -0
- package/dist/src/algorithms/pagerank.d.ts +39 -0
- package/dist/src/algorithms/pagerank.d.ts.map +1 -0
- package/dist/src/algorithms/pagerank.js +298 -0
- package/dist/src/algorithms/pagerank.js.map +1 -0
- package/dist/src/algorithms/power-iteration.d.ts +109 -0
- package/dist/src/algorithms/power-iteration.d.ts.map +1 -0
- package/dist/src/algorithms/power-iteration.js +206 -0
- package/dist/src/algorithms/power-iteration.js.map +1 -0
- package/dist/src/algorithms/scope.d.ts +26 -0
- package/dist/src/algorithms/scope.d.ts.map +1 -0
- package/dist/src/algorithms/scope.js +41 -0
- package/dist/src/algorithms/scope.js.map +1 -0
- package/dist/src/algorithms/spectral.d.ts +50 -0
- package/dist/src/algorithms/spectral.d.ts.map +1 -0
- package/dist/src/algorithms/spectral.js +247 -0
- package/dist/src/algorithms/spectral.js.map +1 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/kernel/dispatch.d.ts +4 -1
- package/dist/src/kernel/dispatch.d.ts.map +1 -1
- package/dist/src/kernel/dispatch.js +12 -5
- package/dist/src/kernel/dispatch.js.map +1 -1
- package/dist/src/kernels.d.ts +20 -4
- package/dist/src/kernels.d.ts.map +1 -1
- package/dist/src/kernels.js +172 -2
- package/dist/src/kernels.js.map +1 -1
- package/dist/src/memory/residency.d.ts.map +1 -1
- package/dist/src/memory/residency.js +164 -11
- package/dist/src/memory/residency.js.map +1 -1
- package/dist/src/primitives/core-shape.d.ts +41 -0
- package/dist/src/primitives/core-shape.d.ts.map +1 -0
- package/dist/src/primitives/core-shape.js +89 -0
- package/dist/src/primitives/core-shape.js.map +1 -0
- package/dist/src/primitives/segmented-reduce.d.ts.map +1 -1
- package/dist/src/primitives/segmented-reduce.js +4 -30
- package/dist/src/primitives/segmented-reduce.js.map +1 -1
- package/dist/src/primitives/spmv.d.ts +56 -0
- package/dist/src/primitives/spmv.d.ts.map +1 -0
- package/dist/src/primitives/spmv.js +101 -0
- package/dist/src/primitives/spmv.js.map +1 -0
- package/dist/src/types/accelerator.d.ts +14 -2
- package/dist/src/types/accelerator.d.ts.map +1 -1
- package/dist/src/types/algorithms.d.ts +73 -0
- package/dist/src/types/algorithms.d.ts.map +1 -0
- package/dist/src/types/algorithms.js +17 -0
- package/dist/src/types/algorithms.js.map +1 -0
- package/dist/src/wgsl/pr-finalize.wgsl.d.ts +11 -0
- package/dist/src/wgsl/pr-finalize.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/pr-finalize.wgsl.js +36 -0
- package/dist/src/wgsl/pr-finalize.wgsl.js.map +1 -0
- package/dist/src/wgsl/pr-scale.wgsl.d.ts +14 -0
- package/dist/src/wgsl/pr-scale.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/pr-scale.wgsl.js +48 -0
- package/dist/src/wgsl/pr-scale.wgsl.js.map +1 -0
- package/dist/src/wgsl/spmv-pull.wgsl.d.ts +15 -0
- package/dist/src/wgsl/spmv-pull.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/spmv-pull.wgsl.js +47 -0
- package/dist/src/wgsl/spmv-pull.wgsl.js.map +1 -0
- package/dist/src/wgsl/wcc-compress.wgsl.d.ts +9 -0
- package/dist/src/wgsl/wcc-compress.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/wcc-compress.wgsl.js +26 -0
- package/dist/src/wgsl/wcc-compress.wgsl.js.map +1 -0
- package/dist/src/wgsl/wcc-link-edges.wgsl.d.ts +13 -0
- package/dist/src/wgsl/wcc-link-edges.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/wcc-link-edges.wgsl.js +46 -0
- package/dist/src/wgsl/wcc-link-edges.wgsl.js.map +1 -0
- package/dist/src/wgsl/wcc-link-sample.wgsl.d.ts +11 -0
- package/dist/src/wgsl/wcc-link-sample.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/wcc-link-sample.wgsl.js +45 -0
- package/dist/src/wgsl/wcc-link-sample.wgsl.js.map +1 -0
- package/dist/src/wgsl/wcc-sample.wgsl.d.ts +10 -0
- package/dist/src/wgsl/wcc-sample.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/wcc-sample.wgsl.js +18 -0
- package/dist/src/wgsl/wcc-sample.wgsl.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/webgpu-graph-algorithms.js +1550 -29
- package/dist/webgpu-graph-algorithms.js.map +1 -1
- package/package.json +3 -3
- package/src/accelerator.ts +101 -7
- package/src/algorithms/components.ts +348 -0
- package/src/algorithms/pagerank.ts +343 -0
- package/src/algorithms/power-iteration.ts +278 -0
- package/src/algorithms/scope.ts +52 -0
- package/src/algorithms/spectral.ts +300 -0
- package/src/index.ts +18 -0
- package/src/kernel/dispatch.ts +12 -5
- package/src/kernels.ts +206 -5
- package/src/memory/residency.ts +200 -11
- package/src/primitives/core-shape.ts +103 -0
- package/src/primitives/segmented-reduce.ts +4 -36
- package/src/primitives/spmv.ts +155 -0
- package/src/types/accelerator.ts +28 -2
- package/src/types/algorithms.ts +82 -0
- package/src/wgsl/pr-finalize.wgsl.ts +36 -0
- package/src/wgsl/pr-scale.wgsl.ts +48 -0
- package/src/wgsl/spmv-pull.wgsl.ts +47 -0
- package/src/wgsl/wcc-compress.wgsl.ts +26 -0
- package/src/wgsl/wcc-link-edges.wgsl.ts +46 -0
- package/src/wgsl/wcc-link-sample.wgsl.ts +45 -0
- package/src/wgsl/wcc-sample.wgsl.ts +18 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graphty/webgpu-graph-algorithms",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "WebGPU-accelerated graph algorithms and layouts over the @graphty/graph-format snapshot, for Node (Dawn) and browsers",
|
|
5
5
|
"author": "Adam Powers <apowers@ato.ms>",
|
|
6
6
|
"type": "module",
|
|
@@ -60,11 +60,11 @@
|
|
|
60
60
|
"homepage": "https://github.com/graphty-org/graphty-monorepo/tree/master/webgpu-graph-algorithms#readme",
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@webgpu/types": "^0.1.72",
|
|
63
|
-
"@graphty/graph-format": "^0.
|
|
63
|
+
"@graphty/graph-format": "^1.0.0"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|
|
66
66
|
"@graphty/algorithms": "^1.0.0",
|
|
67
|
-
"@graphty/graph-format": "^0.
|
|
67
|
+
"@graphty/graph-format": "^1.0.0",
|
|
68
68
|
"@graphty/layout": "^1.0.0",
|
|
69
69
|
"webgpu": ">=0.4.0 <1.0.0"
|
|
70
70
|
},
|
package/src/accelerator.ts
CHANGED
|
@@ -1,18 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* createAccelerator (spec 3.3, 9; contract 3.14): the injectable object that satisfies the CPU packages'
|
|
3
3
|
* AlgorithmAccelerator and LayoutAccelerator interfaces STRUCTURALLY (spec 9.2, 9.3; the mirrors of
|
|
4
|
-
* src/types/accelerator.ts until W1, D27).
|
|
5
|
-
* else: the CPU-side dispatchers (`accelerated()`,
|
|
6
|
-
* route to the CPU when the member is
|
|
7
|
-
*
|
|
8
|
-
* from
|
|
4
|
+
* src/types/accelerator.ts until W1, D27). It carries P3's `forceAtlas2`, `release` and `dispose` and P7's seven
|
|
5
|
+
* algorithm members (spec 8.2, 8.3; M8b-T8, PD-14) and nothing else: the CPU-side dispatchers (`accelerated()`,
|
|
6
|
+
* `createSimulation()`) test `acc.betweennessCentrality !== undefined` and route to the CPU when the member is
|
|
7
|
+
* absent (spec 2.4 row "method missing"), so a method the GPU does not implement must not exist here -- never a
|
|
8
|
+
* throwing stub. The remaining algorithm members arrive one per shipped algorithm from P8; `fruchtermanReingold` /
|
|
9
|
+
* `springElectrical` with P5.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
|
-
import { type GraphSnapshot } from "@graphty/graph-format";
|
|
12
|
+
import { type F32, type F64, type GraphSnapshot } from "@graphty/graph-format";
|
|
12
13
|
|
|
14
|
+
import { connectedComponents } from "./algorithms/components.js";
|
|
15
|
+
import { pageRank, personalizedPageRank } from "./algorithms/pagerank.js";
|
|
16
|
+
import { eigenvectorCentrality, hits, katzCentrality } from "./algorithms/spectral.js";
|
|
13
17
|
import { type GpuContext } from "./context.js";
|
|
14
18
|
import { createForceAtlas2 } from "./layouts/forceatlas2.js";
|
|
15
19
|
import { type AcceleratorOptions, type GpuAccelerator } from "./types/accelerator.js";
|
|
20
|
+
import {
|
|
21
|
+
type ComponentsOptions,
|
|
22
|
+
type EigenvectorOptions,
|
|
23
|
+
type GpuHitsResult,
|
|
24
|
+
type GpuLabelResult,
|
|
25
|
+
type GpuPageRankResult,
|
|
26
|
+
type GpuScoresResult,
|
|
27
|
+
type HitsOptions,
|
|
28
|
+
type KatzOptions,
|
|
29
|
+
type PageRankOptions,
|
|
30
|
+
} from "./types/algorithms.js";
|
|
16
31
|
import { type ForceAtlas2Stats, type GpuLayoutSimulation, type GpuLayoutTuning } from "./types/layout.js";
|
|
17
32
|
import { type ForceAtlas2Options } from "./types/options.js";
|
|
18
33
|
|
|
@@ -73,7 +88,9 @@ function freezeOptions(options: AcceleratorOptions | undefined): Readonly<Accele
|
|
|
73
88
|
|
|
74
89
|
/**
|
|
75
90
|
* Spec 3.3 createAccelerator, verbatim: the object implementing AlgorithmAccelerator & LayoutAccelerator
|
|
76
|
-
* structurally;
|
|
91
|
+
* structurally; P3's forceAtlas2, release and dispose plus P7's seven algorithm members, each a delegation to
|
|
92
|
+
* its algorithm with `ctx.assertReady()` first. The accelerator's algorithm defaults are not consulted by any of
|
|
93
|
+
* them: only `betweenness` has any, and it belongs to P9. One per call (the app creates one and injects
|
|
77
94
|
* it, spec 2.4); `kind` is "webgpu"; `options` is a frozen deep copy; `forceAtlas2(o)` is
|
|
78
95
|
* `createForceAtlas2(ctx, { ...o, ...options.layout })`, so the GPU tuning given here wins over anything the
|
|
79
96
|
* CPU-typed option object carries (spec 3.3: tuning never comes from the caller of the accelerator method);
|
|
@@ -100,6 +117,83 @@ export function createAccelerator(ctx: GpuContext, options?: AcceleratorOptions)
|
|
|
100
117
|
ctx.assertReady();
|
|
101
118
|
return createForceAtlas2(ctx, { ...o, ...frozen.layout });
|
|
102
119
|
},
|
|
120
|
+
/**
|
|
121
|
+
* PageRank on the device (spec 8.2; contract 3.14).
|
|
122
|
+
* @param gs - the snapshot
|
|
123
|
+
* @param o - the CPU option record (spec 9.2 PageRankOptions)
|
|
124
|
+
* @returns the f32 scores with `precision: "f32"` (spec 9.7)
|
|
125
|
+
*/
|
|
126
|
+
async pageRank(gs: GraphSnapshot, o?: PageRankOptions): Promise<GpuPageRankResult> {
|
|
127
|
+
ctx.assertReady();
|
|
128
|
+
return await pageRank(ctx, gs, o);
|
|
129
|
+
},
|
|
130
|
+
/**
|
|
131
|
+
* Personalized PageRank on the device (spec 8.2; contract 3.14). The mirror admits an f64 personalization
|
|
132
|
+
* (spec 9.2 `F32 | F64`); the kernel reads f32, so an f64 vector is narrowed on the host first.
|
|
133
|
+
* @param gs - the snapshot
|
|
134
|
+
* @param personalization - one finite non-negative mass per node, not all zero
|
|
135
|
+
* @param o - the CPU option record (spec 9.2 PageRankOptions)
|
|
136
|
+
* @returns the f32 scores with `precision: "f32"` (spec 9.7)
|
|
137
|
+
*/
|
|
138
|
+
async personalizedPageRank(
|
|
139
|
+
gs: GraphSnapshot,
|
|
140
|
+
personalization: F32 | F64,
|
|
141
|
+
o?: PageRankOptions,
|
|
142
|
+
): Promise<GpuPageRankResult> {
|
|
143
|
+
ctx.assertReady();
|
|
144
|
+
const mass = personalization instanceof Float32Array ? personalization : Float32Array.from(personalization);
|
|
145
|
+
return await personalizedPageRank(ctx, gs, mass, o);
|
|
146
|
+
},
|
|
147
|
+
/**
|
|
148
|
+
* HITS hubs and authorities on the device (spec 8.2; contract 3.14).
|
|
149
|
+
* @param gs - the snapshot
|
|
150
|
+
* @param o - the CPU option record (spec 9.2 HitsOptions)
|
|
151
|
+
* @returns hubs and authorities with `precision: "f32"` (spec 9.7)
|
|
152
|
+
*/
|
|
153
|
+
async hits(gs: GraphSnapshot, o?: HitsOptions): Promise<GpuHitsResult> {
|
|
154
|
+
ctx.assertReady();
|
|
155
|
+
return await hits(ctx, gs, o);
|
|
156
|
+
},
|
|
157
|
+
/**
|
|
158
|
+
* Eigenvector centrality on the device (spec 8.2; contract 3.14).
|
|
159
|
+
* @param gs - the snapshot
|
|
160
|
+
* @param o - the CPU option record (spec 9.2 EigenvectorOptions)
|
|
161
|
+
* @returns the f32 scores with `precision: "f32"` (spec 9.7)
|
|
162
|
+
*/
|
|
163
|
+
async eigenvectorCentrality(gs: GraphSnapshot, o?: EigenvectorOptions): Promise<GpuScoresResult> {
|
|
164
|
+
ctx.assertReady();
|
|
165
|
+
return await eigenvectorCentrality(ctx, gs, o);
|
|
166
|
+
},
|
|
167
|
+
/**
|
|
168
|
+
* Katz centrality on the device (spec 8.2; contract 3.14).
|
|
169
|
+
* @param gs - the snapshot
|
|
170
|
+
* @param o - the CPU option record (spec 9.2 KatzOptions)
|
|
171
|
+
* @returns the f32 scores with `precision: "f32"` (spec 9.7)
|
|
172
|
+
*/
|
|
173
|
+
async katzCentrality(gs: GraphSnapshot, o?: KatzOptions): Promise<GpuScoresResult> {
|
|
174
|
+
ctx.assertReady();
|
|
175
|
+
return await katzCentrality(ctx, gs, o);
|
|
176
|
+
},
|
|
177
|
+
/**
|
|
178
|
+
* Weakly connected components on the device (spec 8.3; contract 3.14): WCC semantics on directed input.
|
|
179
|
+
* @param gs - the snapshot
|
|
180
|
+
* @param o - `renumber` (default true); the mirror passes none
|
|
181
|
+
* @returns the labels, the count and groups() (spec 9.7 LabelResultLike)
|
|
182
|
+
*/
|
|
183
|
+
async connectedComponents(gs: GraphSnapshot, o?: ComponentsOptions): Promise<GpuLabelResult> {
|
|
184
|
+
ctx.assertReady();
|
|
185
|
+
return await connectedComponents(ctx, gs, o);
|
|
186
|
+
},
|
|
187
|
+
/**
|
|
188
|
+
* The same algorithm as `connectedComponents` under the mirror's other name (spec 3.3, 9.2).
|
|
189
|
+
* @param gs - the snapshot
|
|
190
|
+
* @param o - `renumber` (default true); the mirror passes none
|
|
191
|
+
* @returns the labels, the count and groups() (spec 9.7 LabelResultLike)
|
|
192
|
+
*/
|
|
193
|
+
async weaklyConnectedComponents(gs: GraphSnapshot, o?: ComponentsOptions): Promise<GpuLabelResult> {
|
|
194
|
+
ctx.assertReady();
|
|
195
|
+
return await connectedComponents(ctx, gs, o);
|
|
196
|
+
},
|
|
103
197
|
/**
|
|
104
198
|
* Destroys every device buffer recorded for the snapshot (spec 4.5); delegates to ctx.release.
|
|
105
199
|
* @param s - the snapshot the app is done with
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Weakly connected components on the device (spec 8.3, 3.3): Afforest from GAP's cc.cc, step for step. `comp[v] = v`
|
|
3
|
+
* by `fill` mode 1; two sampled link rounds over the r-th out-neighbour of every row plus a compress (one batch, no
|
|
4
|
+
* readback); a 1,024-vertex label sample the host takes the mode of, which is the giant component (PD-12); then
|
|
5
|
+
* each-edge-once link rounds over `edgeList()` -- correct for directed and undirected input alike, which is what
|
|
6
|
+
* makes the partition the WEAK one -- in batches of four `link + compress` pairs, ONE readback of the changed flag per
|
|
7
|
+
* batch, until the flag stays 0; a final compress; readback; `renumberPartition` on the host in first-seen order so
|
|
8
|
+
* the labels are IDENTICAL to the CPU's (spec 9.7), or the raw roots when `renumber: false`.
|
|
9
|
+
*
|
|
10
|
+
* PLAN DECISION PD-4: the changed flag is the word at `comp[n]`, so the link kernels bind three buffers (design 8.10).
|
|
11
|
+
* PLAN DECISION PD-11: `E_PARTITION` is NOT a pass-through. The kernels only ever store a value read out of `comp`,
|
|
12
|
+
* which `fill` seeded with `v < n`, so no label can be `INVALID_INDEX`; the host verifies every label is `< n` before
|
|
13
|
+
* `renumberPartition` sees it and raises `E_VALIDATION { label: "connectedComponents/labels" }` if that ever fails --
|
|
14
|
+
* it would mean a GPU bug, which no caller could act on as `E_PARTITION`.
|
|
15
|
+
* DEP-M8B-C: no `dedupe` primitive; Afforest's link is idempotent, so a repeated edge is a no-op.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { type GraphSnapshot, renumberPartition, type U32 } from "@graphty/graph-format";
|
|
19
|
+
|
|
20
|
+
import { U32_MAX } from "../constants.js";
|
|
21
|
+
import { type GpuContext } from "../context.js";
|
|
22
|
+
import { isWebGpuGraphError, WebGpuGraphError } from "../errors.js";
|
|
23
|
+
import { CommandBatch } from "../kernel/batch.js";
|
|
24
|
+
import { plan1d, planGridStride } from "../kernel/dispatch.js";
|
|
25
|
+
import { FILL_PARAMS, graphBindings, graphOverrides, kernelSpec, WCC_PARAMS } from "../kernels.js";
|
|
26
|
+
import { type CoreBinding } from "../memory/residency.js";
|
|
27
|
+
import { type ComponentsOptions, type GpuLabelResult } from "../types/algorithms.js";
|
|
28
|
+
import { type Binding } from "../types/memory.js";
|
|
29
|
+
import { type GpuRunOptions } from "../types/run.js";
|
|
30
|
+
import { algorithmScope } from "./scope.js";
|
|
31
|
+
|
|
32
|
+
const ALGORITHM = "connectedComponents";
|
|
33
|
+
/** Link rounds between two reads of the changed flag (design 8.3: "checked every 4 rounds"). */
|
|
34
|
+
const ROUNDS_PER_BATCH = 4;
|
|
35
|
+
/** The edge-round cap; the flag not settling by then is a hard error, never a fallback. */
|
|
36
|
+
const MAX_WCC_ROUNDS = 64;
|
|
37
|
+
/** The sampled vertices the giant component is found from (design 8.3). */
|
|
38
|
+
const SAMPLE_SIZE = 1024;
|
|
39
|
+
/** The bound of a CAS retry loop and of a compress walk (PD-5). */
|
|
40
|
+
const MAX_STEPS = 1024;
|
|
41
|
+
/** Params slots of the largest batch: four link + four compress blocks; the setup batch uses fill + 2 links + compress. */
|
|
42
|
+
const RING_SLOTS = 2 * ROUNDS_PER_BATCH;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Validates `options.dest` for a label result of `n` elements.
|
|
46
|
+
* @param dest - the caller's destination array, if any
|
|
47
|
+
* @param n - the node count
|
|
48
|
+
* @returns the destination as a U32, or null when none was given
|
|
49
|
+
*/
|
|
50
|
+
function checkDest(dest: Float32Array | Uint32Array | undefined, n: number): U32 | null {
|
|
51
|
+
if (dest === undefined) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
if (dest instanceof Uint32Array && dest.length === n && dest.buffer instanceof ArrayBuffer) {
|
|
55
|
+
return dest as U32;
|
|
56
|
+
}
|
|
57
|
+
throw new WebGpuGraphError(
|
|
58
|
+
"E_INVALID_ARGUMENT",
|
|
59
|
+
`${ALGORITHM}: dest must be a Uint32Array of length ${n} over an ArrayBuffer`,
|
|
60
|
+
{
|
|
61
|
+
argument: "dest",
|
|
62
|
+
value: `${dest.constructor.name}(${dest.length})`,
|
|
63
|
+
expected: `Uint32Array(${n}) over an ArrayBuffer`,
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The resident core; a windowed plan (`E_TOO_LARGE { path: "windowed", algorithm: null }`, spec 3.8) is re-thrown
|
|
70
|
+
* with the algorithm name (3.12). Transcribed from degree.ts, whose coreOf is module-private.
|
|
71
|
+
* @param ctx - the context
|
|
72
|
+
* @param s - the snapshot
|
|
73
|
+
* @returns the core binding
|
|
74
|
+
*/
|
|
75
|
+
function coreOf(ctx: GpuContext, s: GraphSnapshot): CoreBinding {
|
|
76
|
+
try {
|
|
77
|
+
return ctx.residency.core(s);
|
|
78
|
+
} catch (error: unknown) {
|
|
79
|
+
if (isWebGpuGraphError(error) && error.code === "E_TOO_LARGE" && error.details.path === "windowed") {
|
|
80
|
+
throw new WebGpuGraphError(
|
|
81
|
+
"E_TOO_LARGE",
|
|
82
|
+
`${ALGORITHM}: the arc arrays need a windowed upload, which P1-P3 plan but do not execute`,
|
|
83
|
+
{ ...error.details, algorithm: ALGORITHM },
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Whole-buffer binding of a scratch buffer over its first `size` bytes.
|
|
92
|
+
* @param buffer - the buffer
|
|
93
|
+
* @param size - the bound byte length
|
|
94
|
+
* @returns the binding
|
|
95
|
+
*/
|
|
96
|
+
function bindingOf(buffer: GPUBuffer, size: number): Binding {
|
|
97
|
+
return { buffer, offset: 0, size, window: null };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The result object over a label array whose labels are all `< n`: `count` blocks, `groups()` built lazily once in
|
|
102
|
+
* first-seen label order (which for renumbered labels is index order).
|
|
103
|
+
* @param labels - the labels
|
|
104
|
+
* @param count - the block count
|
|
105
|
+
* @returns the result
|
|
106
|
+
*/
|
|
107
|
+
function labelResult(labels: U32, count: number): GpuLabelResult {
|
|
108
|
+
let groups: U32[] | null = null;
|
|
109
|
+
return {
|
|
110
|
+
labels,
|
|
111
|
+
count,
|
|
112
|
+
groups(): U32[] {
|
|
113
|
+
if (groups !== null) {
|
|
114
|
+
return groups;
|
|
115
|
+
}
|
|
116
|
+
const n = labels.length;
|
|
117
|
+
const dense = new Uint32Array(n).fill(U32_MAX);
|
|
118
|
+
const sizes = new Uint32Array(count);
|
|
119
|
+
let next = 0;
|
|
120
|
+
for (let v = 0; v < n; v++) {
|
|
121
|
+
let k = dense[labels[v]];
|
|
122
|
+
if (k === U32_MAX) {
|
|
123
|
+
k = next++;
|
|
124
|
+
dense[labels[v]] = k;
|
|
125
|
+
}
|
|
126
|
+
sizes[k]++;
|
|
127
|
+
}
|
|
128
|
+
const built: U32[] = Array.from(sizes, (size) => new Uint32Array(size));
|
|
129
|
+
const filled = new Uint32Array(count);
|
|
130
|
+
for (let v = 0; v < n; v++) {
|
|
131
|
+
const k = dense[labels[v]];
|
|
132
|
+
built[k][filled[k]++] = v;
|
|
133
|
+
}
|
|
134
|
+
groups = built;
|
|
135
|
+
return built;
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The mode of the sampled labels (GAP's SampleFrequentElement, counted on the host; PD-12).
|
|
142
|
+
* @param sample - the sampled labels
|
|
143
|
+
* @returns the most frequent label
|
|
144
|
+
*/
|
|
145
|
+
function modeOf(sample: Uint32Array): number {
|
|
146
|
+
const counts = new Map<number, number>();
|
|
147
|
+
let best = sample[0];
|
|
148
|
+
let bestCount = 0;
|
|
149
|
+
for (const label of sample) {
|
|
150
|
+
const c = (counts.get(label) ?? 0) + 1;
|
|
151
|
+
counts.set(label, c);
|
|
152
|
+
if (c > bestCount) {
|
|
153
|
+
best = label;
|
|
154
|
+
bestCount = c;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return best;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Verifies every label is a node index (so `renumberPartition` can never see `INVALID_INDEX`, PD-11) and counts the
|
|
162
|
+
* distinct roots.
|
|
163
|
+
* @param raw - the labels the device produced
|
|
164
|
+
* @returns the number of distinct labels
|
|
165
|
+
*/
|
|
166
|
+
function checkLabels(raw: U32): number {
|
|
167
|
+
const n = raw.length;
|
|
168
|
+
const seen = new Uint8Array(n);
|
|
169
|
+
let count = 0;
|
|
170
|
+
for (let v = 0; v < n; v++) {
|
|
171
|
+
const label = raw[v];
|
|
172
|
+
if (label >= n) {
|
|
173
|
+
throw new WebGpuGraphError("E_VALIDATION", `${ALGORITHM}: labels[${v}] = ${label} is not a node index`, {
|
|
174
|
+
label: `${ALGORITHM}/labels`,
|
|
175
|
+
message: `the device produced a label outside [0, ${n})`,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
if (seen[label] === 0) {
|
|
179
|
+
seen[label] = 1;
|
|
180
|
+
count++;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return count;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Weakly connected components on the device (spec 3.3, 8.3): Afforest over the resident core and the edge list;
|
|
188
|
+
* labels dense in first-seen order (`renumber: true`, the default) or the raw roots (`renumber: false`).
|
|
189
|
+
* @param ctx - the context whose device runs the kernels
|
|
190
|
+
* @param s - the snapshot (uploaded through ctx.residency, or found there)
|
|
191
|
+
* @param options - renumber (default true), plus dest / signal / onProgress
|
|
192
|
+
* @returns the labels, the component count and groups()
|
|
193
|
+
*/
|
|
194
|
+
export async function connectedComponents(
|
|
195
|
+
ctx: GpuContext,
|
|
196
|
+
s: GraphSnapshot,
|
|
197
|
+
options?: ComponentsOptions & GpuRunOptions,
|
|
198
|
+
): Promise<GpuLabelResult> {
|
|
199
|
+
ctx.assertReady();
|
|
200
|
+
const n = s.nodeCount;
|
|
201
|
+
const renumber = options?.renumber !== false;
|
|
202
|
+
const dest = checkDest(options?.dest, n);
|
|
203
|
+
if (options?.signal?.aborted) {
|
|
204
|
+
throw new WebGpuGraphError("E_ABORTED", `${ALGORITHM}: the signal was aborted before any work started`, {});
|
|
205
|
+
}
|
|
206
|
+
if (n === 0) {
|
|
207
|
+
options?.onProgress?.(1, 1);
|
|
208
|
+
return labelResult(dest ?? new Uint32Array(0), 0);
|
|
209
|
+
}
|
|
210
|
+
const core = coreOf(ctx, s);
|
|
211
|
+
if (s.arcCount === 0) {
|
|
212
|
+
// every node is its own block, and the identity is already first-seen dense: renumbered and raw agree
|
|
213
|
+
const labels = dest ?? new Uint32Array(n);
|
|
214
|
+
for (let v = 0; v < n; v++) {
|
|
215
|
+
labels[v] = v;
|
|
216
|
+
}
|
|
217
|
+
options?.onProgress?.(1, 1);
|
|
218
|
+
return labelResult(labels, n);
|
|
219
|
+
}
|
|
220
|
+
const edges = ctx.residency.view(s, "edgeList");
|
|
221
|
+
const edgeCount = edges.scalars.edgeCount[0];
|
|
222
|
+
const scope = algorithmScope(ctx, ALGORITHM, RING_SLOTS);
|
|
223
|
+
try {
|
|
224
|
+
const compBytes = 4 * (n + 1);
|
|
225
|
+
const comp = scope.scratch(compBytes, "comp");
|
|
226
|
+
const items = Math.min(SAMPLE_SIZE, n);
|
|
227
|
+
const hist = scope.scratch(4 * items, "hist");
|
|
228
|
+
await ctx.allocator.check();
|
|
229
|
+
const fill = await ctx.pipelines.kernel(kernelSpec("fill", {}));
|
|
230
|
+
const linkSample = await ctx.pipelines.kernel(kernelSpec("wcc-link-sample", graphOverrides(core, null)));
|
|
231
|
+
const linkEdges = await ctx.pipelines.kernel(kernelSpec("wcc-link-edges", {}));
|
|
232
|
+
const compress = await ctx.pipelines.kernel(kernelSpec("wcc-compress", {}));
|
|
233
|
+
const sample = await ctx.pipelines.kernel(kernelSpec("wcc-sample", {}));
|
|
234
|
+
const { queue } = ctx.device;
|
|
235
|
+
const compBinding = bindingOf(comp, compBytes);
|
|
236
|
+
const histBinding = bindingOf(hist, 4 * items);
|
|
237
|
+
const rowPlan = planGridStride(n, ctx.workgroupSize, ctx.caps);
|
|
238
|
+
const edgePlan = planGridStride(edgeCount, ctx.workgroupSize, ctx.caps);
|
|
239
|
+
const flagIndex = n;
|
|
240
|
+
const zero = new Uint32Array(1);
|
|
241
|
+
const wccParams = (fields: {
|
|
242
|
+
items: number;
|
|
243
|
+
stride: number;
|
|
244
|
+
r: number;
|
|
245
|
+
giant: number;
|
|
246
|
+
}): { binding: Binding; offset: number } =>
|
|
247
|
+
scope.params(WCC_PARAMS, { n, ...fields, flagIndex, maxSteps: MAX_STEPS, pad0: 0 });
|
|
248
|
+
const recordCompress = (pass: GPUComputePassEncoder): void => {
|
|
249
|
+
const params = wccParams({ items: n, stride: rowPlan.stride ?? n, r: 0, giant: U32_MAX });
|
|
250
|
+
compress.dispatch(pass, compress.bind({ comp: compBinding, P: params.binding }), rowPlan, [params.offset]);
|
|
251
|
+
};
|
|
252
|
+
const submit = (batch: CommandBatch): ReturnType<CommandBatch["submit"]> => {
|
|
253
|
+
scope.flush();
|
|
254
|
+
return batch.submit();
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// batch 1: comp[v] = v, the flag cleared, two sampled link rounds over the r-th out-neighbour, one compress
|
|
258
|
+
queue.writeBuffer(comp, 4 * flagIndex, zero);
|
|
259
|
+
const setup = new CommandBatch(ctx, `${ALGORITHM}/setup`);
|
|
260
|
+
let pass = setup.pass("sample-rounds");
|
|
261
|
+
const fillParams = scope.params(FILL_PARAMS, { count: n, value: 0, mode: 1, pad0: 0 });
|
|
262
|
+
fill.dispatch(
|
|
263
|
+
pass,
|
|
264
|
+
fill.bind({ dst: bindingOf(comp, 4 * n), P: fillParams.binding }),
|
|
265
|
+
plan1d(n, ctx.workgroupSize, ctx.caps),
|
|
266
|
+
[fillParams.offset],
|
|
267
|
+
);
|
|
268
|
+
const graph = graphBindings(core, null);
|
|
269
|
+
for (let r = 0; r < 2; r++) {
|
|
270
|
+
const params = wccParams({ items: n, stride: rowPlan.stride ?? n, r, giant: U32_MAX });
|
|
271
|
+
const bound = linkSample.bind({ ...graph, comp: compBinding, P: params.binding });
|
|
272
|
+
linkSample.dispatch(pass, bound, rowPlan, [params.offset]);
|
|
273
|
+
}
|
|
274
|
+
recordCompress(pass);
|
|
275
|
+
setup.endPass();
|
|
276
|
+
await submit(setup).readback;
|
|
277
|
+
ctx.assertReady();
|
|
278
|
+
|
|
279
|
+
// batch 2: the label sample, whose mode is the giant component
|
|
280
|
+
const sampler = new CommandBatch(ctx, `${ALGORITHM}/sample`);
|
|
281
|
+
pass = sampler.pass("sample");
|
|
282
|
+
const sampleParams = wccParams({ items, stride: 0, r: 0, giant: U32_MAX });
|
|
283
|
+
sample.dispatch(
|
|
284
|
+
pass,
|
|
285
|
+
sample.bind({ comp: compBinding, hist: histBinding, P: sampleParams.binding }),
|
|
286
|
+
plan1d(items, ctx.workgroupSize, ctx.caps),
|
|
287
|
+
[sampleParams.offset],
|
|
288
|
+
);
|
|
289
|
+
sampler.endPass();
|
|
290
|
+
const histRequest = sampler.readback(hist, 0, 4 * items);
|
|
291
|
+
const histBytes = await submit(sampler).readback;
|
|
292
|
+
ctx.assertReady();
|
|
293
|
+
const giant = modeOf(new Uint32Array(histBytes, histRequest.offset, items));
|
|
294
|
+
|
|
295
|
+
// the edge rounds: four (link + compress) per batch, one readback of the changed flag per batch
|
|
296
|
+
const edgeBindings = { edgeSrc: edges.bindings.src, edgeDst: edges.bindings.dst, comp: compBinding };
|
|
297
|
+
let rounds = 0;
|
|
298
|
+
for (;;) {
|
|
299
|
+
queue.writeBuffer(comp, 4 * flagIndex, zero);
|
|
300
|
+
const batch = new CommandBatch(ctx, `${ALGORITHM}/rounds`);
|
|
301
|
+
pass = batch.pass("edge-rounds");
|
|
302
|
+
for (let i = 0; i < ROUNDS_PER_BATCH; i++) {
|
|
303
|
+
const params = wccParams({ items: edgeCount, stride: edgePlan.stride ?? edgeCount, r: 0, giant });
|
|
304
|
+
const bound = linkEdges.bind({ ...edgeBindings, P: params.binding });
|
|
305
|
+
linkEdges.dispatch(pass, bound, edgePlan, [params.offset]);
|
|
306
|
+
recordCompress(pass);
|
|
307
|
+
}
|
|
308
|
+
batch.endPass();
|
|
309
|
+
const flagRequest = batch.readback(comp, 4 * flagIndex, 4);
|
|
310
|
+
const submitted = submit(batch);
|
|
311
|
+
const back = await submitted.readback;
|
|
312
|
+
rounds += ROUNDS_PER_BATCH;
|
|
313
|
+
ctx.assertReady();
|
|
314
|
+
if (options?.signal?.aborted) {
|
|
315
|
+
throw new WebGpuGraphError("E_ABORTED", `${ALGORITHM}: the signal was aborted`, { batchId: submitted.id });
|
|
316
|
+
}
|
|
317
|
+
if (new Uint32Array(back, flagRequest.offset, 1)[0] === 0) {
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
if (rounds >= MAX_WCC_ROUNDS) {
|
|
321
|
+
throw new WebGpuGraphError(
|
|
322
|
+
"E_VALIDATION",
|
|
323
|
+
`${ALGORITHM}: the changed flag never settled in ${MAX_WCC_ROUNDS} rounds`,
|
|
324
|
+
{ label: ALGORITHM, message: `the changed flag never settled in ${MAX_WCC_ROUNDS} rounds` },
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// the final compress, then the labels
|
|
330
|
+
const final = new CommandBatch(ctx, `${ALGORITHM}/final`);
|
|
331
|
+
recordCompress(final.pass("compress"));
|
|
332
|
+
final.endPass();
|
|
333
|
+
await submit(final).readback;
|
|
334
|
+
ctx.assertReady();
|
|
335
|
+
const raw = !renumber && dest !== null ? dest : new Uint32Array(n);
|
|
336
|
+
await ctx.readback.read(comp, 4 * n, raw);
|
|
337
|
+
ctx.assertReady();
|
|
338
|
+
const distinct = checkLabels(raw);
|
|
339
|
+
options?.onProgress?.(1, 1);
|
|
340
|
+
if (!renumber) {
|
|
341
|
+
return labelResult(raw, distinct);
|
|
342
|
+
}
|
|
343
|
+
const { labels, count } = renumberPartition(raw, dest ?? undefined);
|
|
344
|
+
return labelResult(labels, count);
|
|
345
|
+
} finally {
|
|
346
|
+
scope.dispose();
|
|
347
|
+
}
|
|
348
|
+
}
|