@graphty/webgpu-graph-algorithms 0.6.14 → 0.6.16
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 +52 -52
- package/dist/browser.js +1 -1
- package/dist/chunks/{context-Bi6AhScG.js → context-VIvatQOo.js} +69 -34
- package/dist/chunks/context-VIvatQOo.js.map +1 -0
- package/dist/node.js +1 -1
- package/dist/src/accelerator.d.ts +5 -3
- package/dist/src/accelerator.d.ts.map +1 -1
- package/dist/src/accelerator.js +101 -5
- 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/betweenness.d.ts +70 -0
- package/dist/src/algorithms/betweenness.d.ts.map +1 -0
- package/dist/src/algorithms/betweenness.js +538 -0
- package/dist/src/algorithms/betweenness.js.map +1 -0
- package/dist/src/algorithms/closeness.d.ts +15 -5
- package/dist/src/algorithms/closeness.d.ts.map +1 -1
- package/dist/src/algorithms/closeness.js +112 -26
- package/dist/src/algorithms/closeness.js.map +1 -1
- 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 +53 -0
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +53 -0
- package/dist/src/constants.js.map +1 -1
- package/dist/src/index.d.ts +12 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +8 -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 +24 -6
- package/dist/src/kernels.d.ts.map +1 -1
- package/dist/src/kernels.js +373 -7
- 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/frontier.d.ts +2 -0
- package/dist/src/primitives/frontier.d.ts.map +1 -1
- package/dist/src/primitives/frontier.js +2 -0
- package/dist/src/primitives/frontier.js.map +1 -1
- 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 +19 -7
- package/dist/src/types/accelerator.d.ts.map +1 -1
- package/dist/src/types/algorithms.d.ts +4 -0
- package/dist/src/types/algorithms.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/betweenness.d.ts +35 -0
- package/dist/src/types/betweenness.d.ts.map +1 -0
- package/dist/src/types/betweenness.js +7 -0
- package/dist/src/types/betweenness.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/bc-backward.wgsl.d.ts +15 -0
- package/dist/src/wgsl/bc-backward.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/bc-backward.wgsl.js +34 -0
- package/dist/src/wgsl/bc-backward.wgsl.js.map +1 -0
- package/dist/src/wgsl/bc-edge-gather.wgsl.d.ts +12 -0
- package/dist/src/wgsl/bc-edge-gather.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/bc-edge-gather.wgsl.js +36 -0
- package/dist/src/wgsl/bc-edge-gather.wgsl.js.map +1 -0
- package/dist/src/wgsl/bc-finalize.wgsl.d.ts +21 -0
- package/dist/src/wgsl/bc-finalize.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/bc-finalize.wgsl.js +47 -0
- package/dist/src/wgsl/bc-finalize.wgsl.js.map +1 -0
- package/dist/src/wgsl/bc-forward-edge.wgsl.d.ts +15 -0
- package/dist/src/wgsl/bc-forward-edge.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/bc-forward-edge.wgsl.js +76 -0
- package/dist/src/wgsl/bc-forward-edge.wgsl.js.map +1 -0
- package/dist/src/wgsl/bc-forward.wgsl.d.ts +23 -0
- package/dist/src/wgsl/bc-forward.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/bc-forward.wgsl.js +106 -0
- package/dist/src/wgsl/bc-forward.wgsl.js.map +1 -0
- package/dist/src/wgsl/bc-gather.wgsl.d.ts +9 -0
- package/dist/src/wgsl/bc-gather.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/bc-gather.wgsl.js +20 -0
- package/dist/src/wgsl/bc-gather.wgsl.js.map +1 -0
- package/dist/src/wgsl/closeness-reduce.wgsl.d.ts +4 -1
- package/dist/src/wgsl/closeness-reduce.wgsl.d.ts.map +1 -1
- package/dist/src/wgsl/closeness-reduce.wgsl.js +8 -4
- package/dist/src/wgsl/closeness-reduce.wgsl.js.map +1 -1
- package/dist/src/wgsl/closeness-sweep.wgsl.d.ts +4 -2
- package/dist/src/wgsl/closeness-sweep.wgsl.d.ts.map +1 -1
- package/dist/src/wgsl/closeness-sweep.wgsl.js +12 -2
- package/dist/src/wgsl/closeness-sweep.wgsl.js.map +1 -1
- 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 +2828 -321
- package/dist/webgpu-graph-algorithms.js.map +1 -1
- package/package.json +5 -5
- package/src/accelerator.ts +130 -7
- package/src/algorithms/all-pairs.ts +228 -0
- package/src/algorithms/betweenness.ts +739 -0
- package/src/algorithms/closeness.ts +124 -32
- 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 +53 -0
- package/src/index.ts +20 -1
- package/src/kernel/prelude.ts +6 -0
- package/src/kernels.ts +411 -10
- package/src/memory/residency.ts +15 -4
- package/src/primitives/coo-to-csr.ts +251 -0
- package/src/primitives/frontier.ts +4 -0
- package/src/primitives/group-by-key.ts +209 -0
- package/src/types/accelerator.ts +26 -6
- package/src/types/algorithms.ts +5 -0
- package/src/types/all-pairs.ts +37 -0
- package/src/types/betweenness.ts +38 -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/bc-backward.wgsl.ts +33 -0
- package/src/wgsl/bc-edge-gather.wgsl.ts +35 -0
- package/src/wgsl/bc-finalize.wgsl.ts +46 -0
- package/src/wgsl/bc-forward-edge.wgsl.ts +75 -0
- package/src/wgsl/bc-forward.wgsl.ts +105 -0
- package/src/wgsl/bc-gather.wgsl.ts +19 -0
- package/src/wgsl/closeness-reduce.wgsl.ts +8 -4
- package/src/wgsl/closeness-sweep.wgsl.ts +12 -2
- 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-Bi6AhScG.js.map +0 -1
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
* seam's placeholder keys and an exact traversal has neither, so a defined value is REFUSED before any device work
|
|
24
24
|
* (`E_UNSUPPORTED { option }`, the package's rule for an option it does not implement, PD-25); `undefined` is legal.
|
|
25
25
|
* `iterations` reports the source batches run (the sources, on the weighted route), `converged` is always true.
|
|
26
|
+
* A SAMPLED run (`sources`, issue #426; undirected snapshots only) seeds its batches from the listed sources (the
|
|
27
|
+
* reduce's role 2 reads the list the host wrote after the per-node sums in `perSource`), and the sweep also adds each
|
|
28
|
+
* claim's distance into a per-node sum (`perNode`), read back with every submit and folded on the host in f64 into
|
|
29
|
+
* `1 / sum` per NODE, where the exact run folds per SOURCE: on an undirected graph the distance from a source to a node
|
|
30
|
+
* is the distance from the node to the source, which is what the CPU port's sampled closeness sums.
|
|
26
31
|
*
|
|
27
32
|
* Cost, stated so nobody is surprised: closeness is O(n x m) on any device -- at 1M nodes it is 31,250 batches of a
|
|
28
33
|
* full multi-source traversal, minutes on the card, and no target in design 10.4 asks for less. `compact.record`
|
|
@@ -54,8 +59,8 @@ import { assertWholeCore } from "../primitives/core-shape.js";
|
|
|
54
59
|
import { W } from "../primitives/frontier.js";
|
|
55
60
|
import { type ReduceScope } from "../primitives/reduce.js";
|
|
56
61
|
import { assertDeviceComputes } from "../primitives/verify.js";
|
|
57
|
-
import { type HitsOptionsLike } from "../types/accelerator.js";
|
|
58
|
-
import { type
|
|
62
|
+
import { type ClosenessAcceleratorOptions, type HitsOptionsLike } from "../types/accelerator.js";
|
|
63
|
+
import { type GpuClosenessResult } from "../types/algorithms.js";
|
|
59
64
|
import { type Binding } from "../types/memory.js";
|
|
60
65
|
import { type GpuRunOptions } from "../types/run.js";
|
|
61
66
|
import { algorithmScope } from "./scope.js";
|
|
@@ -116,10 +121,12 @@ function reusingScratch(scope: ReduceScope): ReduceScope {
|
|
|
116
121
|
}
|
|
117
122
|
|
|
118
123
|
/**
|
|
119
|
-
* The weighted route: one `sssp` per source, the sums reduced on the host.
|
|
124
|
+
* The weighted route: one `sssp` per source, the sums reduced on the host. With `sources` (a sampled run on an
|
|
125
|
+
* undirected snapshot) each search adds its distances into the sums of the nodes it reaches instead of its own.
|
|
120
126
|
* @param ctx - the context
|
|
121
127
|
* @param s - the snapshot
|
|
122
128
|
* @param scores - the destination
|
|
129
|
+
* @param sources - a sampled run's sources, or null for every node
|
|
123
130
|
* @param options - the run options
|
|
124
131
|
* @returns the result
|
|
125
132
|
*/
|
|
@@ -127,25 +134,40 @@ async function weightedRoute(
|
|
|
127
134
|
ctx: GpuContext,
|
|
128
135
|
s: GraphSnapshot,
|
|
129
136
|
scores: F32,
|
|
137
|
+
sources: readonly number[] | null,
|
|
130
138
|
options: GpuRunOptions | undefined,
|
|
131
|
-
): Promise<
|
|
139
|
+
): Promise<GpuClosenessResult> {
|
|
132
140
|
const n = s.nodeCount;
|
|
133
|
-
|
|
141
|
+
const count = sources?.length ?? n;
|
|
142
|
+
const totals = sources === null ? null : new Float64Array(n);
|
|
143
|
+
for (let i = 0; i < count; i++) {
|
|
134
144
|
if (options?.signal?.aborted) {
|
|
135
145
|
throw aborted(ALGORITHM);
|
|
136
146
|
}
|
|
147
|
+
const source = sources === null ? i : sources[i];
|
|
137
148
|
const { dist } = await sssp(ctx, s, source, { signal: options?.signal });
|
|
138
149
|
let sum = 0;
|
|
139
150
|
for (let v = 0; v < n; v++) {
|
|
140
151
|
const d = dist[v];
|
|
141
152
|
if (v !== source && d !== Infinity) {
|
|
142
|
-
|
|
153
|
+
if (totals === null) {
|
|
154
|
+
sum += d;
|
|
155
|
+
} else {
|
|
156
|
+
totals[v] += d;
|
|
157
|
+
}
|
|
143
158
|
}
|
|
144
159
|
}
|
|
145
|
-
|
|
146
|
-
|
|
160
|
+
if (totals === null) {
|
|
161
|
+
scores[source] = sum === 0 ? 0 : 1 / sum;
|
|
162
|
+
}
|
|
163
|
+
options?.onProgress?.(i + 1, count);
|
|
164
|
+
}
|
|
165
|
+
if (totals !== null) {
|
|
166
|
+
totals.forEach((sum, v) => {
|
|
167
|
+
scores[v] = sum === 0 ? 0 : 1 / sum;
|
|
168
|
+
});
|
|
147
169
|
}
|
|
148
|
-
return { scores, iterations:
|
|
170
|
+
return { scores, iterations: count, converged: true, precision: "f32", sourcesUsed: count };
|
|
149
171
|
}
|
|
150
172
|
|
|
151
173
|
/**
|
|
@@ -153,6 +175,7 @@ async function weightedRoute(
|
|
|
153
175
|
* @param ctx - the context
|
|
154
176
|
* @param s - the snapshot
|
|
155
177
|
* @param scores - the destination
|
|
178
|
+
* @param sources - a sampled run's sources, or null for every node
|
|
156
179
|
* @param levelsPerSubmit - the submit cadence
|
|
157
180
|
* @param options - the run options
|
|
158
181
|
* @param tuning - the knobs
|
|
@@ -162,13 +185,15 @@ async function sweepRoute(
|
|
|
162
185
|
ctx: GpuContext,
|
|
163
186
|
s: GraphSnapshot,
|
|
164
187
|
scores: F32,
|
|
188
|
+
sources: readonly number[] | null,
|
|
165
189
|
levelsPerSubmit: number,
|
|
166
190
|
options: GpuRunOptions | undefined,
|
|
167
191
|
tuning: ClosenessTuning,
|
|
168
|
-
): Promise<
|
|
192
|
+
): Promise<GpuClosenessResult> {
|
|
169
193
|
const n = s.nodeCount;
|
|
170
|
-
|
|
171
|
-
|
|
194
|
+
const seedCount = sources?.length ?? n;
|
|
195
|
+
if (seedCount === 0) {
|
|
196
|
+
return { scores, iterations: 0, converged: true, precision: "f32", sourcesUsed: 0 };
|
|
172
197
|
}
|
|
173
198
|
const core = ctx.residency.core(s);
|
|
174
199
|
assertWholeCore(core, s.arcCount, ctx.caps.limits.maxStorageBufferBindingSize, ALGORITHM);
|
|
@@ -195,7 +220,18 @@ async function sweepRoute(
|
|
|
195
220
|
FRONTIER_COUNTERS.byteLength,
|
|
196
221
|
);
|
|
197
222
|
const perSourceBytes = 4 * PER_SOURCE_WORDS;
|
|
198
|
-
|
|
223
|
+
// a sampled run appends the per-node distance sums (bitsBase words) and then its source list
|
|
224
|
+
const zeroedWords = PER_SOURCE_WORDS + (sources === null ? 0 : bitsBase);
|
|
225
|
+
const perSourceAll = 4 * (zeroedWords + (sources === null ? 0 : sources.length));
|
|
226
|
+
const perSource = bindingOf(scope.scratch(perSourceAll, "per-source"), perSourceAll);
|
|
227
|
+
if (sources !== null) {
|
|
228
|
+
ctx.device.queue.writeBuffer(
|
|
229
|
+
perSource.buffer,
|
|
230
|
+
perSource.offset + 4 * zeroedWords,
|
|
231
|
+
Uint32Array.from(sources),
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
const totals = sources === null ? null : new Float64Array(n);
|
|
199
235
|
await ctx.allocator.check();
|
|
200
236
|
const compact = await prepareCompact(reusingScratch(scope));
|
|
201
237
|
const sweep = await ctx.pipelines.kernel(kernelSpec("closeness-sweep", graphOverrides(core, null)));
|
|
@@ -222,7 +258,7 @@ async function sweepRoute(
|
|
|
222
258
|
ctx.assertReady();
|
|
223
259
|
|
|
224
260
|
let batches = 0;
|
|
225
|
-
for (let batchStart = 0; batchStart <
|
|
261
|
+
for (let batchStart = 0; batchStart < seedCount; batchStart += SOURCES_PER_BATCH) {
|
|
226
262
|
let level = 0;
|
|
227
263
|
for (let first = true; ; first = false) {
|
|
228
264
|
const batch = new CommandBatch(ctx, `${ALGORITHM}/levels`);
|
|
@@ -231,8 +267,13 @@ async function sweepRoute(
|
|
|
231
267
|
// the batch's seed: the four regions and the block zeroed, then role 1 (the sources' bits, their
|
|
232
268
|
// flags, counters[0] = k, level = U32_MAX)
|
|
233
269
|
recordFill(pass, bits, 4 * bitsBase, 0);
|
|
234
|
-
recordFill(pass, perSource,
|
|
235
|
-
const seed = scope.params(FRONTIER_PARAMS, {
|
|
270
|
+
recordFill(pass, perSource, zeroedWords, 0);
|
|
271
|
+
const seed = scope.params(FRONTIER_PARAMS, {
|
|
272
|
+
role: sources === null ? 1 : 2,
|
|
273
|
+
n: seedCount,
|
|
274
|
+
bitsBase,
|
|
275
|
+
source: batchStart,
|
|
276
|
+
});
|
|
236
277
|
reduce.dispatch(pass, reduce.bind({ counters, perSource, bits, P: seed.binding }), onePlan, [
|
|
237
278
|
seed.offset,
|
|
238
279
|
]);
|
|
@@ -253,6 +294,7 @@ async function sweepRoute(
|
|
|
253
294
|
arcEnd: s.arcCount,
|
|
254
295
|
mode,
|
|
255
296
|
stride: sweepPlan.stride ?? wg,
|
|
297
|
+
perNode: sources === null ? 0 : 1,
|
|
256
298
|
});
|
|
257
299
|
return {
|
|
258
300
|
bound: sweep.bind({ ...graph, frontierList, counters, bits, perSource, P: params.binding }),
|
|
@@ -277,6 +319,8 @@ async function sweepRoute(
|
|
|
277
319
|
batch.endPass();
|
|
278
320
|
const doneRequest = batch.readback(counters.buffer, counters.offset + 4 * W.done, 4);
|
|
279
321
|
const blockRequest = batch.readback(perSource.buffer, perSource.offset, perSourceBytes);
|
|
322
|
+
const nodeRequest =
|
|
323
|
+
totals === null ? null : batch.readback(perSource.buffer, perSource.offset + perSourceBytes, 4 * n);
|
|
280
324
|
const submitted = submit(batch);
|
|
281
325
|
const back = await submitted.readback;
|
|
282
326
|
ctx.assertReady();
|
|
@@ -285,10 +329,18 @@ async function sweepRoute(
|
|
|
285
329
|
}
|
|
286
330
|
if (new Uint32Array(back, doneRequest.offset, 1)[0] !== 0) {
|
|
287
331
|
const block = new Uint32Array(back, blockRequest.offset, PER_SOURCE_WORDS);
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
332
|
+
if (totals === null || nodeRequest === null) {
|
|
333
|
+
const count = Math.min(SOURCES_PER_BATCH, n - batchStart);
|
|
334
|
+
for (let i = 0; i < count; i++) {
|
|
335
|
+
const sum = block[3 * SOURCES_PER_BATCH + i] * 2 ** 32 + block[2 * SOURCES_PER_BATCH + i];
|
|
336
|
+
scores[batchStart + i] = sum === 0 ? 0 : 1 / sum;
|
|
337
|
+
}
|
|
338
|
+
} else {
|
|
339
|
+
// at most 32 (n - 1) per node per batch, so a u32 word never wraps below 134M nodes
|
|
340
|
+
const sums = new Uint32Array(back, nodeRequest.offset, n);
|
|
341
|
+
for (let v = 0; v < n; v++) {
|
|
342
|
+
totals[v] += sums[v];
|
|
343
|
+
}
|
|
292
344
|
}
|
|
293
345
|
tuning.onBatch?.(batchStart, block.slice());
|
|
294
346
|
break;
|
|
@@ -303,29 +355,63 @@ async function sweepRoute(
|
|
|
303
355
|
}
|
|
304
356
|
}
|
|
305
357
|
batches += 1;
|
|
306
|
-
options?.onProgress?.(Math.min(batchStart + SOURCES_PER_BATCH,
|
|
358
|
+
options?.onProgress?.(Math.min(batchStart + SOURCES_PER_BATCH, seedCount), seedCount);
|
|
307
359
|
}
|
|
308
|
-
|
|
360
|
+
totals?.forEach((sum, v) => {
|
|
361
|
+
scores[v] = sum === 0 ? 0 : 1 / sum;
|
|
362
|
+
});
|
|
363
|
+
return { scores, iterations: batches, converged: true, precision: "f32", sourcesUsed: seedCount };
|
|
309
364
|
} finally {
|
|
310
365
|
scope.dispose();
|
|
311
366
|
}
|
|
312
367
|
}
|
|
313
368
|
|
|
369
|
+
/**
|
|
370
|
+
* A sampled run's sources, checked: node indices of `s`, on an undirected snapshot only.
|
|
371
|
+
* @param s - the snapshot
|
|
372
|
+
* @param sources - the caller's list, or undefined for every node
|
|
373
|
+
* @returns the list, or null for every node
|
|
374
|
+
* @throws WebGpuGraphError E_INVALID_ARGUMENT for an index outside the snapshot, E_UNSUPPORTED on a directed snapshot
|
|
375
|
+
*/
|
|
376
|
+
function checkSources(s: GraphSnapshot, sources: readonly number[] | undefined): readonly number[] | null {
|
|
377
|
+
if (sources === undefined) {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
if (s.directed) {
|
|
381
|
+
// a search FROM a source measures distance to the nodes it reaches, which is the distance FROM them to the
|
|
382
|
+
// source only when every edge runs both ways
|
|
383
|
+
throw new WebGpuGraphError("E_UNSUPPORTED", `${ALGORITHM}: sampled sources need an undirected snapshot`, {
|
|
384
|
+
feature: "closenessCentrality.directedSources",
|
|
385
|
+
hint: "run the CPU port, which searches the in-arcs",
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
for (const v of sources) {
|
|
389
|
+
if (!Number.isInteger(v) || v < 0 || v >= s.nodeCount) {
|
|
390
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${ALGORITHM}: a source is not a node index`, {
|
|
391
|
+
argument: "sources",
|
|
392
|
+
value: v,
|
|
393
|
+
expected: `an integer in [0, ${s.nodeCount})`,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return sources;
|
|
398
|
+
}
|
|
399
|
+
|
|
314
400
|
/**
|
|
315
401
|
* Closeness with the test knobs of PD-26's shape; `closenessCentrality` is this with an empty tuning.
|
|
316
402
|
* @internal
|
|
317
403
|
* @param ctx - the context whose device runs the kernels
|
|
318
404
|
* @param s - the snapshot (uploaded through ctx.residency, or found there)
|
|
319
|
-
* @param options -
|
|
405
|
+
* @param options - `weighted` and a sampled run's `sources` honoured, the placeholder `maxIterations` / `tolerance` refused when defined, plus dest / signal / onProgress
|
|
320
406
|
* @param tuning - the knobs
|
|
321
|
-
* @returns the scores, the batches run, `converged: true
|
|
407
|
+
* @returns the scores, the batches run, `converged: true`, `precision: "f32"` and `sourcesUsed`
|
|
322
408
|
*/
|
|
323
409
|
export async function closenessWithTuning(
|
|
324
410
|
ctx: GpuContext,
|
|
325
411
|
s: GraphSnapshot,
|
|
326
|
-
options: (HitsOptionsLike & GpuRunOptions) | undefined,
|
|
412
|
+
options: (ClosenessAcceleratorOptions & HitsOptionsLike & GpuRunOptions) | undefined,
|
|
327
413
|
tuning: ClosenessTuning,
|
|
328
|
-
): Promise<
|
|
414
|
+
): Promise<GpuClosenessResult> {
|
|
329
415
|
ctx.assertReady();
|
|
330
416
|
await assertDeviceComputes(ctx);
|
|
331
417
|
for (const key of ["maxIterations", "tolerance"] as const) {
|
|
@@ -337,6 +423,7 @@ export async function closenessWithTuning(
|
|
|
337
423
|
}
|
|
338
424
|
}
|
|
339
425
|
const n = s.nodeCount;
|
|
426
|
+
const sources = checkSources(s, options?.sources);
|
|
340
427
|
const levelsPerSubmit = tuning.levelsPerSubmit ?? MAX_LEVELS_PER_SUBMIT;
|
|
341
428
|
if (!Number.isInteger(levelsPerSubmit) || levelsPerSubmit < 1 || levelsPerSubmit > MAX_LEVELS_PER_SUBMIT) {
|
|
342
429
|
throw new WebGpuGraphError(
|
|
@@ -370,9 +457,9 @@ export async function closenessWithTuning(
|
|
|
370
457
|
feature: "closenessCentrality.nonFiniteWeights",
|
|
371
458
|
});
|
|
372
459
|
}
|
|
373
|
-
return weightedRoute(ctx, s, scores, options);
|
|
460
|
+
return weightedRoute(ctx, s, scores, sources, options);
|
|
374
461
|
}
|
|
375
|
-
return sweepRoute(ctx, s, scores, levelsPerSubmit, options, tuning);
|
|
462
|
+
return sweepRoute(ctx, s, scores, sources, levelsPerSubmit, options, tuning);
|
|
376
463
|
}
|
|
377
464
|
|
|
378
465
|
/**
|
|
@@ -381,15 +468,20 @@ export async function closenessWithTuning(
|
|
|
381
468
|
* `closenessCentrality`, unweighted by one bit-parallel multi-source search per 32 sources, weighted by one `sssp`
|
|
382
469
|
* per source; `weighted` defaults to the snapshot's flag, `maxIterations` / `tolerance` are refused when defined
|
|
383
470
|
* (PD-25). `iterations` is the source batches run and `converged` is always true.
|
|
471
|
+
*
|
|
472
|
+
* SAMPLED (`sources`, node indices, duplicates run twice; undirected snapshots only, E_UNSUPPORTED
|
|
473
|
+
* `closenessCentrality.directedSources` otherwise): the batches seed the listed sources instead of every node, and
|
|
474
|
+
* each node's score is `1 / sum` of its distances to the sources that reach it (itself excluded), `0` when none does:
|
|
475
|
+
* the sampled score of the CPU port, unscaled. `sourcesUsed` is the list's length (`n` exact).
|
|
384
476
|
* @param ctx - the context whose device runs the kernels
|
|
385
477
|
* @param s - the snapshot (uploaded through ctx.residency, or found there)
|
|
386
|
-
* @param options -
|
|
387
|
-
* @returns the scores, the batches run, `converged: true
|
|
478
|
+
* @param options - `weighted`, `sources`, plus dest (a Float32Array of length n for `scores`) / signal / onProgress
|
|
479
|
+
* @returns the scores, the batches run, `converged: true`, `precision: "f32"` and `sourcesUsed`
|
|
388
480
|
*/
|
|
389
481
|
export function closenessCentrality(
|
|
390
482
|
ctx: GpuContext,
|
|
391
483
|
s: GraphSnapshot,
|
|
392
|
-
options?: HitsOptionsLike & GpuRunOptions,
|
|
393
|
-
): Promise<
|
|
484
|
+
options?: ClosenessAcceleratorOptions & HitsOptionsLike & GpuRunOptions,
|
|
485
|
+
): Promise<GpuClosenessResult> {
|
|
394
486
|
return closenessWithTuning(ctx, s, options, {});
|
|
395
487
|
}
|
|
@@ -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
|
+
}
|