@graphty/webgpu-graph-algorithms 0.6.15 → 0.6.17
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 +5 -5
- package/dist/browser.js +1 -1
- package/dist/chunks/{context-oXphO3yj.js → context-VIvatQOo.js} +61 -34
- package/dist/chunks/context-VIvatQOo.js.map +1 -0
- package/dist/node.js +1 -1
- package/dist/src/accelerator.d.ts +2 -1
- package/dist/src/accelerator.d.ts.map +1 -1
- package/dist/src/accelerator.js +53 -1
- package/dist/src/accelerator.js.map +1 -1
- package/dist/src/algorithms/all-pairs.d.ts +41 -0
- package/dist/src/algorithms/all-pairs.d.ts.map +1 -0
- package/dist/src/algorithms/all-pairs.js +181 -0
- package/dist/src/algorithms/all-pairs.js.map +1 -0
- package/dist/src/algorithms/components.d.ts +9 -1
- package/dist/src/algorithms/components.d.ts.map +1 -1
- package/dist/src/algorithms/components.js +2 -2
- package/dist/src/algorithms/components.js.map +1 -1
- package/dist/src/algorithms/label-propagation.d.ts +31 -0
- package/dist/src/algorithms/label-propagation.d.ts.map +1 -0
- package/dist/src/algorithms/label-propagation.js +254 -0
- package/dist/src/algorithms/label-propagation.js.map +1 -0
- package/dist/src/algorithms/simple-symmetric.d.ts +88 -0
- package/dist/src/algorithms/simple-symmetric.d.ts.map +1 -0
- package/dist/src/algorithms/simple-symmetric.js +347 -0
- package/dist/src/algorithms/simple-symmetric.js.map +1 -0
- package/dist/src/algorithms/triangles.d.ts +34 -0
- package/dist/src/algorithms/triangles.d.ts.map +1 -0
- package/dist/src/algorithms/triangles.js +203 -0
- package/dist/src/algorithms/triangles.js.map +1 -0
- package/dist/src/constants.d.ts +45 -0
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +45 -0
- package/dist/src/constants.js.map +1 -1
- package/dist/src/index.d.ts +8 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/kernel/prelude.d.ts.map +1 -1
- package/dist/src/kernel/prelude.js +4 -1
- package/dist/src/kernel/prelude.js.map +1 -1
- package/dist/src/kernels.d.ts +16 -4
- package/dist/src/kernels.d.ts.map +1 -1
- package/dist/src/kernels.js +223 -3
- package/dist/src/kernels.js.map +1 -1
- package/dist/src/memory/residency.js +15 -4
- package/dist/src/memory/residency.js.map +1 -1
- package/dist/src/primitives/coo-to-csr.d.ts +73 -0
- package/dist/src/primitives/coo-to-csr.d.ts.map +1 -0
- package/dist/src/primitives/coo-to-csr.js +183 -0
- package/dist/src/primitives/coo-to-csr.js.map +1 -0
- package/dist/src/primitives/group-by-key.d.ts +82 -0
- package/dist/src/primitives/group-by-key.d.ts.map +1 -0
- package/dist/src/primitives/group-by-key.js +147 -0
- package/dist/src/primitives/group-by-key.js.map +1 -0
- package/dist/src/types/accelerator.d.ts +10 -2
- package/dist/src/types/accelerator.d.ts.map +1 -1
- package/dist/src/types/all-pairs.d.ts +35 -0
- package/dist/src/types/all-pairs.d.ts.map +1 -0
- package/dist/src/types/all-pairs.js +8 -0
- package/dist/src/types/all-pairs.js.map +1 -0
- package/dist/src/types/community.d.ts +18 -0
- package/dist/src/types/community.d.ts.map +1 -0
- package/dist/src/types/community.js +5 -0
- package/dist/src/types/community.js.map +1 -0
- package/dist/src/types/structure.d.ts +27 -0
- package/dist/src/types/structure.d.ts.map +1 -0
- package/dist/src/types/structure.js +8 -0
- package/dist/src/types/structure.js.map +1 -0
- package/dist/src/wgsl/apsp-fw.wgsl.d.ts +25 -0
- package/dist/src/wgsl/apsp-fw.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/apsp-fw.wgsl.js +113 -0
- package/dist/src/wgsl/apsp-fw.wgsl.js.map +1 -0
- package/dist/src/wgsl/apsp-init.wgsl.d.ts +12 -0
- package/dist/src/wgsl/apsp-init.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/apsp-init.wgsl.js +26 -0
- package/dist/src/wgsl/apsp-init.wgsl.js.map +1 -0
- package/dist/src/wgsl/coo-emit.wgsl.d.ts +10 -0
- package/dist/src/wgsl/coo-emit.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/coo-emit.wgsl.js +33 -0
- package/dist/src/wgsl/coo-emit.wgsl.js.map +1 -0
- package/dist/src/wgsl/coo-scatter.wgsl.d.ts +15 -0
- package/dist/src/wgsl/coo-scatter.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/coo-scatter.wgsl.js +32 -0
- package/dist/src/wgsl/coo-scatter.wgsl.js.map +1 -0
- package/dist/src/wgsl/group-by-key-row.wgsl.d.ts +26 -0
- package/dist/src/wgsl/group-by-key-row.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/group-by-key-row.wgsl.js +146 -0
- package/dist/src/wgsl/group-by-key-row.wgsl.js.map +1 -0
- package/dist/src/wgsl/lpa-step.wgsl.d.ts +10 -0
- package/dist/src/wgsl/lpa-step.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/lpa-step.wgsl.js +35 -0
- package/dist/src/wgsl/lpa-step.wgsl.js.map +1 -0
- package/dist/src/wgsl/orient-flags.wgsl.d.ts +9 -0
- package/dist/src/wgsl/orient-flags.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/orient-flags.wgsl.js +21 -0
- package/dist/src/wgsl/orient-flags.wgsl.js.map +1 -0
- package/dist/src/wgsl/run-flags.wgsl.d.ts +8 -0
- package/dist/src/wgsl/run-flags.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/run-flags.wgsl.js +18 -0
- package/dist/src/wgsl/run-flags.wgsl.js.map +1 -0
- package/dist/src/wgsl/tri-intersect.wgsl.d.ts +11 -0
- package/dist/src/wgsl/tri-intersect.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/tri-intersect.wgsl.js +64 -0
- package/dist/src/wgsl/tri-intersect.wgsl.js.map +1 -0
- package/dist/webgpu-graph-algorithms.js +1819 -181
- package/dist/webgpu-graph-algorithms.js.map +1 -1
- package/package.json +4 -4
- package/src/accelerator.ts +56 -1
- package/src/algorithms/all-pairs.ts +228 -0
- package/src/algorithms/components.ts +2 -2
- package/src/algorithms/label-propagation.ts +280 -0
- package/src/algorithms/simple-symmetric.ts +409 -0
- package/src/algorithms/triangles.ts +240 -0
- package/src/constants.ts +45 -0
- package/src/index.ts +12 -1
- package/src/kernel/prelude.ts +6 -0
- package/src/kernels.ts +248 -6
- package/src/memory/residency.ts +15 -4
- package/src/primitives/coo-to-csr.ts +251 -0
- package/src/primitives/group-by-key.ts +209 -0
- package/src/types/accelerator.ts +10 -2
- package/src/types/all-pairs.ts +37 -0
- package/src/types/community.ts +18 -0
- package/src/types/structure.ts +28 -0
- package/src/wgsl/apsp-fw.wgsl.ts +112 -0
- package/src/wgsl/apsp-init.wgsl.ts +25 -0
- package/src/wgsl/coo-emit.wgsl.ts +32 -0
- package/src/wgsl/coo-scatter.wgsl.ts +31 -0
- package/src/wgsl/group-by-key-row.wgsl.ts +145 -0
- package/src/wgsl/lpa-step.wgsl.ts +34 -0
- package/src/wgsl/orient-flags.wgsl.ts +20 -0
- package/src/wgsl/run-flags.wgsl.ts +17 -0
- package/src/wgsl/tri-intersect.wgsl.ts +63 -0
- package/dist/chunks/context-oXphO3yj.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graphty/webgpu-graph-algorithms",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.17",
|
|
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",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"homepage": "https://github.com/graphty-org/graphty-monorepo/tree/master/webgpu-graph-algorithms#readme",
|
|
62
62
|
"dependencies": {
|
|
63
63
|
"@webgpu/types": "^0.1.72",
|
|
64
|
-
"@graphty/graph-format": "^1.2.
|
|
64
|
+
"@graphty/graph-format": "^1.2.3"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
67
|
"@graphty/algorithms": "^1.0.0 || ^2.0.0 || ^3.0.0",
|
|
@@ -95,8 +95,8 @@
|
|
|
95
95
|
"vite": "^7.0.5",
|
|
96
96
|
"vitest": "4.1.11",
|
|
97
97
|
"webgpu": "0.4.0",
|
|
98
|
-
"@graphty/
|
|
99
|
-
"@graphty/
|
|
98
|
+
"@graphty/algorithms": "^3.1.1",
|
|
99
|
+
"@graphty/layout": "^2.0.3"
|
|
100
100
|
},
|
|
101
101
|
"scripts": {
|
|
102
102
|
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json",
|
package/src/accelerator.ts
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* P5's `fruchtermanReingold` and `springElectrical` (the two other layout members of spec 9.3, landed together once
|
|
8
8
|
* both models were green, P5 PD-19), P7's seven algorithm members (spec 8.2, 8.3; M8b-T8, PD-14) and P8's four
|
|
9
9
|
* traversal members (spec 8.4; P8-T13 PD-16, PD-19: `breadthFirstSearch`, `sssp`, `bellmanFord`,
|
|
10
|
-
* `closenessCentrality`, each taking the seam's own option type)
|
|
10
|
+
* `closenessCentrality`, each taking the seam's own option type), `allPairsShortestPath` (design 8.7), P11's
|
|
11
|
+
* `triangleCount` and `labelPropagation`, and nothing else: the CPU-side dispatchers
|
|
11
12
|
* (`accelerated()`, `createSimulation()`) test `acc.betweennessCentrality !== undefined` /
|
|
12
13
|
* `acc.fruchtermanReingold !== undefined` and route to the CPU when the member is absent (spec 2.4 row "method
|
|
13
14
|
* missing"), so a method the GPU does not implement must not exist here -- never a throwing stub. The remaining
|
|
@@ -16,15 +17,19 @@
|
|
|
16
17
|
|
|
17
18
|
import { type F32, type F64, type GraphSnapshot } from "@graphty/graph-format";
|
|
18
19
|
|
|
20
|
+
import { allPairsShortestPath } from "./algorithms/all-pairs.js";
|
|
19
21
|
import { bellmanFord } from "./algorithms/bellman-ford.js";
|
|
20
22
|
import { betweennessCentrality, edgeBetweennessCentrality } from "./algorithms/betweenness.js";
|
|
21
23
|
import { breadthFirstSearch } from "./algorithms/bfs.js";
|
|
22
24
|
import { closenessCentrality } from "./algorithms/closeness.js";
|
|
23
25
|
import { connectedComponents } from "./algorithms/components.js";
|
|
26
|
+
import { labelPropagation } from "./algorithms/label-propagation.js";
|
|
24
27
|
import { pageRank, personalizedPageRank } from "./algorithms/pagerank.js";
|
|
25
28
|
import { eigenvectorCentrality, hits, katzCentrality } from "./algorithms/spectral.js";
|
|
26
29
|
import { sssp } from "./algorithms/sssp.js";
|
|
30
|
+
import { triangleCount } from "./algorithms/triangles.js";
|
|
27
31
|
import { type GpuContext } from "./context.js";
|
|
32
|
+
import { WebGpuGraphError } from "./errors.js";
|
|
28
33
|
import { createForceAtlas2 } from "./layouts/forceatlas2.js";
|
|
29
34
|
import { createFruchtermanReingold } from "./layouts/fruchterman-reingold.js";
|
|
30
35
|
import { createSpringElectrical } from "./layouts/spring-electrical.js";
|
|
@@ -34,6 +39,7 @@ import {
|
|
|
34
39
|
type BfsOptions,
|
|
35
40
|
type ClosenessAcceleratorOptions,
|
|
36
41
|
type GpuAccelerator,
|
|
42
|
+
type HitsOptionsLike,
|
|
37
43
|
type SsspOptions,
|
|
38
44
|
} from "./types/accelerator.js";
|
|
39
45
|
import {
|
|
@@ -48,6 +54,7 @@ import {
|
|
|
48
54
|
type KatzOptions,
|
|
49
55
|
type PageRankOptions,
|
|
50
56
|
} from "./types/algorithms.js";
|
|
57
|
+
import { type GpuApspResult } from "./types/all-pairs.js";
|
|
51
58
|
import { type GpuBetweennessResult, type GpuEdgeScoresResult } from "./types/betweenness.js";
|
|
52
59
|
import {
|
|
53
60
|
type ForceAtlas2Stats,
|
|
@@ -61,6 +68,7 @@ import {
|
|
|
61
68
|
type FruchtermanReingoldOptions,
|
|
62
69
|
type SpringElectricalOptions,
|
|
63
70
|
} from "./types/options.js";
|
|
71
|
+
import { type GpuTriangleResult } from "./types/structure.js";
|
|
64
72
|
import { type GpuBellmanFordResult, type GpuBfsResult, type GpuSsspResult } from "./types/traversal.js";
|
|
65
73
|
|
|
66
74
|
/** The `algorithms` record of AcceleratorOptions (spec 3.3), named for the copy helpers. */
|
|
@@ -357,6 +365,53 @@ export function createAccelerator(ctx: GpuContext, options?: AcceleratorOptions)
|
|
|
357
365
|
ctx.assertReady();
|
|
358
366
|
return await closenessCentrality(ctx, gs, o);
|
|
359
367
|
},
|
|
368
|
+
/**
|
|
369
|
+
* All-pairs shortest paths on the device (design 8.7): blocked Floyd-Warshall, `E_TOO_LARGE` above the device's
|
|
370
|
+
* storage-binding ceiling. The seam passes `SsspOptions`; neither of its keys has an all-pairs meaning, so a
|
|
371
|
+
* defined `cutoff` (it would change what `+Infinity` means) or `weights` (a per-arc override is a different
|
|
372
|
+
* matrix from the snapshot's resident column) is `E_UNSUPPORTED { option }`, never silently dropped.
|
|
373
|
+
* @param gs - the snapshot
|
|
374
|
+
* @param o - the seam's `SsspOptions`; both keys refused when defined
|
|
375
|
+
* @returns the row-major `n x n` distances and `n` (spec 3.3 line 835)
|
|
376
|
+
*/
|
|
377
|
+
async allPairsShortestPath(gs: GraphSnapshot, o?: SsspOptions): Promise<GpuApspResult> {
|
|
378
|
+
ctx.assertReady();
|
|
379
|
+
for (const key of ["cutoff", "weights"] as const) {
|
|
380
|
+
if (o?.[key] !== undefined) {
|
|
381
|
+
throw new WebGpuGraphError("E_UNSUPPORTED", `allPairsShortestPath: ${key} is not supported`, {
|
|
382
|
+
option: key,
|
|
383
|
+
hint: "all-pairs shortest paths runs over the snapshot's own weights with no cutoff",
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return await allPairsShortestPath(ctx, gs);
|
|
388
|
+
},
|
|
389
|
+
/**
|
|
390
|
+
* Triangle counting with the clustering coefficient and the transitivity (design 8.5; P11).
|
|
391
|
+
* @param gs - the snapshot
|
|
392
|
+
* @returns perNode, total, coefficient and transitivity
|
|
393
|
+
*/
|
|
394
|
+
async triangleCount(gs: GraphSnapshot): Promise<GpuTriangleResult> {
|
|
395
|
+
ctx.assertReady();
|
|
396
|
+
return await triangleCount(ctx, gs);
|
|
397
|
+
},
|
|
398
|
+
/**
|
|
399
|
+
* Label propagation (design 8.6; P11): `maxIterations` and `weighted` are honoured, `tolerance` is refused when
|
|
400
|
+
* defined (a label propagation stops at a fixed point, not below a tolerance).
|
|
401
|
+
* @param gs - the snapshot
|
|
402
|
+
* @param o - the seam's placeholder `HitsOptionsLike`
|
|
403
|
+
* @returns the labels dense in first-seen order, the community count and groups()
|
|
404
|
+
*/
|
|
405
|
+
async labelPropagation(gs: GraphSnapshot, o?: HitsOptionsLike): Promise<GpuLabelResult> {
|
|
406
|
+
ctx.assertReady();
|
|
407
|
+
if (o?.tolerance !== undefined) {
|
|
408
|
+
throw new WebGpuGraphError("E_UNSUPPORTED", "labelPropagation: tolerance has no meaning here", {
|
|
409
|
+
option: "tolerance",
|
|
410
|
+
hint: "label propagation stops at a fixed point or after maxIterations passes",
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
return await labelPropagation(ctx, gs, { maxIterations: o?.maxIterations, weighted: o?.weighted });
|
|
414
|
+
},
|
|
360
415
|
/**
|
|
361
416
|
* Destroys every device buffer recorded for the snapshot (spec 4.5); delegates to ctx.release.
|
|
362
417
|
* @param s - the snapshot the app is done with
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* All-pairs shortest paths on the device (design 8.7, 3.3 lines 813 and 835, 9.7): blocked Floyd-Warshall over
|
|
3
|
+
* `APSP_TILE x APSP_TILE` tiles of ONE `n x n` f32 matrix. `fill` sets the matrix to `+Infinity`, `apsp-init` writes
|
|
4
|
+
* the arcs (the cheapest of parallel arcs) and the diagonal zero, then `B = ceil(n / APSP_TILE)` rounds each run the
|
|
5
|
+
* three `apsp-fw` phases in order: the pivot block, the pivot row and column, everything else. The whole sweep --
|
|
6
|
+
* `3 B` dispatches -- is recorded into ONE compute pass: WebGPU runs the dispatches of a pass in order and makes each
|
|
7
|
+
* one's writes visible to the next, so nothing is read back until the matrix is done. Above
|
|
8
|
+
* `APSP_MAX_DISPATCHES_PER_SUBMIT` dispatches the sweep is split into further submits and `signal` is checked
|
|
9
|
+
* between them -- but no binding WebGPU devices offer today reaches that (a 4 GiB binding is 32,767 nodes, 3,072
|
|
10
|
+
* dispatches), so in practice the sweep is one submit: `signal` is checked before it and after it, never during,
|
|
11
|
+
* and `onProgress` fires once. The result is bitwise reproducible (no atomics, and a cell is only ever written by one
|
|
12
|
+
* lane per dispatch), exact for hop counts, and the minimum of f32 path sums for weights.
|
|
13
|
+
*
|
|
14
|
+
* The ceiling: the matrix is exactly `n * n` (never padded to the tile) and is bound as ONE storage binding, so
|
|
15
|
+
* `n <= floor(sqrt(limit / 4))` with `limit` the smaller of the device's `maxStorageBufferBindingSize` and
|
|
16
|
+
* `maxBufferSize`. A context's default `limits: "raise"` takes the adapter's own limits: 23,170 nodes on a hardware
|
|
17
|
+
* adapter under Dawn (a 2 GiB binding), 5,792 on lavapipe or under `limits: "default"` (the 128 MiB spec default).
|
|
18
|
+
* Above it the call throws `E_TOO_LARGE` naming the node count, the ceiling, the limit it read and
|
|
19
|
+
* `GpuContextOptions.limits` as the way to raise it; the rows are never windowed. Refused before any device work: a negative weight (`E_UNSUPPORTED
|
|
20
|
+
* allPairs.negativeWeights` -- Floyd-Warshall's in-place tile update is race-free only while the diagonal stays 0)
|
|
21
|
+
* and a NaN or infinite one (`allPairs.nonFiniteWeights`). `weighted` defaults to "the snapshot has weights";
|
|
22
|
+
* `weighted: false` on a weighted snapshot computes hop counts. The empty graph returns an empty matrix without a
|
|
23
|
+
* dispatch. `allPairsWithTuning` is what the tests drive; nothing public exposes it.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { type GraphSnapshot } from "@graphty/graph-format";
|
|
27
|
+
|
|
28
|
+
import { APSP_MAX_DISPATCHES_PER_SUBMIT, APSP_TILE, F32_INF_BITS } from "../constants.js";
|
|
29
|
+
import { type GpuContext } from "../context.js";
|
|
30
|
+
import { WebGpuGraphError } from "../errors.js";
|
|
31
|
+
import { CommandBatch } from "../kernel/batch.js";
|
|
32
|
+
import { plan1d, plan2d } from "../kernel/dispatch.js";
|
|
33
|
+
import { APSP_PARAMS, FILL_PARAMS, graphBindings, graphOverrides, kernelSpec } from "../kernels.js";
|
|
34
|
+
import { assertWholeCore } from "../primitives/core-shape.js";
|
|
35
|
+
import { assertDeviceComputes } from "../primitives/verify.js";
|
|
36
|
+
import { type ApspOptions, type GpuApspResult } from "../types/all-pairs.js";
|
|
37
|
+
import { type GpuRunOptions } from "../types/run.js";
|
|
38
|
+
import { algorithmScope } from "./scope.js";
|
|
39
|
+
import { aborted, bindingOf, checkDest } from "./sssp.js";
|
|
40
|
+
|
|
41
|
+
const ALGORITHM = "allPairsShortestPath";
|
|
42
|
+
|
|
43
|
+
/** The rounds one submit holds by default: three dispatches per round. */
|
|
44
|
+
const DEFAULT_ROUNDS_PER_SUBMIT = Math.floor(APSP_MAX_DISPATCHES_PER_SUBMIT / 3);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The knobs the tests need and nothing public offers.
|
|
48
|
+
* @internal
|
|
49
|
+
*/
|
|
50
|
+
export interface AllPairsTuning {
|
|
51
|
+
/** Rounds recorded per submit (default `floor(APSP_MAX_DISPATCHES_PER_SUBMIT / 3)`); 1 submits every round alone. */
|
|
52
|
+
readonly roundsPerSubmit?: number | undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The largest node count whose `n x n` f32 matrix fits the device (design 8.7): `floor(sqrt(limit / 4))` over the
|
|
57
|
+
* smaller of `maxStorageBufferBindingSize` and `maxBufferSize`, corrected so the float square root can never
|
|
58
|
+
* overshoot by one.
|
|
59
|
+
* @internal
|
|
60
|
+
* @param limits - the device limits
|
|
61
|
+
* @returns the ceiling and the limit that set it
|
|
62
|
+
*/
|
|
63
|
+
export function allPairsCeiling(limits: Pick<GPUSupportedLimits, "maxStorageBufferBindingSize" | "maxBufferSize">): {
|
|
64
|
+
readonly maxNodes: number;
|
|
65
|
+
readonly limit: number;
|
|
66
|
+
readonly limitName: "maxStorageBufferBindingSize" | "maxBufferSize";
|
|
67
|
+
} {
|
|
68
|
+
const binding = limits.maxStorageBufferBindingSize;
|
|
69
|
+
const bufferSize = limits.maxBufferSize;
|
|
70
|
+
const limitName = bufferSize < binding ? "maxBufferSize" : "maxStorageBufferBindingSize";
|
|
71
|
+
const limit = Math.min(binding, bufferSize);
|
|
72
|
+
let maxNodes = Math.floor(Math.sqrt(limit / 4));
|
|
73
|
+
while (4 * maxNodes * maxNodes > limit) {
|
|
74
|
+
maxNodes -= 1;
|
|
75
|
+
}
|
|
76
|
+
while (4 * (maxNodes + 1) * (maxNodes + 1) <= limit) {
|
|
77
|
+
maxNodes += 1;
|
|
78
|
+
}
|
|
79
|
+
return { maxNodes, limit, limitName };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* All-pairs shortest paths with the test knobs; `allPairsShortestPath` is this with an empty tuning.
|
|
84
|
+
* @internal
|
|
85
|
+
* @param ctx - the context whose device runs the kernels
|
|
86
|
+
* @param s - the snapshot (uploaded through ctx.residency, or found there)
|
|
87
|
+
* @param options - `weighted`, plus dest (a Float32Array of length n * n) / signal / onProgress (rounds done of `ceil(n / 32)`)
|
|
88
|
+
* @param tuning - the knobs
|
|
89
|
+
* @returns the row-major `n x n` distances and `n`
|
|
90
|
+
*/
|
|
91
|
+
export async function allPairsWithTuning(
|
|
92
|
+
ctx: GpuContext,
|
|
93
|
+
s: GraphSnapshot,
|
|
94
|
+
options: (ApspOptions & GpuRunOptions) | undefined,
|
|
95
|
+
tuning: AllPairsTuning,
|
|
96
|
+
): Promise<GpuApspResult> {
|
|
97
|
+
ctx.assertReady();
|
|
98
|
+
const n = s.nodeCount;
|
|
99
|
+
const roundsPerSubmit = tuning.roundsPerSubmit ?? DEFAULT_ROUNDS_PER_SUBMIT;
|
|
100
|
+
if (!Number.isInteger(roundsPerSubmit) || roundsPerSubmit < 1 || roundsPerSubmit > DEFAULT_ROUNDS_PER_SUBMIT) {
|
|
101
|
+
throw new WebGpuGraphError(
|
|
102
|
+
"E_INVALID_ARGUMENT",
|
|
103
|
+
`${ALGORITHM}: roundsPerSubmit must be an integer in [1, ${DEFAULT_ROUNDS_PER_SUBMIT}]`,
|
|
104
|
+
{
|
|
105
|
+
argument: "roundsPerSubmit",
|
|
106
|
+
value: roundsPerSubmit,
|
|
107
|
+
expected: `an integer in [1, ${DEFAULT_ROUNDS_PER_SUBMIT}]`,
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
const { maxNodes, limit, limitName } = allPairsCeiling(ctx.caps.limits);
|
|
112
|
+
if (n > maxNodes) {
|
|
113
|
+
throw new WebGpuGraphError(
|
|
114
|
+
"E_TOO_LARGE",
|
|
115
|
+
`${ALGORITHM}: ${n} nodes need a ${4 * n * n}-byte distance matrix in one storage binding; this device's ${limitName} of ${limit} bytes holds at most ${maxNodes} nodes -- raise it through GpuContextOptions.limits`,
|
|
116
|
+
{
|
|
117
|
+
needed: 4 * n * n,
|
|
118
|
+
limit,
|
|
119
|
+
path: "allPairs.matrix",
|
|
120
|
+
algorithm: ALGORITHM,
|
|
121
|
+
nodes: n,
|
|
122
|
+
maxNodes,
|
|
123
|
+
limitName,
|
|
124
|
+
hint: `raise ${limitName} through GpuContextOptions.limits`,
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const dist = checkDest(ALGORITHM, options?.dest, n * n) ?? new Float32Array(n * n);
|
|
129
|
+
const weighted = (options?.weighted ?? s.weights !== null) && s.weights !== null && !s.flags.allWeightsOne;
|
|
130
|
+
if (weighted && !s.flags.nonNegativeWeights) {
|
|
131
|
+
throw new WebGpuGraphError("E_UNSUPPORTED", `${ALGORITHM}: a negative weight is not supported`, {
|
|
132
|
+
feature: "allPairs.negativeWeights",
|
|
133
|
+
hint: "the blocked Floyd-Warshall sweep needs non-negative weights; pass weighted: false for hop counts",
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (weighted && !s.flags.finiteWeights) {
|
|
137
|
+
throw new WebGpuGraphError("E_UNSUPPORTED", `${ALGORITHM}: a NaN or infinite weight has no shortest path`, {
|
|
138
|
+
feature: "allPairs.nonFiniteWeights",
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (options?.signal?.aborted) {
|
|
142
|
+
throw aborted(ALGORITHM);
|
|
143
|
+
}
|
|
144
|
+
if (n === 0) {
|
|
145
|
+
return { dist, n };
|
|
146
|
+
}
|
|
147
|
+
await assertDeviceComputes(ctx);
|
|
148
|
+
const core = ctx.residency.core(s);
|
|
149
|
+
assertWholeCore(core, s.arcCount, ctx.caps.limits.maxStorageBufferBindingSize, ALGORITHM);
|
|
150
|
+
const blocks = Math.ceil(n / APSP_TILE);
|
|
151
|
+
const scope = algorithmScope(ctx, ALGORITHM, Math.min(roundsPerSubmit, blocks) + 2);
|
|
152
|
+
try {
|
|
153
|
+
const wg = ctx.workgroupSize;
|
|
154
|
+
const bytes = 4 * n * n;
|
|
155
|
+
const matrix = bindingOf(scope.scratch(bytes, "dist"), bytes);
|
|
156
|
+
await ctx.allocator.check();
|
|
157
|
+
const weightsBinding = weighted ? undefined : null;
|
|
158
|
+
const fill = await ctx.pipelines.kernel(kernelSpec("fill"));
|
|
159
|
+
const init = await ctx.pipelines.kernel(kernelSpec("apsp-init", graphOverrides(core, null, weightsBinding)));
|
|
160
|
+
const phases = await Promise.all(
|
|
161
|
+
[0, 1, 2].map((phase) => ctx.pipelines.kernel(kernelSpec("apsp-fw", { PHASE: phase }))),
|
|
162
|
+
);
|
|
163
|
+
const graph = graphBindings(core, null, weightsBinding);
|
|
164
|
+
const others = blocks - 1;
|
|
165
|
+
const phasePlans = [plan2d(1, ctx.caps), plan2d(2 * others, ctx.caps), plan2d(others * others, ctx.caps)];
|
|
166
|
+
|
|
167
|
+
for (let first = 0; first < blocks; first += roundsPerSubmit) {
|
|
168
|
+
const batch = new CommandBatch(ctx, `${ALGORITHM}/sweep`);
|
|
169
|
+
const pass = batch.pass("apsp");
|
|
170
|
+
if (first === 0) {
|
|
171
|
+
const fillParams = scope.params(FILL_PARAMS, { count: n * n, value: F32_INF_BITS, mode: 0, pad0: 0 });
|
|
172
|
+
fill.dispatch(pass, fill.bind({ dst: matrix, P: fillParams.binding }), plan1d(n * n, wg, ctx.caps), [
|
|
173
|
+
fillParams.offset,
|
|
174
|
+
]);
|
|
175
|
+
const initParams = scope.params(APSP_PARAMS, { n, round: 0, blocks, infBits: F32_INF_BITS });
|
|
176
|
+
init.dispatch(
|
|
177
|
+
pass,
|
|
178
|
+
init.bind({ ...graph, dist: matrix, P: initParams.binding }),
|
|
179
|
+
plan1d(n, wg, ctx.caps),
|
|
180
|
+
[initParams.offset],
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
const last = Math.min(first + roundsPerSubmit, blocks);
|
|
184
|
+
for (let round = first; round < last; round++) {
|
|
185
|
+
const params = scope.params(APSP_PARAMS, { n, round, blocks, infBits: F32_INF_BITS });
|
|
186
|
+
for (let phase = 0; phase < 3; phase++) {
|
|
187
|
+
const kernel = phases[phase];
|
|
188
|
+
kernel.dispatch(pass, kernel.bind({ dist: matrix, P: params.binding }), phasePlans[phase], [
|
|
189
|
+
params.offset,
|
|
190
|
+
]);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
batch.endPass();
|
|
194
|
+
scope.flush();
|
|
195
|
+
const submitted = batch.submit();
|
|
196
|
+
await submitted.readback;
|
|
197
|
+
ctx.assertReady();
|
|
198
|
+
options?.onProgress?.(last, blocks);
|
|
199
|
+
if (options?.signal?.aborted) {
|
|
200
|
+
throw aborted(ALGORITHM, submitted.id);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
await ctx.readback.read(matrix.buffer, bytes, dist);
|
|
204
|
+
ctx.assertReady();
|
|
205
|
+
return { dist, n };
|
|
206
|
+
} finally {
|
|
207
|
+
scope.dispose();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* All-pairs shortest paths on the device (design 8.7, 3.3 line 813): blocked Floyd-Warshall over 32 x 32 tiles of one
|
|
213
|
+
* `n x n` f32 matrix. `dist[i * n + j]` is the distance from `i` to `j`, `+Infinity` when unreachable, `0` on the
|
|
214
|
+
* diagonal. `E_TOO_LARGE` above `floor(sqrt(maxStorageBufferBindingSize / 4))` nodes (23,170 at a 2 GiB binding, the
|
|
215
|
+
* usual hardware adapter under the context's default `limits: "raise"`; 5,792 at the 128 MiB spec default);
|
|
216
|
+
* `E_UNSUPPORTED` for a negative or non-finite weight unless `weighted: false`.
|
|
217
|
+
* @param ctx - the context whose device runs the kernels
|
|
218
|
+
* @param s - the snapshot (uploaded through ctx.residency, or found there)
|
|
219
|
+
* @param options - `weighted` (default: the snapshot has weights), plus dest (a Float32Array of length n * n) / signal / onProgress
|
|
220
|
+
* @returns the row-major `n x n` distances and `n`
|
|
221
|
+
*/
|
|
222
|
+
export function allPairsShortestPath(
|
|
223
|
+
ctx: GpuContext,
|
|
224
|
+
s: GraphSnapshot,
|
|
225
|
+
options?: ApspOptions & GpuRunOptions,
|
|
226
|
+
): Promise<GpuApspResult> {
|
|
227
|
+
return allPairsWithTuning(ctx, s, options, {});
|
|
228
|
+
}
|
|
@@ -92,12 +92,12 @@ function bindingOf(buffer: GPUBuffer, size: number): Binding {
|
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
94
|
* The result object over a label array whose labels are all `< n`: `count` blocks, `groups()` built lazily once in
|
|
95
|
-
* first-seen label order (which for renumbered labels is index order).
|
|
95
|
+
* first-seen label order (which for renumbered labels is index order). Shared with label propagation.
|
|
96
96
|
* @param labels - the labels
|
|
97
97
|
* @param count - the block count
|
|
98
98
|
* @returns the result
|
|
99
99
|
*/
|
|
100
|
-
function labelResult(labels: U32, count: number): GpuLabelResult {
|
|
100
|
+
export function labelResult(labels: U32, count: number): GpuLabelResult {
|
|
101
101
|
let groups: U32[] | null = null;
|
|
102
102
|
return {
|
|
103
103
|
labels,
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Label propagation (design 8.6, 3.3 line 807; the P11 plan's P11-T6) over the simple symmetric graph of the
|
|
3
|
+
* snapshot (`buildSimpleSymmetric`). Every vertex starts with its own index as its label; each pass,
|
|
4
|
+
* `group-by-key-row` finds every vertex's weighted mode of its neighbours' labels (the lowest label on a tie) and
|
|
5
|
+
* `lpa-step` adopts it synchronously -- but only in the pass's direction: down to a lower label on even passes, up to
|
|
6
|
+
* a higher one on odd passes (cuGraph's swap-avoidance rule), which is what stops two neighbours trading labels
|
|
7
|
+
* forever. A pass that moves nothing in either direction after one that moved nothing in the other is a fixed point,
|
|
8
|
+
* and the run stops there or at `maxIterations` passes.
|
|
9
|
+
*
|
|
10
|
+
* Passes are recorded LABEL_PROP_PASSES_PER_SUBMIT to a submit with one readback of their move counts, because a
|
|
11
|
+
* readback per pass costs more than the passes on a small graph; the passes after the fixed point inside the last
|
|
12
|
+
* submit change nothing, so the labels are those of the fixed point. The first submit is the graph build's second.
|
|
13
|
+
* The labels are renumbered in first-seen order on the host (`renumberPartition`), as connected components' are.
|
|
14
|
+
*
|
|
15
|
+
* The result is bitwise reproducible on one device and between devices: the grouping is order-independent and every
|
|
16
|
+
* sum is an integer.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { type GraphSnapshot, renumberPartition, type U32 } from "@graphty/graph-format";
|
|
20
|
+
|
|
21
|
+
import { LABEL_PROP_PASSES_PER_SUBMIT } from "../constants.js";
|
|
22
|
+
import { type GpuContext } from "../context.js";
|
|
23
|
+
import { WebGpuGraphError } from "../errors.js";
|
|
24
|
+
import { CommandBatch } from "../kernel/batch.js";
|
|
25
|
+
import { plan1d } from "../kernel/dispatch.js";
|
|
26
|
+
import { FILL_PARAMS, kernelSpec, LPA_PARAMS } from "../kernels.js";
|
|
27
|
+
import { planGroupRows, prepareGroupByKeyRow } from "../primitives/group-by-key.js";
|
|
28
|
+
import { assertDeviceComputes } from "../primitives/verify.js";
|
|
29
|
+
import { type GpuLabelResult } from "../types/algorithms.js";
|
|
30
|
+
import { type LabelPropagationOptions } from "../types/community.js";
|
|
31
|
+
import { type Binding } from "../types/memory.js";
|
|
32
|
+
import { type GpuRunOptions } from "../types/run.js";
|
|
33
|
+
import { labelResult } from "./components.js";
|
|
34
|
+
import { algorithmScope } from "./scope.js";
|
|
35
|
+
import { assertBindable, assertBuildSorted, buildSimpleSymmetric } from "./simple-symmetric.js";
|
|
36
|
+
|
|
37
|
+
const ALGORITHM = "labelPropagation";
|
|
38
|
+
/** The default pass cap, as in `@graphty/algorithms`' labelPropagation. */
|
|
39
|
+
const DEFAULT_MAX_ITERATIONS = 100;
|
|
40
|
+
/** Params slots of the largest batch: the graph build's second submit plus one batch of passes. */
|
|
41
|
+
const RING_SLOTS = 1024;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Validates `options.dest` for a label result of `n` elements.
|
|
45
|
+
* @param dest - the caller's destination array, if any
|
|
46
|
+
* @param n - the node count
|
|
47
|
+
* @returns the destination as a U32, or null when none was given
|
|
48
|
+
*/
|
|
49
|
+
function checkDest(dest: Float32Array | Uint32Array | undefined, n: number): U32 | null {
|
|
50
|
+
if (dest === undefined) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
if (dest instanceof Uint32Array && dest.length === n && dest.buffer instanceof ArrayBuffer) {
|
|
54
|
+
return dest as U32;
|
|
55
|
+
}
|
|
56
|
+
throw new WebGpuGraphError(
|
|
57
|
+
"E_INVALID_ARGUMENT",
|
|
58
|
+
`${ALGORITHM}: dest must be a Uint32Array of length ${n} over an ArrayBuffer`,
|
|
59
|
+
{
|
|
60
|
+
argument: "dest",
|
|
61
|
+
value: `${dest.constructor.name}(${dest.length})`,
|
|
62
|
+
expected: `Uint32Array(${n}) over an ArrayBuffer`,
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The pass cap.
|
|
69
|
+
* @param value - options.maxIterations
|
|
70
|
+
* @returns the cap
|
|
71
|
+
*/
|
|
72
|
+
function maxIterationsOf(value: number | undefined): number {
|
|
73
|
+
if (value === undefined) {
|
|
74
|
+
return DEFAULT_MAX_ITERATIONS;
|
|
75
|
+
}
|
|
76
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
77
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${ALGORITHM}: maxIterations must be a non-negative integer`, {
|
|
78
|
+
argument: "maxIterations",
|
|
79
|
+
value,
|
|
80
|
+
expected: "a non-negative integer",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* An upper bound of every vertex's number of distinct neighbours in the simple symmetric graph: its arcs in both
|
|
88
|
+
* directions, which the merge of parallel edges and the drop of self-loops can only shrink.
|
|
89
|
+
* @param s - the snapshot
|
|
90
|
+
* @returns one bound per vertex
|
|
91
|
+
*/
|
|
92
|
+
function neighbourBound(s: GraphSnapshot): Uint32Array {
|
|
93
|
+
const out = s.outDegree();
|
|
94
|
+
if (!s.directed) {
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
const inDegree = s.inDegree();
|
|
98
|
+
const bound = new Uint32Array(s.nodeCount);
|
|
99
|
+
for (let v = 0; v < bound.length; v++) {
|
|
100
|
+
bound[v] = out[v] + inDegree[v];
|
|
101
|
+
}
|
|
102
|
+
return bound;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The labels as a result: every label must be a node index (a device bug otherwise), then renumbered first-seen.
|
|
107
|
+
* @param raw - the labels the device produced
|
|
108
|
+
* @param dest - the caller's destination, if any
|
|
109
|
+
* @returns the result
|
|
110
|
+
*/
|
|
111
|
+
function resultOf(raw: U32, dest: U32 | null): GpuLabelResult {
|
|
112
|
+
const n = raw.length;
|
|
113
|
+
for (let v = 0; v < n; v++) {
|
|
114
|
+
if (raw[v] >= n) {
|
|
115
|
+
throw new WebGpuGraphError("E_VALIDATION", `${ALGORITHM}: labels[${v}] = ${raw[v]} is not a node index`, {
|
|
116
|
+
label: `${ALGORITHM}/labels`,
|
|
117
|
+
message: `the device produced a label outside [0, ${n})`,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const { labels, count } = renumberPartition(raw, dest ?? undefined);
|
|
122
|
+
return labelResult(labels, count);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The identity labelling: every vertex its own community.
|
|
127
|
+
* @param n - the node count
|
|
128
|
+
* @param dest - the caller's destination, if any
|
|
129
|
+
* @returns the result
|
|
130
|
+
*/
|
|
131
|
+
function identityResult(n: number, dest: U32 | null): GpuLabelResult {
|
|
132
|
+
const labels = dest ?? new Uint32Array(n);
|
|
133
|
+
for (let v = 0; v < n; v++) {
|
|
134
|
+
labels[v] = v;
|
|
135
|
+
}
|
|
136
|
+
return labelResult(labels, n);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Label propagation on the device (see the file header).
|
|
141
|
+
* @param ctx - the context whose device runs the kernels
|
|
142
|
+
* @param s - the snapshot (its edge list is uploaded through ctx.residency, or found there)
|
|
143
|
+
* @param options - maxIterations (default 100), weighted (default true), plus dest / signal / onProgress
|
|
144
|
+
* @returns the labels, dense in first-seen order, the community count and groups()
|
|
145
|
+
*/
|
|
146
|
+
export async function labelPropagation(
|
|
147
|
+
ctx: GpuContext,
|
|
148
|
+
s: GraphSnapshot,
|
|
149
|
+
options?: LabelPropagationOptions & GpuRunOptions,
|
|
150
|
+
): Promise<GpuLabelResult> {
|
|
151
|
+
ctx.assertReady();
|
|
152
|
+
await assertDeviceComputes(ctx);
|
|
153
|
+
const n = s.nodeCount;
|
|
154
|
+
const dest = checkDest(options?.dest, n);
|
|
155
|
+
const maxIterations = maxIterationsOf(options?.maxIterations);
|
|
156
|
+
const weighted = options?.weighted !== false;
|
|
157
|
+
if (options?.signal?.aborted) {
|
|
158
|
+
throw new WebGpuGraphError("E_ABORTED", `${ALGORITHM}: the signal was aborted before any work started`, {});
|
|
159
|
+
}
|
|
160
|
+
if (n === 0 || s.edgeCount === 0 || maxIterations === 0) {
|
|
161
|
+
options?.onProgress?.(1, 1);
|
|
162
|
+
return identityResult(n, dest);
|
|
163
|
+
}
|
|
164
|
+
// the hash region of the workgroup tier is bound whole: refuse a graph it outgrows before any device work
|
|
165
|
+
const plan = planGroupRows(neighbourBound(s));
|
|
166
|
+
assertBindable(ctx, 4 * plan.regionWords, "the group-by hash region", ALGORITHM);
|
|
167
|
+
const scope = algorithmScope(ctx, ALGORITHM, RING_SLOTS);
|
|
168
|
+
try {
|
|
169
|
+
const build = await buildSimpleSymmetric(ctx, s, scope, weighted, ALGORITHM);
|
|
170
|
+
const { graph } = build;
|
|
171
|
+
if (graph.colIdx === null) {
|
|
172
|
+
// only self-loops: no vertex has a neighbour, so nothing ever moves
|
|
173
|
+
scope.flush();
|
|
174
|
+
const bytes = await build.batch.submit().readback;
|
|
175
|
+
ctx.assertReady();
|
|
176
|
+
assertBuildSorted(bytes, build, ALGORITHM);
|
|
177
|
+
options?.onProgress?.(1, 1);
|
|
178
|
+
return identityResult(n, dest);
|
|
179
|
+
}
|
|
180
|
+
const wg = ctx.workgroupSize;
|
|
181
|
+
const words = (count: number, label: string): Binding => {
|
|
182
|
+
const size = 4 * Math.max(1, count);
|
|
183
|
+
return { buffer: scope.scratch(size, label), offset: 0, size, window: null };
|
|
184
|
+
};
|
|
185
|
+
const rows = words(plan.words.length, "rows");
|
|
186
|
+
const region = words(plan.regionWords, "hashRegion");
|
|
187
|
+
const labelsA = words(n, "labelsA");
|
|
188
|
+
const labelsB = words(n, "labelsB");
|
|
189
|
+
const bestKey = words(n, "bestKey");
|
|
190
|
+
const bestScore = words(n, "bestScore");
|
|
191
|
+
const counters = words(LABEL_PROP_PASSES_PER_SUBMIT, "counters");
|
|
192
|
+
await ctx.allocator.check();
|
|
193
|
+
const { queue } = ctx.device;
|
|
194
|
+
queue.writeBuffer(rows.buffer, 0, plan.words);
|
|
195
|
+
queue.writeBuffer(region.buffer, 0, new Uint32Array(1));
|
|
196
|
+
const groupBy = await prepareGroupByKeyRow(scope);
|
|
197
|
+
const step = await ctx.pipelines.kernel(kernelSpec("lpa-step"));
|
|
198
|
+
const fill = await ctx.pipelines.kernel(kernelSpec("fill"));
|
|
199
|
+
const nodePlan = plan1d(n, wg, ctx.caps);
|
|
200
|
+
|
|
201
|
+
let { batch } = build;
|
|
202
|
+
let first = true;
|
|
203
|
+
let cur = labelsA;
|
|
204
|
+
let next = labelsB;
|
|
205
|
+
let done = 0;
|
|
206
|
+
let previousLast = -1;
|
|
207
|
+
for (;;) {
|
|
208
|
+
const k = Math.min(LABEL_PROP_PASSES_PER_SUBMIT, maxIterations - done);
|
|
209
|
+
queue.writeBuffer(counters.buffer, 0, new Uint32Array(k));
|
|
210
|
+
const pass = batch.pass("passes");
|
|
211
|
+
if (first) {
|
|
212
|
+
const iota = scope.params(FILL_PARAMS, { count: n, value: 0, mode: 1, pad0: 0 });
|
|
213
|
+
fill.dispatch(pass, fill.bind({ dst: labelsA, P: iota.binding }), nodePlan, [iota.offset]);
|
|
214
|
+
}
|
|
215
|
+
for (let i = 0; i < k; i++) {
|
|
216
|
+
groupBy.record(pass, {
|
|
217
|
+
rowPtr: graph.rowPtr,
|
|
218
|
+
colIdx: graph.colIdx,
|
|
219
|
+
weights: weighted ? graph.weights : null,
|
|
220
|
+
keyIn: cur,
|
|
221
|
+
plan,
|
|
222
|
+
rows,
|
|
223
|
+
hashRegion: region,
|
|
224
|
+
bestKey,
|
|
225
|
+
bestScore,
|
|
226
|
+
});
|
|
227
|
+
const params = scope.params(LPA_PARAMS, {
|
|
228
|
+
n,
|
|
229
|
+
direction: (done + i) % 2,
|
|
230
|
+
counterIndex: i,
|
|
231
|
+
pad0: 0,
|
|
232
|
+
});
|
|
233
|
+
step.dispatch(
|
|
234
|
+
pass,
|
|
235
|
+
step.bind({ labelsIn: cur, bestKey, labelsOut: next, counters, P: params.binding }),
|
|
236
|
+
nodePlan,
|
|
237
|
+
[params.offset],
|
|
238
|
+
);
|
|
239
|
+
[cur, next] = [next, cur];
|
|
240
|
+
}
|
|
241
|
+
batch.endPass();
|
|
242
|
+
const movesRequest = batch.readback(counters.buffer, 0, 4 * k);
|
|
243
|
+
const exhaustedRequest = batch.readback(region.buffer, 0, 4);
|
|
244
|
+
scope.flush();
|
|
245
|
+
const submitted = batch.submit();
|
|
246
|
+
const bytes = await submitted.readback;
|
|
247
|
+
ctx.assertReady();
|
|
248
|
+
if (first) {
|
|
249
|
+
assertBuildSorted(bytes, build, ALGORITHM);
|
|
250
|
+
first = false;
|
|
251
|
+
}
|
|
252
|
+
if (new Uint32Array(bytes, exhaustedRequest.offset, 1)[0] !== 0) {
|
|
253
|
+
throw new WebGpuGraphError("E_VALIDATION", `${ALGORITHM}: a hash probe exhausted its bound`, {
|
|
254
|
+
label: `${ALGORITHM}/group-by-key`,
|
|
255
|
+
message: "a compare-exchange loop of the workgroup tier ran out of steps",
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
const moves = new Uint32Array(bytes, movesRequest.offset, k);
|
|
259
|
+
done += k;
|
|
260
|
+
const lastTwo = k >= 2 ? moves[k - 2] + moves[k - 1] : previousLast + moves[0];
|
|
261
|
+
previousLast = moves[k - 1];
|
|
262
|
+
if (options?.signal?.aborted) {
|
|
263
|
+
throw new WebGpuGraphError("E_ABORTED", `${ALGORITHM}: the signal was aborted`, {
|
|
264
|
+
batchId: submitted.id,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
options?.onProgress?.(done, maxIterations);
|
|
268
|
+
if (lastTwo === 0 || done >= maxIterations) {
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
batch = new CommandBatch(ctx, `${ALGORITHM}/passes`);
|
|
272
|
+
}
|
|
273
|
+
const raw = new Uint32Array(n);
|
|
274
|
+
await ctx.readback.read(cur.buffer, 4 * n, raw);
|
|
275
|
+
ctx.assertReady();
|
|
276
|
+
return resultOf(raw, dest);
|
|
277
|
+
} finally {
|
|
278
|
+
scope.dispose();
|
|
279
|
+
}
|
|
280
|
+
}
|