@graphty/webgpu-graph-algorithms 0.6.3 → 0.6.5
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 +62 -32
- package/dist/browser.js +1 -1
- package/dist/chunks/{context-BXqgCifx.js → context-hzGggHeM.js} +68 -24
- package/dist/chunks/context-hzGggHeM.js.map +1 -0
- package/dist/node.js +1 -1
- package/dist/src/accelerator.d.ts +8 -6
- package/dist/src/accelerator.d.ts.map +1 -1
- package/dist/src/accelerator.js +57 -6
- package/dist/src/accelerator.js.map +1 -1
- package/dist/src/algorithms/bellman-ford.d.ts +60 -0
- package/dist/src/algorithms/bellman-ford.d.ts.map +1 -0
- package/dist/src/algorithms/bellman-ford.js +301 -0
- package/dist/src/algorithms/bellman-ford.js.map +1 -0
- package/dist/src/algorithms/bfs.d.ts +67 -0
- package/dist/src/algorithms/bfs.d.ts.map +1 -0
- package/dist/src/algorithms/bfs.js +534 -0
- package/dist/src/algorithms/bfs.js.map +1 -0
- package/dist/src/algorithms/closeness.d.ts +53 -0
- package/dist/src/algorithms/closeness.d.ts.map +1 -0
- package/dist/src/algorithms/closeness.js +323 -0
- package/dist/src/algorithms/closeness.js.map +1 -0
- package/dist/src/algorithms/scope.d.ts +3 -1
- package/dist/src/algorithms/scope.d.ts.map +1 -1
- package/dist/src/algorithms/scope.js +1 -0
- package/dist/src/algorithms/scope.js.map +1 -1
- package/dist/src/algorithms/sssp.d.ts +72 -0
- package/dist/src/algorithms/sssp.d.ts.map +1 -0
- package/dist/src/algorithms/sssp.js +586 -0
- package/dist/src/algorithms/sssp.js.map +1 -0
- package/dist/src/constants.d.ts +10 -0
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +10 -0
- package/dist/src/constants.js.map +1 -1
- package/dist/src/index.d.ts +8 -2
- 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 +4 -4
- package/dist/src/kernel/prelude.d.ts.map +1 -1
- package/dist/src/kernel/prelude.js +39 -5
- package/dist/src/kernel/prelude.js.map +1 -1
- package/dist/src/kernel/uniform-ring.d.ts +8 -0
- package/dist/src/kernel/uniform-ring.d.ts.map +1 -1
- package/dist/src/kernel/uniform-ring.js +13 -0
- package/dist/src/kernel/uniform-ring.js.map +1 -1
- package/dist/src/kernels.d.ts +45 -4
- package/dist/src/kernels.d.ts.map +1 -1
- package/dist/src/kernels.js +368 -3
- package/dist/src/kernels.js.map +1 -1
- package/dist/src/primitives/advance.d.ts +63 -0
- package/dist/src/primitives/advance.d.ts.map +1 -0
- package/dist/src/primitives/advance.js +95 -0
- package/dist/src/primitives/advance.js.map +1 -0
- package/dist/src/primitives/compact.d.ts +89 -0
- package/dist/src/primitives/compact.d.ts.map +1 -0
- package/dist/src/primitives/compact.js +233 -0
- package/dist/src/primitives/compact.js.map +1 -0
- package/dist/src/primitives/core-shape.d.ts +22 -1
- package/dist/src/primitives/core-shape.d.ts.map +1 -1
- package/dist/src/primitives/core-shape.js +33 -3
- package/dist/src/primitives/core-shape.js.map +1 -1
- package/dist/src/primitives/frontier.d.ts +151 -0
- package/dist/src/primitives/frontier.d.ts.map +1 -0
- package/dist/src/primitives/frontier.js +250 -0
- package/dist/src/primitives/frontier.js.map +1 -0
- package/dist/src/types/accelerator.d.ts +16 -7
- package/dist/src/types/accelerator.d.ts.map +1 -1
- package/dist/src/types/traversal.d.ts +53 -0
- package/dist/src/types/traversal.d.ts.map +1 -0
- package/dist/src/types/traversal.js +10 -0
- package/dist/src/types/traversal.js.map +1 -0
- package/dist/src/wgsl/advance-expand.wgsl.d.ts +19 -0
- package/dist/src/wgsl/advance-expand.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/advance-expand.wgsl.js +69 -0
- package/dist/src/wgsl/advance-expand.wgsl.js.map +1 -0
- package/dist/src/wgsl/bf-relax.wgsl.d.ts +22 -0
- package/dist/src/wgsl/bf-relax.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/bf-relax.wgsl.js +58 -0
- package/dist/src/wgsl/bf-relax.wgsl.js.map +1 -0
- package/dist/src/wgsl/bfs-bitset-build.wgsl.d.ts +15 -0
- package/dist/src/wgsl/bfs-bitset-build.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/bfs-bitset-build.wgsl.js +24 -0
- package/dist/src/wgsl/bfs-bitset-build.wgsl.js.map +1 -0
- package/dist/src/wgsl/bfs-bottom-up.wgsl.d.ts +20 -0
- package/dist/src/wgsl/bfs-bottom-up.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/bfs-bottom-up.wgsl.js +67 -0
- package/dist/src/wgsl/bfs-bottom-up.wgsl.js.map +1 -0
- package/dist/src/wgsl/bfs-contract.wgsl.d.ts +20 -0
- package/dist/src/wgsl/bfs-contract.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/bfs-contract.wgsl.js +55 -0
- package/dist/src/wgsl/bfs-contract.wgsl.js.map +1 -0
- package/dist/src/wgsl/bfs-fused.wgsl.d.ts +25 -0
- package/dist/src/wgsl/bfs-fused.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/bfs-fused.wgsl.js +78 -0
- package/dist/src/wgsl/bfs-fused.wgsl.js.map +1 -0
- package/dist/src/wgsl/bfs-unvisited-flags.wgsl.d.ts +18 -0
- package/dist/src/wgsl/bfs-unvisited-flags.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/bfs-unvisited-flags.wgsl.js +42 -0
- package/dist/src/wgsl/bfs-unvisited-flags.wgsl.js.map +1 -0
- package/dist/src/wgsl/closeness-reduce.wgsl.d.ts +17 -0
- package/dist/src/wgsl/closeness-reduce.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/closeness-reduce.wgsl.js +65 -0
- package/dist/src/wgsl/closeness-reduce.wgsl.js.map +1 -0
- package/dist/src/wgsl/closeness-sweep.wgsl.d.ts +20 -0
- package/dist/src/wgsl/closeness-sweep.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/closeness-sweep.wgsl.js +96 -0
- package/dist/src/wgsl/closeness-sweep.wgsl.js.map +1 -0
- package/dist/src/wgsl/compact-scatter.wgsl.d.ts +9 -0
- package/dist/src/wgsl/compact-scatter.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/compact-scatter.wgsl.js +17 -0
- package/dist/src/wgsl/compact-scatter.wgsl.js.map +1 -0
- package/dist/src/wgsl/dedupe-claim.wgsl.d.ts +10 -0
- package/dist/src/wgsl/dedupe-claim.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/dedupe-claim.wgsl.js +19 -0
- package/dist/src/wgsl/dedupe-claim.wgsl.js.map +1 -0
- package/dist/src/wgsl/dedupe-filter.wgsl.d.ts +12 -0
- package/dist/src/wgsl/dedupe-filter.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/dedupe-filter.wgsl.js +46 -0
- package/dist/src/wgsl/dedupe-filter.wgsl.js.map +1 -0
- package/dist/src/wgsl/frontier-finalize.wgsl.d.ts +53 -0
- package/dist/src/wgsl/frontier-finalize.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/frontier-finalize.wgsl.js +164 -0
- package/dist/src/wgsl/frontier-finalize.wgsl.js.map +1 -0
- package/dist/src/wgsl/sssp-pred.wgsl.d.ts +28 -0
- package/dist/src/wgsl/sssp-pred.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/sssp-pred.wgsl.js +80 -0
- package/dist/src/wgsl/sssp-pred.wgsl.js.map +1 -0
- package/dist/src/wgsl/sssp-relax.wgsl.d.ts +30 -0
- package/dist/src/wgsl/sssp-relax.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/sssp-relax.wgsl.js +72 -0
- package/dist/src/wgsl/sssp-relax.wgsl.js.map +1 -0
- package/dist/webgpu-graph-algorithms.js +3155 -384
- package/dist/webgpu-graph-algorithms.js.map +1 -1
- package/package.json +5 -4
- package/src/accelerator.ts +65 -7
- package/src/algorithms/bellman-ford.ts +387 -0
- package/src/algorithms/bfs.ts +626 -0
- package/src/algorithms/closeness.ts +395 -0
- package/src/algorithms/scope.ts +4 -1
- package/src/algorithms/sssp.ts +768 -0
- package/src/constants.ts +10 -0
- package/src/index.ts +14 -1
- package/src/kernel/prelude.ts +39 -4
- package/src/kernel/uniform-ring.ts +14 -0
- package/src/kernels.ts +447 -6
- package/src/primitives/advance.ts +131 -0
- package/src/primitives/compact.ts +323 -0
- package/src/primitives/core-shape.ts +41 -3
- package/src/primitives/frontier.ts +372 -0
- package/src/types/accelerator.ts +18 -5
- package/src/types/traversal.ts +56 -0
- package/src/wgsl/advance-expand.wgsl.ts +68 -0
- package/src/wgsl/bf-relax.wgsl.ts +57 -0
- package/src/wgsl/bfs-bitset-build.wgsl.ts +23 -0
- package/src/wgsl/bfs-bottom-up.wgsl.ts +66 -0
- package/src/wgsl/bfs-contract.wgsl.ts +54 -0
- package/src/wgsl/bfs-fused.wgsl.ts +77 -0
- package/src/wgsl/bfs-unvisited-flags.wgsl.ts +41 -0
- package/src/wgsl/closeness-reduce.wgsl.ts +64 -0
- package/src/wgsl/closeness-sweep.wgsl.ts +95 -0
- package/src/wgsl/compact-scatter.wgsl.ts +16 -0
- package/src/wgsl/dedupe-claim.wgsl.ts +18 -0
- package/src/wgsl/dedupe-filter.wgsl.ts +45 -0
- package/src/wgsl/frontier-finalize.wgsl.ts +163 -0
- package/src/wgsl/sssp-pred.wgsl.ts +79 -0
- package/src/wgsl/sssp-relax.wgsl.ts +71 -0
- package/dist/chunks/context-BXqgCifx.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { W as WebGpuGraphError, U as UNIFORM_SLOT_BYTES, B as BufferUsage, M as MAX_WORKGROUPS_PER_DIM, a as WGSL_RESERVED_WORDS, S as STATE_HEADER_BYTES, d as deviceLostError, i as isWebGpuGraphError, b as U32_MAX$2, R as RADIX_BINS,
|
|
2
|
-
import { A, G,
|
|
3
|
-
import { renumberPartition, makeMask, maskTest, expandEdges, fromEdgeArrays } from "@graphty/graph-format";
|
|
1
|
+
import { W as WebGpuGraphError, U as UNIFORM_SLOT_BYTES, B as BufferUsage, M as MAX_WORKGROUPS_PER_DIM, a as WGSL_RESERVED_WORDS, S as STATE_HEADER_BYTES, d as deviceLostError, i as isWebGpuGraphError, b as U32_MAX$2, c as MAX_LEVELS_PER_SUBMIT, R as RADIX_BINS, F as FUSED_FRONTIER_MAX, e as BEAMER_BETA, f as SSSP_DELTA_FACTOR, g as F32_INF_BITS, h as GRID_COARSEST_SIDE, j as GRID_MIN_SIDE, k as GRID_SORT_BITS, l as FA2_DEFAULTS, m as MAX_ITERATIONS_PER_STEP, n as MAX_1D_ITEMS, o as hasErrorCode, p as FA2_FLAG_FIRST, P as PARTIAL_BYTES, I as INDIRECT_ARGS_STRIDE, q as GRID_HUB_CELL, L as LAYOUT_TUNING_DEFAULTS, E as EXACT_MAX_NODES, T as TRACE_RECORD_BYTES, r as GRID_BBOX_MARGIN, s as GRID_EXTENT_FLOOR, t as FR_ADAPTIVE_MAX_ITERATIONS, u as FR_START_TEMPERATURE, v as FA2_FLAG_ADAPTIVE, w as FR_REHEAT_FRACTION, x as FR_DEFAULTS, y as SE_DEFAULTS, z as SE_SCALE_REFERENCE_NODES } from "./chunks/context-hzGggHeM.js";
|
|
2
|
+
import { A, G, C, D, H, J } from "./chunks/context-hzGggHeM.js";
|
|
3
|
+
import { renumberPartition, INVALID_INDEX, makeMask, maskTest, expandEdges, fromEdgeArrays } from "@graphty/graph-format";
|
|
4
4
|
class UniformRing {
|
|
5
5
|
/**
|
|
6
6
|
* Creates the ring buffer (`slots x UNIFORM_SLOT_BYTES` bytes, UNIFORM | COPY_DST) through the allocator.
|
|
@@ -10,6 +10,7 @@ class UniformRing {
|
|
|
10
10
|
* @param label - the buffer label
|
|
11
11
|
*/
|
|
12
12
|
constructor(device, allocator, slots, label) {
|
|
13
|
+
this.overruns = 0;
|
|
13
14
|
this.next = 0;
|
|
14
15
|
this.dirtyLo = -1;
|
|
15
16
|
this.dirtyHi = -1;
|
|
@@ -31,6 +32,7 @@ class UniformRing {
|
|
|
31
32
|
this.label = label;
|
|
32
33
|
this.shadow = new ArrayBuffer(slots * UNIFORM_SLOT_BYTES);
|
|
33
34
|
this.view = new DataView(this.shadow);
|
|
35
|
+
this.dirty = new Uint8Array(slots);
|
|
34
36
|
this.buffer = allocator.createBuffer({
|
|
35
37
|
label,
|
|
36
38
|
size: slots * UNIFORM_SLOT_BYTES,
|
|
@@ -79,6 +81,9 @@ class UniformRing {
|
|
|
79
81
|
}
|
|
80
82
|
const first = this.next;
|
|
81
83
|
this.next += count;
|
|
84
|
+
if (this.dirty.subarray(first, first + count).includes(1)) {
|
|
85
|
+
this.overruns += 1;
|
|
86
|
+
}
|
|
82
87
|
return first;
|
|
83
88
|
}
|
|
84
89
|
/**
|
|
@@ -91,6 +96,7 @@ class UniformRing {
|
|
|
91
96
|
this.checkSlot(slot);
|
|
92
97
|
this.assertFits(block);
|
|
93
98
|
block.write(this.view, values, slot * UNIFORM_SLOT_BYTES);
|
|
99
|
+
this.dirty[slot] = 1;
|
|
94
100
|
if (this.dirtyLo < 0 || slot < this.dirtyLo) {
|
|
95
101
|
this.dirtyLo = slot;
|
|
96
102
|
}
|
|
@@ -110,6 +116,7 @@ class UniformRing {
|
|
|
110
116
|
const begin = this.dirtyLo * UNIFORM_SLOT_BYTES;
|
|
111
117
|
const end = (this.dirtyHi + 1) * UNIFORM_SLOT_BYTES;
|
|
112
118
|
this.device.queue.writeBuffer(this.buffer, begin, this.shadow, begin, end - begin);
|
|
119
|
+
this.dirty.fill(0);
|
|
113
120
|
this.dirtyLo = -1;
|
|
114
121
|
this.dirtyHi = -1;
|
|
115
122
|
}
|
|
@@ -153,7 +160,7 @@ class UniformRing {
|
|
|
153
160
|
function perDimension(caps) {
|
|
154
161
|
return Math.min(MAX_WORKGROUPS_PER_DIM, caps.limits.maxComputeWorkgroupsPerDimension);
|
|
155
162
|
}
|
|
156
|
-
function assertCount(argument, value) {
|
|
163
|
+
function assertCount$1(argument, value) {
|
|
157
164
|
if (!Number.isInteger(value) || value < 0) {
|
|
158
165
|
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${argument} must be a non-negative integer, got ${value}`, {
|
|
159
166
|
argument,
|
|
@@ -183,8 +190,8 @@ function grid(groups, items, caps) {
|
|
|
183
190
|
if (groups <= limit) {
|
|
184
191
|
return { x: groups, y: 1, z: 1, items, stride: null };
|
|
185
192
|
}
|
|
186
|
-
const
|
|
187
|
-
if (
|
|
193
|
+
const y = Math.ceil(groups / limit);
|
|
194
|
+
if (y > limit) {
|
|
188
195
|
throw new WebGpuGraphError(
|
|
189
196
|
"E_TOO_LARGE",
|
|
190
197
|
`${groups} workgroups exceed the 2D dispatch limit of ${limit * limit}`,
|
|
@@ -197,15 +204,15 @@ function grid(groups, items, caps) {
|
|
|
197
204
|
}
|
|
198
205
|
);
|
|
199
206
|
}
|
|
200
|
-
return { x: limit, y
|
|
207
|
+
return { x: limit, y, z: 1, items, stride: null };
|
|
201
208
|
}
|
|
202
209
|
function plan1d(items, wg, caps) {
|
|
203
|
-
assertCount("items", items);
|
|
210
|
+
assertCount$1("items", items);
|
|
204
211
|
assertWorkgroupSize(wg);
|
|
205
212
|
return grid(Math.ceil(items / wg), items, caps);
|
|
206
213
|
}
|
|
207
214
|
function planGridStride(items, wg, caps, maxGroups) {
|
|
208
|
-
assertCount("items", items);
|
|
215
|
+
assertCount$1("items", items);
|
|
209
216
|
assertWorkgroupSize(wg);
|
|
210
217
|
if (items === 0) {
|
|
211
218
|
return { x: 0, y: 1, z: 1, items, stride: null };
|
|
@@ -544,6 +551,432 @@ ${lines.join("\n")}
|
|
|
544
551
|
return lanes;
|
|
545
552
|
}
|
|
546
553
|
}
|
|
554
|
+
const advanceExpandWgsl = (
|
|
555
|
+
/* wgsl */
|
|
556
|
+
`
|
|
557
|
+
var<workgroup> sh: array<u32, WG>; // the block's degrees, then their inclusive scan
|
|
558
|
+
var<workgroup> rowStart: array<u32, WG>; // the first bound arc of each entry's row
|
|
559
|
+
var<workgroup> base: u32; // the block's reserved span in the edge queue
|
|
560
|
+
var<workgroup> wcount: u32; // the frontier's length on a two-phase level, 0 on any other
|
|
561
|
+
|
|
562
|
+
@compute @workgroup_size(WG)
|
|
563
|
+
fn advance_expand(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
|
|
564
|
+
if (lid.x == 0u) { wcount = select(0u, atomicLoad(&counters[0]), atomicLoad(&counters[24]) == 1u); } // frontierCount, on the two-phase path only (the path word)
|
|
565
|
+
let count = workgroupUniformLoad(&wcount); // uniform: the block loop below holds barriers
|
|
566
|
+
for (var b0 = group_id(wid) * WG; b0 < count; b0 = b0 + P.stride) { // grid-stride over blocks of WG entries (a direct dispatch of the plan's groups)
|
|
567
|
+
let i = b0 + lid.x; // this lane's frontier entry
|
|
568
|
+
var deg = 0u;
|
|
569
|
+
var start = 0u;
|
|
570
|
+
if (i < count) { // guarded loads into locals (3.5 rule 1)
|
|
571
|
+
let v = frontierIn[i];
|
|
572
|
+
let lo = max(rowPtr[v], P.arcBase); // the row clipped to the bound window (P8-T12)
|
|
573
|
+
let hi = min(rowPtr[v + 1u], P.arcEnd);
|
|
574
|
+
start = lo;
|
|
575
|
+
deg = select(0u, hi - lo, hi > lo);
|
|
576
|
+
}
|
|
577
|
+
let inclusive = wg_scan_u32(deg, lid.x); // the prelude's inclusive scan in LANE order (P8-T1 Step 5); the twin
|
|
578
|
+
sh[lid.x] = inclusive; // sh is monotone in lid.x, the index the binary search walks
|
|
579
|
+
rowStart[lid.x] = start;
|
|
580
|
+
workgroupBarrier();
|
|
581
|
+
let aggregate = workgroupUniformLoad(&sh[WG - 1u]); // uniform; includes a barrier
|
|
582
|
+
if (lid.x == 0u) {
|
|
583
|
+
base = atomicAdd(&counters[8], aggregate); // edgeCount: ONE reservation per workgroup, not one per arc
|
|
584
|
+
atomicAdd(&counters[9], aggregate); // edgeCountUnclamped: the overflow detector (PD-23)
|
|
585
|
+
atomicAdd(&counters[2], aggregate); // frontierDegreeSum: Beamer's m_f (P8-T8)
|
|
586
|
+
}
|
|
587
|
+
workgroupBarrier();
|
|
588
|
+
for (var p = lid.x; p < aggregate; p = p + WG) { // strip [0, aggregate): lane j takes j, j + WG, ...
|
|
589
|
+
var lo = 0u; // upper_bound: the first k with sh[k] > p owns arc p
|
|
590
|
+
var hi = WG;
|
|
591
|
+
loop {
|
|
592
|
+
if (lo >= hi) { break; }
|
|
593
|
+
let mid = (lo + hi) / 2u;
|
|
594
|
+
if (sh[mid] > p) { hi = mid; } else { lo = mid + 1u; }
|
|
595
|
+
}
|
|
596
|
+
let k = lo;
|
|
597
|
+
var exclusive = 0u;
|
|
598
|
+
if (k > 0u) { exclusive = sh[k - 1u]; }
|
|
599
|
+
let arc = rowStart[k] + (p - exclusive);
|
|
600
|
+
let q = base + p;
|
|
601
|
+
if (q < P.edgeCapacity) { edgeQueue[q] = colIdx[arc - P.arcBase]; } // the clamp of PD-23; the queue holds the target vertex only (PD-24)
|
|
602
|
+
}
|
|
603
|
+
workgroupBarrier(); // sh, rowStart and base are reused by the next block
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
`
|
|
607
|
+
);
|
|
608
|
+
const bfRelaxWgsl = (
|
|
609
|
+
/* wgsl */
|
|
610
|
+
`
|
|
611
|
+
fn relax(v: u32, nd: f32) {
|
|
612
|
+
var cur = atomicLoad(&dist[v]);
|
|
613
|
+
var tries = 0u;
|
|
614
|
+
loop {
|
|
615
|
+
if (!(nd < bitcast<f32>(cur))) { break; } // no improvement; +Inf is greater than every finite nd
|
|
616
|
+
let r = atomicCompareExchangeWeak(&dist[v], cur, bitcast<u32>(nd));
|
|
617
|
+
if (r.exchanged) { atomicStore(&flags[0], 1u); break; } // changed
|
|
618
|
+
cur = r.old_value;
|
|
619
|
+
tries = tries + 1u;
|
|
620
|
+
if (tries >= P.maxRetries) { atomicStore(&flags[1], 1u); break; } // retryExhausted (PD-12)
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
@compute @workgroup_size(WG)
|
|
625
|
+
fn bf_relax(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
|
|
626
|
+
let first = linear_id(wid, lid.x);
|
|
627
|
+
let cutoff = bitcast<f32>(P.cutoffBits);
|
|
628
|
+
for (var e = first; e < P.edgeCount; e = e + P.stride) { // each logical edge once (edgeList)
|
|
629
|
+
let u = edgeSrc[e];
|
|
630
|
+
let v = edgeDst[e];
|
|
631
|
+
let w = weights[edgeToArc[e]]; // the edge's weight through its forward arc
|
|
632
|
+
let du = atomicLoad(&dist[u]);
|
|
633
|
+
if (du != F32_INF_BITS) { // unreached is tested as the bit pattern, as sssp-pred does
|
|
634
|
+
let nd = bitcast<f32>(du) + w;
|
|
635
|
+
if (nd <= cutoff) { relax(v, nd); }
|
|
636
|
+
}
|
|
637
|
+
if (UNDIRECTED) { // the other direction of an undirected edge
|
|
638
|
+
let dv = atomicLoad(&dist[v]);
|
|
639
|
+
if (dv != F32_INF_BITS) {
|
|
640
|
+
let nd = bitcast<f32>(dv) + w;
|
|
641
|
+
if (nd <= cutoff) { relax(u, nd); }
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
`
|
|
647
|
+
);
|
|
648
|
+
const bfsBitsetBuildWgsl = (
|
|
649
|
+
/* wgsl */
|
|
650
|
+
`
|
|
651
|
+
@compute @workgroup_size(WG)
|
|
652
|
+
fn bfs_bitset_build(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
|
|
653
|
+
let count = select(0u, atomicLoad(&counters[0]), atomicLoad(&counters[24]) == 3u); // frontierCount, on the bottom-up path only (the path word)
|
|
654
|
+
for (var i = linear_id(wid, lid.x); i < count; i = i + P.stride) { // grid-stride; no barrier anywhere
|
|
655
|
+
let v = frontierIn[i];
|
|
656
|
+
atomicOr(&bits[P.bitsBase + (v >> 5u)], 1u << (v & 31u));
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
`
|
|
660
|
+
);
|
|
661
|
+
const bfsBottomUpWgsl = (
|
|
662
|
+
/* wgsl */
|
|
663
|
+
`
|
|
664
|
+
var<workgroup> sh: array<u32, WG>;
|
|
665
|
+
var<workgroup> base: u32;
|
|
666
|
+
var<workgroup> wcount: u32; // the unvisited list's length on a bottom-up level, 0 on any other
|
|
667
|
+
|
|
668
|
+
@compute @workgroup_size(WG)
|
|
669
|
+
fn bfs_bottom_up(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
|
|
670
|
+
let claim = atomicLoad(&counters[11]) + 1u;
|
|
671
|
+
if (lid.x == 0u) { wcount = select(0u, atomicLoad(&counters[7]), atomicLoad(&counters[24]) == 3u); } // unvisitedListLen, on the bottom-up path only (the path word)
|
|
672
|
+
let len = workgroupUniformLoad(&wcount); // uniform: the block loop below holds barriers
|
|
673
|
+
for (var b0 = group_id(wid) * WG; b0 < len; b0 = b0 + P.stride) { // grid-stride over blocks of WG entries
|
|
674
|
+
let i = b0 + lid.x;
|
|
675
|
+
var won = 0u;
|
|
676
|
+
var v = 0u;
|
|
677
|
+
var reads = 0u;
|
|
678
|
+
if (i < len) { // guarded work into locals
|
|
679
|
+
v = sweepIn[i];
|
|
680
|
+
if (atomicLoad(&depth[v]) == INVALID_INDEX) { // a stale entry, claimed since the rebuild, is skipped
|
|
681
|
+
let end = min(rowPtr[v + 1u], P.arcEnd);
|
|
682
|
+
for (var a = max(rowPtr[v], P.arcBase); a < end; a = a + 1u) { // in-neighbours through the reverse core
|
|
683
|
+
reads = reads + 1u;
|
|
684
|
+
let u = colIdx[a - P.arcBase];
|
|
685
|
+
if (mask_bit(sweepIn[P.bitsBase + (u >> 5u)], u)) { won = 1u; break; } // the early exit: a real break, never a flag
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
sh[lid.x] = won;
|
|
690
|
+
workgroupBarrier();
|
|
691
|
+
for (var s = 1u; s < WG; s = s * 2u) { // Hillis-Steele inclusive scan of won (bfs-contract's)
|
|
692
|
+
var t = 0u;
|
|
693
|
+
if (lid.x >= s) { t = sh[lid.x - s]; }
|
|
694
|
+
workgroupBarrier();
|
|
695
|
+
sh[lid.x] = sh[lid.x] + t;
|
|
696
|
+
workgroupBarrier();
|
|
697
|
+
}
|
|
698
|
+
let inclusive = sh[lid.x];
|
|
699
|
+
let readsTotal = wg_reduce_u32(reads, lid.x, 0u); // arcsScanned, one atomic per workgroup (the sabotage witness, Step 6)
|
|
700
|
+
if (lid.x == WG - 1u) { base = atomicAdd(&counters[1], inclusive); }
|
|
701
|
+
if (lid.x == 0u) { atomicAdd(&counters[16], readsTotal); }
|
|
702
|
+
workgroupBarrier();
|
|
703
|
+
if (won == 1u) {
|
|
704
|
+
atomicStore(&depth[v], claim); // no claim race: the list holds v once and the sweep is vertex-parallel
|
|
705
|
+
frontierOut[base + inclusive - 1u] = v;
|
|
706
|
+
}
|
|
707
|
+
workgroupBarrier(); // sh and base are reused by the next block
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
`
|
|
711
|
+
);
|
|
712
|
+
const bfsContractWgsl = (
|
|
713
|
+
/* wgsl */
|
|
714
|
+
`
|
|
715
|
+
var<workgroup> sh: array<u32, WG>;
|
|
716
|
+
var<workgroup> base: u32;
|
|
717
|
+
var<workgroup> wcount: u32; // the clamped edge count on a two-phase level, 0 on any other
|
|
718
|
+
|
|
719
|
+
@compute @workgroup_size(WG)
|
|
720
|
+
fn bfs_contract(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
|
|
721
|
+
if (lid.x == 0u) { wcount = select(0u, atomicLoad(&counters[8]), atomicLoad(&counters[24]) == 1u); } // edgeCount, clamped by role 1; the two-phase path only (the path word)
|
|
722
|
+
let count = workgroupUniformLoad(&wcount); // uniform: the block loop below holds barriers
|
|
723
|
+
let claim = atomicLoad(&counters[11]) + 1u; // the depth this level assigns
|
|
724
|
+
for (var b0 = group_id(wid) * WG; b0 < count; b0 = b0 + P.stride) { // grid-stride over blocks of WG entries
|
|
725
|
+
let i = b0 + lid.x;
|
|
726
|
+
var won = 0u;
|
|
727
|
+
var v = 0u;
|
|
728
|
+
if (i < count) { // guarded work into locals
|
|
729
|
+
v = edgeQueue[i];
|
|
730
|
+
let old = atomicMin(&depth[v], claim);
|
|
731
|
+
won = select(0u, 1u, old == INVALID_INDEX); // PD-6: the unique winner
|
|
732
|
+
}
|
|
733
|
+
sh[lid.x] = won;
|
|
734
|
+
workgroupBarrier();
|
|
735
|
+
for (var s = 1u; s < WG; s = s * 2u) { // Hillis-Steele inclusive scan of won
|
|
736
|
+
var t = 0u;
|
|
737
|
+
if (lid.x >= s) { t = sh[lid.x - s]; }
|
|
738
|
+
workgroupBarrier();
|
|
739
|
+
sh[lid.x] = sh[lid.x] + t;
|
|
740
|
+
workgroupBarrier();
|
|
741
|
+
}
|
|
742
|
+
let inclusive = sh[lid.x];
|
|
743
|
+
if (lid.x == WG - 1u) { base = atomicAdd(&counters[1], inclusive); } // nextFrontierCount: one reservation per workgroup
|
|
744
|
+
workgroupBarrier();
|
|
745
|
+
if (won == 1u) { frontierOut[base + inclusive - 1u] = v; }
|
|
746
|
+
workgroupBarrier(); // sh and base are reused by the next block
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
`
|
|
750
|
+
);
|
|
751
|
+
const bfsFusedWgsl = (
|
|
752
|
+
/* wgsl */
|
|
753
|
+
`
|
|
754
|
+
var<workgroup> sh: array<u32, WG>;
|
|
755
|
+
var<workgroup> wdeg: u32;
|
|
756
|
+
var<workgroup> wstart: u32;
|
|
757
|
+
var<workgroup> base: u32;
|
|
758
|
+
var<workgroup> wcount: u32; // the frontier's length on a fused or retry level, 0 on any other
|
|
759
|
+
|
|
760
|
+
@compute @workgroup_size(WG)
|
|
761
|
+
fn bfs_fused(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
|
|
762
|
+
let claim = atomicLoad(&counters[11]) + 1u;
|
|
763
|
+
if (lid.x == 0u) {
|
|
764
|
+
let path = atomicLoad(&counters[24]); // 2 the fused level, 4 the overflow retry (PD-23)
|
|
765
|
+
wcount = select(0u, atomicLoad(&counters[0]), path == 2u || path == 4u);
|
|
766
|
+
}
|
|
767
|
+
let count = workgroupUniformLoad(&wcount); // uniform: the entry loop below holds barriers
|
|
768
|
+
for (var g = group_id(wid); g < count; g = g + P.stride) { // one workgroup per frontier entry; P.stride is the dispatch's GROUP count
|
|
769
|
+
if (lid.x == 0u) {
|
|
770
|
+
let u = frontierIn[g];
|
|
771
|
+
let a0 = max(rowPtr[u], P.arcBase);
|
|
772
|
+
let a1 = min(rowPtr[u + 1u], P.arcEnd);
|
|
773
|
+
let d = select(0u, a1 - a0, a1 > a0);
|
|
774
|
+
wdeg = d;
|
|
775
|
+
wstart = a0;
|
|
776
|
+
atomicAdd(&counters[2], d); // frontierDegreeSum, so Beamer's test (P8-T8) sees fused levels too
|
|
777
|
+
}
|
|
778
|
+
let deg = workgroupUniformLoad(&wdeg); // uniform: the loop below may hold barriers
|
|
779
|
+
let start = workgroupUniformLoad(&wstart);
|
|
780
|
+
for (var p0 = 0u; p0 < deg; p0 = p0 + WG) { // strip the row WG arcs at a time
|
|
781
|
+
let p = p0 + lid.x;
|
|
782
|
+
var won = 0u;
|
|
783
|
+
var v = 0u;
|
|
784
|
+
if (p < deg) { // guarded claim into locals
|
|
785
|
+
v = colIdx[start + p - P.arcBase];
|
|
786
|
+
let old = atomicMin(&depth[v], claim);
|
|
787
|
+
won = select(0u, 1u, old == INVALID_INDEX);
|
|
788
|
+
}
|
|
789
|
+
sh[lid.x] = won;
|
|
790
|
+
workgroupBarrier();
|
|
791
|
+
for (var s = 1u; s < WG; s = s * 2u) { // Hillis-Steele inclusive scan of won (bfs-contract's, verbatim)
|
|
792
|
+
var t = 0u;
|
|
793
|
+
if (lid.x >= s) { t = sh[lid.x - s]; }
|
|
794
|
+
workgroupBarrier();
|
|
795
|
+
sh[lid.x] = sh[lid.x] + t;
|
|
796
|
+
workgroupBarrier();
|
|
797
|
+
}
|
|
798
|
+
let inclusive = sh[lid.x];
|
|
799
|
+
if (lid.x == WG - 1u) { base = atomicAdd(&counters[1], inclusive); }
|
|
800
|
+
workgroupBarrier();
|
|
801
|
+
if (won == 1u) { frontierOut[base + inclusive - 1u] = v; }
|
|
802
|
+
workgroupBarrier(); // sh and base are reused by the next strip
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
`
|
|
807
|
+
);
|
|
808
|
+
const bfsUnvisitedFlagsWgsl = (
|
|
809
|
+
/* wgsl */
|
|
810
|
+
`
|
|
811
|
+
@compute @workgroup_size(WG)
|
|
812
|
+
fn bfs_unvisited_flags(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
|
|
813
|
+
let first = linear_id(wid, lid.x);
|
|
814
|
+
var cnt = 0u;
|
|
815
|
+
var degSum = 0u;
|
|
816
|
+
var len = 0u;
|
|
817
|
+
for (var v = first; v < P.n; v = v + P.stride) { // no barrier inside: the trip count is per lane
|
|
818
|
+
let unv = depth[v] == INVALID_INDEX;
|
|
819
|
+
let listed = unv && (inDegree[v] != 0u);
|
|
820
|
+
flags[v] = select(0u, 1u, listed);
|
|
821
|
+
cnt = cnt + select(0u, 1u, unv);
|
|
822
|
+
degSum = degSum + select(0u, outDegree[v], unv); // the OUT-degree: Beamer's m_u counts the edges top-down would examine
|
|
823
|
+
len = len + select(0u, 1u, listed);
|
|
824
|
+
}
|
|
825
|
+
let c = wg_reduce_u32(cnt, lid.x, 0u); // the prelude's workgroup sum (combine_u's sum code); uniform: after the loop
|
|
826
|
+
let d = wg_reduce_u32(degSum, lid.x, 0u);
|
|
827
|
+
let l = wg_reduce_u32(len, lid.x, 0u);
|
|
828
|
+
if (lid.x == 0u) { // ONE atomic per word per workgroup
|
|
829
|
+
atomicAdd(&counters[5], c);
|
|
830
|
+
atomicAdd(&counters[6], d);
|
|
831
|
+
atomicAdd(&counters[7], l);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
`
|
|
835
|
+
);
|
|
836
|
+
const closenessReduceWgsl = (
|
|
837
|
+
/* wgsl */
|
|
838
|
+
`
|
|
839
|
+
@compute @workgroup_size(WG)
|
|
840
|
+
fn closeness_reduce(@builtin(local_invocation_id) lid: vec3<u32>) {
|
|
841
|
+
if (lid.x != 0u) { return; } // one lane; no barrier follows (3.5 rule 1)
|
|
842
|
+
if (P.role == 1u) { // the seed of a batch: P.source is its first source
|
|
843
|
+
let k = min(32u, P.n - P.source);
|
|
844
|
+
for (var s = 0u; s < k; s = s + 1u) {
|
|
845
|
+
let v = P.source + s;
|
|
846
|
+
let bit = 1u << s;
|
|
847
|
+
bits[v] = bit; // visited
|
|
848
|
+
bits[P.bitsBase + v] = bit; // the frontier level 0 reads (region 1: level 0's parity is 0)
|
|
849
|
+
bits[3u * P.bitsBase + v] = 1u; // flags: level 0's compact turns them into the list
|
|
850
|
+
}
|
|
851
|
+
atomicStore(&counters[0], k); // not done
|
|
852
|
+
atomicStore(&counters[11], U32_MAX); // level: the first boundary brings it to 0
|
|
853
|
+
atomicStore(&counters[15], 0u); // done
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
// role 0: the level boundary -- done from the previous level's compacted count, then the accumulation
|
|
857
|
+
let count = atomicLoad(&counters[0]);
|
|
858
|
+
atomicStore(&counters[15], select(0u, 1u, count == 0u));
|
|
859
|
+
let level = atomicLoad(&counters[11]);
|
|
860
|
+
let d = level + 1u; // the distance of the claims the level just run made
|
|
861
|
+
for (var s = 0u; s < 32u; s = s + 1u) {
|
|
862
|
+
let c = atomicLoad(&perSource[s]); // newCount[s]
|
|
863
|
+
atomicStore(&perSource[32u + s], atomicLoad(&perSource[32u + s]) + c); // reached[s]
|
|
864
|
+
// sum[s] += c x d in 64 bits: the 16-bit split product (pLo, pHi), then the add with its carry
|
|
865
|
+
let cLo = c & 0xFFFFu;
|
|
866
|
+
let cHi = c >> 16u;
|
|
867
|
+
let dLo = d & 0xFFFFu;
|
|
868
|
+
let dHi = d >> 16u;
|
|
869
|
+
let ll = cLo * dLo;
|
|
870
|
+
let lh = cLo * dHi;
|
|
871
|
+
let hl = cHi * dLo;
|
|
872
|
+
let mid = (ll >> 16u) + (lh & 0xFFFFu) + (hl & 0xFFFFu);
|
|
873
|
+
let pLo = (ll & 0xFFFFu) | ((mid & 0xFFFFu) << 16u);
|
|
874
|
+
let pHi = (cHi * dHi) + (lh >> 16u) + (hl >> 16u) + (mid >> 16u);
|
|
875
|
+
var lo = atomicLoad(&perSource[64u + s]); // sumLo[s]
|
|
876
|
+
var hi = atomicLoad(&perSource[96u + s]); // sumHi[s]
|
|
877
|
+
let before = lo;
|
|
878
|
+
lo = lo + pLo;
|
|
879
|
+
hi = hi + pHi + select(0u, 1u, lo < before);
|
|
880
|
+
atomicStore(&perSource[64u + s], lo);
|
|
881
|
+
atomicStore(&perSource[96u + s], hi);
|
|
882
|
+
atomicStore(&perSource[s], 0u);
|
|
883
|
+
}
|
|
884
|
+
atomicStore(&counters[11], level + 1u);
|
|
885
|
+
}
|
|
886
|
+
`
|
|
887
|
+
);
|
|
888
|
+
const closenessSweepWgsl = (
|
|
889
|
+
/* wgsl */
|
|
890
|
+
`
|
|
891
|
+
var<workgroup> sh: array<u32, WG>; // the block's degrees, then their inclusive scan
|
|
892
|
+
var<workgroup> rowStart: array<u32, WG>; // the first bound arc of each entry's row
|
|
893
|
+
var<workgroup> rowOf: array<u32, WG>; // the frontier vertex of each entry (the source end of its arcs)
|
|
894
|
+
var<workgroup> local: array<atomic<u32>, 32>; // this workgroup's fresh claims per source
|
|
895
|
+
var<workgroup> wcount: u32; // the frontier list's length
|
|
896
|
+
|
|
897
|
+
@compute @workgroup_size(WG)
|
|
898
|
+
fn closeness_sweep(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
|
|
899
|
+
if (lid.x == 0u) { wcount = atomicLoad(&counters[0]); } // the frontier list's length (compact's total)
|
|
900
|
+
let count = workgroupUniformLoad(&wcount); // uniform: the block loop below holds barriers
|
|
901
|
+
let nextBase = select(2u * P.bitsBase, P.bitsBase, P.mode == 1u); // the region that is next this level
|
|
902
|
+
let frontierBase = 3u * P.bitsBase - nextBase; // the other one: the region that is the frontier
|
|
903
|
+
for (var b0 = group_id(wid) * WG; b0 < count; b0 = b0 + P.stride) { // grid-stride over blocks of WG entries
|
|
904
|
+
let i = b0 + lid.x; // this lane's frontier entry
|
|
905
|
+
var deg = 0u;
|
|
906
|
+
var start = 0u;
|
|
907
|
+
var u = 0u;
|
|
908
|
+
if (i < count) { // guarded loads into locals (3.5 rule 1)
|
|
909
|
+
u = frontierList[i];
|
|
910
|
+
let lo = max(rowPtr[u], P.arcBase);
|
|
911
|
+
let hi = min(rowPtr[u + 1u], P.arcEnd);
|
|
912
|
+
start = lo;
|
|
913
|
+
deg = select(0u, hi - lo, hi > lo);
|
|
914
|
+
}
|
|
915
|
+
if (lid.x < 32u) { atomicStore(&local[lid.x], 0u); } // zeroed before the strip loop (WebGPU zero-initialises workgroup memory; said anyway)
|
|
916
|
+
sh[lid.x] = deg;
|
|
917
|
+
rowStart[lid.x] = start;
|
|
918
|
+
rowOf[lid.x] = u;
|
|
919
|
+
workgroupBarrier();
|
|
920
|
+
for (var s = 1u; s < WG; s = s * 2u) { // Hillis-Steele inclusive scan of the degrees (bfs-contract's, inlined)
|
|
921
|
+
var t = 0u;
|
|
922
|
+
if (lid.x >= s) { t = sh[lid.x - s]; }
|
|
923
|
+
workgroupBarrier();
|
|
924
|
+
sh[lid.x] = sh[lid.x] + t;
|
|
925
|
+
workgroupBarrier();
|
|
926
|
+
}
|
|
927
|
+
let aggregate = workgroupUniformLoad(&sh[WG - 1u]); // uniform; includes a barrier
|
|
928
|
+
for (var p = lid.x; p < aggregate; p = p + WG) { // strip [0, aggregate): lane j takes j, j + WG, ...
|
|
929
|
+
var lo = 0u; // upper_bound: the first k with sh[k] > p owns arc p
|
|
930
|
+
var hi = WG;
|
|
931
|
+
loop {
|
|
932
|
+
if (lo >= hi) { break; }
|
|
933
|
+
let mid = (lo + hi) / 2u;
|
|
934
|
+
if (sh[mid] > p) { hi = mid; } else { lo = mid + 1u; }
|
|
935
|
+
}
|
|
936
|
+
let k = lo;
|
|
937
|
+
var exclusive = 0u;
|
|
938
|
+
if (k > 0u) { exclusive = sh[k - 1u]; }
|
|
939
|
+
let arc = rowStart[k] + (p - exclusive);
|
|
940
|
+
let x = colIdx[arc - P.arcBase];
|
|
941
|
+
let mask = atomicLoad(&bits[frontierBase + rowOf[k]]) & ~atomicLoad(&bits[x]); // the sources at u that have not reached x
|
|
942
|
+
if (mask != 0u) {
|
|
943
|
+
let old = atomicOr(&bits[x], mask); // visited: the claim, one read-modify-write
|
|
944
|
+
let fresh = mask & ~old; // the sources whose claim this lane won
|
|
945
|
+
if (fresh != 0u) {
|
|
946
|
+
atomicOr(&bits[nextBase + x], fresh);
|
|
947
|
+
atomicStore(&bits[3u * P.bitsBase + x], 1u); // flags: x is in the next frontier list (compact reads it)
|
|
948
|
+
var b = fresh;
|
|
949
|
+
loop { // one tally per set bit of fresh
|
|
950
|
+
if (b == 0u) { break; }
|
|
951
|
+
let s = firstTrailingBit(b);
|
|
952
|
+
atomicAdd(&local[s], 1u);
|
|
953
|
+
b = b & (b - 1u);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
workgroupBarrier(); // uniform: the loop's bound is the uniform aggregate
|
|
959
|
+
if (lid.x < 32u) { // ONE global atomic per source per workgroup
|
|
960
|
+
let c = atomicLoad(&local[lid.x]);
|
|
961
|
+
if (c != 0u) { atomicAdd(&perSource[lid.x], c); } // newCount[s]
|
|
962
|
+
}
|
|
963
|
+
workgroupBarrier(); // sh, rowStart, rowOf and local are reused by the next block
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
`
|
|
967
|
+
);
|
|
968
|
+
const compactScatterWgsl = (
|
|
969
|
+
/* wgsl */
|
|
970
|
+
`
|
|
971
|
+
@compute @workgroup_size(WG)
|
|
972
|
+
fn compact_scatter(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
|
|
973
|
+
let i = linear_id(wid, lid.x);
|
|
974
|
+
if (i == 0u) { outCount[P.outIndex] = offsets[P.count - 1u] + flags[P.count - 1u]; } // the exclusive scan's total; the planner never dispatches for count 0
|
|
975
|
+
if (i >= P.count) { return; } // no barrier follows
|
|
976
|
+
if (flags[i] != 0u) { out[offsets[i]] = queue[i]; }
|
|
977
|
+
}
|
|
978
|
+
`
|
|
979
|
+
);
|
|
547
980
|
const countingScatterWgsl = (
|
|
548
981
|
/* wgsl */
|
|
549
982
|
`
|
|
@@ -557,6 +990,57 @@ fn counting_scatter(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invoca
|
|
|
557
990
|
}
|
|
558
991
|
`
|
|
559
992
|
);
|
|
993
|
+
const dedupeClaimWgsl = (
|
|
994
|
+
/* wgsl */
|
|
995
|
+
`
|
|
996
|
+
@compute @workgroup_size(WG)
|
|
997
|
+
fn dedupe_claim(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
|
|
998
|
+
var count = P.count;
|
|
999
|
+
if (P.countIndex != U32_MAX) { count = min(atomicLoad(&counters[P.countIndex]), P.count); } // a device-side count, clamped to the capacity
|
|
1000
|
+
for (var i = linear_id(wid, lid.x); i < count; i = i + P.stride) { // grid-stride; no barrier anywhere
|
|
1001
|
+
atomicStore(&owner[queue[i]], i);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
`
|
|
1005
|
+
);
|
|
1006
|
+
const dedupeFilterWgsl = (
|
|
1007
|
+
/* wgsl */
|
|
1008
|
+
`
|
|
1009
|
+
var<workgroup> sh: array<u32, WG>;
|
|
1010
|
+
var<workgroup> base: u32;
|
|
1011
|
+
var<workgroup> wcount: u32;
|
|
1012
|
+
|
|
1013
|
+
@compute @workgroup_size(WG)
|
|
1014
|
+
fn dedupe_filter(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
|
|
1015
|
+
if (lid.x == 0u) {
|
|
1016
|
+
var count = P.count;
|
|
1017
|
+
if (P.countIndex != U32_MAX) { count = min(atomicLoad(&outCount[P.countIndex]), P.count); } // the same count source as the claim
|
|
1018
|
+
wcount = count;
|
|
1019
|
+
}
|
|
1020
|
+
let count = workgroupUniformLoad(&wcount); // uniform: the block loop below holds barriers
|
|
1021
|
+
for (var b0 = group_id(wid) * WG; b0 < count; b0 = b0 + P.stride) { // grid-stride over blocks of WG entries
|
|
1022
|
+
let i = b0 + lid.x;
|
|
1023
|
+
var keep = 0u;
|
|
1024
|
+
var v = 0u;
|
|
1025
|
+
if (i < count) { v = queue[i]; keep = select(0u, 1u, atomicLoad(&owner[v]) == i); } // guarded work into locals
|
|
1026
|
+
sh[lid.x] = keep;
|
|
1027
|
+
workgroupBarrier(); // every lane, unconditionally (3.5 rule 1)
|
|
1028
|
+
for (var s = 1u; s < WG; s = s * 2u) { // Hillis-Steele inclusive scan of keep
|
|
1029
|
+
var t = 0u;
|
|
1030
|
+
if (lid.x >= s) { t = sh[lid.x - s]; }
|
|
1031
|
+
workgroupBarrier();
|
|
1032
|
+
sh[lid.x] = sh[lid.x] + t;
|
|
1033
|
+
workgroupBarrier();
|
|
1034
|
+
}
|
|
1035
|
+
let inclusive = sh[lid.x];
|
|
1036
|
+
if (lid.x == WG - 1u) { base = atomicAdd(&outCount[P.outIndex], inclusive); } // ONE atomic per workgroup: the block's aggregate
|
|
1037
|
+
workgroupBarrier();
|
|
1038
|
+
if (keep == 1u) { out[base + inclusive - 1u] = v; }
|
|
1039
|
+
workgroupBarrier(); // sh and base are reused by the next block
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
`
|
|
1043
|
+
);
|
|
560
1044
|
const degreeWgsl = (
|
|
561
1045
|
/* wgsl */
|
|
562
1046
|
`
|
|
@@ -988,6 +1472,121 @@ fn fill(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid
|
|
|
988
1472
|
}
|
|
989
1473
|
`
|
|
990
1474
|
);
|
|
1475
|
+
const frontierFinalizeWgsl = (
|
|
1476
|
+
/* wgsl */
|
|
1477
|
+
`
|
|
1478
|
+
@compute @workgroup_size(WG)
|
|
1479
|
+
fn frontier_finalize(@builtin(local_invocation_id) lid: vec3<u32>) {
|
|
1480
|
+
if (lid.x != 0u) { return; } // one lane; no barrier follows (3.5 rule 1)
|
|
1481
|
+
if (P.role == 0u) { // the level boundary
|
|
1482
|
+
if (atomicLoad(&counters[15]) != 0u) { // done already: a no-op level the host recorded past the end
|
|
1483
|
+
atomicStore(&counters[24], 0u); // the path word is the only word that moves (P8-T6's levels formula reads the rest)
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
let finished = atomicLoad(&counters[0]);
|
|
1487
|
+
let next = atomicLoad(&counters[1]);
|
|
1488
|
+
let degSum = atomicLoad(&counters[2]);
|
|
1489
|
+
atomicStore(&counters[3], finished); // prevFrontierCount
|
|
1490
|
+
atomicStore(&counters[4], degSum); // prevDegreeSum
|
|
1491
|
+
atomicStore(&counters[0], next); // the rotation
|
|
1492
|
+
atomicStore(&counters[1], 0u);
|
|
1493
|
+
atomicStore(&counters[2], 0u);
|
|
1494
|
+
atomicStore(&counters[8], 0u); // edgeCount
|
|
1495
|
+
atomicStore(&counters[9], 0u); // edgeCountUnclamped
|
|
1496
|
+
atomicStore(&counters[12], atomicLoad(&counters[12]) + next); // visitedCount
|
|
1497
|
+
if (P.firstOfSubmit >= 1u) { // b = the boundary's index inside its submit, clamped to 2 (P8-T8, PD-18)
|
|
1498
|
+
atomicStore(&counters[5], atomicLoad(&counters[5]) - next); // unvisitedCount (exact): F_b was inside the submit's rebuilt sum iff b >= 1
|
|
1499
|
+
}
|
|
1500
|
+
if (P.firstOfSubmit >= 2u) {
|
|
1501
|
+
atomicStore(&counters[6], atomicLoad(&counters[6]) - degSum); // unvisitedDegreeSum (one level stale): F_{b-1} was inside it iff b >= 2
|
|
1502
|
+
}
|
|
1503
|
+
let level = atomicLoad(&counters[11]) + 1u; // the seed is U32_MAX, so the first boundary lands on 0
|
|
1504
|
+
atomicStore(&counters[11], level);
|
|
1505
|
+
let done = (next == 0u) || (level >= P.maxDepth);
|
|
1506
|
+
atomicStore(&counters[15], select(0u, 1u, done));
|
|
1507
|
+
var direction = atomicLoad(&counters[14]);
|
|
1508
|
+
if (P.mode == 1u) {
|
|
1509
|
+
direction = 0u; // top-down only (the test seam)
|
|
1510
|
+
} else if (direction == 0u) {
|
|
1511
|
+
if (degSum > atomicLoad(&counters[6]) / P.alpha && next > finished) { direction = 1u; } // m_f > m_u / alpha and growing
|
|
1512
|
+
} else {
|
|
1513
|
+
if (next * P.beta < atomicLoad(&counters[5]) && next < finished) { direction = 0u; } // next * beta < unvisited and shrinking
|
|
1514
|
+
}
|
|
1515
|
+
if (direction != atomicLoad(&counters[14])) { atomicStore(&counters[13], atomicLoad(&counters[13]) + 1u); } // switches
|
|
1516
|
+
var path = 0u; // word 24: what the level's kernels run (0 nothing, 1 two-phase, 2 fused, 3 bottom-up; role 1 writes 4 for the retry)
|
|
1517
|
+
if (done) {
|
|
1518
|
+
path = 0u;
|
|
1519
|
+
} else if (direction == 1u) { // the bottom-up level (P8-T8): the bits fill, the bitset build, the sweep
|
|
1520
|
+
path = 3u;
|
|
1521
|
+
atomicStore(&counters[19], atomicLoad(&counters[19]) + 1u);
|
|
1522
|
+
} else if (next < P.fusedMax) { // P8-T7 makes this branch reachable (fusedMax is 0 until then)
|
|
1523
|
+
path = 2u; // bfs-fused: one WORKGROUP per frontier entry
|
|
1524
|
+
atomicStore(&counters[17], atomicLoad(&counters[17]) + 1u);
|
|
1525
|
+
} else {
|
|
1526
|
+
path = 1u; // advance-expand, then role 1 and bfs-contract
|
|
1527
|
+
}
|
|
1528
|
+
atomicStore(&counters[14], direction);
|
|
1529
|
+
atomicStore(&counters[24], path);
|
|
1530
|
+
} else if (P.role == 1u) { // the edge queue is filled
|
|
1531
|
+
if (atomicLoad(&counters[24]) != 1u) { // role 0 did not choose the two-phase path (done, fused or bottom-up): nothing to clamp, nothing to count
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
let clamped = min(atomicLoad(&counters[8]), P.edgeCapacity);
|
|
1535
|
+
atomicStore(&counters[8], clamped);
|
|
1536
|
+
if (atomicLoad(&counters[9]) > P.edgeCapacity) { // PD-23: the fused retry
|
|
1537
|
+
atomicStore(&counters[24], 4u); // the path word: bfs-fused runs the retry over frontierCount, bfs-contract nothing
|
|
1538
|
+
atomicStore(&counters[10], atomicLoad(&counters[10]) + 1u);
|
|
1539
|
+
atomicStore(&counters[17], atomicLoad(&counters[17]) + 1u);
|
|
1540
|
+
} else {
|
|
1541
|
+
atomicStore(&counters[18], atomicLoad(&counters[18]) + 1u); // twoPhaseLevels counts the CHOICE role 0 made, even for zero edges (P8-T7 Step 4's invariant)
|
|
1542
|
+
}
|
|
1543
|
+
} else if (P.role == 2u) { // the SSSP round boundary (P8-T9, PD-20): which pile this round relaxes
|
|
1544
|
+
atomicStore(&counters[8], 0u); // the dedupe counts (words 8 and 9, the SSSP sense) and the path word: nothing unless a pile is chosen below
|
|
1545
|
+
atomicStore(&counters[9], 0u);
|
|
1546
|
+
atomicStore(&counters[24], 0u);
|
|
1547
|
+
if (atomicLoad(&counters[15]) != 0u) { // done already: a no-op round the host recorded past the end (rule 1)
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
let nearRaw = atomicLoad(&counters[1]); // the raw near half's appends, unclamped
|
|
1551
|
+
let farRaw = atomicLoad(&counters[21]); // the raw far half's appends, unclamped
|
|
1552
|
+
if (nearRaw > P.edgeCapacity || farRaw > P.edgeCapacity) { // a pile overflowed its half: the host raises E_TOO_LARGE
|
|
1553
|
+
atomicStore(&counters[15], 2u);
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
if (nearRaw != 0u) { // a near round: dedupe the near half into nearIn
|
|
1557
|
+
atomicStore(&counters[0], 0u); // the deduped near count, accumulated by dedupe-filter
|
|
1558
|
+
atomicStore(&counters[14], 0u); // mode 0
|
|
1559
|
+
atomicStore(&counters[8], nearRaw); // the near dedupe's count word (dedupe-claim, dedupe-filter over the near half)
|
|
1560
|
+
atomicStore(&counters[24], 5u); // the path word: sssp-relax role 0 runs, role 1 nothing
|
|
1561
|
+
atomicStore(&counters[11], atomicLoad(&counters[11]) + 1u); // rounds dispatched (the done boundary is not counted)
|
|
1562
|
+
} else if (farRaw != 0u) { // the near pile is empty: raise the threshold and re-bucket the far pile
|
|
1563
|
+
let threshold = bitcast<f32>(atomicLoad(&counters[22]));
|
|
1564
|
+
let raised = threshold + bitcast<f32>(atomicLoad(&counters[23])); // ONE f32 add on the bit patterns (PD-9)
|
|
1565
|
+
if (raised == threshold) { // the delta is below the threshold's ulp: the host raises E_UNSUPPORTED
|
|
1566
|
+
atomicStore(&counters[15], 3u);
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
atomicStore(&counters[4], atomicLoad(&counters[22])); // prevThresholdBits: what the pass-through drops below
|
|
1570
|
+
atomicStore(&counters[22], bitcast<u32>(raised));
|
|
1571
|
+
atomicStore(&counters[20], 0u); // the deduped far count, accumulated by dedupe-filter
|
|
1572
|
+
atomicStore(&counters[14], 1u); // mode 1
|
|
1573
|
+
atomicStore(&counters[9], farRaw); // the far dedupe's count word (dedupe-claim, dedupe-filter over the far half)
|
|
1574
|
+
atomicStore(&counters[24], 6u); // the path word: sssp-relax role 1 runs, role 0 nothing
|
|
1575
|
+
atomicStore(&counters[11], atomicLoad(&counters[11]) + 1u);
|
|
1576
|
+
} else { // both piles empty: finished
|
|
1577
|
+
atomicStore(&counters[15], 1u);
|
|
1578
|
+
}
|
|
1579
|
+
} else if (P.role == 3u) { // the piles are deduped: restart the raw half the round consumed
|
|
1580
|
+
if (atomicLoad(&counters[15]) != 0u) { return; } // role 2 chose no pile this round
|
|
1581
|
+
if (atomicLoad(&counters[14]) == 0u) {
|
|
1582
|
+
atomicStore(&counters[1], 0u); // the raw near half restarts (sssp-relax role 0 sizes itself from word 0)
|
|
1583
|
+
} else {
|
|
1584
|
+
atomicStore(&counters[21], 0u); // the raw far half restarts (the pass-through re-appends what stays far; role 1 sizes itself from word 20)
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
`
|
|
1589
|
+
);
|
|
991
1590
|
const gridCellKeyWgsl = (
|
|
992
1591
|
/* wgsl */
|
|
993
1592
|
`
|
|
@@ -1729,6 +2328,108 @@ fn spmv_pull(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id
|
|
|
1729
2328
|
}
|
|
1730
2329
|
`
|
|
1731
2330
|
);
|
|
2331
|
+
const ssspPredWgsl = (
|
|
2332
|
+
/* wgsl */
|
|
2333
|
+
`
|
|
2334
|
+
@compute @workgroup_size(WG)
|
|
2335
|
+
fn sssp_pred(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
|
|
2336
|
+
let first = linear_id(wid, lid.x);
|
|
2337
|
+
let hb = ((P.n + 63u) / 64u) * 64u; // MODE 0 only: hops[v] is pred[hb + v]; pred[2 hb] is the changed word, pred[2 hb + 1] the orphan word (PD-27)
|
|
2338
|
+
if (P.role == 1u && P.iteration != 0u && atomicLoad(&pred[2u * hb]) < P.iteration) { return; } // the previous hop pass changed nothing: converged (no barrier anywhere, so the early return is legal)
|
|
2339
|
+
for (var u = first; u < P.n; u = u + P.stride) { // grid-stride over the rows (planGridStride(n)); no barrier anywhere
|
|
2340
|
+
let du = dist[u];
|
|
2341
|
+
var unreached = false;
|
|
2342
|
+
if (MODE == 1u) { unreached = du == INVALID_INDEX; } else { unreached = du == F32_INF_BITS; }
|
|
2343
|
+
if (unreached) { continue; }
|
|
2344
|
+
var hu = 0u; // u's hop count (MODE 0, roles 1 and 2)
|
|
2345
|
+
if (MODE == 0u && P.role != 0u) {
|
|
2346
|
+
hu = atomicLoad(&pred[hb + u]);
|
|
2347
|
+
if (hu == INVALID_INDEX) { // the key has not reached u yet
|
|
2348
|
+
if (P.role == 2u && u != P.source) { atomicAdd(&pred[2u * hb + 1u], 1u); } // an orphan: only bellmanFord can make one (P8-T10 Step 3)
|
|
2349
|
+
continue;
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
let end = min(rowPtr[u + 1u], P.arcEnd);
|
|
2353
|
+
for (var a = max(rowPtr[u], P.arcBase); a < end; a = a + 1u) {
|
|
2354
|
+
let v = colIdx[a - P.arcBase];
|
|
2355
|
+
if (v == P.source) { continue; } // the source keeps INVALID_INDEX whatever attains it (a zero-weight arc could)
|
|
2356
|
+
let dv = dist[v];
|
|
2357
|
+
var tight = false; // the arc explains dist[v]
|
|
2358
|
+
var below = false; // and its source sits at a strictly smaller distance
|
|
2359
|
+
if (MODE == 1u) {
|
|
2360
|
+
tight = (du + 1u) == dv; // depth mode: BFS parent, one depth down
|
|
2361
|
+
} else {
|
|
2362
|
+
let w = select(1.0, weights[a - P.arcBase], HAS_WEIGHTS);
|
|
2363
|
+
tight = (dv != F32_INF_BITS) && (bitcast<u32>(bitcast<f32>(du) + w) == dv); // one f32 add, compared as the bit pattern PD-9 stores; never into an unreached v (an overflowed sum is +Inf too)
|
|
2364
|
+
below = bitcast<f32>(du) < bitcast<f32>(dv);
|
|
2365
|
+
}
|
|
2366
|
+
if (!tight) { continue; }
|
|
2367
|
+
var admit = false; // this arc is one key step below v
|
|
2368
|
+
if (MODE == 1u) {
|
|
2369
|
+
admit = true;
|
|
2370
|
+
} else if (P.role == 0u) { // the roots pass (the plateau rule only; bellmanFord seeds the source alone)
|
|
2371
|
+
if (P.mode == 0u && below) { atomicMin(&pred[hb + v], 0u); }
|
|
2372
|
+
} else if (P.role == 1u) { // a hop pass: one hop along a plateau arc (mode 0) or along any tight arc (mode 1)
|
|
2373
|
+
if (P.mode == 1u || du == dv) {
|
|
2374
|
+
let old = atomicMin(&pred[hb + v], hu + 1u);
|
|
2375
|
+
if (hu + 1u < old) { atomicMax(&pred[2u * hb], P.iteration + 1u); } // this pass changed something
|
|
2376
|
+
}
|
|
2377
|
+
} else { // the predecessor pass: the smallest tight arc one key step below v
|
|
2378
|
+
let hv = atomicLoad(&pred[hb + v]);
|
|
2379
|
+
if (P.mode == 1u) { admit = hu + 1u == hv; } else if (hv == 0u) { admit = below; } else { admit = (du == dv) && (hu + 1u == hv); }
|
|
2380
|
+
}
|
|
2381
|
+
if (admit) { atomicMin(&pred[v], select(a, u, P.predKind == 1u)); }
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
`
|
|
2386
|
+
);
|
|
2387
|
+
const ssspRelaxWgsl = (
|
|
2388
|
+
/* wgsl */
|
|
2389
|
+
`
|
|
2390
|
+
@compute @workgroup_size(WG)
|
|
2391
|
+
fn sssp_relax(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
|
|
2392
|
+
let first = linear_id(wid, lid.x);
|
|
2393
|
+
let mine = select(5u, 6u, P.role == 1u); // the path word role 2 wrote: 5 a near round, 6 a far pass-through
|
|
2394
|
+
let chosen = atomicLoad(&counters[24]) == mine; // the other role's dispatch of the round is a no-op
|
|
2395
|
+
let count = select(0u, min(atomicLoad(&counters[select(0u, 20u, P.role == 1u)]), P.n), chosen); // the deduped near or far pile
|
|
2396
|
+
let threshold = bitcast<f32>(atomicLoad(&counters[22]));
|
|
2397
|
+
let cutoff = bitcast<f32>(P.cutoffBits);
|
|
2398
|
+
for (var i = first; i < count; i = i + P.stride) { // no barrier anywhere: the loops may be per lane
|
|
2399
|
+
let u = queueIn[i];
|
|
2400
|
+
let du = bitcast<f32>(atomicLoad(&dist[u]));
|
|
2401
|
+
if (P.role == 1u) { // the pass-through (PD-20): re-bucket a far entry
|
|
2402
|
+
if (du < bitcast<f32>(atomicLoad(&counters[4]))) { continue; } // below the previous threshold: relaxed in an earlier bucket
|
|
2403
|
+
if (du < threshold) {
|
|
2404
|
+
let q = atomicAdd(&counters[1], 1u);
|
|
2405
|
+
if (q < P.edgeCapacity) { queueOut[q] = u; }
|
|
2406
|
+
} else {
|
|
2407
|
+
let q = atomicAdd(&counters[21], 1u);
|
|
2408
|
+
if (q < P.edgeCapacity) { queueOut[P.edgeCapacity + q] = u; }
|
|
2409
|
+
}
|
|
2410
|
+
continue;
|
|
2411
|
+
}
|
|
2412
|
+
let end = rowPtr[u + 1u];
|
|
2413
|
+
for (var a = rowPtr[u]; a < end; a = a + 1u) { // the whole row: never windowed (DEP-P8-E)
|
|
2414
|
+
let v = colIdx[a];
|
|
2415
|
+
let nd = du + select(1.0, weights[a], HAS_WEIGHTS); // ONE f32 add (PD-9)
|
|
2416
|
+
if (nd > cutoff) { continue; } // SsspOptions.cutoff: the CPU port's dv <= cutoff
|
|
2417
|
+
let bits = bitcast<u32>(nd);
|
|
2418
|
+
let old = atomicMin(&dist[v], bits); // exact on non-negative floats
|
|
2419
|
+
if (bits < old) { // this lane improved v, so it owns the append
|
|
2420
|
+
if (nd < threshold) {
|
|
2421
|
+
let q = atomicAdd(&counters[1], 1u);
|
|
2422
|
+
if (q < P.edgeCapacity) { queueOut[q] = v; }
|
|
2423
|
+
} else {
|
|
2424
|
+
let q = atomicAdd(&counters[21], 1u);
|
|
2425
|
+
if (q < P.edgeCapacity) { queueOut[P.edgeCapacity + q] = v; }
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
`
|
|
2432
|
+
);
|
|
1732
2433
|
const wccCompressWgsl = (
|
|
1733
2434
|
/* wgsl */
|
|
1734
2435
|
`
|
|
@@ -2029,6 +2730,81 @@ const RADIX_PARAMS = UniformBlock.define("RadixParams", [
|
|
|
2029
2730
|
["groups", "u32"],
|
|
2030
2731
|
["pad0", "u32"]
|
|
2031
2732
|
]);
|
|
2733
|
+
const COMPACT_PARAMS = UniformBlock.define("CompactParams", [
|
|
2734
|
+
["count", "u32"],
|
|
2735
|
+
["outIndex", "u32"],
|
|
2736
|
+
["countIndex", "u32"],
|
|
2737
|
+
["stride", "u32"]
|
|
2738
|
+
]);
|
|
2739
|
+
const FRONTIER_COUNTERS = UniformBlock.define(
|
|
2740
|
+
"FrontierCounters",
|
|
2741
|
+
[
|
|
2742
|
+
["frontierCount", "u32"],
|
|
2743
|
+
["nextFrontierCount", "u32"],
|
|
2744
|
+
["frontierDegreeSum", "u32"],
|
|
2745
|
+
["prevFrontierCount", "u32"],
|
|
2746
|
+
["prevDegreeSum", "u32"],
|
|
2747
|
+
["unvisitedCount", "u32"],
|
|
2748
|
+
["unvisitedDegreeSum", "u32"],
|
|
2749
|
+
["unvisitedListLen", "u32"],
|
|
2750
|
+
["edgeCount", "u32"],
|
|
2751
|
+
["edgeCountUnclamped", "u32"],
|
|
2752
|
+
["overflowLevels", "u32"],
|
|
2753
|
+
["level", "u32"],
|
|
2754
|
+
["visitedCount", "u32"],
|
|
2755
|
+
["switches", "u32"],
|
|
2756
|
+
["direction", "u32"],
|
|
2757
|
+
["done", "u32"],
|
|
2758
|
+
["arcsScanned", "u32"],
|
|
2759
|
+
["fusedLevels", "u32"],
|
|
2760
|
+
["twoPhaseLevels", "u32"],
|
|
2761
|
+
["bottomUpLevels", "u32"],
|
|
2762
|
+
["farCount", "u32"],
|
|
2763
|
+
["nextFarCount", "u32"],
|
|
2764
|
+
["thresholdBits", "u32"],
|
|
2765
|
+
["deltaBits", "u32"],
|
|
2766
|
+
["path", "u32"]
|
|
2767
|
+
],
|
|
2768
|
+
{ layout: "storage" }
|
|
2769
|
+
);
|
|
2770
|
+
const FRONTIER_PARAMS = UniformBlock.define("FrontierParams", [
|
|
2771
|
+
["role", "u32"],
|
|
2772
|
+
["wg", "u32"],
|
|
2773
|
+
["alpha", "u32"],
|
|
2774
|
+
["beta", "u32"],
|
|
2775
|
+
["fusedMax", "u32"],
|
|
2776
|
+
["edgeCapacity", "u32"],
|
|
2777
|
+
["maxDepth", "u32"],
|
|
2778
|
+
["n", "u32"],
|
|
2779
|
+
["mode", "u32"],
|
|
2780
|
+
["cutoffBits", "u32"],
|
|
2781
|
+
["arcBase", "u32"],
|
|
2782
|
+
["arcEnd", "u32"],
|
|
2783
|
+
["predKind", "u32"],
|
|
2784
|
+
["bitsBase", "u32"],
|
|
2785
|
+
["source", "u32"],
|
|
2786
|
+
["stride", "u32"],
|
|
2787
|
+
["firstOfSubmit", "u32"],
|
|
2788
|
+
["iteration", "u32"],
|
|
2789
|
+
["pad1", "u32"],
|
|
2790
|
+
["pad2", "u32"]
|
|
2791
|
+
]);
|
|
2792
|
+
const BF_PARAMS = UniformBlock.define("BfParams", [
|
|
2793
|
+
["edgeCount", "u32"],
|
|
2794
|
+
["stride", "u32"],
|
|
2795
|
+
["maxRetries", "u32"],
|
|
2796
|
+
["cutoffBits", "u32"]
|
|
2797
|
+
]);
|
|
2798
|
+
const BF_FLAGS = UniformBlock.define(
|
|
2799
|
+
"BfFlags",
|
|
2800
|
+
[
|
|
2801
|
+
["changed", "u32"],
|
|
2802
|
+
["retryExhausted", "u32"],
|
|
2803
|
+
["pad0", "u32"],
|
|
2804
|
+
["pad1", "u32"]
|
|
2805
|
+
],
|
|
2806
|
+
{ layout: "storage" }
|
|
2807
|
+
);
|
|
2032
2808
|
function decl(group, binding, name, kind, wgslType) {
|
|
2033
2809
|
return { group, binding, name, kind, wgslType };
|
|
2034
2810
|
}
|
|
@@ -2535,37 +3311,299 @@ const GRID_NEAR_FIELD = {
|
|
|
2535
3311
|
snippetSlots: [],
|
|
2536
3312
|
phase: "P4"
|
|
2537
3313
|
};
|
|
2538
|
-
const
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
"
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
"
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
3314
|
+
const COMPACT_SCATTER = {
|
|
3315
|
+
id: "compact-scatter",
|
|
3316
|
+
body: compactScatterWgsl,
|
|
3317
|
+
entryPoint: "compact_scatter",
|
|
3318
|
+
bindings: [
|
|
3319
|
+
decl(1, 0, "queue", "storage-ro", "array<u32>"),
|
|
3320
|
+
decl(1, 1, "flags", "storage-ro", "array<u32>"),
|
|
3321
|
+
decl(1, 2, "offsets", "storage-ro", "array<u32>"),
|
|
3322
|
+
decl(1, 3, "out", "storage", "array<u32>"),
|
|
3323
|
+
decl(1, 4, "outCount", "storage", "array<u32>"),
|
|
3324
|
+
decl(2, 0, "P", "uniform", "CompactParams")
|
|
3325
|
+
],
|
|
3326
|
+
overrideDecls: [],
|
|
3327
|
+
uniforms: [COMPACT_PARAMS],
|
|
3328
|
+
needs: [],
|
|
3329
|
+
snippetSlots: [],
|
|
3330
|
+
phase: "P8"
|
|
3331
|
+
};
|
|
3332
|
+
const DEDUPE_CLAIM = {
|
|
3333
|
+
id: "dedupe-claim",
|
|
3334
|
+
body: dedupeClaimWgsl,
|
|
3335
|
+
entryPoint: "dedupe_claim",
|
|
3336
|
+
bindings: [
|
|
3337
|
+
decl(1, 0, "queue", "storage-ro", "array<u32>"),
|
|
3338
|
+
decl(1, 1, "owner", "storage", "array<atomic<u32>>"),
|
|
3339
|
+
decl(1, 2, "counters", "storage", "array<atomic<u32>>"),
|
|
3340
|
+
decl(2, 0, "P", "uniform", "CompactParams")
|
|
3341
|
+
],
|
|
3342
|
+
overrideDecls: [],
|
|
3343
|
+
uniforms: [COMPACT_PARAMS],
|
|
3344
|
+
needs: [],
|
|
3345
|
+
snippetSlots: [],
|
|
3346
|
+
phase: "P8"
|
|
3347
|
+
};
|
|
3348
|
+
const DEDUPE_FILTER = {
|
|
3349
|
+
id: "dedupe-filter",
|
|
3350
|
+
body: dedupeFilterWgsl,
|
|
3351
|
+
entryPoint: "dedupe_filter",
|
|
3352
|
+
bindings: [
|
|
3353
|
+
decl(1, 0, "queue", "storage-ro", "array<u32>"),
|
|
3354
|
+
decl(1, 1, "owner", "storage", "array<atomic<u32>>"),
|
|
3355
|
+
decl(1, 2, "out", "storage", "array<u32>"),
|
|
3356
|
+
decl(1, 3, "outCount", "storage", "array<atomic<u32>>"),
|
|
3357
|
+
decl(2, 0, "P", "uniform", "CompactParams")
|
|
3358
|
+
],
|
|
3359
|
+
overrideDecls: [],
|
|
3360
|
+
uniforms: [COMPACT_PARAMS],
|
|
3361
|
+
needs: [],
|
|
3362
|
+
snippetSlots: [],
|
|
3363
|
+
phase: "P8"
|
|
3364
|
+
};
|
|
3365
|
+
const FRONTIER_FINALIZE = {
|
|
3366
|
+
id: "frontier-finalize",
|
|
3367
|
+
body: frontierFinalizeWgsl,
|
|
3368
|
+
entryPoint: "frontier_finalize",
|
|
3369
|
+
bindings: [decl(1, 0, "counters", "storage", "array<atomic<u32>>"), decl(2, 0, "P", "uniform", "FrontierParams")],
|
|
3370
|
+
overrideDecls: [],
|
|
3371
|
+
uniforms: [FRONTIER_PARAMS],
|
|
3372
|
+
needs: [],
|
|
3373
|
+
snippetSlots: [],
|
|
3374
|
+
phase: "P8"
|
|
3375
|
+
};
|
|
3376
|
+
const ADVANCE_EXPAND = {
|
|
3377
|
+
id: "advance-expand",
|
|
3378
|
+
body: advanceExpandWgsl,
|
|
3379
|
+
entryPoint: "advance_expand",
|
|
3380
|
+
bindings: GRAPH_SLOTS.concat(
|
|
3381
|
+
decl(1, 0, "frontierIn", "storage-ro", "array<u32>"),
|
|
3382
|
+
decl(1, 1, "counters", "storage", "array<atomic<u32>>"),
|
|
3383
|
+
decl(1, 2, "edgeQueue", "storage", "array<u32>"),
|
|
3384
|
+
decl(2, 0, "P", "uniform", "FrontierParams")
|
|
3385
|
+
),
|
|
3386
|
+
overrideDecls: [],
|
|
3387
|
+
uniforms: [FRONTIER_PARAMS],
|
|
3388
|
+
needs: ["subgroups"],
|
|
3389
|
+
snippetSlots: [],
|
|
3390
|
+
phase: "P8"
|
|
3391
|
+
};
|
|
3392
|
+
const BFS_CONTRACT = {
|
|
3393
|
+
id: "bfs-contract",
|
|
3394
|
+
body: bfsContractWgsl,
|
|
3395
|
+
entryPoint: "bfs_contract",
|
|
3396
|
+
bindings: [
|
|
3397
|
+
decl(1, 0, "edgeQueue", "storage-ro", "array<u32>"),
|
|
3398
|
+
decl(1, 1, "counters", "storage", "array<atomic<u32>>"),
|
|
3399
|
+
decl(1, 2, "depth", "storage", "array<atomic<u32>>"),
|
|
3400
|
+
decl(1, 3, "frontierOut", "storage", "array<u32>"),
|
|
3401
|
+
decl(2, 0, "P", "uniform", "FrontierParams")
|
|
3402
|
+
],
|
|
3403
|
+
overrideDecls: [],
|
|
3404
|
+
uniforms: [FRONTIER_PARAMS],
|
|
3405
|
+
needs: [],
|
|
3406
|
+
snippetSlots: [],
|
|
3407
|
+
phase: "P8"
|
|
3408
|
+
};
|
|
3409
|
+
const SSSP_PRED = {
|
|
3410
|
+
id: "sssp-pred",
|
|
3411
|
+
body: ssspPredWgsl,
|
|
3412
|
+
entryPoint: "sssp_pred",
|
|
3413
|
+
bindings: GRAPH_SLOTS.concat(
|
|
3414
|
+
decl(1, 0, "dist", "storage-ro", "array<u32>"),
|
|
3415
|
+
decl(1, 1, "pred", "storage", "array<atomic<u32>>"),
|
|
3416
|
+
decl(2, 0, "P", "uniform", "FrontierParams")
|
|
3417
|
+
),
|
|
3418
|
+
overrideDecls: [{ name: "MODE", type: "u32", default: 0 }],
|
|
3419
|
+
uniforms: [FRONTIER_PARAMS],
|
|
3420
|
+
needs: [],
|
|
3421
|
+
snippetSlots: [],
|
|
3422
|
+
phase: "P8"
|
|
3423
|
+
};
|
|
3424
|
+
const BFS_FUSED = {
|
|
3425
|
+
id: "bfs-fused",
|
|
3426
|
+
body: bfsFusedWgsl,
|
|
3427
|
+
entryPoint: "bfs_fused",
|
|
3428
|
+
bindings: GRAPH_SLOTS.concat(
|
|
3429
|
+
decl(1, 0, "frontierIn", "storage-ro", "array<u32>"),
|
|
3430
|
+
decl(1, 1, "counters", "storage", "array<atomic<u32>>"),
|
|
3431
|
+
decl(1, 2, "depth", "storage", "array<atomic<u32>>"),
|
|
3432
|
+
decl(1, 3, "frontierOut", "storage", "array<u32>"),
|
|
3433
|
+
decl(2, 0, "P", "uniform", "FrontierParams")
|
|
3434
|
+
),
|
|
3435
|
+
overrideDecls: [],
|
|
3436
|
+
uniforms: [FRONTIER_PARAMS],
|
|
3437
|
+
needs: [],
|
|
3438
|
+
snippetSlots: [],
|
|
3439
|
+
phase: "P8"
|
|
3440
|
+
};
|
|
3441
|
+
const BFS_BOTTOM_UP = {
|
|
3442
|
+
id: "bfs-bottom-up",
|
|
3443
|
+
body: bfsBottomUpWgsl,
|
|
3444
|
+
entryPoint: "bfs_bottom_up",
|
|
3445
|
+
bindings: GRAPH_SLOTS.concat(
|
|
3446
|
+
decl(1, 0, "sweepIn", "storage-ro", "array<u32>"),
|
|
3447
|
+
decl(1, 1, "counters", "storage", "array<atomic<u32>>"),
|
|
3448
|
+
decl(1, 2, "depth", "storage", "array<atomic<u32>>"),
|
|
3449
|
+
decl(1, 3, "frontierOut", "storage", "array<u32>"),
|
|
3450
|
+
decl(2, 0, "P", "uniform", "FrontierParams")
|
|
3451
|
+
),
|
|
3452
|
+
overrideDecls: [],
|
|
3453
|
+
uniforms: [FRONTIER_PARAMS],
|
|
3454
|
+
needs: ["subgroups"],
|
|
3455
|
+
snippetSlots: [],
|
|
3456
|
+
phase: "P8"
|
|
3457
|
+
};
|
|
3458
|
+
const BFS_BITSET_BUILD = {
|
|
3459
|
+
id: "bfs-bitset-build",
|
|
3460
|
+
body: bfsBitsetBuildWgsl,
|
|
3461
|
+
entryPoint: "bfs_bitset_build",
|
|
3462
|
+
bindings: [
|
|
3463
|
+
decl(1, 0, "frontierIn", "storage-ro", "array<u32>"),
|
|
3464
|
+
decl(1, 1, "counters", "storage", "array<atomic<u32>>"),
|
|
3465
|
+
decl(1, 2, "bits", "storage", "array<atomic<u32>>"),
|
|
3466
|
+
decl(2, 0, "P", "uniform", "FrontierParams")
|
|
3467
|
+
],
|
|
3468
|
+
overrideDecls: [],
|
|
3469
|
+
uniforms: [FRONTIER_PARAMS],
|
|
3470
|
+
needs: [],
|
|
3471
|
+
snippetSlots: [],
|
|
3472
|
+
phase: "P8"
|
|
3473
|
+
};
|
|
3474
|
+
const BFS_UNVISITED_FLAGS = {
|
|
3475
|
+
id: "bfs-unvisited-flags",
|
|
3476
|
+
body: bfsUnvisitedFlagsWgsl,
|
|
3477
|
+
entryPoint: "bfs_unvisited_flags",
|
|
3478
|
+
bindings: [
|
|
3479
|
+
decl(1, 0, "outDegree", "storage-ro", "array<u32>"),
|
|
3480
|
+
decl(1, 1, "inDegree", "storage-ro", "array<u32>"),
|
|
3481
|
+
decl(1, 2, "depth", "storage-ro", "array<u32>"),
|
|
3482
|
+
decl(1, 3, "flags", "storage", "array<u32>"),
|
|
3483
|
+
decl(1, 4, "counters", "storage", "array<atomic<u32>>"),
|
|
3484
|
+
decl(2, 0, "P", "uniform", "FrontierParams")
|
|
3485
|
+
],
|
|
3486
|
+
overrideDecls: [],
|
|
3487
|
+
uniforms: [FRONTIER_PARAMS],
|
|
3488
|
+
needs: ["subgroups"],
|
|
3489
|
+
snippetSlots: [],
|
|
3490
|
+
phase: "P8"
|
|
3491
|
+
};
|
|
3492
|
+
const SSSP_RELAX = {
|
|
3493
|
+
id: "sssp-relax",
|
|
3494
|
+
body: ssspRelaxWgsl,
|
|
3495
|
+
entryPoint: "sssp_relax",
|
|
3496
|
+
bindings: GRAPH_SLOTS.concat(
|
|
3497
|
+
decl(1, 0, "dist", "storage", "array<atomic<u32>>"),
|
|
3498
|
+
decl(1, 1, "counters", "storage", "array<atomic<u32>>"),
|
|
3499
|
+
decl(1, 2, "queueIn", "storage-ro", "array<u32>"),
|
|
3500
|
+
decl(1, 3, "queueOut", "storage", "array<u32>"),
|
|
3501
|
+
decl(2, 0, "P", "uniform", "FrontierParams")
|
|
3502
|
+
),
|
|
3503
|
+
overrideDecls: [],
|
|
3504
|
+
uniforms: [FRONTIER_PARAMS],
|
|
3505
|
+
needs: [],
|
|
3506
|
+
snippetSlots: [],
|
|
3507
|
+
phase: "P8"
|
|
3508
|
+
};
|
|
3509
|
+
const BF_RELAX = {
|
|
3510
|
+
id: "bf-relax",
|
|
3511
|
+
body: bfRelaxWgsl,
|
|
3512
|
+
entryPoint: "bf_relax",
|
|
3513
|
+
bindings: [
|
|
3514
|
+
decl(1, 0, "edgeSrc", "storage-ro", "array<u32>"),
|
|
3515
|
+
decl(1, 1, "edgeDst", "storage-ro", "array<u32>"),
|
|
3516
|
+
decl(1, 2, "edgeToArc", "storage-ro", "array<u32>"),
|
|
3517
|
+
decl(1, 3, "weights", "storage-ro", "array<f32>"),
|
|
3518
|
+
decl(1, 4, "dist", "storage", "array<atomic<u32>>"),
|
|
3519
|
+
decl(1, 5, "flags", "storage", "array<atomic<u32>>"),
|
|
3520
|
+
decl(2, 0, "P", "uniform", "BfParams")
|
|
3521
|
+
],
|
|
3522
|
+
overrideDecls: [{ name: "UNDIRECTED", type: "bool", default: false }],
|
|
3523
|
+
uniforms: [BF_PARAMS],
|
|
3524
|
+
needs: [],
|
|
3525
|
+
snippetSlots: [],
|
|
3526
|
+
phase: "P8"
|
|
3527
|
+
};
|
|
3528
|
+
const CLOSENESS_SWEEP = {
|
|
3529
|
+
id: "closeness-sweep",
|
|
3530
|
+
body: closenessSweepWgsl,
|
|
3531
|
+
entryPoint: "closeness_sweep",
|
|
3532
|
+
bindings: GRAPH_SLOTS.concat(
|
|
3533
|
+
decl(1, 0, "frontierList", "storage-ro", "array<u32>"),
|
|
3534
|
+
decl(1, 1, "counters", "storage", "array<atomic<u32>>"),
|
|
3535
|
+
decl(1, 2, "bits", "storage", "array<atomic<u32>>"),
|
|
3536
|
+
decl(1, 3, "perSource", "storage", "array<atomic<u32>>"),
|
|
3537
|
+
decl(2, 0, "P", "uniform", "FrontierParams")
|
|
3538
|
+
),
|
|
3539
|
+
overrideDecls: [],
|
|
3540
|
+
uniforms: [FRONTIER_PARAMS],
|
|
3541
|
+
needs: [],
|
|
3542
|
+
snippetSlots: [],
|
|
3543
|
+
phase: "P8"
|
|
3544
|
+
};
|
|
3545
|
+
const CLOSENESS_REDUCE = {
|
|
3546
|
+
id: "closeness-reduce",
|
|
3547
|
+
body: closenessReduceWgsl,
|
|
3548
|
+
entryPoint: "closeness_reduce",
|
|
3549
|
+
bindings: [
|
|
3550
|
+
decl(1, 0, "counters", "storage", "array<atomic<u32>>"),
|
|
3551
|
+
decl(1, 1, "perSource", "storage", "array<atomic<u32>>"),
|
|
3552
|
+
decl(1, 2, "bits", "storage", "array<u32>"),
|
|
3553
|
+
decl(2, 0, "P", "uniform", "FrontierParams")
|
|
3554
|
+
],
|
|
3555
|
+
overrideDecls: [],
|
|
3556
|
+
uniforms: [FRONTIER_PARAMS],
|
|
3557
|
+
needs: [],
|
|
3558
|
+
snippetSlots: [],
|
|
3559
|
+
phase: "P8"
|
|
3560
|
+
};
|
|
3561
|
+
const REGISTRY = Object.freeze({
|
|
3562
|
+
degree: DEGREE,
|
|
3563
|
+
reduce: REDUCE,
|
|
3564
|
+
fill: FILL,
|
|
3565
|
+
"segmented-reduce": SEGMENTED_REDUCE,
|
|
3566
|
+
"fa2-stats-finalize": FA2_STATS_FINALIZE,
|
|
3567
|
+
"fa2-attraction": FA2_ATTRACTION,
|
|
3568
|
+
"fa2-repulsion-exact": FA2_REPULSION_EXACT,
|
|
3569
|
+
"fa2-speed-finalize": FA2_SPEED_FINALIZE,
|
|
3570
|
+
"fa2-integrate": FA2_INTEGRATE,
|
|
3571
|
+
"fa2-to-scene": FA2_TO_SCENE,
|
|
3572
|
+
"spmv-pull": SPMV_PULL,
|
|
3573
|
+
"pr-scale": PR_SCALE,
|
|
3574
|
+
"pr-finalize": PR_FINALIZE,
|
|
3575
|
+
"wcc-link-sample": WCC_LINK_SAMPLE,
|
|
3576
|
+
"wcc-link-edges": WCC_LINK_EDGES,
|
|
3577
|
+
"wcc-compress": WCC_COMPRESS,
|
|
3578
|
+
"wcc-sample": WCC_SAMPLE,
|
|
3579
|
+
"indirect-finalize": INDIRECT_FINALIZE,
|
|
3580
|
+
"scan-block": SCAN_BLOCK,
|
|
3581
|
+
"scan-add": SCAN_ADD,
|
|
3582
|
+
histogram: HISTOGRAM,
|
|
3583
|
+
"counting-scatter": COUNTING_SCATTER,
|
|
3584
|
+
"radix-hist": RADIX_HIST,
|
|
3585
|
+
"radix-scatter": RADIX_SCATTER,
|
|
3586
|
+
"grid-cell-key": GRID_CELL_KEY,
|
|
3587
|
+
"grid-centroid": GRID_CENTROID,
|
|
3588
|
+
"grid-centroid-hub": GRID_CENTROID_HUB,
|
|
2566
3589
|
"grid-downsample": GRID_DOWNSAMPLE,
|
|
2567
3590
|
"grid-far-field": GRID_FAR_FIELD,
|
|
2568
|
-
"grid-near-field": GRID_NEAR_FIELD
|
|
3591
|
+
"grid-near-field": GRID_NEAR_FIELD,
|
|
3592
|
+
"compact-scatter": COMPACT_SCATTER,
|
|
3593
|
+
"dedupe-claim": DEDUPE_CLAIM,
|
|
3594
|
+
"dedupe-filter": DEDUPE_FILTER,
|
|
3595
|
+
"frontier-finalize": FRONTIER_FINALIZE,
|
|
3596
|
+
"advance-expand": ADVANCE_EXPAND,
|
|
3597
|
+
"bfs-contract": BFS_CONTRACT,
|
|
3598
|
+
"sssp-pred": SSSP_PRED,
|
|
3599
|
+
"bfs-fused": BFS_FUSED,
|
|
3600
|
+
"bfs-bottom-up": BFS_BOTTOM_UP,
|
|
3601
|
+
"bfs-bitset-build": BFS_BITSET_BUILD,
|
|
3602
|
+
"bfs-unvisited-flags": BFS_UNVISITED_FLAGS,
|
|
3603
|
+
"sssp-relax": SSSP_RELAX,
|
|
3604
|
+
"bf-relax": BF_RELAX,
|
|
3605
|
+
"closeness-sweep": CLOSENESS_SWEEP,
|
|
3606
|
+
"closeness-reduce": CLOSENESS_REDUCE
|
|
2569
3607
|
});
|
|
2570
3608
|
const bodyOverrides = /* @__PURE__ */ new Map();
|
|
2571
3609
|
function entryOf(id) {
|
|
@@ -2724,7 +3762,7 @@ class ScanPlannerImpl {
|
|
|
2724
3762
|
}
|
|
2725
3763
|
const POISON = 3735928559;
|
|
2726
3764
|
const CHECK_BLOCKS = 32;
|
|
2727
|
-
const RING_SLOTS$
|
|
3765
|
+
const RING_SLOTS$6 = 8;
|
|
2728
3766
|
const checked = /* @__PURE__ */ new WeakMap();
|
|
2729
3767
|
function inputAt(i) {
|
|
2730
3768
|
return i + 1;
|
|
@@ -2752,7 +3790,7 @@ async function runCheck(ctx) {
|
|
|
2752
3790
|
const count = CHECK_BLOCKS * wg + 1;
|
|
2753
3791
|
const bytes = 4 * count;
|
|
2754
3792
|
const lease = ctx.pool.lease();
|
|
2755
|
-
const ring = new UniformRing(ctx.device, ctx.allocator, RING_SLOTS$
|
|
3793
|
+
const ring = new UniformRing(ctx.device, ctx.allocator, RING_SLOTS$6, "device-check/ring");
|
|
2756
3794
|
try {
|
|
2757
3795
|
const scope = {
|
|
2758
3796
|
device: ctx.device,
|
|
@@ -2892,8 +3930,8 @@ function rowCountOf(core, primitive) {
|
|
|
2892
3930
|
function arcCountOf(core) {
|
|
2893
3931
|
return core.colIdx === null ? 0 : core.colIdx.size / 4;
|
|
2894
3932
|
}
|
|
2895
|
-
function windowBinding(core, name,
|
|
2896
|
-
const buffer = core.arcBuffers?.[name][
|
|
3933
|
+
function windowBinding(core, name, w) {
|
|
3934
|
+
const buffer = core.arcBuffers?.[name][w.bufferIndex];
|
|
2897
3935
|
if (buffer === void 0) {
|
|
2898
3936
|
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `windowBinding: the core has no window buffers of ${name}`, {
|
|
2899
3937
|
argument: "core",
|
|
@@ -2901,7 +3939,21 @@ function windowBinding(core, name, w2) {
|
|
|
2901
3939
|
expected: "a windowed core whose plan uploaded the array"
|
|
2902
3940
|
});
|
|
2903
3941
|
}
|
|
2904
|
-
return { buffer, offset:
|
|
3942
|
+
return { buffer, offset: w.offset, size: 4 * (w.end - w.start), window: w };
|
|
3943
|
+
}
|
|
3944
|
+
function coreWindows(core) {
|
|
3945
|
+
if (core.windows === null) {
|
|
3946
|
+
return [{ arcBase: 0, arcEnd: arcCountOf(core), core }];
|
|
3947
|
+
}
|
|
3948
|
+
return core.windows.map((w, k, all) => ({
|
|
3949
|
+
arcBase: w.start,
|
|
3950
|
+
arcEnd: k + 1 < all.length ? all[k + 1].start : w.end,
|
|
3951
|
+
core: {
|
|
3952
|
+
...core,
|
|
3953
|
+
colIdx: windowBinding(core, "colIdx", w),
|
|
3954
|
+
weights: core.weights === null ? null : windowBinding(core, "weights", w)
|
|
3955
|
+
}
|
|
3956
|
+
}));
|
|
2905
3957
|
}
|
|
2906
3958
|
function assertNotWindowed(core, primitive) {
|
|
2907
3959
|
if (core.plan === "windowed" || core.windows !== null) {
|
|
@@ -2919,22 +3971,22 @@ function assertWholeCore(core, arcCount, limit, algorithm) {
|
|
|
2919
3971
|
);
|
|
2920
3972
|
}
|
|
2921
3973
|
}
|
|
2922
|
-
function coreOfView(
|
|
2923
|
-
const { rowPtr } =
|
|
3974
|
+
function coreOfView(v, arcCount) {
|
|
3975
|
+
const { rowPtr } = v.bindings;
|
|
2924
3976
|
if (rowPtr === void 0) {
|
|
2925
|
-
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `the ${
|
|
3977
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `the ${v.view} view has no rowPtr binding`, {
|
|
2926
3978
|
argument: "view",
|
|
2927
|
-
value:
|
|
3979
|
+
value: v.view,
|
|
2928
3980
|
expected: "a view with a rowPtr binding (reverse)"
|
|
2929
3981
|
});
|
|
2930
3982
|
}
|
|
2931
|
-
const colIdx =
|
|
2932
|
-
const weights =
|
|
3983
|
+
const colIdx = v.bindings.colIdx === void 0 || v.bindings.colIdx.size === 0 ? null : v.bindings.colIdx;
|
|
3984
|
+
const weights = v.bindings.weights === void 0 || v.bindings.weights.size === 0 ? null : v.bindings.weights;
|
|
2933
3985
|
const bound = colIdx === null ? 0 : colIdx.size / 4;
|
|
2934
3986
|
if (bound !== arcCount) {
|
|
2935
3987
|
throw new WebGpuGraphError(
|
|
2936
3988
|
"E_INVALID_ARGUMENT",
|
|
2937
|
-
`the ${
|
|
3989
|
+
`the ${v.view} view binds ${bound} arcs but its arcCount scalar says ${arcCount}`,
|
|
2938
3990
|
{
|
|
2939
3991
|
argument: "arcCount",
|
|
2940
3992
|
value: arcCount,
|
|
@@ -2955,7 +4007,7 @@ function coreOfView(v2, arcCount) {
|
|
|
2955
4007
|
hasWeights: weights !== null
|
|
2956
4008
|
});
|
|
2957
4009
|
}
|
|
2958
|
-
function checkDest$
|
|
4010
|
+
function checkDest$5(dest, n) {
|
|
2959
4011
|
if (dest === void 0) {
|
|
2960
4012
|
return null;
|
|
2961
4013
|
}
|
|
@@ -2984,7 +4036,7 @@ async function degree(ctx, s, options) {
|
|
|
2984
4036
|
ctx.assertReady();
|
|
2985
4037
|
await assertDeviceComputes(ctx);
|
|
2986
4038
|
const n = s.nodeCount;
|
|
2987
|
-
const dest = checkDest$
|
|
4039
|
+
const dest = checkDest$5(options?.dest, n);
|
|
2988
4040
|
if (options?.signal?.aborted) {
|
|
2989
4041
|
throw new WebGpuGraphError("E_ABORTED", "degree: the signal was aborted before any work started", {});
|
|
2990
4042
|
}
|
|
@@ -3021,22 +4073,22 @@ async function degree(ctx, s, options) {
|
|
|
3021
4073
|
const fill = await ctx.pipelines.kernel(kernelSpec("fill"));
|
|
3022
4074
|
const zero = fill.bind({ dst: outBinding, P: params(FILL_PARAMS, { count: n, value: 0, mode: 0 }) });
|
|
3023
4075
|
fill.dispatch(pass, zero, plan1d(n, ctx.workgroupSize, ctx.caps), [0]);
|
|
3024
|
-
for (const
|
|
4076
|
+
for (const w of core.windows) {
|
|
3025
4077
|
const windowed = {
|
|
3026
4078
|
...core,
|
|
3027
|
-
colIdx: windowBinding(core, "colIdx",
|
|
3028
|
-
weights: core.weights === null ? null : windowBinding(core, "weights",
|
|
4079
|
+
colIdx: windowBinding(core, "colIdx", w),
|
|
4080
|
+
weights: core.weights === null ? null : windowBinding(core, "weights", w)
|
|
3029
4081
|
};
|
|
3030
4082
|
const P = params(RANGE_PARAMS, {
|
|
3031
|
-
start:
|
|
3032
|
-
end:
|
|
3033
|
-
arcBase:
|
|
3034
|
-
arcEnd:
|
|
4083
|
+
start: w.rowFirst,
|
|
4084
|
+
end: w.rowLast + 1,
|
|
4085
|
+
arcBase: w.start,
|
|
4086
|
+
arcEnd: w.end,
|
|
3035
4087
|
accumulate: 1,
|
|
3036
4088
|
n
|
|
3037
4089
|
});
|
|
3038
4090
|
const bound = kernel.bind({ ...graphBindings(windowed, null), out: outBinding, P });
|
|
3039
|
-
kernel.dispatch(pass, bound, plan1d(
|
|
4091
|
+
kernel.dispatch(pass, bound, plan1d(w.rowLast - w.rowFirst + 1, ctx.workgroupSize, ctx.caps), [0]);
|
|
3040
4092
|
}
|
|
3041
4093
|
}
|
|
3042
4094
|
pass.end();
|
|
@@ -3342,9 +4394,9 @@ class CommandBatch {
|
|
|
3342
4394
|
countDispatches(pass) {
|
|
3343
4395
|
const direct = pass.dispatchWorkgroups.bind(pass);
|
|
3344
4396
|
const indirect = pass.dispatchWorkgroupsIndirect.bind(pass);
|
|
3345
|
-
pass.dispatchWorkgroups = (
|
|
4397
|
+
pass.dispatchWorkgroups = (x, y, z) => {
|
|
3346
4398
|
this.dispatchCount += 1;
|
|
3347
|
-
direct(
|
|
4399
|
+
direct(x, y, z);
|
|
3348
4400
|
return void 0;
|
|
3349
4401
|
};
|
|
3350
4402
|
pass.dispatchWorkgroupsIndirect = (buffer, offset) => {
|
|
@@ -3401,16 +4453,17 @@ function algorithmScope(ctx, label, slots) {
|
|
|
3401
4453
|
dispose() {
|
|
3402
4454
|
ring.destroy();
|
|
3403
4455
|
lease.release();
|
|
3404
|
-
}
|
|
4456
|
+
},
|
|
4457
|
+
ringOverruns: () => ring.overruns
|
|
3405
4458
|
};
|
|
3406
4459
|
}
|
|
3407
|
-
const ALGORITHM = "connectedComponents";
|
|
3408
|
-
const ROUNDS_PER_BATCH = 4;
|
|
4460
|
+
const ALGORITHM$4 = "connectedComponents";
|
|
4461
|
+
const ROUNDS_PER_BATCH$1 = 4;
|
|
3409
4462
|
const MAX_WCC_ROUNDS = 64;
|
|
3410
4463
|
const SAMPLE_SIZE = 1024;
|
|
3411
4464
|
const MAX_STEPS = 1024;
|
|
3412
|
-
const RING_SLOTS$
|
|
3413
|
-
function checkDest$
|
|
4465
|
+
const RING_SLOTS$5 = 2 * ROUNDS_PER_BATCH$1;
|
|
4466
|
+
function checkDest$4(dest, n) {
|
|
3414
4467
|
if (dest === void 0) {
|
|
3415
4468
|
return null;
|
|
3416
4469
|
}
|
|
@@ -3419,7 +4472,7 @@ function checkDest$2(dest, n) {
|
|
|
3419
4472
|
}
|
|
3420
4473
|
throw new WebGpuGraphError(
|
|
3421
4474
|
"E_INVALID_ARGUMENT",
|
|
3422
|
-
`${ALGORITHM}: dest must be a Uint32Array of length ${n} over an ArrayBuffer`,
|
|
4475
|
+
`${ALGORITHM$4}: dest must be a Uint32Array of length ${n} over an ArrayBuffer`,
|
|
3423
4476
|
{
|
|
3424
4477
|
argument: "dest",
|
|
3425
4478
|
value: `${dest.constructor.name}(${dest.length})`,
|
|
@@ -3429,10 +4482,10 @@ function checkDest$2(dest, n) {
|
|
|
3429
4482
|
}
|
|
3430
4483
|
function coreOf$2(ctx, s) {
|
|
3431
4484
|
const core = ctx.residency.core(s);
|
|
3432
|
-
assertWholeCore(core, s.arcCount, ctx.caps.limits.maxStorageBufferBindingSize, ALGORITHM);
|
|
4485
|
+
assertWholeCore(core, s.arcCount, ctx.caps.limits.maxStorageBufferBindingSize, ALGORITHM$4);
|
|
3433
4486
|
return core;
|
|
3434
4487
|
}
|
|
3435
|
-
function bindingOf$
|
|
4488
|
+
function bindingOf$3(buffer, size) {
|
|
3436
4489
|
return { buffer, offset: 0, size, window: null };
|
|
3437
4490
|
}
|
|
3438
4491
|
function labelResult(labels, count) {
|
|
@@ -3448,19 +4501,19 @@ function labelResult(labels, count) {
|
|
|
3448
4501
|
const dense = new Uint32Array(n).fill(U32_MAX$2);
|
|
3449
4502
|
const sizes = new Uint32Array(count);
|
|
3450
4503
|
let next = 0;
|
|
3451
|
-
for (let
|
|
3452
|
-
let k = dense[labels[
|
|
4504
|
+
for (let v = 0; v < n; v++) {
|
|
4505
|
+
let k = dense[labels[v]];
|
|
3453
4506
|
if (k === U32_MAX$2) {
|
|
3454
4507
|
k = next++;
|
|
3455
|
-
dense[labels[
|
|
4508
|
+
dense[labels[v]] = k;
|
|
3456
4509
|
}
|
|
3457
4510
|
sizes[k]++;
|
|
3458
4511
|
}
|
|
3459
4512
|
const built = Array.from(sizes, (size) => new Uint32Array(size));
|
|
3460
4513
|
const filled = new Uint32Array(count);
|
|
3461
|
-
for (let
|
|
3462
|
-
const k = dense[labels[
|
|
3463
|
-
built[k][filled[k]++] =
|
|
4514
|
+
for (let v = 0; v < n; v++) {
|
|
4515
|
+
const k = dense[labels[v]];
|
|
4516
|
+
built[k][filled[k]++] = v;
|
|
3464
4517
|
}
|
|
3465
4518
|
groups = built;
|
|
3466
4519
|
return built;
|
|
@@ -3485,11 +4538,11 @@ function checkLabels(raw) {
|
|
|
3485
4538
|
const n = raw.length;
|
|
3486
4539
|
const seen = new Uint8Array(n);
|
|
3487
4540
|
let count = 0;
|
|
3488
|
-
for (let
|
|
3489
|
-
const label = raw[
|
|
4541
|
+
for (let v = 0; v < n; v++) {
|
|
4542
|
+
const label = raw[v];
|
|
3490
4543
|
if (label >= n) {
|
|
3491
|
-
throw new WebGpuGraphError("E_VALIDATION", `${ALGORITHM}: labels[${
|
|
3492
|
-
label: `${ALGORITHM}/labels`,
|
|
4544
|
+
throw new WebGpuGraphError("E_VALIDATION", `${ALGORITHM$4}: labels[${v}] = ${label} is not a node index`, {
|
|
4545
|
+
label: `${ALGORITHM$4}/labels`,
|
|
3493
4546
|
message: `the device produced a label outside [0, ${n})`
|
|
3494
4547
|
});
|
|
3495
4548
|
}
|
|
@@ -3505,9 +4558,9 @@ async function connectedComponents(ctx, s, options) {
|
|
|
3505
4558
|
await assertDeviceComputes(ctx);
|
|
3506
4559
|
const n = s.nodeCount;
|
|
3507
4560
|
const renumber = options?.renumber !== false;
|
|
3508
|
-
const dest = checkDest$
|
|
4561
|
+
const dest = checkDest$4(options?.dest, n);
|
|
3509
4562
|
if (options?.signal?.aborted) {
|
|
3510
|
-
throw new WebGpuGraphError("E_ABORTED", `${ALGORITHM}: the signal was aborted before any work started`, {});
|
|
4563
|
+
throw new WebGpuGraphError("E_ABORTED", `${ALGORITHM$4}: the signal was aborted before any work started`, {});
|
|
3511
4564
|
}
|
|
3512
4565
|
if (n === 0) {
|
|
3513
4566
|
options?.onProgress?.(1, 1);
|
|
@@ -3516,15 +4569,15 @@ async function connectedComponents(ctx, s, options) {
|
|
|
3516
4569
|
const core = coreOf$2(ctx, s);
|
|
3517
4570
|
if (s.arcCount === 0) {
|
|
3518
4571
|
const labels = dest ?? new Uint32Array(n);
|
|
3519
|
-
for (let
|
|
3520
|
-
labels[
|
|
4572
|
+
for (let v = 0; v < n; v++) {
|
|
4573
|
+
labels[v] = v;
|
|
3521
4574
|
}
|
|
3522
4575
|
options?.onProgress?.(1, 1);
|
|
3523
4576
|
return labelResult(labels, n);
|
|
3524
4577
|
}
|
|
3525
4578
|
const edges = ctx.residency.view(s, "edgeList");
|
|
3526
4579
|
const edgeCount = edges.scalars.edgeCount[0];
|
|
3527
|
-
const scope = algorithmScope(ctx, ALGORITHM, RING_SLOTS$
|
|
4580
|
+
const scope = algorithmScope(ctx, ALGORITHM$4, RING_SLOTS$5);
|
|
3528
4581
|
try {
|
|
3529
4582
|
const compBytes = 4 * (n + 1);
|
|
3530
4583
|
const comp = scope.scratch(compBytes, "comp");
|
|
@@ -3537,8 +4590,8 @@ async function connectedComponents(ctx, s, options) {
|
|
|
3537
4590
|
const compress = await ctx.pipelines.kernel(kernelSpec("wcc-compress", {}));
|
|
3538
4591
|
const sample = await ctx.pipelines.kernel(kernelSpec("wcc-sample", {}));
|
|
3539
4592
|
const { queue } = ctx.device;
|
|
3540
|
-
const compBinding = bindingOf$
|
|
3541
|
-
const histBinding = bindingOf$
|
|
4593
|
+
const compBinding = bindingOf$3(comp, compBytes);
|
|
4594
|
+
const histBinding = bindingOf$3(hist, 4 * items);
|
|
3542
4595
|
const rowPlan = planGridStride(n, ctx.workgroupSize, ctx.caps);
|
|
3543
4596
|
const edgePlan = planGridStride(edgeCount, ctx.workgroupSize, ctx.caps);
|
|
3544
4597
|
const flagIndex = n;
|
|
@@ -3553,12 +4606,12 @@ async function connectedComponents(ctx, s, options) {
|
|
|
3553
4606
|
return batch.submit();
|
|
3554
4607
|
};
|
|
3555
4608
|
queue.writeBuffer(comp, 4 * flagIndex, zero);
|
|
3556
|
-
const setup = new CommandBatch(ctx, `${ALGORITHM}/setup`);
|
|
4609
|
+
const setup = new CommandBatch(ctx, `${ALGORITHM$4}/setup`);
|
|
3557
4610
|
let pass = setup.pass("sample-rounds");
|
|
3558
4611
|
const fillParams = scope.params(FILL_PARAMS, { count: n, value: 0, mode: 1, pad0: 0 });
|
|
3559
4612
|
fill.dispatch(
|
|
3560
4613
|
pass,
|
|
3561
|
-
fill.bind({ dst: bindingOf$
|
|
4614
|
+
fill.bind({ dst: bindingOf$3(comp, 4 * n), P: fillParams.binding }),
|
|
3562
4615
|
plan1d(n, ctx.workgroupSize, ctx.caps),
|
|
3563
4616
|
[fillParams.offset]
|
|
3564
4617
|
);
|
|
@@ -3572,7 +4625,7 @@ async function connectedComponents(ctx, s, options) {
|
|
|
3572
4625
|
setup.endPass();
|
|
3573
4626
|
await submit(setup).readback;
|
|
3574
4627
|
ctx.assertReady();
|
|
3575
|
-
const sampler = new CommandBatch(ctx, `${ALGORITHM}/sample`);
|
|
4628
|
+
const sampler = new CommandBatch(ctx, `${ALGORITHM$4}/sample`);
|
|
3576
4629
|
pass = sampler.pass("sample");
|
|
3577
4630
|
const sampleParams = wccParams({ items, stride: 0, r: 0, giant: U32_MAX$2 });
|
|
3578
4631
|
sample.dispatch(
|
|
@@ -3590,9 +4643,9 @@ async function connectedComponents(ctx, s, options) {
|
|
|
3590
4643
|
let rounds = 0;
|
|
3591
4644
|
for (; ; ) {
|
|
3592
4645
|
queue.writeBuffer(comp, 4 * flagIndex, zero);
|
|
3593
|
-
const batch = new CommandBatch(ctx, `${ALGORITHM}/rounds`);
|
|
4646
|
+
const batch = new CommandBatch(ctx, `${ALGORITHM$4}/rounds`);
|
|
3594
4647
|
pass = batch.pass("edge-rounds");
|
|
3595
|
-
for (let i = 0; i < ROUNDS_PER_BATCH; i++) {
|
|
4648
|
+
for (let i = 0; i < ROUNDS_PER_BATCH$1; i++) {
|
|
3596
4649
|
const params = wccParams({ items: edgeCount, stride: edgePlan.stride ?? edgeCount, r: 0, giant });
|
|
3597
4650
|
const bound = linkEdges.bind({ ...edgeBindings, P: params.binding });
|
|
3598
4651
|
linkEdges.dispatch(pass, bound, edgePlan, [params.offset]);
|
|
@@ -3602,10 +4655,10 @@ async function connectedComponents(ctx, s, options) {
|
|
|
3602
4655
|
const flagRequest = batch.readback(comp, 4 * flagIndex, 4);
|
|
3603
4656
|
const submitted = submit(batch);
|
|
3604
4657
|
const back = await submitted.readback;
|
|
3605
|
-
rounds += ROUNDS_PER_BATCH;
|
|
4658
|
+
rounds += ROUNDS_PER_BATCH$1;
|
|
3606
4659
|
ctx.assertReady();
|
|
3607
4660
|
if (options?.signal?.aborted) {
|
|
3608
|
-
throw new WebGpuGraphError("E_ABORTED", `${ALGORITHM}: the signal was aborted`, {
|
|
4661
|
+
throw new WebGpuGraphError("E_ABORTED", `${ALGORITHM$4}: the signal was aborted`, {
|
|
3609
4662
|
batchId: submitted.id
|
|
3610
4663
|
});
|
|
3611
4664
|
}
|
|
@@ -3615,12 +4668,12 @@ async function connectedComponents(ctx, s, options) {
|
|
|
3615
4668
|
if (rounds >= MAX_WCC_ROUNDS) {
|
|
3616
4669
|
throw new WebGpuGraphError(
|
|
3617
4670
|
"E_VALIDATION",
|
|
3618
|
-
`${ALGORITHM}: the changed flag never settled in ${MAX_WCC_ROUNDS} rounds`,
|
|
3619
|
-
{ label: ALGORITHM, message: `the changed flag never settled in ${MAX_WCC_ROUNDS} rounds` }
|
|
4671
|
+
`${ALGORITHM$4}: the changed flag never settled in ${MAX_WCC_ROUNDS} rounds`,
|
|
4672
|
+
{ label: ALGORITHM$4, message: `the changed flag never settled in ${MAX_WCC_ROUNDS} rounds` }
|
|
3620
4673
|
);
|
|
3621
4674
|
}
|
|
3622
4675
|
}
|
|
3623
|
-
const final = new CommandBatch(ctx, `${ALGORITHM}/final`);
|
|
4676
|
+
const final = new CommandBatch(ctx, `${ALGORITHM$4}/final`);
|
|
3624
4677
|
recordCompress(final.pass("compress"));
|
|
3625
4678
|
final.endPass();
|
|
3626
4679
|
await submit(final).readback;
|
|
@@ -3801,13 +4854,13 @@ class TieredPlanner {
|
|
|
3801
4854
|
this.dispatches++;
|
|
3802
4855
|
}
|
|
3803
4856
|
const { windows } = core;
|
|
3804
|
-
windows.forEach((
|
|
4857
|
+
windows.forEach((w, k) => {
|
|
3805
4858
|
const windowed = {
|
|
3806
4859
|
...core,
|
|
3807
|
-
colIdx: windowBinding(core, "colIdx",
|
|
3808
|
-
weights: core.weights === null ? null : windowBinding(core, "weights",
|
|
4860
|
+
colIdx: windowBinding(core, "colIdx", w),
|
|
4861
|
+
weights: core.weights === null ? null : windowBinding(core, "weights", w)
|
|
3809
4862
|
};
|
|
3810
|
-
this.recordWindow(pass, windowed, out, n,
|
|
4863
|
+
this.recordWindow(pass, windowed, out, n, w, k + 1 < windows.length ? windows[k + 1].start : w.end);
|
|
3811
4864
|
});
|
|
3812
4865
|
}
|
|
3813
4866
|
/**
|
|
@@ -3824,16 +4877,16 @@ class TieredPlanner {
|
|
|
3824
4877
|
* @param w - the window, or null for the single whole-core dispatch
|
|
3825
4878
|
* @param nextStart - the next window's first arc (w.end for the last window)
|
|
3826
4879
|
*/
|
|
3827
|
-
recordWindow(pass, core, out, n,
|
|
4880
|
+
recordWindow(pass, core, out, n, w, nextStart) {
|
|
3828
4881
|
const wg = this.scope.workgroupSize;
|
|
3829
4882
|
for (const { tier, kernel, start, end } of this.tiers) {
|
|
3830
4883
|
let first = start;
|
|
3831
4884
|
let last = tier === 0 ? n : end;
|
|
3832
|
-
let arcEnd =
|
|
3833
|
-
if (
|
|
3834
|
-
first =
|
|
3835
|
-
last =
|
|
3836
|
-
arcEnd =
|
|
4885
|
+
let arcEnd = w === null ? arcCountOf(core) : Math.min(w.end, nextStart);
|
|
4886
|
+
if (w !== null && this.perm === null) {
|
|
4887
|
+
first = w.rowFirst;
|
|
4888
|
+
last = w.rowLast + 1;
|
|
4889
|
+
arcEnd = w.end;
|
|
3837
4890
|
}
|
|
3838
4891
|
const rows = last - first;
|
|
3839
4892
|
if (rows <= 0) {
|
|
@@ -3842,9 +4895,9 @@ class TieredPlanner {
|
|
|
3842
4895
|
const params = this.scope.params(RANGE_PARAMS, {
|
|
3843
4896
|
start: first,
|
|
3844
4897
|
end: last,
|
|
3845
|
-
arcBase:
|
|
4898
|
+
arcBase: w === null ? 0 : w.start,
|
|
3846
4899
|
arcEnd,
|
|
3847
|
-
accumulate:
|
|
4900
|
+
accumulate: w !== null || this.accumulate ? 1 : 0,
|
|
3848
4901
|
n
|
|
3849
4902
|
});
|
|
3850
4903
|
const bound = kernel.bind({ ...graphBindings(core, this.perm), out, P: params.binding });
|
|
@@ -3986,8 +5039,8 @@ async function prepareSpmvPull(scope, rev, options) {
|
|
|
3986
5039
|
return new SpmvPullPlannerImpl(scope, compiled, perm, options.weights);
|
|
3987
5040
|
}
|
|
3988
5041
|
const PR_BATCH = 8;
|
|
3989
|
-
const RING_SLOTS$
|
|
3990
|
-
function checkDest$
|
|
5042
|
+
const RING_SLOTS$4 = 2 * PR_BATCH + 1;
|
|
5043
|
+
function checkDest$3(dest, n, algorithm) {
|
|
3991
5044
|
if (dest === void 0) {
|
|
3992
5045
|
return null;
|
|
3993
5046
|
}
|
|
@@ -4009,10 +5062,10 @@ function coreOf$1(ctx, s, algorithm) {
|
|
|
4009
5062
|
assertWholeCore(core, s.arcCount, ctx.caps.limits.maxStorageBufferBindingSize, algorithm);
|
|
4010
5063
|
return core;
|
|
4011
5064
|
}
|
|
4012
|
-
function bindingOf(buffer, size) {
|
|
5065
|
+
function bindingOf$2(buffer, size) {
|
|
4013
5066
|
return { buffer, offset: 0, size, window: null };
|
|
4014
5067
|
}
|
|
4015
|
-
function aborted$
|
|
5068
|
+
function aborted$3(algorithm, batchId) {
|
|
4016
5069
|
return new WebGpuGraphError(
|
|
4017
5070
|
"E_ABORTED",
|
|
4018
5071
|
`${algorithm}: the signal was aborted`,
|
|
@@ -4034,9 +5087,9 @@ async function run(ctx, s, personalization, options, algorithm) {
|
|
|
4034
5087
|
expected: "a positive integer"
|
|
4035
5088
|
});
|
|
4036
5089
|
}
|
|
4037
|
-
const dest = checkDest$
|
|
5090
|
+
const dest = checkDest$3(options?.dest, n, algorithm);
|
|
4038
5091
|
if (options?.signal?.aborted) {
|
|
4039
|
-
throw aborted$
|
|
5092
|
+
throw aborted$3(algorithm);
|
|
4040
5093
|
}
|
|
4041
5094
|
if (n === 0) {
|
|
4042
5095
|
options?.onProgress?.(maxIterations, maxIterations);
|
|
@@ -4064,7 +5117,7 @@ async function run(ctx, s, personalization, options, algorithm) {
|
|
|
4064
5117
|
const weights = useWeights ? void 0 : null;
|
|
4065
5118
|
const weightedCore = useWeights ? core : { ...core, weights: null, hasWeights: false };
|
|
4066
5119
|
const weightedRev = useWeights ? rev : { ...rev, weights: null, hasWeights: false };
|
|
4067
|
-
const scope = algorithmScope(ctx, algorithm, RING_SLOTS$
|
|
5120
|
+
const scope = algorithmScope(ctx, algorithm, RING_SLOTS$4);
|
|
4068
5121
|
let uploaded = null;
|
|
4069
5122
|
try {
|
|
4070
5123
|
const bytes = 4 * n;
|
|
@@ -4100,10 +5153,10 @@ async function run(ctx, s, personalization, options, algorithm) {
|
|
|
4100
5153
|
const header = new ArrayBuffer(PR_PARTIAL.byteLength);
|
|
4101
5154
|
PR_PARTIAL.write(new DataView(header), { firstConverged: U32_MAX$2, iteration: 0 });
|
|
4102
5155
|
queue.writeBuffer(partials, 0, header);
|
|
4103
|
-
const rank = [bindingOf(rankA, bytes), bindingOf(rankB, bytes)];
|
|
4104
|
-
const xNormBinding = bindingOf(xNorm, bytes);
|
|
4105
|
-
const outWeightSumBinding = bindingOf(outWeightSum, bytes);
|
|
4106
|
-
const partialsBinding = bindingOf(partials, partialsBytes);
|
|
5156
|
+
const rank = [bindingOf$2(rankA, bytes), bindingOf$2(rankB, bytes)];
|
|
5157
|
+
const xNormBinding = bindingOf$2(xNorm, bytes);
|
|
5158
|
+
const outWeightSumBinding = bindingOf$2(outWeightSum, bytes);
|
|
5159
|
+
const partialsBinding = bindingOf$2(partials, partialsBytes);
|
|
4107
5160
|
const coefficients = { alpha, beta: 1 - alpha, uniformP: 1 / n };
|
|
4108
5161
|
let cur = 0;
|
|
4109
5162
|
let iterationsRun = 0;
|
|
@@ -4156,7 +5209,7 @@ async function run(ctx, s, personalization, options, algorithm) {
|
|
|
4156
5209
|
iterationsRun += k;
|
|
4157
5210
|
ctx.assertReady();
|
|
4158
5211
|
if (options?.signal?.aborted) {
|
|
4159
|
-
throw aborted$
|
|
5212
|
+
throw aborted$3(algorithm, submitted.id);
|
|
4160
5213
|
}
|
|
4161
5214
|
options?.onProgress?.(iterationsRun, maxIterations);
|
|
4162
5215
|
const folded = PR_PARTIAL.read(new DataView(back), headerRequest.offset);
|
|
@@ -4199,8 +5252,8 @@ async function personalizedPageRank(ctx, s, personalization, options) {
|
|
|
4199
5252
|
);
|
|
4200
5253
|
}
|
|
4201
5254
|
let total = 0;
|
|
4202
|
-
for (let
|
|
4203
|
-
const mass = personalization[
|
|
5255
|
+
for (let v = 0; v < n; v++) {
|
|
5256
|
+
const mass = personalization[v];
|
|
4204
5257
|
if (!Number.isFinite(mass) || mass < 0) {
|
|
4205
5258
|
throw invalid2(mass, "finite and non-negative in every entry");
|
|
4206
5259
|
}
|
|
@@ -4210,14 +5263,14 @@ async function personalizedPageRank(ctx, s, personalization, options) {
|
|
|
4210
5263
|
throw invalid2(total, "a vector whose entries sum to a positive number");
|
|
4211
5264
|
}
|
|
4212
5265
|
const normalised2 = new Float32Array(n);
|
|
4213
|
-
for (let
|
|
4214
|
-
normalised2[
|
|
5266
|
+
for (let v = 0; v < n; v++) {
|
|
5267
|
+
normalised2[v] = personalization[v] / total;
|
|
4215
5268
|
}
|
|
4216
5269
|
return run(ctx, s, normalised2, options, "personalizedPageRank");
|
|
4217
5270
|
}
|
|
4218
5271
|
const BATCH = 8;
|
|
4219
|
-
const RING_SLOTS = 4 * BATCH + 8;
|
|
4220
|
-
function checkDest(dest, n, algorithm) {
|
|
5272
|
+
const RING_SLOTS$3 = 4 * BATCH + 8;
|
|
5273
|
+
function checkDest$2(dest, n, algorithm) {
|
|
4221
5274
|
if (dest === void 0) {
|
|
4222
5275
|
return null;
|
|
4223
5276
|
}
|
|
@@ -4243,7 +5296,7 @@ function reverseOf(ctx, s) {
|
|
|
4243
5296
|
const view = ctx.residency.view(s, "reverse");
|
|
4244
5297
|
return coreOfView(view, view.scalars.arcCount[0]);
|
|
4245
5298
|
}
|
|
4246
|
-
function aborted(algorithm, batchId) {
|
|
5299
|
+
function aborted$2(algorithm, batchId) {
|
|
4247
5300
|
return new WebGpuGraphError(
|
|
4248
5301
|
"E_ABORTED",
|
|
4249
5302
|
`${algorithm}: the signal was aborted`,
|
|
@@ -4255,7 +5308,7 @@ function whole(buffer, size) {
|
|
|
4255
5308
|
}
|
|
4256
5309
|
async function runPowerIteration(ctx, n, config) {
|
|
4257
5310
|
await assertDeviceComputes(ctx);
|
|
4258
|
-
const scope = algorithmScope(ctx, config.label, RING_SLOTS);
|
|
5311
|
+
const scope = algorithmScope(ctx, config.label, RING_SLOTS$3);
|
|
4259
5312
|
try {
|
|
4260
5313
|
const bytes = 4 * n;
|
|
4261
5314
|
const ring = (config.alternate === null ? ["rankA", "rankB"] : ["rankA", "rankB", "rankC"]).map(
|
|
@@ -4328,7 +5381,7 @@ async function runPowerIteration(ctx, n, config) {
|
|
|
4328
5381
|
iterationsRun += k;
|
|
4329
5382
|
ctx.assertReady();
|
|
4330
5383
|
if (config.signal?.aborted === true) {
|
|
4331
|
-
throw aborted(config.label, submitted.id);
|
|
5384
|
+
throw aborted$2(config.label, submitted.id);
|
|
4332
5385
|
}
|
|
4333
5386
|
config.onProgress?.(iterationsRun, config.maxIterations);
|
|
4334
5387
|
const folded = PR_PARTIAL.read(new DataView(back), headerRequest.offset);
|
|
@@ -4363,9 +5416,9 @@ function resolve(ctx, s, options, algorithm) {
|
|
|
4363
5416
|
expected: "a positive integer"
|
|
4364
5417
|
});
|
|
4365
5418
|
}
|
|
4366
|
-
const dest = checkDest(options?.dest, n, algorithm);
|
|
5419
|
+
const dest = checkDest$2(options?.dest, n, algorithm);
|
|
4367
5420
|
if (options?.signal?.aborted) {
|
|
4368
|
-
throw aborted(algorithm);
|
|
5421
|
+
throw aborted$2(algorithm);
|
|
4369
5422
|
}
|
|
4370
5423
|
return { n, maxIterations, tolerance, weights: options?.weighted === false ? null : void 0, dest };
|
|
4371
5424
|
}
|
|
@@ -4382,13 +5435,13 @@ function finite(value, fallback, argument) {
|
|
|
4382
5435
|
}
|
|
4383
5436
|
function normalised(raw, dest, norm) {
|
|
4384
5437
|
let acc = 0;
|
|
4385
|
-
for (const
|
|
4386
|
-
acc += norm === "sum" ? Math.abs(
|
|
5438
|
+
for (const v of raw) {
|
|
5439
|
+
acc += norm === "sum" ? Math.abs(v) : v * v;
|
|
4387
5440
|
}
|
|
4388
5441
|
const scale = norm === "sum" ? acc : Math.sqrt(acc);
|
|
4389
5442
|
const out = dest ?? raw;
|
|
4390
|
-
for (let
|
|
4391
|
-
out[
|
|
5443
|
+
for (let v = 0; v < raw.length; v++) {
|
|
5444
|
+
out[v] = scale > 0 ? raw[v] / scale : raw[v];
|
|
4392
5445
|
}
|
|
4393
5446
|
return out;
|
|
4394
5447
|
}
|
|
@@ -4433,76 +5486,1857 @@ async function eigenvectorCentrality(ctx, s, options) {
|
|
|
4433
5486
|
});
|
|
4434
5487
|
return scoresOf(run2, r.dest, "l2");
|
|
4435
5488
|
}
|
|
4436
|
-
async function katzCentrality(ctx, s, options) {
|
|
4437
|
-
const algorithm = "katzCentrality";
|
|
4438
|
-
const alpha = finite(options?.alpha, 0.1, "alpha");
|
|
4439
|
-
const beta = finite(options?.beta, 1, "beta");
|
|
4440
|
-
const r = resolve(ctx, s, options, algorithm);
|
|
4441
|
-
if (r.n === 0) {
|
|
4442
|
-
return empty(r.dest, r.maxIterations, options?.onProgress);
|
|
5489
|
+
async function katzCentrality(ctx, s, options) {
|
|
5490
|
+
const algorithm = "katzCentrality";
|
|
5491
|
+
const alpha = finite(options?.alpha, 0.1, "alpha");
|
|
5492
|
+
const beta = finite(options?.beta, 1, "beta");
|
|
5493
|
+
const r = resolve(ctx, s, options, algorithm);
|
|
5494
|
+
if (r.n === 0) {
|
|
5495
|
+
return empty(r.dest, r.maxIterations, options?.onProgress);
|
|
5496
|
+
}
|
|
5497
|
+
const run2 = await runPowerIteration(ctx, r.n, {
|
|
5498
|
+
normMode: 4,
|
|
5499
|
+
adjacency: reverseOf(ctx, s),
|
|
5500
|
+
alternate: null,
|
|
5501
|
+
alpha,
|
|
5502
|
+
beta,
|
|
5503
|
+
uniformP: 1,
|
|
5504
|
+
maxIterations: r.maxIterations,
|
|
5505
|
+
tolerance: r.tolerance,
|
|
5506
|
+
weights: r.weights,
|
|
5507
|
+
label: algorithm,
|
|
5508
|
+
signal: options?.signal,
|
|
5509
|
+
onProgress: options?.onProgress
|
|
5510
|
+
});
|
|
5511
|
+
return scoresOf(run2, r.dest, "l2");
|
|
5512
|
+
}
|
|
5513
|
+
async function hits(ctx, s, options) {
|
|
5514
|
+
const algorithm = "hits";
|
|
5515
|
+
const r = resolve(ctx, s, options, algorithm);
|
|
5516
|
+
const total = 2 * r.maxIterations;
|
|
5517
|
+
if (r.n === 0) {
|
|
5518
|
+
const { scores, iterations, converged, precision } = empty(r.dest, total, options?.onProgress);
|
|
5519
|
+
return { hubs: scores, authorities: new Float32Array(0), iterations, converged, precision };
|
|
5520
|
+
}
|
|
5521
|
+
const onProgress = options?.onProgress;
|
|
5522
|
+
const shared = {
|
|
5523
|
+
normMode: 1,
|
|
5524
|
+
alpha: 1,
|
|
5525
|
+
beta: 0,
|
|
5526
|
+
uniformP: 0,
|
|
5527
|
+
maxIterations: r.maxIterations,
|
|
5528
|
+
tolerance: r.tolerance,
|
|
5529
|
+
weights: r.weights,
|
|
5530
|
+
signal: options?.signal
|
|
5531
|
+
};
|
|
5532
|
+
const forward = coreOf(ctx, s, algorithm);
|
|
5533
|
+
const reverse = reverseOf(ctx, s);
|
|
5534
|
+
const hubSeeded = await runPowerIteration(ctx, r.n, {
|
|
5535
|
+
...shared,
|
|
5536
|
+
adjacency: reverse,
|
|
5537
|
+
alternate: forward,
|
|
5538
|
+
label: `${algorithm}/hub-seeded`,
|
|
5539
|
+
onProgress: onProgress === void 0 ? void 0 : (done) => {
|
|
5540
|
+
onProgress(done, total);
|
|
5541
|
+
}
|
|
5542
|
+
});
|
|
5543
|
+
const authoritySeeded = await runPowerIteration(ctx, r.n, {
|
|
5544
|
+
...shared,
|
|
5545
|
+
adjacency: forward,
|
|
5546
|
+
alternate: reverse,
|
|
5547
|
+
label: `${algorithm}/authority-seeded`,
|
|
5548
|
+
onProgress: onProgress === void 0 ? void 0 : (done) => {
|
|
5549
|
+
onProgress(r.maxIterations + done, total);
|
|
5550
|
+
}
|
|
5551
|
+
});
|
|
5552
|
+
return {
|
|
5553
|
+
hubs: normalised(latestOfKind(hubSeeded, authoritySeeded), r.dest, "sum"),
|
|
5554
|
+
authorities: normalised(latestOfKind(authoritySeeded, hubSeeded), null, "sum"),
|
|
5555
|
+
iterations: Math.max(hubSeeded.iterations, authoritySeeded.iterations),
|
|
5556
|
+
converged: hubSeeded.converged && authoritySeeded.converged,
|
|
5557
|
+
precision: "f32"
|
|
5558
|
+
};
|
|
5559
|
+
}
|
|
5560
|
+
async function prepareCompact(scope) {
|
|
5561
|
+
const scatter = await scope.pipelines.kernel(kernelSpec("compact-scatter"));
|
|
5562
|
+
const claim = await scope.pipelines.kernel(kernelSpec("dedupe-claim"));
|
|
5563
|
+
const filter = await scope.pipelines.kernel(kernelSpec("dedupe-filter"));
|
|
5564
|
+
const scan = await prepareScan(scope);
|
|
5565
|
+
return new CompactPlannerImpl(scope, scatter, claim, filter, scan);
|
|
5566
|
+
}
|
|
5567
|
+
function checkCount(what, count) {
|
|
5568
|
+
if (!Number.isSafeInteger(count) || count < 0 || count > U32_MAX$2) {
|
|
5569
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${what}: count must be a non-negative integer below 2^32`, {
|
|
5570
|
+
argument: "count",
|
|
5571
|
+
value: count
|
|
5572
|
+
});
|
|
5573
|
+
}
|
|
5574
|
+
}
|
|
5575
|
+
function checkWords$1(what, name, binding, words) {
|
|
5576
|
+
if (binding.size < 4 * words) {
|
|
5577
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${what}: ${name} is smaller than 4 x ${words} bytes`, {
|
|
5578
|
+
argument: name,
|
|
5579
|
+
value: binding.size,
|
|
5580
|
+
expected: 4 * words
|
|
5581
|
+
});
|
|
5582
|
+
}
|
|
5583
|
+
}
|
|
5584
|
+
function checkWordIndex(what, name, index, block) {
|
|
5585
|
+
if (!Number.isSafeInteger(index) || index < 0 || 4 * (index + 1) > block.size) {
|
|
5586
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${what}: ${name} ${index} is outside the block`, {
|
|
5587
|
+
argument: name,
|
|
5588
|
+
value: index,
|
|
5589
|
+
expected: `0 <= ${name} < ${Math.floor(block.size / 4)}`
|
|
5590
|
+
});
|
|
5591
|
+
}
|
|
5592
|
+
}
|
|
5593
|
+
function checkCompactArguments(r) {
|
|
5594
|
+
checkCount("compact", r.count);
|
|
5595
|
+
checkWords$1("compact", "queue", r.queue, r.count);
|
|
5596
|
+
checkWords$1("compact", "flags", r.flags, r.count);
|
|
5597
|
+
checkWords$1("compact", "out", r.out, r.count);
|
|
5598
|
+
checkWordIndex("compact", "outIndex", r.outIndex, r.outCount);
|
|
5599
|
+
}
|
|
5600
|
+
function checkDedupeArguments(r) {
|
|
5601
|
+
checkCount("dedupe", r.count);
|
|
5602
|
+
checkWords$1("dedupe", "queue", r.queue, r.count);
|
|
5603
|
+
checkWords$1("dedupe", "out", r.out, Math.min(r.count, Math.floor(r.owner.size / 4)));
|
|
5604
|
+
checkWordIndex("dedupe", "outIndex", r.outIndex, r.outCount);
|
|
5605
|
+
if (r.countIndex === U32_MAX$2) {
|
|
5606
|
+
return;
|
|
5607
|
+
}
|
|
5608
|
+
checkWordIndex("dedupe", "countIndex", r.countIndex, r.counters);
|
|
5609
|
+
if (r.countIndex === r.outIndex) {
|
|
5610
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", "dedupe: countIndex must not be the output word outIndex", {
|
|
5611
|
+
argument: "countIndex",
|
|
5612
|
+
value: r.countIndex
|
|
5613
|
+
});
|
|
5614
|
+
}
|
|
5615
|
+
if (r.counters.buffer !== r.outCount.buffer || r.counters.offset !== r.outCount.offset || r.counters.size !== r.outCount.size) {
|
|
5616
|
+
throw new WebGpuGraphError(
|
|
5617
|
+
"E_INVALID_ARGUMENT",
|
|
5618
|
+
"dedupe: counters must be the same range as outCount when countIndex names a device word",
|
|
5619
|
+
{
|
|
5620
|
+
argument: "counters",
|
|
5621
|
+
value: r.counters.size,
|
|
5622
|
+
expected: r.outCount.size
|
|
5623
|
+
}
|
|
5624
|
+
);
|
|
5625
|
+
}
|
|
5626
|
+
}
|
|
5627
|
+
class CompactPlannerImpl {
|
|
5628
|
+
/**
|
|
5629
|
+
* Wraps the resolved kernels; use prepareCompact().
|
|
5630
|
+
* @param scope - the caller's scope
|
|
5631
|
+
* @param scatter - the `compact-scatter` kernel
|
|
5632
|
+
* @param claim - the `dedupe-claim` kernel
|
|
5633
|
+
* @param filter - the `dedupe-filter` kernel
|
|
5634
|
+
* @param scan - the scan planner of the same scope
|
|
5635
|
+
*/
|
|
5636
|
+
constructor(scope, scatter, claim, filter, scan) {
|
|
5637
|
+
this.dispatches = 0;
|
|
5638
|
+
this.scope = scope;
|
|
5639
|
+
this.scatter = scatter;
|
|
5640
|
+
this.claim = claim;
|
|
5641
|
+
this.filter = filter;
|
|
5642
|
+
this.scan = scan;
|
|
5643
|
+
}
|
|
5644
|
+
/**
|
|
5645
|
+
* Dispatches the last record*() issued.
|
|
5646
|
+
* @returns the count
|
|
5647
|
+
*/
|
|
5648
|
+
get lastDispatches() {
|
|
5649
|
+
return this.dispatches;
|
|
5650
|
+
}
|
|
5651
|
+
/**
|
|
5652
|
+
* Records the scan and the scatter (see the interface).
|
|
5653
|
+
* @param pass - the compute pass
|
|
5654
|
+
* @param r - the record
|
|
5655
|
+
*/
|
|
5656
|
+
record(pass, r) {
|
|
5657
|
+
checkCompactArguments(r);
|
|
5658
|
+
if (r.count === 0) {
|
|
5659
|
+
this.dispatches = 0;
|
|
5660
|
+
return;
|
|
5661
|
+
}
|
|
5662
|
+
const size = 4 * r.count;
|
|
5663
|
+
const offsets = { buffer: this.scope.scratch(size, "compact/offsets"), offset: 0, size, window: null };
|
|
5664
|
+
this.scan.record(pass, r.flags, r.count, offsets);
|
|
5665
|
+
const params = this.scope.params(COMPACT_PARAMS, {
|
|
5666
|
+
count: r.count,
|
|
5667
|
+
outIndex: r.outIndex,
|
|
5668
|
+
countIndex: U32_MAX$2,
|
|
5669
|
+
stride: 0
|
|
5670
|
+
});
|
|
5671
|
+
const bound = this.scatter.bind({
|
|
5672
|
+
queue: r.queue,
|
|
5673
|
+
flags: r.flags,
|
|
5674
|
+
offsets,
|
|
5675
|
+
out: r.out,
|
|
5676
|
+
outCount: r.outCount,
|
|
5677
|
+
P: params.binding
|
|
5678
|
+
});
|
|
5679
|
+
this.scatter.dispatch(pass, bound, plan1d(r.count, this.scope.workgroupSize, this.scope.caps), [params.offset]);
|
|
5680
|
+
this.dispatches = this.scan.lastDispatches + 1;
|
|
5681
|
+
}
|
|
5682
|
+
/**
|
|
5683
|
+
* Records the claim and the filter over plan1d(count) (see the interface).
|
|
5684
|
+
* @param pass - the compute pass
|
|
5685
|
+
* @param r - the record
|
|
5686
|
+
*/
|
|
5687
|
+
recordDedupe(pass, r) {
|
|
5688
|
+
checkDedupeArguments(r);
|
|
5689
|
+
if (r.count === 0) {
|
|
5690
|
+
this.dispatches = 0;
|
|
5691
|
+
return;
|
|
5692
|
+
}
|
|
5693
|
+
const plan = planGridStride(r.count, this.scope.workgroupSize, this.scope.caps);
|
|
5694
|
+
const bound = this.bindDedupe(r, plan.stride ?? this.scope.workgroupSize);
|
|
5695
|
+
this.claim.dispatch(pass, bound.claim, plan, [bound.offset]);
|
|
5696
|
+
this.filter.dispatch(pass, bound.filter, plan, [bound.offset]);
|
|
5697
|
+
this.dispatches = 2;
|
|
5698
|
+
}
|
|
5699
|
+
/**
|
|
5700
|
+
* One params record (both kernels read the same values) and the two bind groups.
|
|
5701
|
+
* @param r - the record
|
|
5702
|
+
* @param stride - the grid-stride plan's stride (entries per pass over the grid)
|
|
5703
|
+
* @returns the bound kernels and the record's dynamic offset
|
|
5704
|
+
*/
|
|
5705
|
+
bindDedupe(r, stride) {
|
|
5706
|
+
const params = this.scope.params(COMPACT_PARAMS, {
|
|
5707
|
+
count: r.count,
|
|
5708
|
+
outIndex: r.outIndex,
|
|
5709
|
+
countIndex: r.countIndex,
|
|
5710
|
+
stride
|
|
5711
|
+
});
|
|
5712
|
+
return {
|
|
5713
|
+
claim: this.claim.bind({ queue: r.queue, owner: r.owner, counters: r.counters, P: params.binding }),
|
|
5714
|
+
filter: this.filter.bind({
|
|
5715
|
+
queue: r.queue,
|
|
5716
|
+
owner: r.owner,
|
|
5717
|
+
out: r.out,
|
|
5718
|
+
outCount: r.outCount,
|
|
5719
|
+
P: params.binding
|
|
5720
|
+
}),
|
|
5721
|
+
offset: params.offset
|
|
5722
|
+
};
|
|
5723
|
+
}
|
|
5724
|
+
}
|
|
5725
|
+
const W = Object.freeze({
|
|
5726
|
+
frontierCount: 0,
|
|
5727
|
+
nextFrontierCount: 1,
|
|
5728
|
+
frontierDegreeSum: 2,
|
|
5729
|
+
prevFrontierCount: 3,
|
|
5730
|
+
prevDegreeSum: 4,
|
|
5731
|
+
unvisitedCount: 5,
|
|
5732
|
+
unvisitedDegreeSum: 6,
|
|
5733
|
+
unvisitedListLen: 7,
|
|
5734
|
+
edgeCount: 8,
|
|
5735
|
+
edgeCountUnclamped: 9,
|
|
5736
|
+
overflowLevels: 10,
|
|
5737
|
+
level: 11,
|
|
5738
|
+
visitedCount: 12,
|
|
5739
|
+
switches: 13,
|
|
5740
|
+
direction: 14,
|
|
5741
|
+
done: 15,
|
|
5742
|
+
arcsScanned: 16,
|
|
5743
|
+
fusedLevels: 17,
|
|
5744
|
+
twoPhaseLevels: 18,
|
|
5745
|
+
bottomUpLevels: 19,
|
|
5746
|
+
farCount: 20,
|
|
5747
|
+
nextFarCount: 21,
|
|
5748
|
+
thresholdBits: 22,
|
|
5749
|
+
deltaBits: 23,
|
|
5750
|
+
path: 24
|
|
5751
|
+
});
|
|
5752
|
+
function definedWords(words) {
|
|
5753
|
+
const out = {};
|
|
5754
|
+
for (const [name, value] of Object.entries(words)) {
|
|
5755
|
+
if (value !== void 0) {
|
|
5756
|
+
out[name] = value;
|
|
5757
|
+
}
|
|
5758
|
+
}
|
|
5759
|
+
return out;
|
|
5760
|
+
}
|
|
5761
|
+
const MAX_ROLE = 3;
|
|
5762
|
+
function assertCount(argument, value) {
|
|
5763
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
5764
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `frontier: ${argument} must be a non-negative integer`, {
|
|
5765
|
+
argument,
|
|
5766
|
+
value,
|
|
5767
|
+
expected: "a non-negative integer"
|
|
5768
|
+
});
|
|
5769
|
+
}
|
|
5770
|
+
}
|
|
5771
|
+
class Frontier {
|
|
5772
|
+
/**
|
|
5773
|
+
* Wraps the leased buffers; use prepareFrontier().
|
|
5774
|
+
* @param vertices - the two vertex queues
|
|
5775
|
+
* @param counters - the counters block
|
|
5776
|
+
* @param edgeQueue - the edge queue
|
|
5777
|
+
* @param edgeCapacity - the edge queue's entry count
|
|
5778
|
+
* @param n - the vertex count
|
|
5779
|
+
*/
|
|
5780
|
+
constructor(vertices, counters, edgeQueue, edgeCapacity, n) {
|
|
5781
|
+
this.sideIndex = 0;
|
|
5782
|
+
this.vertices = vertices;
|
|
5783
|
+
this.counters = counters;
|
|
5784
|
+
this.edgeQueue = edgeQueue;
|
|
5785
|
+
this.edgeCapacity = edgeCapacity;
|
|
5786
|
+
this.n = n;
|
|
5787
|
+
}
|
|
5788
|
+
/**
|
|
5789
|
+
* Which vertex queue is the input of the current level (a host-side index between two cached bind-group sets).
|
|
5790
|
+
* @returns 0 or 1
|
|
5791
|
+
*/
|
|
5792
|
+
get side() {
|
|
5793
|
+
return this.sideIndex;
|
|
5794
|
+
}
|
|
5795
|
+
/**
|
|
5796
|
+
* The vertex queue the current level expands from.
|
|
5797
|
+
* @returns `vertices[side]`
|
|
5798
|
+
*/
|
|
5799
|
+
get input() {
|
|
5800
|
+
return this.sideIndex === 0 ? this.vertices[0] : this.vertices[1];
|
|
5801
|
+
}
|
|
5802
|
+
/**
|
|
5803
|
+
* The vertex queue the current level's claims append to.
|
|
5804
|
+
* @returns `vertices[1 - side]`
|
|
5805
|
+
*/
|
|
5806
|
+
get output() {
|
|
5807
|
+
return this.sideIndex === 0 ? this.vertices[1] : this.vertices[0];
|
|
5808
|
+
}
|
|
5809
|
+
/** Flips the two vertex queues; the counts rotate inside the block, so nothing else moves. */
|
|
5810
|
+
swap() {
|
|
5811
|
+
this.sideIndex = this.sideIndex === 0 ? 1 : 0;
|
|
5812
|
+
}
|
|
5813
|
+
/**
|
|
5814
|
+
* Seeds a traversal: one `queue.writeBuffer` of the whole 112-byte block (zero except the caller's words) and one of
|
|
5815
|
+
* `vertices[0][0] = source`, both ordered before the submit that follows; the source is on side 0 afterwards.
|
|
5816
|
+
* `frontierCount` is not a word to seed: the first boundary rotates word 1 into it (the BFS seed is
|
|
5817
|
+
* `{ nextFrontierCount: 1, level: U32_MAX }`). A source outside `[0, n)`, an unknown word or a value that is not a
|
|
5818
|
+
* u32 is E_INVALID_ARGUMENT before anything is written.
|
|
5819
|
+
* @param queue - the device queue
|
|
5820
|
+
* @param source - the source vertex
|
|
5821
|
+
* @param seed - the words to seed
|
|
5822
|
+
*/
|
|
5823
|
+
reset(queue, source, seed) {
|
|
5824
|
+
if (!Number.isInteger(source) || source < 0 || source >= this.n) {
|
|
5825
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `frontier: source ${source} is outside [0, ${this.n})`, {
|
|
5826
|
+
argument: "source",
|
|
5827
|
+
value: source,
|
|
5828
|
+
expected: `an integer in [0, ${this.n})`
|
|
5829
|
+
});
|
|
5830
|
+
}
|
|
5831
|
+
const bytes = new ArrayBuffer(FRONTIER_COUNTERS.byteLength);
|
|
5832
|
+
FRONTIER_COUNTERS.write(new DataView(bytes), definedWords(seed));
|
|
5833
|
+
this.sideIndex = 0;
|
|
5834
|
+
queue.writeBuffer(this.counters.buffer, this.counters.offset, bytes);
|
|
5835
|
+
queue.writeBuffer(this.vertices[0].buffer, this.vertices[0].offset, Uint32Array.of(source));
|
|
5836
|
+
}
|
|
5837
|
+
}
|
|
5838
|
+
async function prepareFrontier(scope, n, arcCount, edgeCapacity) {
|
|
5839
|
+
assertCount("n", n);
|
|
5840
|
+
assertCount("arcCount", arcCount);
|
|
5841
|
+
const capacity = edgeCapacity ?? Math.max(1, Math.min(arcCount, Math.floor(scope.caps.limits.maxStorageBufferBindingSize / 4)));
|
|
5842
|
+
if (!Number.isSafeInteger(capacity) || capacity < 1 || capacity > U32_MAX$2) {
|
|
5843
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", "frontier: edgeCapacity must be an integer >= 1", {
|
|
5844
|
+
argument: "edgeCapacity",
|
|
5845
|
+
value: capacity,
|
|
5846
|
+
expected: "an integer in [1, 2^32)"
|
|
5847
|
+
});
|
|
5848
|
+
}
|
|
5849
|
+
const kernel = await scope.pipelines.kernel(kernelSpec("frontier-finalize"));
|
|
5850
|
+
const queueBytes = 4 * Math.max(1, n);
|
|
5851
|
+
const vertices = [
|
|
5852
|
+
{ buffer: scope.scratch(queueBytes, "frontier/vertices-0"), offset: 0, size: queueBytes, window: null },
|
|
5853
|
+
{ buffer: scope.scratch(queueBytes, "frontier/vertices-1"), offset: 0, size: queueBytes, window: null }
|
|
5854
|
+
];
|
|
5855
|
+
const counters = {
|
|
5856
|
+
buffer: scope.scratch(FRONTIER_COUNTERS.byteLength, "frontier/counters"),
|
|
5857
|
+
offset: 0,
|
|
5858
|
+
size: FRONTIER_COUNTERS.byteLength,
|
|
5859
|
+
window: null
|
|
5860
|
+
};
|
|
5861
|
+
const edgeQueue = {
|
|
5862
|
+
buffer: scope.scratch(4 * capacity, "frontier/edge-queue"),
|
|
5863
|
+
offset: 0,
|
|
5864
|
+
size: 4 * capacity,
|
|
5865
|
+
window: null
|
|
5866
|
+
};
|
|
5867
|
+
const frontier = new Frontier(vertices, counters, edgeQueue, capacity, n);
|
|
5868
|
+
return new FrontierPlannerImpl(scope, kernel, frontier);
|
|
5869
|
+
}
|
|
5870
|
+
class FrontierPlannerImpl {
|
|
5871
|
+
/**
|
|
5872
|
+
* Wraps the resolved kernel; use prepareFrontier().
|
|
5873
|
+
* @param scope - the caller's scope
|
|
5874
|
+
* @param kernel - the `frontier-finalize` kernel
|
|
5875
|
+
* @param frontier - the leased queue
|
|
5876
|
+
*/
|
|
5877
|
+
constructor(scope, kernel, frontier) {
|
|
5878
|
+
this.scope = scope;
|
|
5879
|
+
this.kernel = kernel;
|
|
5880
|
+
this.frontier = frontier;
|
|
5881
|
+
}
|
|
5882
|
+
/**
|
|
5883
|
+
* Records one selector dispatch (see the interface).
|
|
5884
|
+
* @param pass - the compute pass
|
|
5885
|
+
* @param role - the role
|
|
5886
|
+
* @param level - the level inside the submit
|
|
5887
|
+
* @param fields - the caller's fields
|
|
5888
|
+
*/
|
|
5889
|
+
recordFinalize(pass, role, level, fields) {
|
|
5890
|
+
if (!Number.isInteger(level) || level < 0 || level >= MAX_LEVELS_PER_SUBMIT) {
|
|
5891
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `frontier: level ${level} is outside the submit`, {
|
|
5892
|
+
argument: "level",
|
|
5893
|
+
value: level,
|
|
5894
|
+
expected: `an integer in [0, ${MAX_LEVELS_PER_SUBMIT})`
|
|
5895
|
+
});
|
|
5896
|
+
}
|
|
5897
|
+
if (!Number.isInteger(role) || role < 0 || role > MAX_ROLE) {
|
|
5898
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `frontier: role ${role} is not a finalize role`, {
|
|
5899
|
+
argument: "role",
|
|
5900
|
+
value: role,
|
|
5901
|
+
expected: `an integer in [0, ${MAX_ROLE}]`
|
|
5902
|
+
});
|
|
5903
|
+
}
|
|
5904
|
+
const { scope, frontier } = this;
|
|
5905
|
+
const params = scope.params(FRONTIER_PARAMS, {
|
|
5906
|
+
...definedWords(fields),
|
|
5907
|
+
role,
|
|
5908
|
+
wg: scope.workgroupSize,
|
|
5909
|
+
edgeCapacity: frontier.edgeCapacity,
|
|
5910
|
+
n: frontier.n
|
|
5911
|
+
});
|
|
5912
|
+
const bound = this.kernel.bind({ counters: frontier.counters, P: params.binding });
|
|
5913
|
+
this.kernel.dispatch(pass, bound, plan1d(1, scope.workgroupSize, scope.caps), [params.offset]);
|
|
5914
|
+
}
|
|
5915
|
+
}
|
|
5916
|
+
async function prepareAdvance(scope, core) {
|
|
5917
|
+
const kernel = await scope.pipelines.kernel(kernelSpec("advance-expand", graphOverrides(core, null)));
|
|
5918
|
+
return new AdvancePlannerImpl(scope, core, kernel);
|
|
5919
|
+
}
|
|
5920
|
+
class AdvancePlannerImpl {
|
|
5921
|
+
/**
|
|
5922
|
+
* Wraps the resolved kernel; use prepareAdvance().
|
|
5923
|
+
* @param scope - the caller's scope
|
|
5924
|
+
* @param core - the core the kernel was compiled for
|
|
5925
|
+
* @param kernel - the `advance-expand` kernel
|
|
5926
|
+
*/
|
|
5927
|
+
constructor(scope, core, kernel) {
|
|
5928
|
+
this.scope = scope;
|
|
5929
|
+
this.kernel = kernel;
|
|
5930
|
+
this.windows = coreWindows(core);
|
|
5931
|
+
this.n = rowCountOf(core, "advance");
|
|
5932
|
+
}
|
|
5933
|
+
/**
|
|
5934
|
+
* Records one direct expansion per window (see the interface).
|
|
5935
|
+
* @param pass - the compute pass
|
|
5936
|
+
* @param frontier - the queue to expand
|
|
5937
|
+
*/
|
|
5938
|
+
record(pass, frontier) {
|
|
5939
|
+
if (frontier.n !== this.n) {
|
|
5940
|
+
throw new WebGpuGraphError(
|
|
5941
|
+
"E_INVALID_ARGUMENT",
|
|
5942
|
+
`advance: the frontier holds ${frontier.n} vertices, the core ${this.n} rows`,
|
|
5943
|
+
{
|
|
5944
|
+
argument: "frontier",
|
|
5945
|
+
value: frontier.n,
|
|
5946
|
+
expected: this.n
|
|
5947
|
+
}
|
|
5948
|
+
);
|
|
5949
|
+
}
|
|
5950
|
+
const { scope } = this;
|
|
5951
|
+
const plan = planGridStride(frontier.n, scope.workgroupSize, scope.caps);
|
|
5952
|
+
for (const w of this.windows) {
|
|
5953
|
+
const params = scope.params(FRONTIER_PARAMS, {
|
|
5954
|
+
wg: scope.workgroupSize,
|
|
5955
|
+
edgeCapacity: frontier.edgeCapacity,
|
|
5956
|
+
n: frontier.n,
|
|
5957
|
+
arcBase: w.arcBase,
|
|
5958
|
+
arcEnd: w.arcEnd,
|
|
5959
|
+
stride: plan.stride ?? scope.workgroupSize
|
|
5960
|
+
});
|
|
5961
|
+
const bound = this.kernel.bind({
|
|
5962
|
+
...graphBindings(w.core, null),
|
|
5963
|
+
frontierIn: frontier.input,
|
|
5964
|
+
counters: frontier.counters,
|
|
5965
|
+
edgeQueue: frontier.edgeQueue,
|
|
5966
|
+
P: params.binding
|
|
5967
|
+
});
|
|
5968
|
+
this.kernel.dispatch(pass, bound, plan, [params.offset]);
|
|
5969
|
+
}
|
|
5970
|
+
}
|
|
5971
|
+
}
|
|
5972
|
+
const RADIX_DIGIT_BITS = 8;
|
|
5973
|
+
function radixHistBytes(count, wg) {
|
|
5974
|
+
return 4 * RADIX_BINS * Math.ceil(count / wg);
|
|
5975
|
+
}
|
|
5976
|
+
async function prepareRadixSort(scope) {
|
|
5977
|
+
const hist = await scope.pipelines.kernel(kernelSpec("radix-hist"));
|
|
5978
|
+
const scatter = await scope.pipelines.kernel(kernelSpec("radix-scatter"));
|
|
5979
|
+
const scan = await prepareScan(scope);
|
|
5980
|
+
return new RadixSortPlannerImpl(scope, hist, scatter, scan);
|
|
5981
|
+
}
|
|
5982
|
+
function checkBinding(argument, binding, bytes) {
|
|
5983
|
+
if (binding.size < bytes) {
|
|
5984
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `radixSort: ${argument} is smaller than ${bytes} bytes`, {
|
|
5985
|
+
argument,
|
|
5986
|
+
value: binding.size,
|
|
5987
|
+
expected: bytes
|
|
5988
|
+
});
|
|
5989
|
+
}
|
|
5990
|
+
}
|
|
5991
|
+
function checkRecordArguments(keys, vals, count, bits, scratch, wg) {
|
|
5992
|
+
if (bits !== 8 && bits !== 16 && bits !== 24 && bits !== 32) {
|
|
5993
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", "radixSort: bits must be 8, 16, 24 or 32", {
|
|
5994
|
+
argument: "bits",
|
|
5995
|
+
value: bits,
|
|
5996
|
+
expected: [8, 16, 24, 32]
|
|
5997
|
+
});
|
|
5998
|
+
}
|
|
5999
|
+
if (!Number.isSafeInteger(count) || count < 0 || count > U32_MAX$2) {
|
|
6000
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", "radixSort: count must be a non-negative integer below 2^32", {
|
|
6001
|
+
argument: "count",
|
|
6002
|
+
value: count
|
|
6003
|
+
});
|
|
6004
|
+
}
|
|
6005
|
+
const pairBytes = 4 * count;
|
|
6006
|
+
checkBinding("keys", keys, pairBytes);
|
|
6007
|
+
checkBinding("vals", vals, pairBytes);
|
|
6008
|
+
checkBinding("scratch.keys", scratch.keys, pairBytes);
|
|
6009
|
+
checkBinding("scratch.vals", scratch.vals, pairBytes);
|
|
6010
|
+
const tableBytes = radixHistBytes(count, wg);
|
|
6011
|
+
checkBinding("scratch.hist", scratch.hist, tableBytes);
|
|
6012
|
+
checkBinding("scratch.offsets", scratch.offsets, tableBytes);
|
|
6013
|
+
}
|
|
6014
|
+
class RadixSortPlannerImpl {
|
|
6015
|
+
/**
|
|
6016
|
+
* Wraps the resolved kernels; use prepareRadixSort().
|
|
6017
|
+
* @param scope - the caller's scope
|
|
6018
|
+
* @param hist - the `radix-hist` kernel
|
|
6019
|
+
* @param scatter - the `radix-scatter` kernel
|
|
6020
|
+
* @param scan - the scan planner of the same scope
|
|
6021
|
+
*/
|
|
6022
|
+
constructor(scope, hist, scatter, scan) {
|
|
6023
|
+
this.dispatches = 0;
|
|
6024
|
+
this.scope = scope;
|
|
6025
|
+
this.hist = hist;
|
|
6026
|
+
this.scatter = scatter;
|
|
6027
|
+
this.scan = scan;
|
|
6028
|
+
}
|
|
6029
|
+
/**
|
|
6030
|
+
* Dispatches the last record() issued.
|
|
6031
|
+
* @returns the count
|
|
6032
|
+
*/
|
|
6033
|
+
get lastDispatches() {
|
|
6034
|
+
return this.dispatches;
|
|
6035
|
+
}
|
|
6036
|
+
/**
|
|
6037
|
+
* Records the passes into the pass (see the interface).
|
|
6038
|
+
* @param pass - the compute pass
|
|
6039
|
+
* @param keys - the keys
|
|
6040
|
+
* @param vals - the values
|
|
6041
|
+
* @param count - the pair count
|
|
6042
|
+
* @param bits - the key width
|
|
6043
|
+
* @param scratch - the second pair, the histogram table and its scanned twin
|
|
6044
|
+
* @returns the pair the result lives in
|
|
6045
|
+
*/
|
|
6046
|
+
record(pass, keys, vals, count, bits, scratch) {
|
|
6047
|
+
const wg = this.scope.workgroupSize;
|
|
6048
|
+
checkRecordArguments(keys, vals, count, bits, scratch, wg);
|
|
6049
|
+
if (count === 0) {
|
|
6050
|
+
this.dispatches = 0;
|
|
6051
|
+
return { keys, vals };
|
|
6052
|
+
}
|
|
6053
|
+
const groups = Math.ceil(count / wg);
|
|
6054
|
+
const plan = plan1d(count, wg, this.scope.caps);
|
|
6055
|
+
const tableWords = RADIX_BINS * groups;
|
|
6056
|
+
const tableBytes = 4 * tableWords;
|
|
6057
|
+
const histTable = { ...scratch.hist, size: tableBytes };
|
|
6058
|
+
const offsets = { ...scratch.offsets, size: tableBytes };
|
|
6059
|
+
let src = { keys, vals };
|
|
6060
|
+
let dst = { keys: scratch.keys, vals: scratch.vals };
|
|
6061
|
+
let dispatches = 0;
|
|
6062
|
+
const passes = bits / RADIX_DIGIT_BITS;
|
|
6063
|
+
for (let p = 0; p < passes; p++) {
|
|
6064
|
+
const params = this.scope.params(RADIX_PARAMS, { count, shift: RADIX_DIGIT_BITS * p, groups, pad0: 0 });
|
|
6065
|
+
const histBound = this.hist.bind({ keys: src.keys, hist: histTable, P: params.binding });
|
|
6066
|
+
this.hist.dispatch(pass, histBound, plan, [params.offset]);
|
|
6067
|
+
this.scan.record(pass, histTable, tableWords, offsets);
|
|
6068
|
+
const scatterBound = this.scatter.bind({
|
|
6069
|
+
keys: src.keys,
|
|
6070
|
+
vals: src.vals,
|
|
6071
|
+
offsets,
|
|
6072
|
+
keysOut: dst.keys,
|
|
6073
|
+
valsOut: dst.vals,
|
|
6074
|
+
P: params.binding
|
|
6075
|
+
});
|
|
6076
|
+
this.scatter.dispatch(pass, scatterBound, plan, [params.offset]);
|
|
6077
|
+
dispatches += 2 + this.scan.lastDispatches;
|
|
6078
|
+
[src, dst] = [dst, src];
|
|
6079
|
+
}
|
|
6080
|
+
this.dispatches = dispatches;
|
|
6081
|
+
return src;
|
|
6082
|
+
}
|
|
6083
|
+
}
|
|
6084
|
+
const ALGORITHM$3 = "breadthFirstSearch";
|
|
6085
|
+
function bfsRingSlots(windows, levelsPerSubmit) {
|
|
6086
|
+
return Math.max((5 + 4 * windows) * levelsPerSubmit + 16, RESULT_BATCH_SLOTS + windows);
|
|
6087
|
+
}
|
|
6088
|
+
const RESULT_BATCH_SLOTS = 2 + 4 * 8;
|
|
6089
|
+
function normaliseMaxDepth(maxDepth) {
|
|
6090
|
+
if (maxDepth === void 0 || Number.isNaN(maxDepth) || maxDepth >= U32_MAX$2) {
|
|
6091
|
+
return U32_MAX$2;
|
|
6092
|
+
}
|
|
6093
|
+
return Math.max(0, Math.ceil(maxDepth));
|
|
6094
|
+
}
|
|
6095
|
+
function checkDest$1(dest, n) {
|
|
6096
|
+
if (dest === void 0) {
|
|
6097
|
+
return null;
|
|
6098
|
+
}
|
|
6099
|
+
if (dest instanceof Uint32Array && dest.length === n && dest.buffer instanceof ArrayBuffer) {
|
|
6100
|
+
return dest;
|
|
6101
|
+
}
|
|
6102
|
+
throw new WebGpuGraphError(
|
|
6103
|
+
"E_INVALID_ARGUMENT",
|
|
6104
|
+
`${ALGORITHM$3}: dest must be a Uint32Array of length ${n} over an ArrayBuffer`,
|
|
6105
|
+
{
|
|
6106
|
+
argument: "dest",
|
|
6107
|
+
value: `${dest.constructor.name}(${dest.length})`,
|
|
6108
|
+
expected: `Uint32Array(${n}) over an ArrayBuffer`
|
|
6109
|
+
}
|
|
6110
|
+
);
|
|
6111
|
+
}
|
|
6112
|
+
function bindingOf$1(buffer, size) {
|
|
6113
|
+
return { buffer, offset: 0, size, window: null };
|
|
6114
|
+
}
|
|
6115
|
+
function degreeView(ctx, s, name) {
|
|
6116
|
+
const { bindings } = ctx.residency.view(s, name);
|
|
6117
|
+
const { [name]: binding } = bindings;
|
|
6118
|
+
if (binding === void 0) {
|
|
6119
|
+
throw new WebGpuGraphError("E_VALIDATION", `${ALGORITHM$3}: the ${name} view has no ${name} binding`, {
|
|
6120
|
+
label: `${ALGORITHM$3}/${name}`,
|
|
6121
|
+
message: `the ${name} view has no ${name} binding`
|
|
6122
|
+
});
|
|
6123
|
+
}
|
|
6124
|
+
return binding;
|
|
6125
|
+
}
|
|
6126
|
+
function aborted$1(batchId) {
|
|
6127
|
+
return new WebGpuGraphError(
|
|
6128
|
+
"E_ABORTED",
|
|
6129
|
+
`${ALGORITHM$3}: the signal was aborted`,
|
|
6130
|
+
batchId === void 0 ? {} : { batchId }
|
|
6131
|
+
);
|
|
6132
|
+
}
|
|
6133
|
+
function wordOf$1(block, name) {
|
|
6134
|
+
const value = block[name];
|
|
6135
|
+
if (typeof value !== "number") {
|
|
6136
|
+
throw new WebGpuGraphError("E_VALIDATION", `${ALGORITHM$3}: counters.${name} did not decode to a number`, {
|
|
6137
|
+
label: `${ALGORITHM$3}/counters`,
|
|
6138
|
+
message: `the field ${name} did not decode to a number`
|
|
6139
|
+
});
|
|
6140
|
+
}
|
|
6141
|
+
return value;
|
|
6142
|
+
}
|
|
6143
|
+
async function bfsWithTuning(ctx, s, source, options, tuning) {
|
|
6144
|
+
ctx.assertReady();
|
|
6145
|
+
await assertDeviceComputes(ctx);
|
|
6146
|
+
const n = s.nodeCount;
|
|
6147
|
+
if (!Number.isInteger(source) || source < 0 || source >= n) {
|
|
6148
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${ALGORITHM$3}: source ${source} is outside [0, ${n})`, {
|
|
6149
|
+
argument: "source",
|
|
6150
|
+
value: source,
|
|
6151
|
+
expected: `an integer in [0, ${n})`
|
|
6152
|
+
});
|
|
6153
|
+
}
|
|
6154
|
+
const levelsPerSubmit = tuning.levelsPerSubmit ?? MAX_LEVELS_PER_SUBMIT;
|
|
6155
|
+
if (!Number.isInteger(levelsPerSubmit) || levelsPerSubmit < 1 || levelsPerSubmit > MAX_LEVELS_PER_SUBMIT) {
|
|
6156
|
+
throw new WebGpuGraphError(
|
|
6157
|
+
"E_INVALID_ARGUMENT",
|
|
6158
|
+
`${ALGORITHM$3}: levelsPerSubmit must be an integer in [1, ${MAX_LEVELS_PER_SUBMIT}]`,
|
|
6159
|
+
{
|
|
6160
|
+
argument: "levelsPerSubmit",
|
|
6161
|
+
value: levelsPerSubmit,
|
|
6162
|
+
expected: `an integer in [1, ${MAX_LEVELS_PER_SUBMIT}]`
|
|
6163
|
+
}
|
|
6164
|
+
);
|
|
6165
|
+
}
|
|
6166
|
+
const dest = checkDest$1(options?.dest, n);
|
|
6167
|
+
const maxDepth = normaliseMaxDepth(options?.maxDepth);
|
|
6168
|
+
const predKind = tuning.predKind ?? 1;
|
|
6169
|
+
if (options?.signal?.aborted) {
|
|
6170
|
+
throw aborted$1();
|
|
6171
|
+
}
|
|
6172
|
+
const core = ctx.residency.core(s);
|
|
6173
|
+
const forward = coreWindows(core);
|
|
6174
|
+
if (s.directed && 4 * s.arcCount > ctx.caps.limits.maxStorageBufferBindingSize) {
|
|
6175
|
+
throw new WebGpuGraphError(
|
|
6176
|
+
"E_TOO_LARGE",
|
|
6177
|
+
`${ALGORITHM$3}: the reverse adjacency of a directed snapshot (${4 * s.arcCount} bytes) needs arc windows, which no view executes (spec 4.3); the bottom-up sweep binds it whole`,
|
|
6178
|
+
{
|
|
6179
|
+
needed: 4 * s.arcCount,
|
|
6180
|
+
limit: ctx.caps.limits.maxStorageBufferBindingSize,
|
|
6181
|
+
path: "windowed",
|
|
6182
|
+
algorithm: ALGORITHM$3
|
|
6183
|
+
}
|
|
6184
|
+
);
|
|
6185
|
+
}
|
|
6186
|
+
const reverse = s.directed ? coreOfView(ctx.residency.view(s, "reverse"), s.arcCount) : core;
|
|
6187
|
+
const backward = coreWindows(reverse);
|
|
6188
|
+
const outDegree = degreeView(ctx, s, "outDegree");
|
|
6189
|
+
const inDegree = degreeView(ctx, s, "inDegree");
|
|
6190
|
+
const scope = algorithmScope(ctx, ALGORITHM$3, bfsRingSlots(forward.length, levelsPerSubmit));
|
|
6191
|
+
tuning.onScope?.(scope);
|
|
6192
|
+
try {
|
|
6193
|
+
const bytes = 4 * n;
|
|
6194
|
+
const wg = ctx.workgroupSize;
|
|
6195
|
+
const depth = bindingOf$1(scope.scratch(bytes, "depth"), bytes);
|
|
6196
|
+
const bitsBase = Math.ceil(n / 64) * 64;
|
|
6197
|
+
const bitsWords = Math.ceil(n / 32);
|
|
6198
|
+
const sweepBytes = 4 * (bitsBase + bitsWords);
|
|
6199
|
+
const sweepIn = bindingOf$1(scope.scratch(sweepBytes, "sweep-in"), sweepBytes);
|
|
6200
|
+
const unvisitedList = { buffer: sweepIn.buffer, offset: 0, size: bytes, window: null };
|
|
6201
|
+
const frontierBits = {
|
|
6202
|
+
buffer: sweepIn.buffer,
|
|
6203
|
+
offset: 4 * bitsBase,
|
|
6204
|
+
size: 4 * bitsWords,
|
|
6205
|
+
window: null
|
|
6206
|
+
};
|
|
6207
|
+
const flags = bindingOf$1(scope.scratch(bytes, "unvisited-flags"), bytes);
|
|
6208
|
+
const iota = bindingOf$1(scope.scratch(bytes, "iota"), bytes);
|
|
6209
|
+
const compactCount = bindingOf$1(scope.scratch(4, "compact-count"), 4);
|
|
6210
|
+
await ctx.allocator.check();
|
|
6211
|
+
const planner = await prepareFrontier(scope, n, s.arcCount, tuning.edgeCapacity);
|
|
6212
|
+
const advance = await prepareAdvance(scope, core);
|
|
6213
|
+
const compact = await prepareCompact(scope);
|
|
6214
|
+
const contract = await ctx.pipelines.kernel(kernelSpec("bfs-contract"));
|
|
6215
|
+
const fused = await ctx.pipelines.kernel(kernelSpec("bfs-fused", graphOverrides(core, null)));
|
|
6216
|
+
const bitset = await ctx.pipelines.kernel(kernelSpec("bfs-bitset-build"));
|
|
6217
|
+
const bottomUp = await ctx.pipelines.kernel(kernelSpec("bfs-bottom-up", graphOverrides(reverse, null)));
|
|
6218
|
+
const unvisited = await ctx.pipelines.kernel(kernelSpec("bfs-unvisited-flags"));
|
|
6219
|
+
const pred = await ctx.pipelines.kernel(kernelSpec("sssp-pred", { ...graphOverrides(core, null), MODE: 1 }));
|
|
6220
|
+
const fill = await ctx.pipelines.kernel(kernelSpec("fill"));
|
|
6221
|
+
const sort = await prepareRadixSort(scope);
|
|
6222
|
+
const { frontier } = planner;
|
|
6223
|
+
const { counters } = frontier;
|
|
6224
|
+
const { queue } = ctx.device;
|
|
6225
|
+
const fillPlan = plan1d(n, wg, ctx.caps);
|
|
6226
|
+
const levelPlan = planGridStride(Math.max(n, frontier.edgeCapacity), wg, ctx.caps);
|
|
6227
|
+
const sweepPlan = planGridStride(n, wg, ctx.caps);
|
|
6228
|
+
const fusedPlan = planGridStride(n * wg, wg, ctx.caps);
|
|
6229
|
+
const bitsPlan = plan1d(bitsWords, wg, ctx.caps);
|
|
6230
|
+
const recordFill = (pass2, dst, value, mode) => {
|
|
6231
|
+
const params = scope.params(FILL_PARAMS, { count: n, value, mode, pad0: 0 });
|
|
6232
|
+
fill.dispatch(pass2, fill.bind({ dst, P: params.binding }), fillPlan, [params.offset]);
|
|
6233
|
+
};
|
|
6234
|
+
const flagsPlan = planGridStride(n, wg, ctx.caps);
|
|
6235
|
+
const recordRebuild = (pass2) => {
|
|
6236
|
+
const params = scope.params(FRONTIER_PARAMS, { wg, n, stride: flagsPlan.stride ?? n });
|
|
6237
|
+
const bound = unvisited.bind({ outDegree, inDegree, depth, flags, counters, P: params.binding });
|
|
6238
|
+
unvisited.dispatch(pass2, bound, flagsPlan, [params.offset]);
|
|
6239
|
+
compact.record(pass2, {
|
|
6240
|
+
queue: iota,
|
|
6241
|
+
flags,
|
|
6242
|
+
count: n,
|
|
6243
|
+
out: unvisitedList,
|
|
6244
|
+
outCount: compactCount,
|
|
6245
|
+
outIndex: 0
|
|
6246
|
+
});
|
|
6247
|
+
};
|
|
6248
|
+
const submit = (batch) => {
|
|
6249
|
+
scope.flush();
|
|
6250
|
+
return batch.submit();
|
|
6251
|
+
};
|
|
6252
|
+
const setup = new CommandBatch(ctx, `${ALGORITHM$3}/setup`);
|
|
6253
|
+
const setupPass = setup.pass("fill");
|
|
6254
|
+
recordFill(setupPass, depth, INVALID_INDEX, 0);
|
|
6255
|
+
recordFill(setupPass, iota, 0, 1);
|
|
6256
|
+
setup.endPass();
|
|
6257
|
+
await submit(setup).readback;
|
|
6258
|
+
ctx.assertReady();
|
|
6259
|
+
queue.writeBuffer(depth.buffer, depth.offset + 4 * source, Uint32Array.of(0));
|
|
6260
|
+
frontier.reset(queue, source, { nextFrontierCount: 1, level: U32_MAX$2 });
|
|
6261
|
+
const fields = {
|
|
6262
|
+
mode: tuning.direction === "top-down" ? 1 : 0,
|
|
6263
|
+
alpha: tuning.alpha ?? Math.max(1, Math.floor(s.arcCount / n)),
|
|
6264
|
+
beta: tuning.beta ?? BEAMER_BETA,
|
|
6265
|
+
fusedMax: tuning.fusedMax ?? FUSED_FRONTIER_MAX,
|
|
6266
|
+
maxDepth
|
|
6267
|
+
};
|
|
6268
|
+
let levelsRecorded = 0;
|
|
6269
|
+
let submits = 0;
|
|
6270
|
+
for (; ; ) {
|
|
6271
|
+
queue.writeBuffer(counters.buffer, counters.offset + 4 * W.unvisitedCount, new Uint32Array(3));
|
|
6272
|
+
const batch = new CommandBatch(ctx, `${ALGORITHM$3}/levels`);
|
|
6273
|
+
const pass2 = batch.pass("bfs");
|
|
6274
|
+
recordRebuild(pass2);
|
|
6275
|
+
const bitsParams = scope.params(FILL_PARAMS, { count: bitsWords, value: 0, mode: 0, pad0: 0 });
|
|
6276
|
+
const boundBitsFill = fill.bind({ dst: frontierBits, P: bitsParams.binding });
|
|
6277
|
+
for (let level2 = 0; level2 < levelsPerSubmit; level2++) {
|
|
6278
|
+
planner.recordFinalize(pass2, 0, level2, { ...fields, firstOfSubmit: Math.min(level2, 2) });
|
|
6279
|
+
advance.record(pass2, frontier);
|
|
6280
|
+
planner.recordFinalize(pass2, 1, level2, fields);
|
|
6281
|
+
const params = scope.params(FRONTIER_PARAMS, {
|
|
6282
|
+
wg,
|
|
6283
|
+
n,
|
|
6284
|
+
edgeCapacity: frontier.edgeCapacity,
|
|
6285
|
+
arcBase: 0,
|
|
6286
|
+
arcEnd: s.arcCount,
|
|
6287
|
+
bitsBase,
|
|
6288
|
+
stride: levelPlan.stride ?? wg
|
|
6289
|
+
});
|
|
6290
|
+
const boundContract = contract.bind({
|
|
6291
|
+
edgeQueue: frontier.edgeQueue,
|
|
6292
|
+
counters,
|
|
6293
|
+
depth,
|
|
6294
|
+
frontierOut: frontier.output,
|
|
6295
|
+
P: params.binding
|
|
6296
|
+
});
|
|
6297
|
+
contract.dispatch(pass2, boundContract, levelPlan, [params.offset]);
|
|
6298
|
+
for (const w of forward) {
|
|
6299
|
+
const fusedParams = scope.params(FRONTIER_PARAMS, {
|
|
6300
|
+
wg,
|
|
6301
|
+
n,
|
|
6302
|
+
edgeCapacity: frontier.edgeCapacity,
|
|
6303
|
+
arcBase: w.arcBase,
|
|
6304
|
+
arcEnd: w.arcEnd,
|
|
6305
|
+
stride: fusedPlan.x * fusedPlan.y
|
|
6306
|
+
});
|
|
6307
|
+
const boundFused = fused.bind({
|
|
6308
|
+
...graphBindings(w.core, null),
|
|
6309
|
+
frontierIn: frontier.input,
|
|
6310
|
+
counters,
|
|
6311
|
+
depth,
|
|
6312
|
+
frontierOut: frontier.output,
|
|
6313
|
+
P: fusedParams.binding
|
|
6314
|
+
});
|
|
6315
|
+
fused.dispatch(pass2, boundFused, fusedPlan, [fusedParams.offset]);
|
|
6316
|
+
}
|
|
6317
|
+
fill.dispatch(pass2, boundBitsFill, bitsPlan, [bitsParams.offset]);
|
|
6318
|
+
const boundBitset = bitset.bind({
|
|
6319
|
+
frontierIn: frontier.input,
|
|
6320
|
+
counters,
|
|
6321
|
+
bits: sweepIn,
|
|
6322
|
+
P: params.binding
|
|
6323
|
+
});
|
|
6324
|
+
bitset.dispatch(pass2, boundBitset, levelPlan, [params.offset]);
|
|
6325
|
+
for (const w of backward) {
|
|
6326
|
+
const sweepParams = scope.params(FRONTIER_PARAMS, {
|
|
6327
|
+
wg,
|
|
6328
|
+
n,
|
|
6329
|
+
arcBase: w.arcBase,
|
|
6330
|
+
arcEnd: w.arcEnd,
|
|
6331
|
+
bitsBase,
|
|
6332
|
+
stride: sweepPlan.stride ?? wg
|
|
6333
|
+
});
|
|
6334
|
+
const boundSweep = bottomUp.bind({
|
|
6335
|
+
...graphBindings(w.core, null),
|
|
6336
|
+
sweepIn,
|
|
6337
|
+
counters,
|
|
6338
|
+
depth,
|
|
6339
|
+
frontierOut: frontier.output,
|
|
6340
|
+
P: sweepParams.binding
|
|
6341
|
+
});
|
|
6342
|
+
bottomUp.dispatch(pass2, boundSweep, sweepPlan, [sweepParams.offset]);
|
|
6343
|
+
}
|
|
6344
|
+
frontier.swap();
|
|
6345
|
+
}
|
|
6346
|
+
batch.endPass();
|
|
6347
|
+
const doneRequest = batch.readback(counters.buffer, counters.offset + 4 * W.done, 4);
|
|
6348
|
+
const inspect = tuning.onLevel === void 0 ? null : {
|
|
6349
|
+
block: batch.readback(counters.buffer, counters.offset, FRONTIER_COUNTERS.byteLength),
|
|
6350
|
+
frontier: batch.readback(frontier.input.buffer, frontier.input.offset, frontier.input.size),
|
|
6351
|
+
count: batch.readback(compactCount.buffer, compactCount.offset, 4)
|
|
6352
|
+
};
|
|
6353
|
+
const submitted = submit(batch);
|
|
6354
|
+
const back2 = await submitted.readback;
|
|
6355
|
+
levelsRecorded += levelsPerSubmit;
|
|
6356
|
+
submits += 1;
|
|
6357
|
+
ctx.assertReady();
|
|
6358
|
+
if (options?.signal?.aborted) {
|
|
6359
|
+
throw aborted$1(submitted.id);
|
|
6360
|
+
}
|
|
6361
|
+
options?.onProgress?.(Math.min(levelsRecorded, n), n);
|
|
6362
|
+
if (inspect !== null && tuning.onLevel !== void 0) {
|
|
6363
|
+
const block2 = FRONTIER_COUNTERS.read(new DataView(back2), inspect.block.offset);
|
|
6364
|
+
const claimed = new Uint32Array(back2, inspect.frontier.offset, wordOf$1(block2, "nextFrontierCount"));
|
|
6365
|
+
const rebuilt = new Uint32Array(back2, inspect.count.offset, 1)[0];
|
|
6366
|
+
tuning.onLevel(levelsRecorded - 1, block2, claimed.slice(), rebuilt);
|
|
6367
|
+
}
|
|
6368
|
+
if (new Uint32Array(back2, doneRequest.offset, 1)[0] !== 0) {
|
|
6369
|
+
break;
|
|
6370
|
+
}
|
|
6371
|
+
if (submits > n + 1) {
|
|
6372
|
+
throw new WebGpuGraphError(
|
|
6373
|
+
"E_VALIDATION",
|
|
6374
|
+
`${ALGORITHM$3}: the done flag never rose in ${submits} submits (a traversal has at most ${n} levels)`,
|
|
6375
|
+
{ label: ALGORITHM$3, message: `the done flag never rose in ${submits} submits` }
|
|
6376
|
+
);
|
|
6377
|
+
}
|
|
6378
|
+
}
|
|
6379
|
+
const keys = bindingOf$1(scope.scratch(bytes, "order/keys"), bytes);
|
|
6380
|
+
const vals = bindingOf$1(scope.scratch(bytes, "order/vals"), bytes);
|
|
6381
|
+
const histBytes = radixHistBytes(n, wg);
|
|
6382
|
+
const scratch = {
|
|
6383
|
+
keys: bindingOf$1(scope.scratch(bytes, "order/keys-scratch"), bytes),
|
|
6384
|
+
vals: bindingOf$1(scope.scratch(bytes, "order/vals-scratch"), bytes),
|
|
6385
|
+
hist: bindingOf$1(scope.scratch(histBytes, "order/hist"), histBytes),
|
|
6386
|
+
offsets: bindingOf$1(scope.scratch(histBytes, "order/offsets"), histBytes)
|
|
6387
|
+
};
|
|
6388
|
+
const parent = bindingOf$1(scope.scratch(bytes, "parent"), bytes);
|
|
6389
|
+
const result = new CommandBatch(ctx, `${ALGORITHM$3}/result`);
|
|
6390
|
+
result.copy(depth, keys, bytes);
|
|
6391
|
+
const pass = result.pass("result");
|
|
6392
|
+
recordFill(pass, vals, 0, 1);
|
|
6393
|
+
const sorted = sort.record(pass, keys, vals, n, 32, scratch);
|
|
6394
|
+
recordFill(pass, parent, INVALID_INDEX, 0);
|
|
6395
|
+
const predPlan = planGridStride(n, wg, ctx.caps);
|
|
6396
|
+
for (const w of forward) {
|
|
6397
|
+
const predParams = scope.params(FRONTIER_PARAMS, {
|
|
6398
|
+
wg,
|
|
6399
|
+
n,
|
|
6400
|
+
arcBase: w.arcBase,
|
|
6401
|
+
arcEnd: w.arcEnd,
|
|
6402
|
+
predKind,
|
|
6403
|
+
source,
|
|
6404
|
+
stride: predPlan.stride ?? n
|
|
6405
|
+
});
|
|
6406
|
+
const predBound = pred.bind({
|
|
6407
|
+
...graphBindings(w.core, null),
|
|
6408
|
+
dist: depth,
|
|
6409
|
+
pred: parent,
|
|
6410
|
+
P: predParams.binding
|
|
6411
|
+
});
|
|
6412
|
+
pred.dispatch(pass, predBound, predPlan, [predParams.offset]);
|
|
6413
|
+
}
|
|
6414
|
+
result.endPass();
|
|
6415
|
+
const depthRequest = result.readback(depth.buffer, depth.offset, bytes);
|
|
6416
|
+
const parentRequest = result.readback(parent.buffer, parent.offset, bytes);
|
|
6417
|
+
const orderRequest = result.readback(sorted.vals.buffer, sorted.vals.offset, bytes);
|
|
6418
|
+
const blockRequest = result.readback(counters.buffer, counters.offset, FRONTIER_COUNTERS.byteLength);
|
|
6419
|
+
const back = await submit(result).readback;
|
|
6420
|
+
ctx.assertReady();
|
|
6421
|
+
const block = FRONTIER_COUNTERS.read(new DataView(back), blockRequest.offset);
|
|
6422
|
+
const visitedCount = wordOf$1(block, "visitedCount");
|
|
6423
|
+
if (visitedCount > n) {
|
|
6424
|
+
throw new WebGpuGraphError(
|
|
6425
|
+
"E_VALIDATION",
|
|
6426
|
+
`${ALGORITHM$3}: visitedCount ${visitedCount} exceeds the ${n} vertices (a duplicate claim)`,
|
|
6427
|
+
{
|
|
6428
|
+
label: `${ALGORITHM$3}/visitedCount`,
|
|
6429
|
+
message: `the device counted ${visitedCount} visits of ${n} vertices`
|
|
6430
|
+
}
|
|
6431
|
+
);
|
|
6432
|
+
}
|
|
6433
|
+
const level = wordOf$1(block, "level");
|
|
6434
|
+
const levels = wordOf$1(block, "frontierCount") === 0 ? level : level + 1;
|
|
6435
|
+
const depthOut = dest ?? new Uint32Array(n);
|
|
6436
|
+
depthOut.set(new Uint32Array(back, depthRequest.offset, n));
|
|
6437
|
+
return {
|
|
6438
|
+
depth: depthOut,
|
|
6439
|
+
parent: new Uint32Array(back, parentRequest.offset, n).slice(),
|
|
6440
|
+
order: new Uint32Array(back, orderRequest.offset, visitedCount).slice(),
|
|
6441
|
+
visitedCount,
|
|
6442
|
+
levels,
|
|
6443
|
+
switches: wordOf$1(block, "switches")
|
|
6444
|
+
};
|
|
6445
|
+
} finally {
|
|
6446
|
+
scope.dispose();
|
|
6447
|
+
}
|
|
6448
|
+
}
|
|
6449
|
+
function breadthFirstSearch(ctx, s, source, options) {
|
|
6450
|
+
return bfsWithTuning(ctx, s, source, options, {});
|
|
6451
|
+
}
|
|
6452
|
+
const ALGORITHM$2 = "sssp";
|
|
6453
|
+
const HALF_ALIGN = 64;
|
|
6454
|
+
const RING_SLOTS$2 = 4 * MAX_LEVELS_PER_SUBMIT + 40;
|
|
6455
|
+
function bitsOf(value) {
|
|
6456
|
+
return new Uint32Array(Float32Array.of(value).buffer)[0];
|
|
6457
|
+
}
|
|
6458
|
+
function bindingOf(buffer, size) {
|
|
6459
|
+
return { buffer, offset: 0, size, window: null };
|
|
6460
|
+
}
|
|
6461
|
+
function aborted(algorithm, batchId) {
|
|
6462
|
+
return new WebGpuGraphError(
|
|
6463
|
+
"E_ABORTED",
|
|
6464
|
+
`${algorithm}: the signal was aborted`,
|
|
6465
|
+
batchId === void 0 ? {} : { batchId }
|
|
6466
|
+
);
|
|
6467
|
+
}
|
|
6468
|
+
function assertSource(algorithm, source, n) {
|
|
6469
|
+
if (!Number.isInteger(source) || source < 0 || source >= n) {
|
|
6470
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${algorithm}: source ${source} is outside [0, ${n})`, {
|
|
6471
|
+
argument: "source",
|
|
6472
|
+
value: source,
|
|
6473
|
+
expected: `an integer in [0, ${n})`
|
|
6474
|
+
});
|
|
6475
|
+
}
|
|
6476
|
+
}
|
|
6477
|
+
function wordOf(block, name) {
|
|
6478
|
+
const value = block[name];
|
|
6479
|
+
if (typeof value !== "number") {
|
|
6480
|
+
throw new WebGpuGraphError("E_VALIDATION", `${ALGORITHM$2}: counters.${name} did not decode to a number`, {
|
|
6481
|
+
label: `${ALGORITHM$2}/counters`,
|
|
6482
|
+
message: `the field ${name} did not decode to a number`
|
|
6483
|
+
});
|
|
6484
|
+
}
|
|
6485
|
+
return value;
|
|
6486
|
+
}
|
|
6487
|
+
function checkDest(algorithm, dest, n) {
|
|
6488
|
+
if (dest === void 0) {
|
|
6489
|
+
return null;
|
|
6490
|
+
}
|
|
6491
|
+
if (dest instanceof Float32Array && dest.length === n && dest.buffer instanceof ArrayBuffer) {
|
|
6492
|
+
return dest;
|
|
6493
|
+
}
|
|
6494
|
+
throw new WebGpuGraphError(
|
|
6495
|
+
"E_INVALID_ARGUMENT",
|
|
6496
|
+
`${algorithm}: dest must be a Float32Array of length ${n} over an ArrayBuffer`,
|
|
6497
|
+
{
|
|
6498
|
+
argument: "dest",
|
|
6499
|
+
value: `${dest.constructor.name}(${dest.length})`,
|
|
6500
|
+
expected: `Float32Array(${n}) over an ArrayBuffer`
|
|
6501
|
+
}
|
|
6502
|
+
);
|
|
6503
|
+
}
|
|
6504
|
+
function normaliseCutoff(algorithm, cutoff) {
|
|
6505
|
+
if (cutoff === void 0) {
|
|
6506
|
+
return Infinity;
|
|
6507
|
+
}
|
|
6508
|
+
if (Number.isNaN(cutoff)) {
|
|
6509
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${algorithm}: cutoff must not be NaN`, {
|
|
6510
|
+
argument: "cutoff",
|
|
6511
|
+
value: cutoff,
|
|
6512
|
+
expected: "a number (the CPU port and the kernel would disagree on NaN)"
|
|
6513
|
+
});
|
|
6514
|
+
}
|
|
6515
|
+
return cutoff;
|
|
6516
|
+
}
|
|
6517
|
+
function resolveWeights$1(algorithm, s, weights) {
|
|
6518
|
+
if (weights === void 0) {
|
|
6519
|
+
if (s.weights === null) {
|
|
6520
|
+
return null;
|
|
6521
|
+
}
|
|
6522
|
+
let sum2 = 0;
|
|
6523
|
+
for (const w of s.weights) {
|
|
6524
|
+
sum2 += w;
|
|
6525
|
+
}
|
|
6526
|
+
return {
|
|
6527
|
+
values: s.weights,
|
|
6528
|
+
override: null,
|
|
6529
|
+
allOne: s.flags.allWeightsOne,
|
|
6530
|
+
nonNegative: s.flags.nonNegativeWeights,
|
|
6531
|
+
finite: s.flags.finiteWeights,
|
|
6532
|
+
sum: sum2
|
|
6533
|
+
};
|
|
6534
|
+
}
|
|
6535
|
+
if (weights.length !== s.arcCount) {
|
|
6536
|
+
throw new WebGpuGraphError(
|
|
6537
|
+
"E_INVALID_ARGUMENT",
|
|
6538
|
+
`${algorithm}: weights has ${weights.length} entries, the snapshot ${s.arcCount} arcs`,
|
|
6539
|
+
{ argument: "weights", value: weights.length, expected: s.arcCount }
|
|
6540
|
+
);
|
|
6541
|
+
}
|
|
6542
|
+
const values = weights instanceof Float32Array ? weights : Float32Array.from(weights);
|
|
6543
|
+
let allOne = true;
|
|
6544
|
+
let nonNegative = true;
|
|
6545
|
+
let finite2 = true;
|
|
6546
|
+
let sum = 0;
|
|
6547
|
+
for (const w of values) {
|
|
6548
|
+
if (w !== 1) {
|
|
6549
|
+
allOne = false;
|
|
6550
|
+
}
|
|
6551
|
+
if (w < 0) {
|
|
6552
|
+
nonNegative = false;
|
|
6553
|
+
}
|
|
6554
|
+
if (!Number.isFinite(w)) {
|
|
6555
|
+
finite2 = false;
|
|
6556
|
+
}
|
|
6557
|
+
sum += w;
|
|
6558
|
+
}
|
|
6559
|
+
return { values, override: values, allOne, nonNegative, finite: finite2, sum };
|
|
6560
|
+
}
|
|
6561
|
+
async function unitWeightRoute(ctx, s, source, cutoff, dest, options) {
|
|
6562
|
+
const maxDepth = cutoff === Infinity ? void 0 : Math.floor(cutoff);
|
|
6563
|
+
const bfs = await bfsWithTuning(
|
|
6564
|
+
ctx,
|
|
6565
|
+
s,
|
|
6566
|
+
source,
|
|
6567
|
+
{ maxDepth, signal: options?.signal, onProgress: options?.onProgress },
|
|
6568
|
+
{ predKind: 0 }
|
|
6569
|
+
);
|
|
6570
|
+
const dist = dest ?? new Float32Array(s.nodeCount);
|
|
6571
|
+
for (let v = 0; v < s.nodeCount; v++) {
|
|
6572
|
+
const d = bfs.depth[v];
|
|
6573
|
+
dist[v] = d === INVALID_INDEX ? Infinity : d;
|
|
6574
|
+
}
|
|
6575
|
+
return { dist, predArc: bfs.parent, reachedCount: bfs.visitedCount };
|
|
6576
|
+
}
|
|
6577
|
+
function predBufferWords(n) {
|
|
6578
|
+
return 2 * Math.ceil(n / 64) * 64 + 64;
|
|
6579
|
+
}
|
|
6580
|
+
async function predecessorPass(input) {
|
|
6581
|
+
const { algorithm, ctx, scope, predKernel, recordFill, graph, dist, pred, n, arcCount, source, mode } = input;
|
|
6582
|
+
const wg = ctx.workgroupSize;
|
|
6583
|
+
const { queue } = ctx.device;
|
|
6584
|
+
const bytes = 4 * n;
|
|
6585
|
+
const hb = Math.ceil(n / 64) * 64;
|
|
6586
|
+
const predArcs = { buffer: pred.buffer, offset: pred.offset, size: bytes, window: null };
|
|
6587
|
+
queue.writeBuffer(pred.buffer, pred.offset + 4 * (hb + source), Uint32Array.of(0));
|
|
6588
|
+
queue.writeBuffer(pred.buffer, pred.offset + 4 * 2 * hb, new Uint32Array(2));
|
|
6589
|
+
const predPlan = planGridStride(n, wg, ctx.caps);
|
|
6590
|
+
const predFields = {
|
|
6591
|
+
wg,
|
|
6592
|
+
n,
|
|
6593
|
+
arcBase: 0,
|
|
6594
|
+
arcEnd: arcCount,
|
|
6595
|
+
predKind: 0,
|
|
6596
|
+
source,
|
|
6597
|
+
stride: predPlan.stride ?? n,
|
|
6598
|
+
mode
|
|
6599
|
+
};
|
|
6600
|
+
const maxBatches = Math.ceil((n + 1) / MAX_LEVELS_PER_SUBMIT) + 1;
|
|
6601
|
+
let batches = 0;
|
|
6602
|
+
for (; ; ) {
|
|
6603
|
+
const batch = new CommandBatch(ctx, `${algorithm}/pred`);
|
|
6604
|
+
const pass = batch.pass("pred");
|
|
6605
|
+
const recordRole = (role, iteration) => {
|
|
6606
|
+
const params = scope.params(FRONTIER_PARAMS, { ...predFields, role, iteration });
|
|
6607
|
+
const bound = predKernel.bind({ ...graph, dist, pred, P: params.binding });
|
|
6608
|
+
predKernel.dispatch(pass, bound, predPlan, [params.offset]);
|
|
6609
|
+
};
|
|
6610
|
+
if (batches === 0 && mode === 0) {
|
|
6611
|
+
recordRole(0, 0);
|
|
6612
|
+
}
|
|
6613
|
+
for (let iteration = 0; iteration < MAX_LEVELS_PER_SUBMIT; iteration++) {
|
|
6614
|
+
recordRole(1, iteration);
|
|
6615
|
+
}
|
|
6616
|
+
recordFill(pass, predArcs, n, INVALID_INDEX);
|
|
6617
|
+
recordRole(2, 0);
|
|
6618
|
+
batch.endPass();
|
|
6619
|
+
const distRequest = batch.readback(dist.buffer, dist.offset, bytes);
|
|
6620
|
+
const predRequest = batch.readback(pred.buffer, pred.offset, bytes);
|
|
6621
|
+
const flagsRequest = batch.readback(pred.buffer, pred.offset + 4 * 2 * hb, 8);
|
|
6622
|
+
scope.flush();
|
|
6623
|
+
const back = await batch.submit().readback;
|
|
6624
|
+
ctx.assertReady();
|
|
6625
|
+
batches += 1;
|
|
6626
|
+
const [changed, orphans] = new Uint32Array(back, flagsRequest.offset, 2);
|
|
6627
|
+
if (changed < MAX_LEVELS_PER_SUBMIT) {
|
|
6628
|
+
return {
|
|
6629
|
+
dist: new Float32Array(back, distRequest.offset, n),
|
|
6630
|
+
predArc: new Uint32Array(back, predRequest.offset, n).slice(),
|
|
6631
|
+
orphans
|
|
6632
|
+
};
|
|
6633
|
+
}
|
|
6634
|
+
if (batches > maxBatches) {
|
|
6635
|
+
throw new WebGpuGraphError(
|
|
6636
|
+
"E_VALIDATION",
|
|
6637
|
+
`${algorithm}: the hop passes still changed something after ${batches} batches (a hop count is at most ${n})`,
|
|
6638
|
+
{ label: `${algorithm}/pred`, message: `the hop passes did not converge in ${batches} batches` }
|
|
6639
|
+
);
|
|
6640
|
+
}
|
|
6641
|
+
queue.writeBuffer(pred.buffer, pred.offset + 4 * 2 * hb, new Uint32Array(2));
|
|
6642
|
+
}
|
|
6643
|
+
}
|
|
6644
|
+
async function ssspWithTuning(ctx, s, source, options, tuning) {
|
|
6645
|
+
ctx.assertReady();
|
|
6646
|
+
await assertDeviceComputes(ctx);
|
|
6647
|
+
const n = s.nodeCount;
|
|
6648
|
+
assertSource(ALGORITHM$2, source, n);
|
|
6649
|
+
const roundsPerSubmit = tuning.roundsPerSubmit ?? MAX_LEVELS_PER_SUBMIT;
|
|
6650
|
+
if (!Number.isInteger(roundsPerSubmit) || roundsPerSubmit < 1 || roundsPerSubmit > MAX_LEVELS_PER_SUBMIT) {
|
|
6651
|
+
throw new WebGpuGraphError(
|
|
6652
|
+
"E_INVALID_ARGUMENT",
|
|
6653
|
+
`${ALGORITHM$2}: roundsPerSubmit must be an integer in [1, ${MAX_LEVELS_PER_SUBMIT}]`,
|
|
6654
|
+
{
|
|
6655
|
+
argument: "roundsPerSubmit",
|
|
6656
|
+
value: roundsPerSubmit,
|
|
6657
|
+
expected: `an integer in [1, ${MAX_LEVELS_PER_SUBMIT}]`
|
|
6658
|
+
}
|
|
6659
|
+
);
|
|
6660
|
+
}
|
|
6661
|
+
if (tuning.delta !== void 0 && !(Number.isFinite(tuning.delta) && tuning.delta > 0)) {
|
|
6662
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${ALGORITHM$2}: delta must be a finite positive number`, {
|
|
6663
|
+
argument: "delta",
|
|
6664
|
+
value: tuning.delta,
|
|
6665
|
+
expected: "a finite positive number"
|
|
6666
|
+
});
|
|
6667
|
+
}
|
|
6668
|
+
const dest = checkDest(ALGORITHM$2, options?.dest, n);
|
|
6669
|
+
const vector2 = resolveWeights$1(ALGORITHM$2, s, options?.weights);
|
|
6670
|
+
const cutoff = normaliseCutoff(ALGORITHM$2, options?.cutoff);
|
|
6671
|
+
if (options?.signal?.aborted) {
|
|
6672
|
+
throw aborted(ALGORITHM$2);
|
|
6673
|
+
}
|
|
6674
|
+
if (vector2 === null || vector2.allOne) {
|
|
6675
|
+
return unitWeightRoute(ctx, s, source, cutoff, dest, options);
|
|
6676
|
+
}
|
|
6677
|
+
if (!vector2.nonNegative) {
|
|
6678
|
+
throw new WebGpuGraphError("E_UNSUPPORTED", `${ALGORITHM$2}: a negative weight has no shortest path here`, {
|
|
6679
|
+
feature: "sssp.negativeWeights",
|
|
6680
|
+
hint: "use bellmanFord"
|
|
6681
|
+
});
|
|
6682
|
+
}
|
|
6683
|
+
if (!vector2.finite) {
|
|
6684
|
+
throw new WebGpuGraphError("E_UNSUPPORTED", `${ALGORITHM$2}: a NaN or infinite weight has no bit-pattern order`, {
|
|
6685
|
+
feature: "sssp.nonFiniteWeights"
|
|
6686
|
+
});
|
|
6687
|
+
}
|
|
6688
|
+
const { arcCount } = s;
|
|
6689
|
+
const core = ctx.residency.core(s);
|
|
6690
|
+
const limit = ctx.caps.limits.maxStorageBufferBindingSize;
|
|
6691
|
+
assertWholeCore(core, arcCount, limit, ALGORITHM$2);
|
|
6692
|
+
const cap = Math.ceil(Math.max(1, arcCount) / HALF_ALIGN) * HALF_ALIGN;
|
|
6693
|
+
if (8 * cap > limit) {
|
|
6694
|
+
throw new WebGpuGraphError(
|
|
6695
|
+
"E_TOO_LARGE",
|
|
6696
|
+
`${ALGORITHM$2}: the near-far queue of ${cap} entries per half needs ${8 * cap} bytes, above the ${limit}-byte binding limit (the relax is never windowed)`,
|
|
6697
|
+
{ needed: 8 * cap, limit, path: "sssp.queue", algorithm: ALGORITHM$2 }
|
|
6698
|
+
);
|
|
6699
|
+
}
|
|
6700
|
+
const delta = Math.fround(
|
|
6701
|
+
tuning.delta ?? (vector2.sum === 0 ? 1 : SSSP_DELTA_FACTOR * (vector2.sum / arcCount) / (arcCount / n))
|
|
6702
|
+
);
|
|
6703
|
+
const deltaBits = bitsOf(delta);
|
|
6704
|
+
const maxRounds = n + Math.ceil(vector2.sum / delta) + 1;
|
|
6705
|
+
const maxSubmits = Math.ceil((maxRounds + 1) / roundsPerSubmit) + 1;
|
|
6706
|
+
const scope = algorithmScope(ctx, ALGORITHM$2, RING_SLOTS$2);
|
|
6707
|
+
try {
|
|
6708
|
+
const wg = ctx.workgroupSize;
|
|
6709
|
+
const bytes = 4 * n;
|
|
6710
|
+
const dist = bindingOf(scope.scratch(bytes, "dist"), bytes);
|
|
6711
|
+
const owner = bindingOf(scope.scratch(bytes, "owner"), bytes);
|
|
6712
|
+
const queueOut = bindingOf(scope.scratch(8 * cap, "queue-out"), 8 * cap);
|
|
6713
|
+
const nearHalf = { buffer: queueOut.buffer, offset: 0, size: 4 * cap, window: null };
|
|
6714
|
+
const farHalf = { buffer: queueOut.buffer, offset: 4 * cap, size: 4 * cap, window: null };
|
|
6715
|
+
const predWords = predBufferWords(n);
|
|
6716
|
+
const pred = bindingOf(scope.scratch(4 * predWords, "pred"), 4 * predWords);
|
|
6717
|
+
const { queue } = ctx.device;
|
|
6718
|
+
let weightsBinding;
|
|
6719
|
+
if (vector2.override !== null) {
|
|
6720
|
+
const uploaded = scope.scratch(4 * arcCount, "weights");
|
|
6721
|
+
queue.writeBuffer(uploaded, 0, vector2.override);
|
|
6722
|
+
weightsBinding = bindingOf(uploaded, 4 * arcCount);
|
|
6723
|
+
}
|
|
6724
|
+
await ctx.allocator.check();
|
|
6725
|
+
const planner = await prepareFrontier(scope, n, arcCount, cap);
|
|
6726
|
+
const compact = await prepareCompact(scope);
|
|
6727
|
+
const overrides = graphOverrides(core, null, weightsBinding);
|
|
6728
|
+
const relax = await ctx.pipelines.kernel(kernelSpec("sssp-relax", overrides));
|
|
6729
|
+
const predKernel = await ctx.pipelines.kernel(kernelSpec("sssp-pred", { ...overrides, MODE: 0 }));
|
|
6730
|
+
const fill = await ctx.pipelines.kernel(kernelSpec("fill"));
|
|
6731
|
+
const graph = graphBindings(core, null, weightsBinding);
|
|
6732
|
+
const { frontier } = planner;
|
|
6733
|
+
const { counters } = frontier;
|
|
6734
|
+
const nearIn = frontier.vertices[0];
|
|
6735
|
+
const farIn = frontier.vertices[1];
|
|
6736
|
+
const recordFill = (pass, dst, count, value) => {
|
|
6737
|
+
const params = scope.params(FILL_PARAMS, { count, value, mode: 0, pad0: 0 });
|
|
6738
|
+
fill.dispatch(pass, fill.bind({ dst, P: params.binding }), plan1d(count, wg, ctx.caps), [params.offset]);
|
|
6739
|
+
};
|
|
6740
|
+
const submit = (batch) => {
|
|
6741
|
+
scope.flush();
|
|
6742
|
+
return batch.submit();
|
|
6743
|
+
};
|
|
6744
|
+
const setup = new CommandBatch(ctx, `${ALGORITHM$2}/setup`);
|
|
6745
|
+
const setupPass = setup.pass("fill");
|
|
6746
|
+
recordFill(setupPass, dist, n, F32_INF_BITS);
|
|
6747
|
+
recordFill(setupPass, pred, predWords, INVALID_INDEX);
|
|
6748
|
+
setup.endPass();
|
|
6749
|
+
await submit(setup).readback;
|
|
6750
|
+
ctx.assertReady();
|
|
6751
|
+
queue.writeBuffer(dist.buffer, dist.offset + 4 * source, Uint32Array.of(0));
|
|
6752
|
+
frontier.reset(queue, source, { nextFrontierCount: 1, thresholdBits: deltaBits, deltaBits });
|
|
6753
|
+
queue.writeBuffer(queueOut.buffer, queueOut.offset, Uint32Array.of(source));
|
|
6754
|
+
const nearDedupe = {
|
|
6755
|
+
queue: nearHalf,
|
|
6756
|
+
count: cap,
|
|
6757
|
+
countIndex: W.edgeCount,
|
|
6758
|
+
// role 2 writes the raw near count here when it chooses a near round, 0 otherwise
|
|
6759
|
+
counters,
|
|
6760
|
+
owner,
|
|
6761
|
+
out: nearIn,
|
|
6762
|
+
outCount: counters,
|
|
6763
|
+
outIndex: W.frontierCount
|
|
6764
|
+
};
|
|
6765
|
+
const farDedupe = {
|
|
6766
|
+
queue: farHalf,
|
|
6767
|
+
count: cap,
|
|
6768
|
+
countIndex: W.edgeCountUnclamped,
|
|
6769
|
+
// and the raw far count here on a far round
|
|
6770
|
+
counters,
|
|
6771
|
+
owner,
|
|
6772
|
+
out: farIn,
|
|
6773
|
+
outCount: counters,
|
|
6774
|
+
outIndex: W.farCount
|
|
6775
|
+
};
|
|
6776
|
+
const relaxPlan = planGridStride(n, wg, ctx.caps);
|
|
6777
|
+
const relaxFields = {
|
|
6778
|
+
wg,
|
|
6779
|
+
n,
|
|
6780
|
+
edgeCapacity: cap,
|
|
6781
|
+
arcBase: 0,
|
|
6782
|
+
arcEnd: arcCount,
|
|
6783
|
+
cutoffBits: bitsOf(cutoff),
|
|
6784
|
+
source,
|
|
6785
|
+
stride: relaxPlan.stride ?? wg
|
|
6786
|
+
};
|
|
6787
|
+
let roundsRecorded = 0;
|
|
6788
|
+
let submits = 0;
|
|
6789
|
+
for (; ; ) {
|
|
6790
|
+
const batch = new CommandBatch(ctx, `${ALGORITHM$2}/rounds`);
|
|
6791
|
+
const pass = batch.pass("sssp");
|
|
6792
|
+
const near = scope.params(FRONTIER_PARAMS, { ...relaxFields, role: 0 });
|
|
6793
|
+
const far = scope.params(FRONTIER_PARAMS, { ...relaxFields, role: 1 });
|
|
6794
|
+
const boundNear = relax.bind({ ...graph, dist, counters, queueIn: nearIn, queueOut, P: near.binding });
|
|
6795
|
+
const boundFar = relax.bind({ ...graph, dist, counters, queueIn: farIn, queueOut, P: far.binding });
|
|
6796
|
+
for (let round = 0; round < roundsPerSubmit; round++) {
|
|
6797
|
+
planner.recordFinalize(pass, 2, round, {});
|
|
6798
|
+
compact.recordDedupe(pass, nearDedupe);
|
|
6799
|
+
compact.recordDedupe(pass, farDedupe);
|
|
6800
|
+
planner.recordFinalize(pass, 3, round, {});
|
|
6801
|
+
relax.dispatch(pass, boundNear, relaxPlan, [near.offset]);
|
|
6802
|
+
relax.dispatch(pass, boundFar, relaxPlan, [far.offset]);
|
|
6803
|
+
}
|
|
6804
|
+
batch.endPass();
|
|
6805
|
+
const doneRequest = batch.readback(counters.buffer, counters.offset + 4 * W.done, 4);
|
|
6806
|
+
const inspect = tuning.onRound === void 0 ? null : batch.readback(counters.buffer, counters.offset, FRONTIER_COUNTERS.byteLength);
|
|
6807
|
+
const submitted = submit(batch);
|
|
6808
|
+
const back = await submitted.readback;
|
|
6809
|
+
roundsRecorded += roundsPerSubmit;
|
|
6810
|
+
submits += 1;
|
|
6811
|
+
ctx.assertReady();
|
|
6812
|
+
if (options?.signal?.aborted) {
|
|
6813
|
+
throw aborted(ALGORITHM$2, submitted.id);
|
|
6814
|
+
}
|
|
6815
|
+
options?.onProgress?.(Math.min(roundsRecorded, maxRounds), maxRounds);
|
|
6816
|
+
if (inspect !== null && tuning.onRound !== void 0) {
|
|
6817
|
+
tuning.onRound(roundsRecorded - 1, FRONTIER_COUNTERS.read(new DataView(back), inspect.offset));
|
|
6818
|
+
}
|
|
6819
|
+
const done = new Uint32Array(back, doneRequest.offset, 1)[0];
|
|
6820
|
+
if (done === 1) {
|
|
6821
|
+
break;
|
|
6822
|
+
}
|
|
6823
|
+
if (done !== 0) {
|
|
6824
|
+
const block = FRONTIER_COUNTERS.read(
|
|
6825
|
+
new DataView(
|
|
6826
|
+
await ctx.readback.read(
|
|
6827
|
+
counters.buffer,
|
|
6828
|
+
FRONTIER_COUNTERS.byteLength,
|
|
6829
|
+
void 0,
|
|
6830
|
+
counters.offset
|
|
6831
|
+
)
|
|
6832
|
+
)
|
|
6833
|
+
);
|
|
6834
|
+
if (done === 2) {
|
|
6835
|
+
const needed = Math.max(wordOf(block, "nextFrontierCount"), wordOf(block, "nextFarCount"));
|
|
6836
|
+
throw new WebGpuGraphError(
|
|
6837
|
+
"E_TOO_LARGE",
|
|
6838
|
+
`${ALGORITHM$2}: a raw pile of ${needed} entries overflowed its ${cap}-entry half`,
|
|
6839
|
+
{ needed, limit: cap, path: "sssp.pile", algorithm: ALGORITHM$2 }
|
|
6840
|
+
);
|
|
6841
|
+
}
|
|
6842
|
+
throw new WebGpuGraphError(
|
|
6843
|
+
"E_UNSUPPORTED",
|
|
6844
|
+
`${ALGORITHM$2}: the f32 threshold ${wordOf(block, "thresholdBits")} absorbed the delta ${deltaBits} (as bit patterns); the far pile can no longer be bucketed`,
|
|
6845
|
+
{ feature: "sssp.thresholdAbsorbed", hint: "the distances outgrew the delta's f32 precision" }
|
|
6846
|
+
);
|
|
6847
|
+
}
|
|
6848
|
+
if (submits > maxSubmits) {
|
|
6849
|
+
throw new WebGpuGraphError(
|
|
6850
|
+
"E_VALIDATION",
|
|
6851
|
+
`${ALGORITHM$2}: the done flag never rose in ${submits} submits (at most ${maxRounds} rounds)`,
|
|
6852
|
+
{ label: `${ALGORITHM$2}/rounds`, message: `the done flag never rose in ${submits} submits` }
|
|
6853
|
+
);
|
|
6854
|
+
}
|
|
6855
|
+
}
|
|
6856
|
+
const passed = await predecessorPass({
|
|
6857
|
+
algorithm: ALGORITHM$2,
|
|
6858
|
+
ctx,
|
|
6859
|
+
scope,
|
|
6860
|
+
predKernel,
|
|
6861
|
+
recordFill,
|
|
6862
|
+
graph,
|
|
6863
|
+
dist,
|
|
6864
|
+
pred,
|
|
6865
|
+
n,
|
|
6866
|
+
arcCount,
|
|
6867
|
+
source,
|
|
6868
|
+
mode: 0
|
|
6869
|
+
});
|
|
6870
|
+
if (passed.orphans !== 0) {
|
|
6871
|
+
throw new WebGpuGraphError(
|
|
6872
|
+
"E_VALIDATION",
|
|
6873
|
+
`${ALGORITHM$2}: ${passed.orphans} reached node(s) the predecessor key never reached (a kernel bug)`,
|
|
6874
|
+
{ label: `${ALGORITHM$2}/pred`, message: `${passed.orphans} orphan(s) in the predecessor pass` }
|
|
6875
|
+
);
|
|
6876
|
+
}
|
|
6877
|
+
const distOut = dest ?? new Float32Array(n);
|
|
6878
|
+
distOut.set(passed.dist);
|
|
6879
|
+
let reachedCount = 0;
|
|
6880
|
+
for (const d of distOut) {
|
|
6881
|
+
if (d !== Infinity) {
|
|
6882
|
+
reachedCount += 1;
|
|
6883
|
+
}
|
|
6884
|
+
}
|
|
6885
|
+
return { dist: distOut, predArc: passed.predArc, reachedCount };
|
|
6886
|
+
} finally {
|
|
6887
|
+
scope.dispose();
|
|
6888
|
+
}
|
|
6889
|
+
}
|
|
6890
|
+
function sssp(ctx, s, source, options) {
|
|
6891
|
+
return ssspWithTuning(ctx, s, source, options, {});
|
|
6892
|
+
}
|
|
6893
|
+
const ALGORITHM$1 = "bellmanFord";
|
|
6894
|
+
const ROUNDS_PER_BATCH = 8;
|
|
6895
|
+
const MAX_RETRIES = 16;
|
|
6896
|
+
const RING_SLOTS$1 = MAX_LEVELS_PER_SUBMIT + 16;
|
|
6897
|
+
function assertSymmetric(s, vector2) {
|
|
6898
|
+
const { arcToEdge, edgeToArc } = s;
|
|
6899
|
+
for (let a = 0; a < s.arcCount; a++) {
|
|
6900
|
+
const forward = edgeToArc[arcToEdge[a]];
|
|
6901
|
+
if (vector2[a] !== vector2[forward]) {
|
|
6902
|
+
throw new WebGpuGraphError(
|
|
6903
|
+
"E_UNSUPPORTED",
|
|
6904
|
+
`${ALGORITHM$1}: weights[${a}] = ${vector2[a]} differs from weights[${forward}] = ${vector2[forward]}, the forward arc of the same undirected edge; the kernel reads one weight per edge`,
|
|
6905
|
+
{ feature: "bellmanFord.asymmetricUndirectedWeights", hint: "use a directed snapshot" }
|
|
6906
|
+
);
|
|
6907
|
+
}
|
|
6908
|
+
}
|
|
6909
|
+
}
|
|
6910
|
+
async function bellmanFordWithTuning(ctx, s, source, options, tuning) {
|
|
6911
|
+
ctx.assertReady();
|
|
6912
|
+
await assertDeviceComputes(ctx);
|
|
6913
|
+
const n = s.nodeCount;
|
|
6914
|
+
assertSource(ALGORITHM$1, source, n);
|
|
6915
|
+
const maxRetries = tuning.maxRetries ?? MAX_RETRIES;
|
|
6916
|
+
if (!Number.isInteger(maxRetries) || maxRetries < 1) {
|
|
6917
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${ALGORITHM$1}: maxRetries must be an integer >= 1`, {
|
|
6918
|
+
argument: "maxRetries",
|
|
6919
|
+
value: maxRetries,
|
|
6920
|
+
expected: "an integer >= 1"
|
|
6921
|
+
});
|
|
6922
|
+
}
|
|
6923
|
+
const roundsPerBatch = tuning.roundsPerBatch ?? ROUNDS_PER_BATCH;
|
|
6924
|
+
if (!Number.isInteger(roundsPerBatch) || roundsPerBatch < 1 || roundsPerBatch > MAX_LEVELS_PER_SUBMIT) {
|
|
6925
|
+
throw new WebGpuGraphError(
|
|
6926
|
+
"E_INVALID_ARGUMENT",
|
|
6927
|
+
`${ALGORITHM$1}: roundsPerBatch must be an integer in [1, ${MAX_LEVELS_PER_SUBMIT}]`,
|
|
6928
|
+
{
|
|
6929
|
+
argument: "roundsPerBatch",
|
|
6930
|
+
value: roundsPerBatch,
|
|
6931
|
+
expected: `an integer in [1, ${MAX_LEVELS_PER_SUBMIT}]`
|
|
6932
|
+
}
|
|
6933
|
+
);
|
|
6934
|
+
}
|
|
6935
|
+
const dest = checkDest(ALGORITHM$1, options?.dest, n);
|
|
6936
|
+
const vector2 = resolveWeights$1(ALGORITHM$1, s, options?.weights);
|
|
6937
|
+
const cutoff = normaliseCutoff(ALGORITHM$1, options?.cutoff);
|
|
6938
|
+
if (options?.signal?.aborted) {
|
|
6939
|
+
throw aborted(ALGORITHM$1);
|
|
6940
|
+
}
|
|
6941
|
+
if (vector2 === null || vector2.allOne) {
|
|
6942
|
+
const unit = await unitWeightRoute(ctx, s, source, cutoff, dest, options);
|
|
6943
|
+
return { result: { ...unit, hasNegativeCycle: false }, rounds: 0, retryExhaustedRounds: 0 };
|
|
6944
|
+
}
|
|
6945
|
+
if (!vector2.finite) {
|
|
6946
|
+
throw new WebGpuGraphError("E_UNSUPPORTED", `${ALGORITHM$1}: a NaN or infinite weight has no shortest path`, {
|
|
6947
|
+
feature: "bellmanFord.nonFiniteWeights"
|
|
6948
|
+
});
|
|
6949
|
+
}
|
|
6950
|
+
if (!s.directed && vector2.override !== null) {
|
|
6951
|
+
assertSymmetric(s, vector2.override);
|
|
6952
|
+
}
|
|
6953
|
+
const { arcCount } = s;
|
|
6954
|
+
const core = ctx.residency.core(s, ["rowPtr", "colIdx", "weights", "edgeToArc"]);
|
|
6955
|
+
assertWholeCore(core, arcCount, ctx.caps.limits.maxStorageBufferBindingSize, ALGORITHM$1);
|
|
6956
|
+
const edges = ctx.residency.view(s, "edgeList");
|
|
6957
|
+
const edgeCount = edges.scalars.edgeCount[0];
|
|
6958
|
+
const scope = algorithmScope(ctx, ALGORITHM$1, RING_SLOTS$1);
|
|
6959
|
+
try {
|
|
6960
|
+
const wg = ctx.workgroupSize;
|
|
6961
|
+
const bytes = 4 * n;
|
|
6962
|
+
const dist = bindingOf(scope.scratch(bytes, "dist"), bytes);
|
|
6963
|
+
const predWords = predBufferWords(n);
|
|
6964
|
+
const pred = bindingOf(scope.scratch(4 * predWords, "pred"), 4 * predWords);
|
|
6965
|
+
const flags = bindingOf(scope.scratch(BF_FLAGS.byteLength, "flags"), BF_FLAGS.byteLength);
|
|
6966
|
+
let iota = null;
|
|
6967
|
+
let { edgeToArc } = core;
|
|
6968
|
+
if (edgeToArc === null) {
|
|
6969
|
+
iota = bindingOf(scope.scratch(4 * edgeCount, "iota"), 4 * edgeCount);
|
|
6970
|
+
edgeToArc = iota;
|
|
6971
|
+
}
|
|
6972
|
+
const { queue } = ctx.device;
|
|
6973
|
+
let weightsBinding;
|
|
6974
|
+
if (vector2.override !== null) {
|
|
6975
|
+
const uploaded = scope.scratch(4 * arcCount, "weights");
|
|
6976
|
+
queue.writeBuffer(uploaded, 0, vector2.override);
|
|
6977
|
+
weightsBinding = bindingOf(uploaded, 4 * arcCount);
|
|
6978
|
+
}
|
|
6979
|
+
await ctx.allocator.check();
|
|
6980
|
+
const overrides = graphOverrides(core, null, weightsBinding);
|
|
6981
|
+
const relax = await ctx.pipelines.kernel(kernelSpec("bf-relax", { UNDIRECTED: !s.directed }));
|
|
6982
|
+
const predKernel = await ctx.pipelines.kernel(kernelSpec("sssp-pred", { ...overrides, MODE: 0 }));
|
|
6983
|
+
const fill = await ctx.pipelines.kernel(kernelSpec("fill"));
|
|
6984
|
+
const graph = graphBindings(core, null, weightsBinding);
|
|
6985
|
+
const recordFill = (pass, dst, count, value, mode = 0) => {
|
|
6986
|
+
const params = scope.params(FILL_PARAMS, { count, value, mode, pad0: 0 });
|
|
6987
|
+
fill.dispatch(pass, fill.bind({ dst, P: params.binding }), plan1d(count, wg, ctx.caps), [params.offset]);
|
|
6988
|
+
};
|
|
6989
|
+
const submit = (batch) => {
|
|
6990
|
+
scope.flush();
|
|
6991
|
+
return batch.submit();
|
|
6992
|
+
};
|
|
6993
|
+
const setup = new CommandBatch(ctx, `${ALGORITHM$1}/setup`);
|
|
6994
|
+
const setupPass = setup.pass("fill");
|
|
6995
|
+
recordFill(setupPass, dist, n, F32_INF_BITS);
|
|
6996
|
+
recordFill(setupPass, pred, predWords, INVALID_INDEX);
|
|
6997
|
+
if (iota !== null) {
|
|
6998
|
+
recordFill(setupPass, iota, edgeCount, 0, 1);
|
|
6999
|
+
}
|
|
7000
|
+
setup.endPass();
|
|
7001
|
+
await submit(setup).readback;
|
|
7002
|
+
ctx.assertReady();
|
|
7003
|
+
queue.writeBuffer(dist.buffer, dist.offset + 4 * source, Uint32Array.of(0));
|
|
7004
|
+
const edgePlan = planGridStride(edgeCount, wg, ctx.caps);
|
|
7005
|
+
const relaxBindings = {
|
|
7006
|
+
edgeSrc: edges.bindings.src,
|
|
7007
|
+
edgeDst: edges.bindings.dst,
|
|
7008
|
+
edgeToArc,
|
|
7009
|
+
weights: graph.weights,
|
|
7010
|
+
dist,
|
|
7011
|
+
flags
|
|
7012
|
+
};
|
|
7013
|
+
const relaxFields = {
|
|
7014
|
+
edgeCount,
|
|
7015
|
+
stride: edgePlan.stride ?? edgeCount,
|
|
7016
|
+
maxRetries,
|
|
7017
|
+
cutoffBits: bitsOf(cutoff)
|
|
7018
|
+
};
|
|
7019
|
+
const zero = new Uint32Array(BF_FLAGS.byteLength / 4);
|
|
7020
|
+
const runRounds = async (count, label) => {
|
|
7021
|
+
queue.writeBuffer(flags.buffer, flags.offset, zero);
|
|
7022
|
+
const batch = new CommandBatch(ctx, `${ALGORITHM$1}/${label}`);
|
|
7023
|
+
const pass = batch.pass("relax");
|
|
7024
|
+
const params = scope.params(BF_PARAMS, relaxFields);
|
|
7025
|
+
const bound = relax.bind({ ...relaxBindings, P: params.binding });
|
|
7026
|
+
for (let round = 0; round < count; round++) {
|
|
7027
|
+
relax.dispatch(pass, bound, edgePlan, [params.offset]);
|
|
7028
|
+
}
|
|
7029
|
+
batch.endPass();
|
|
7030
|
+
const request = batch.readback(flags.buffer, flags.offset, BF_FLAGS.byteLength);
|
|
7031
|
+
const submitted = submit(batch);
|
|
7032
|
+
const back = await submitted.readback;
|
|
7033
|
+
ctx.assertReady();
|
|
7034
|
+
const block = BF_FLAGS.read(new DataView(back), request.offset);
|
|
7035
|
+
return { changed: Number(block.changed), retryExhausted: Number(block.retryExhausted), id: submitted.id };
|
|
7036
|
+
};
|
|
7037
|
+
let rounds = 0;
|
|
7038
|
+
let retryExhaustedRounds = 0;
|
|
7039
|
+
let hasNegativeCycle = false;
|
|
7040
|
+
for (; ; ) {
|
|
7041
|
+
const remaining = n - 1 - rounds;
|
|
7042
|
+
if (remaining <= 0) {
|
|
7043
|
+
const decision = await runRounds(1, "decision");
|
|
7044
|
+
if (decision.retryExhausted !== 0) {
|
|
7045
|
+
throw new WebGpuGraphError(
|
|
7046
|
+
"E_VALIDATION",
|
|
7047
|
+
`${ALGORITHM$1}: a lane exhausted the ${maxRetries}-retry compare-exchange bound in the decision round, so its change is not a verdict`,
|
|
7048
|
+
{
|
|
7049
|
+
label: `${ALGORITHM$1}/retry`,
|
|
7050
|
+
message: "retryExhausted in the decision round",
|
|
7051
|
+
batchId: decision.id
|
|
7052
|
+
}
|
|
7053
|
+
);
|
|
7054
|
+
}
|
|
7055
|
+
hasNegativeCycle = decision.changed !== 0;
|
|
7056
|
+
break;
|
|
7057
|
+
}
|
|
7058
|
+
const count = Math.min(roundsPerBatch, remaining);
|
|
7059
|
+
const batch = await runRounds(count, "rounds");
|
|
7060
|
+
rounds += count;
|
|
7061
|
+
if (batch.retryExhausted !== 0) {
|
|
7062
|
+
retryExhaustedRounds += 1;
|
|
7063
|
+
}
|
|
7064
|
+
if (options?.signal?.aborted) {
|
|
7065
|
+
throw aborted(ALGORITHM$1, batch.id);
|
|
7066
|
+
}
|
|
7067
|
+
options?.onProgress?.(rounds, n);
|
|
7068
|
+
if (batch.changed === 0 && batch.retryExhausted === 0) {
|
|
7069
|
+
break;
|
|
7070
|
+
}
|
|
7071
|
+
}
|
|
7072
|
+
const passed = await predecessorPass({
|
|
7073
|
+
algorithm: ALGORITHM$1,
|
|
7074
|
+
ctx,
|
|
7075
|
+
scope,
|
|
7076
|
+
predKernel,
|
|
7077
|
+
recordFill,
|
|
7078
|
+
graph,
|
|
7079
|
+
dist,
|
|
7080
|
+
pred,
|
|
7081
|
+
n,
|
|
7082
|
+
arcCount,
|
|
7083
|
+
source,
|
|
7084
|
+
mode: 1
|
|
7085
|
+
});
|
|
7086
|
+
if (passed.orphans !== 0 && !hasNegativeCycle) {
|
|
7087
|
+
throw new WebGpuGraphError(
|
|
7088
|
+
"E_UNSUPPORTED",
|
|
7089
|
+
`${ALGORITHM$1}: ${passed.orphans} reached node(s) the tight subgraph never reaches (a cycle of weights below one f32 ulp relaxed once)`,
|
|
7090
|
+
{
|
|
7091
|
+
feature: "bellmanFord.roundedCycle",
|
|
7092
|
+
hint: "a cycle of weights below one f32 ulp relaxed once at a distance above 2^24; scale the weights or shorten the distances"
|
|
7093
|
+
}
|
|
7094
|
+
);
|
|
7095
|
+
}
|
|
7096
|
+
const distOut = dest ?? new Float32Array(n);
|
|
7097
|
+
distOut.set(passed.dist);
|
|
7098
|
+
let reachedCount = 0;
|
|
7099
|
+
for (const d of distOut) {
|
|
7100
|
+
if (d !== Infinity) {
|
|
7101
|
+
reachedCount += 1;
|
|
7102
|
+
}
|
|
7103
|
+
}
|
|
7104
|
+
return {
|
|
7105
|
+
result: { dist: distOut, predArc: passed.predArc, reachedCount, hasNegativeCycle },
|
|
7106
|
+
rounds,
|
|
7107
|
+
retryExhaustedRounds
|
|
7108
|
+
};
|
|
7109
|
+
} finally {
|
|
7110
|
+
scope.dispose();
|
|
7111
|
+
}
|
|
7112
|
+
}
|
|
7113
|
+
async function bellmanFord(ctx, s, source, options) {
|
|
7114
|
+
return (await bellmanFordWithTuning(ctx, s, source, options, {})).result;
|
|
7115
|
+
}
|
|
7116
|
+
const ALGORITHM = "closenessCentrality";
|
|
7117
|
+
const SOURCES_PER_BATCH = 32;
|
|
7118
|
+
const PER_SOURCE_WORDS = 4 * SOURCES_PER_BATCH;
|
|
7119
|
+
const RING_SLOTS = 8 * MAX_LEVELS_PER_SUBMIT + 16;
|
|
7120
|
+
function reusingScratch(scope) {
|
|
7121
|
+
const held = /* @__PURE__ */ new Map();
|
|
7122
|
+
return {
|
|
7123
|
+
...scope,
|
|
7124
|
+
scratch: (byteLength, label) => {
|
|
7125
|
+
const key = `${label}/${byteLength}`;
|
|
7126
|
+
let buffer = held.get(key);
|
|
7127
|
+
if (buffer === void 0) {
|
|
7128
|
+
buffer = scope.scratch(byteLength, label);
|
|
7129
|
+
held.set(key, buffer);
|
|
7130
|
+
}
|
|
7131
|
+
return buffer;
|
|
7132
|
+
}
|
|
7133
|
+
};
|
|
7134
|
+
}
|
|
7135
|
+
async function weightedRoute(ctx, s, scores, options) {
|
|
7136
|
+
const n = s.nodeCount;
|
|
7137
|
+
for (let source = 0; source < n; source++) {
|
|
7138
|
+
if (options?.signal?.aborted) {
|
|
7139
|
+
throw aborted(ALGORITHM);
|
|
7140
|
+
}
|
|
7141
|
+
const { dist } = await sssp(ctx, s, source, { signal: options?.signal });
|
|
7142
|
+
let sum = 0;
|
|
7143
|
+
for (let v = 0; v < n; v++) {
|
|
7144
|
+
const d = dist[v];
|
|
7145
|
+
if (v !== source && d !== Infinity) {
|
|
7146
|
+
sum += d;
|
|
7147
|
+
}
|
|
7148
|
+
}
|
|
7149
|
+
scores[source] = sum === 0 ? 0 : 1 / sum;
|
|
7150
|
+
options?.onProgress?.(source + 1, n);
|
|
7151
|
+
}
|
|
7152
|
+
return { scores, iterations: n, converged: true, precision: "f32" };
|
|
7153
|
+
}
|
|
7154
|
+
async function sweepRoute(ctx, s, scores, levelsPerSubmit, options, tuning) {
|
|
7155
|
+
const n = s.nodeCount;
|
|
7156
|
+
if (n === 0) {
|
|
7157
|
+
return { scores, iterations: 0, converged: true, precision: "f32" };
|
|
7158
|
+
}
|
|
7159
|
+
const core = ctx.residency.core(s);
|
|
7160
|
+
assertWholeCore(core, s.arcCount, ctx.caps.limits.maxStorageBufferBindingSize, ALGORITHM);
|
|
7161
|
+
const scope = algorithmScope(ctx, ALGORITHM, RING_SLOTS);
|
|
7162
|
+
try {
|
|
7163
|
+
const wg = ctx.workgroupSize;
|
|
7164
|
+
const bytes = 4 * n;
|
|
7165
|
+
const bitsBase = Math.ceil(n / 64) * 64;
|
|
7166
|
+
const regionBytes = 4 * bitsBase;
|
|
7167
|
+
const bits = bindingOf(scope.scratch(4 * regionBytes, "bits"), 4 * regionBytes);
|
|
7168
|
+
const region = (index) => ({
|
|
7169
|
+
buffer: bits.buffer,
|
|
7170
|
+
offset: index * regionBytes,
|
|
7171
|
+
size: regionBytes,
|
|
7172
|
+
window: null
|
|
7173
|
+
});
|
|
7174
|
+
const flags = region(3);
|
|
7175
|
+
const frontierList = bindingOf(scope.scratch(bytes, "frontier-list"), bytes);
|
|
7176
|
+
const iota = bindingOf(scope.scratch(bytes, "iota"), bytes);
|
|
7177
|
+
const counters = bindingOf(
|
|
7178
|
+
scope.scratch(FRONTIER_COUNTERS.byteLength, "counters"),
|
|
7179
|
+
FRONTIER_COUNTERS.byteLength
|
|
7180
|
+
);
|
|
7181
|
+
const perSourceBytes = 4 * PER_SOURCE_WORDS;
|
|
7182
|
+
const perSource = bindingOf(scope.scratch(perSourceBytes, "per-source"), perSourceBytes);
|
|
7183
|
+
await ctx.allocator.check();
|
|
7184
|
+
const compact = await prepareCompact(reusingScratch(scope));
|
|
7185
|
+
const sweep = await ctx.pipelines.kernel(kernelSpec("closeness-sweep", graphOverrides(core, null)));
|
|
7186
|
+
const reduce = await ctx.pipelines.kernel(kernelSpec("closeness-reduce"));
|
|
7187
|
+
const fill = await ctx.pipelines.kernel(kernelSpec("fill"));
|
|
7188
|
+
const graph = graphBindings(core, null);
|
|
7189
|
+
const onePlan = plan1d(1, wg, ctx.caps);
|
|
7190
|
+
const regionPlan = plan1d(bitsBase, wg, ctx.caps);
|
|
7191
|
+
const sweepPlan = planGridStride(n, wg, ctx.caps);
|
|
7192
|
+
const recordFill = (pass, dst, count, mode) => {
|
|
7193
|
+
const params = scope.params(FILL_PARAMS, { count, value: 0, mode, pad0: 0 });
|
|
7194
|
+
fill.dispatch(pass, fill.bind({ dst, P: params.binding }), plan1d(count, wg, ctx.caps), [params.offset]);
|
|
7195
|
+
};
|
|
7196
|
+
const submit = (batch) => {
|
|
7197
|
+
scope.flush();
|
|
7198
|
+
return batch.submit();
|
|
7199
|
+
};
|
|
7200
|
+
const setup = new CommandBatch(ctx, `${ALGORITHM}/setup`);
|
|
7201
|
+
recordFill(setup.pass("fill"), iota, n, 1);
|
|
7202
|
+
setup.endPass();
|
|
7203
|
+
await submit(setup).readback;
|
|
7204
|
+
ctx.assertReady();
|
|
7205
|
+
let batches = 0;
|
|
7206
|
+
for (let batchStart = 0; batchStart < n; batchStart += SOURCES_PER_BATCH) {
|
|
7207
|
+
let level = 0;
|
|
7208
|
+
for (let first = true; ; first = false) {
|
|
7209
|
+
const batch = new CommandBatch(ctx, `${ALGORITHM}/levels`);
|
|
7210
|
+
const pass = batch.pass("closeness");
|
|
7211
|
+
if (first) {
|
|
7212
|
+
recordFill(pass, bits, 4 * bitsBase, 0);
|
|
7213
|
+
recordFill(pass, perSource, PER_SOURCE_WORDS, 0);
|
|
7214
|
+
const seed = scope.params(FRONTIER_PARAMS, { role: 1, n, bitsBase, source: batchStart });
|
|
7215
|
+
reduce.dispatch(pass, reduce.bind({ counters, perSource, bits, P: seed.binding }), onePlan, [
|
|
7216
|
+
seed.offset
|
|
7217
|
+
]);
|
|
7218
|
+
}
|
|
7219
|
+
const boundary = scope.params(FRONTIER_PARAMS, { role: 0, n, bitsBase });
|
|
7220
|
+
const boundBoundary = reduce.bind({ counters, perSource, bits, P: boundary.binding });
|
|
7221
|
+
const clear = scope.params(FILL_PARAMS, { count: bitsBase, value: 0, mode: 0, pad0: 0 });
|
|
7222
|
+
const boundClearNext = [region(2), region(1)].map((dst) => fill.bind({ dst, P: clear.binding }));
|
|
7223
|
+
const boundClearFlags = fill.bind({ dst: flags, P: clear.binding });
|
|
7224
|
+
const boundSweep = [0, 1].map((mode) => {
|
|
7225
|
+
const params = scope.params(FRONTIER_PARAMS, {
|
|
7226
|
+
wg,
|
|
7227
|
+
n,
|
|
7228
|
+
bitsBase,
|
|
7229
|
+
arcBase: 0,
|
|
7230
|
+
arcEnd: s.arcCount,
|
|
7231
|
+
mode,
|
|
7232
|
+
stride: sweepPlan.stride ?? wg
|
|
7233
|
+
});
|
|
7234
|
+
return {
|
|
7235
|
+
bound: sweep.bind({ ...graph, frontierList, counters, bits, perSource, P: params.binding }),
|
|
7236
|
+
offset: params.offset
|
|
7237
|
+
};
|
|
7238
|
+
});
|
|
7239
|
+
for (let k = 0; k < levelsPerSubmit; k++, level++) {
|
|
7240
|
+
const parity = level % 2;
|
|
7241
|
+
reduce.dispatch(pass, boundBoundary, onePlan, [boundary.offset]);
|
|
7242
|
+
compact.record(pass, {
|
|
7243
|
+
queue: iota,
|
|
7244
|
+
flags,
|
|
7245
|
+
count: n,
|
|
7246
|
+
out: frontierList,
|
|
7247
|
+
outCount: counters,
|
|
7248
|
+
outIndex: W.frontierCount
|
|
7249
|
+
});
|
|
7250
|
+
fill.dispatch(pass, boundClearNext[parity], regionPlan, [clear.offset]);
|
|
7251
|
+
fill.dispatch(pass, boundClearFlags, regionPlan, [clear.offset]);
|
|
7252
|
+
sweep.dispatch(pass, boundSweep[parity].bound, sweepPlan, [boundSweep[parity].offset]);
|
|
7253
|
+
}
|
|
7254
|
+
batch.endPass();
|
|
7255
|
+
const doneRequest = batch.readback(counters.buffer, counters.offset + 4 * W.done, 4);
|
|
7256
|
+
const blockRequest = batch.readback(perSource.buffer, perSource.offset, perSourceBytes);
|
|
7257
|
+
const submitted = submit(batch);
|
|
7258
|
+
const back = await submitted.readback;
|
|
7259
|
+
ctx.assertReady();
|
|
7260
|
+
if (options?.signal?.aborted) {
|
|
7261
|
+
throw aborted(ALGORITHM, submitted.id);
|
|
7262
|
+
}
|
|
7263
|
+
if (new Uint32Array(back, doneRequest.offset, 1)[0] !== 0) {
|
|
7264
|
+
const block = new Uint32Array(back, blockRequest.offset, PER_SOURCE_WORDS);
|
|
7265
|
+
const count = Math.min(SOURCES_PER_BATCH, n - batchStart);
|
|
7266
|
+
for (let i = 0; i < count; i++) {
|
|
7267
|
+
const sum = block[3 * SOURCES_PER_BATCH + i] * 2 ** 32 + block[2 * SOURCES_PER_BATCH + i];
|
|
7268
|
+
scores[batchStart + i] = sum === 0 ? 0 : 1 / sum;
|
|
7269
|
+
}
|
|
7270
|
+
tuning.onBatch?.(batchStart, block.slice());
|
|
7271
|
+
break;
|
|
7272
|
+
}
|
|
7273
|
+
if (level > n + 3) {
|
|
7274
|
+
throw new WebGpuGraphError(
|
|
7275
|
+
"E_VALIDATION",
|
|
7276
|
+
`${ALGORITHM}: the done flag never rose in ${level} levels of the batch at ${batchStart}`,
|
|
7277
|
+
{ label: ALGORITHM, message: `the done flag never rose in ${level} levels` }
|
|
7278
|
+
);
|
|
7279
|
+
}
|
|
7280
|
+
}
|
|
7281
|
+
batches += 1;
|
|
7282
|
+
options?.onProgress?.(Math.min(batchStart + SOURCES_PER_BATCH, n), n);
|
|
7283
|
+
}
|
|
7284
|
+
return { scores, iterations: batches, converged: true, precision: "f32" };
|
|
7285
|
+
} finally {
|
|
7286
|
+
scope.dispose();
|
|
4443
7287
|
}
|
|
4444
|
-
const run2 = await runPowerIteration(ctx, r.n, {
|
|
4445
|
-
normMode: 4,
|
|
4446
|
-
adjacency: reverseOf(ctx, s),
|
|
4447
|
-
alternate: null,
|
|
4448
|
-
alpha,
|
|
4449
|
-
beta,
|
|
4450
|
-
uniformP: 1,
|
|
4451
|
-
maxIterations: r.maxIterations,
|
|
4452
|
-
tolerance: r.tolerance,
|
|
4453
|
-
weights: r.weights,
|
|
4454
|
-
label: algorithm,
|
|
4455
|
-
signal: options?.signal,
|
|
4456
|
-
onProgress: options?.onProgress
|
|
4457
|
-
});
|
|
4458
|
-
return scoresOf(run2, r.dest, "l2");
|
|
4459
7288
|
}
|
|
4460
|
-
async function
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
const
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
7289
|
+
async function closenessWithTuning(ctx, s, options, tuning) {
|
|
7290
|
+
ctx.assertReady();
|
|
7291
|
+
await assertDeviceComputes(ctx);
|
|
7292
|
+
for (const key of ["maxIterations", "tolerance"]) {
|
|
7293
|
+
if (options?.[key] !== void 0) {
|
|
7294
|
+
throw new WebGpuGraphError("E_UNSUPPORTED", `${ALGORITHM}: ${key} has no meaning for an exact traversal`, {
|
|
7295
|
+
option: key,
|
|
7296
|
+
hint: "closeness is an exact traversal; the option has no meaning here"
|
|
7297
|
+
});
|
|
7298
|
+
}
|
|
4467
7299
|
}
|
|
4468
|
-
const
|
|
4469
|
-
const
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
const
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
7300
|
+
const n = s.nodeCount;
|
|
7301
|
+
const levelsPerSubmit = tuning.levelsPerSubmit ?? MAX_LEVELS_PER_SUBMIT;
|
|
7302
|
+
if (!Number.isInteger(levelsPerSubmit) || levelsPerSubmit < 1 || levelsPerSubmit > MAX_LEVELS_PER_SUBMIT) {
|
|
7303
|
+
throw new WebGpuGraphError(
|
|
7304
|
+
"E_INVALID_ARGUMENT",
|
|
7305
|
+
`${ALGORITHM}: levelsPerSubmit must be an integer in [1, ${MAX_LEVELS_PER_SUBMIT}]`,
|
|
7306
|
+
{
|
|
7307
|
+
argument: "levelsPerSubmit",
|
|
7308
|
+
value: levelsPerSubmit,
|
|
7309
|
+
expected: `an integer in [1, ${MAX_LEVELS_PER_SUBMIT}]`
|
|
7310
|
+
}
|
|
7311
|
+
);
|
|
7312
|
+
}
|
|
7313
|
+
const scores = checkDest(ALGORITHM, options?.dest, n) ?? new Float32Array(n);
|
|
7314
|
+
const weighted = options?.weighted ?? s.flags.weighted;
|
|
7315
|
+
if (options?.signal?.aborted) {
|
|
7316
|
+
throw aborted(ALGORITHM);
|
|
7317
|
+
}
|
|
7318
|
+
if (weighted && s.weights !== null && !s.flags.allWeightsOne) {
|
|
7319
|
+
if (!s.flags.nonNegativeWeights) {
|
|
7320
|
+
throw new WebGpuGraphError(
|
|
7321
|
+
"E_UNSUPPORTED",
|
|
7322
|
+
`${ALGORITHM}: a negative weight has no shortest-path distance to sum`,
|
|
7323
|
+
{
|
|
7324
|
+
feature: "closenessCentrality.negativeWeights",
|
|
7325
|
+
hint: "pass weighted: false to ignore the column"
|
|
7326
|
+
}
|
|
7327
|
+
);
|
|
4488
7328
|
}
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
alternate: reverse,
|
|
4494
|
-
label: `${algorithm}/authority-seeded`,
|
|
4495
|
-
onProgress: onProgress === void 0 ? void 0 : (done) => {
|
|
4496
|
-
onProgress(r.maxIterations + done, total);
|
|
7329
|
+
if (!s.flags.finiteWeights) {
|
|
7330
|
+
throw new WebGpuGraphError("E_UNSUPPORTED", `${ALGORITHM}: a NaN or infinite weight has no shortest path`, {
|
|
7331
|
+
feature: "closenessCentrality.nonFiniteWeights"
|
|
7332
|
+
});
|
|
4497
7333
|
}
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
precision: "f32"
|
|
4505
|
-
};
|
|
7334
|
+
return weightedRoute(ctx, s, scores, options);
|
|
7335
|
+
}
|
|
7336
|
+
return sweepRoute(ctx, s, scores, levelsPerSubmit, options, tuning);
|
|
7337
|
+
}
|
|
7338
|
+
function closenessCentrality(ctx, s, options) {
|
|
7339
|
+
return closenessWithTuning(ctx, s, options, {});
|
|
4506
7340
|
}
|
|
4507
7341
|
const U32_MAX = 4294967295;
|
|
4508
7342
|
async function prepareHistogram(scope) {
|
|
@@ -4646,128 +7480,16 @@ class CountingSortPlannerImpl {
|
|
|
4646
7480
|
this.dispatches += 1;
|
|
4647
7481
|
}
|
|
4648
7482
|
}
|
|
4649
|
-
|
|
4650
|
-
function radixHistBytes(count, wg) {
|
|
4651
|
-
return 4 * RADIX_BINS * Math.ceil(count / wg);
|
|
4652
|
-
}
|
|
4653
|
-
async function prepareRadixSort(scope) {
|
|
4654
|
-
const hist = await scope.pipelines.kernel(kernelSpec("radix-hist"));
|
|
4655
|
-
const scatter = await scope.pipelines.kernel(kernelSpec("radix-scatter"));
|
|
4656
|
-
const scan = await prepareScan(scope);
|
|
4657
|
-
return new RadixSortPlannerImpl(scope, hist, scatter, scan);
|
|
4658
|
-
}
|
|
4659
|
-
function checkBinding(argument, binding, bytes) {
|
|
4660
|
-
if (binding.size < bytes) {
|
|
4661
|
-
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `radixSort: ${argument} is smaller than ${bytes} bytes`, {
|
|
4662
|
-
argument,
|
|
4663
|
-
value: binding.size,
|
|
4664
|
-
expected: bytes
|
|
4665
|
-
});
|
|
4666
|
-
}
|
|
4667
|
-
}
|
|
4668
|
-
function checkRecordArguments(keys, vals, count, bits, scratch, wg) {
|
|
4669
|
-
if (bits !== 8 && bits !== 16 && bits !== 24 && bits !== 32) {
|
|
4670
|
-
throw new WebGpuGraphError("E_INVALID_ARGUMENT", "radixSort: bits must be 8, 16, 24 or 32", {
|
|
4671
|
-
argument: "bits",
|
|
4672
|
-
value: bits,
|
|
4673
|
-
expected: [8, 16, 24, 32]
|
|
4674
|
-
});
|
|
4675
|
-
}
|
|
4676
|
-
if (!Number.isSafeInteger(count) || count < 0 || count > U32_MAX$2) {
|
|
4677
|
-
throw new WebGpuGraphError("E_INVALID_ARGUMENT", "radixSort: count must be a non-negative integer below 2^32", {
|
|
4678
|
-
argument: "count",
|
|
4679
|
-
value: count
|
|
4680
|
-
});
|
|
4681
|
-
}
|
|
4682
|
-
const pairBytes = 4 * count;
|
|
4683
|
-
checkBinding("keys", keys, pairBytes);
|
|
4684
|
-
checkBinding("vals", vals, pairBytes);
|
|
4685
|
-
checkBinding("scratch.keys", scratch.keys, pairBytes);
|
|
4686
|
-
checkBinding("scratch.vals", scratch.vals, pairBytes);
|
|
4687
|
-
const tableBytes = radixHistBytes(count, wg);
|
|
4688
|
-
checkBinding("scratch.hist", scratch.hist, tableBytes);
|
|
4689
|
-
checkBinding("scratch.offsets", scratch.offsets, tableBytes);
|
|
4690
|
-
}
|
|
4691
|
-
class RadixSortPlannerImpl {
|
|
4692
|
-
/**
|
|
4693
|
-
* Wraps the resolved kernels; use prepareRadixSort().
|
|
4694
|
-
* @param scope - the caller's scope
|
|
4695
|
-
* @param hist - the `radix-hist` kernel
|
|
4696
|
-
* @param scatter - the `radix-scatter` kernel
|
|
4697
|
-
* @param scan - the scan planner of the same scope
|
|
4698
|
-
*/
|
|
4699
|
-
constructor(scope, hist, scatter, scan) {
|
|
4700
|
-
this.dispatches = 0;
|
|
4701
|
-
this.scope = scope;
|
|
4702
|
-
this.hist = hist;
|
|
4703
|
-
this.scatter = scatter;
|
|
4704
|
-
this.scan = scan;
|
|
4705
|
-
}
|
|
4706
|
-
/**
|
|
4707
|
-
* Dispatches the last record() issued.
|
|
4708
|
-
* @returns the count
|
|
4709
|
-
*/
|
|
4710
|
-
get lastDispatches() {
|
|
4711
|
-
return this.dispatches;
|
|
4712
|
-
}
|
|
4713
|
-
/**
|
|
4714
|
-
* Records the passes into the pass (see the interface).
|
|
4715
|
-
* @param pass - the compute pass
|
|
4716
|
-
* @param keys - the keys
|
|
4717
|
-
* @param vals - the values
|
|
4718
|
-
* @param count - the pair count
|
|
4719
|
-
* @param bits - the key width
|
|
4720
|
-
* @param scratch - the second pair, the histogram table and its scanned twin
|
|
4721
|
-
* @returns the pair the result lives in
|
|
4722
|
-
*/
|
|
4723
|
-
record(pass, keys, vals, count, bits, scratch) {
|
|
4724
|
-
const wg = this.scope.workgroupSize;
|
|
4725
|
-
checkRecordArguments(keys, vals, count, bits, scratch, wg);
|
|
4726
|
-
if (count === 0) {
|
|
4727
|
-
this.dispatches = 0;
|
|
4728
|
-
return { keys, vals };
|
|
4729
|
-
}
|
|
4730
|
-
const groups = Math.ceil(count / wg);
|
|
4731
|
-
const plan = plan1d(count, wg, this.scope.caps);
|
|
4732
|
-
const tableWords = RADIX_BINS * groups;
|
|
4733
|
-
const tableBytes = 4 * tableWords;
|
|
4734
|
-
const histTable = { ...scratch.hist, size: tableBytes };
|
|
4735
|
-
const offsets = { ...scratch.offsets, size: tableBytes };
|
|
4736
|
-
let src = { keys, vals };
|
|
4737
|
-
let dst = { keys: scratch.keys, vals: scratch.vals };
|
|
4738
|
-
let dispatches = 0;
|
|
4739
|
-
const passes = bits / RADIX_DIGIT_BITS;
|
|
4740
|
-
for (let p = 0; p < passes; p++) {
|
|
4741
|
-
const params = this.scope.params(RADIX_PARAMS, { count, shift: RADIX_DIGIT_BITS * p, groups, pad0: 0 });
|
|
4742
|
-
const histBound = this.hist.bind({ keys: src.keys, hist: histTable, P: params.binding });
|
|
4743
|
-
this.hist.dispatch(pass, histBound, plan, [params.offset]);
|
|
4744
|
-
this.scan.record(pass, histTable, tableWords, offsets);
|
|
4745
|
-
const scatterBound = this.scatter.bind({
|
|
4746
|
-
keys: src.keys,
|
|
4747
|
-
vals: src.vals,
|
|
4748
|
-
offsets,
|
|
4749
|
-
keysOut: dst.keys,
|
|
4750
|
-
valsOut: dst.vals,
|
|
4751
|
-
P: params.binding
|
|
4752
|
-
});
|
|
4753
|
-
this.scatter.dispatch(pass, scatterBound, plan, [params.offset]);
|
|
4754
|
-
dispatches += 2 + this.scan.lastDispatches;
|
|
4755
|
-
[src, dst] = [dst, src];
|
|
4756
|
-
}
|
|
4757
|
-
this.dispatches = dispatches;
|
|
4758
|
-
return src;
|
|
4759
|
-
}
|
|
4760
|
-
}
|
|
4761
|
-
function nextPow2(x2) {
|
|
7483
|
+
function nextPow2(x) {
|
|
4762
7484
|
let p = 1;
|
|
4763
|
-
while (p <
|
|
7485
|
+
while (p < x) {
|
|
4764
7486
|
p *= 2;
|
|
4765
7487
|
}
|
|
4766
7488
|
return p;
|
|
4767
7489
|
}
|
|
4768
|
-
function floorPow2$1(
|
|
7490
|
+
function floorPow2$1(x) {
|
|
4769
7491
|
let p = 1;
|
|
4770
|
-
while (p * 2 <=
|
|
7492
|
+
while (p * 2 <= x) {
|
|
4771
7493
|
p *= 2;
|
|
4772
7494
|
}
|
|
4773
7495
|
return p;
|
|
@@ -4951,15 +7673,15 @@ function resolveCenter(center) {
|
|
|
4951
7673
|
return out;
|
|
4952
7674
|
}
|
|
4953
7675
|
for (let axis = 0; axis < 3 && axis < center.length; axis++) {
|
|
4954
|
-
const
|
|
4955
|
-
if (!Number.isFinite(
|
|
7676
|
+
const v = center[axis];
|
|
7677
|
+
if (!Number.isFinite(v)) {
|
|
4956
7678
|
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `center[${axis}] is not finite`, {
|
|
4957
7679
|
argument: "center",
|
|
4958
|
-
value:
|
|
7680
|
+
value: v,
|
|
4959
7681
|
expected: "finite components"
|
|
4960
7682
|
});
|
|
4961
7683
|
}
|
|
4962
|
-
out[axis] =
|
|
7684
|
+
out[axis] = v;
|
|
4963
7685
|
}
|
|
4964
7686
|
return out;
|
|
4965
7687
|
}
|
|
@@ -4998,13 +7720,13 @@ function seedPositions(s, positions, seed, dim, scale, center, range) {
|
|
|
4998
7720
|
for (let i = 0; i < n; i++) {
|
|
4999
7721
|
let finite2 = true;
|
|
5000
7722
|
for (let axis = 0; axis < dim; axis++) {
|
|
5001
|
-
const
|
|
5002
|
-
if (Number.isFinite(
|
|
5003
|
-
if (
|
|
5004
|
-
lo[axis] =
|
|
7723
|
+
const v = positions[3 * i + axis];
|
|
7724
|
+
if (Number.isFinite(v)) {
|
|
7725
|
+
if (v < lo[axis]) {
|
|
7726
|
+
lo[axis] = v;
|
|
5005
7727
|
}
|
|
5006
|
-
if (
|
|
5007
|
-
hi[axis] =
|
|
7728
|
+
if (v > hi[axis]) {
|
|
7729
|
+
hi[axis] = v;
|
|
5008
7730
|
}
|
|
5009
7731
|
} else {
|
|
5010
7732
|
finite2 = false;
|
|
@@ -5046,8 +7768,8 @@ function seedPositions(s, positions, seed, dim, scale, center, range) {
|
|
|
5046
7768
|
const at = 3 * i + axis;
|
|
5047
7769
|
if (!Number.isFinite(positions[at])) {
|
|
5048
7770
|
const u = rng.next();
|
|
5049
|
-
const
|
|
5050
|
-
positions[at] =
|
|
7771
|
+
const v = boxLo[axis] + u * (boxHi[axis] - boxLo[axis]);
|
|
7772
|
+
positions[at] = v * scale + c[axis];
|
|
5051
7773
|
}
|
|
5052
7774
|
}
|
|
5053
7775
|
if (dim === 2) {
|
|
@@ -5188,11 +7910,11 @@ function centerOf(options) {
|
|
|
5188
7910
|
return out;
|
|
5189
7911
|
}
|
|
5190
7912
|
for (let axis = 0; axis < 3 && axis < center.length; axis++) {
|
|
5191
|
-
const
|
|
5192
|
-
if (!Number.isFinite(
|
|
5193
|
-
throw invalidArgument("center",
|
|
7913
|
+
const v = center[axis];
|
|
7914
|
+
if (!Number.isFinite(v)) {
|
|
7915
|
+
throw invalidArgument("center", v, "finite components", `center[${axis}] is not finite`);
|
|
5194
7916
|
}
|
|
5195
|
-
out[axis] =
|
|
7917
|
+
out[axis] = v;
|
|
5196
7918
|
}
|
|
5197
7919
|
return out;
|
|
5198
7920
|
}
|
|
@@ -5840,12 +8562,12 @@ class ForceSimulation {
|
|
|
5840
8562
|
);
|
|
5841
8563
|
}
|
|
5842
8564
|
let unpinned = false;
|
|
5843
|
-
for (let
|
|
5844
|
-
if (this.fixedWords[
|
|
8565
|
+
for (let w = 0; w < words && !unpinned; w++) {
|
|
8566
|
+
if (this.fixedWords[w] === mask[w]) {
|
|
5845
8567
|
continue;
|
|
5846
8568
|
}
|
|
5847
|
-
const last = Math.min(n,
|
|
5848
|
-
for (let i =
|
|
8569
|
+
const last = Math.min(n, w * 32 + 32);
|
|
8570
|
+
for (let i = w * 32; i < last; i++) {
|
|
5849
8571
|
if (maskTest(this.fixedWords, i) && !maskTest(mask, i)) {
|
|
5850
8572
|
unpinned = true;
|
|
5851
8573
|
break;
|
|
@@ -5867,7 +8589,7 @@ class ForceSimulation {
|
|
|
5867
8589
|
* @param y - scene y
|
|
5868
8590
|
* @param z - scene z (ignored on the device in 2D)
|
|
5869
8591
|
*/
|
|
5870
|
-
setPosition(index,
|
|
8592
|
+
setPosition(index, x, y, z) {
|
|
5871
8593
|
this.assertLoaded();
|
|
5872
8594
|
const { n } = this;
|
|
5873
8595
|
if (!Number.isInteger(index) || index < 0 || index >= n) {
|
|
@@ -5878,10 +8600,10 @@ class ForceSimulation {
|
|
|
5878
8600
|
`setPosition(${index}): index out of range`
|
|
5879
8601
|
);
|
|
5880
8602
|
}
|
|
5881
|
-
if (!Number.isFinite(
|
|
8603
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) {
|
|
5882
8604
|
throw invalidArgument(
|
|
5883
8605
|
"position",
|
|
5884
|
-
[
|
|
8606
|
+
[x, y, z],
|
|
5885
8607
|
"finite coordinates",
|
|
5886
8608
|
"setPosition: coordinates must be finite"
|
|
5887
8609
|
);
|
|
@@ -5890,13 +8612,13 @@ class ForceSimulation {
|
|
|
5890
8612
|
if (owner === null || buffers === null) {
|
|
5891
8613
|
throw new WebGpuGraphError("E_NOT_LOADED", `${this.label}: load() first`, { state: this.stateValue });
|
|
5892
8614
|
}
|
|
5893
|
-
owner[3 * index] =
|
|
5894
|
-
owner[3 * index + 1] =
|
|
8615
|
+
owner[3 * index] = x;
|
|
8616
|
+
owner[3 * index + 1] = y;
|
|
5895
8617
|
owner[3 * index + 2] = z;
|
|
5896
8618
|
const [cx, cy, cz] = this.center;
|
|
5897
8619
|
const layout = new Float32Array([
|
|
5898
|
-
(
|
|
5899
|
-
(
|
|
8620
|
+
(x - cx) / this.scale,
|
|
8621
|
+
(y - cy) / this.scale,
|
|
5900
8622
|
this.dimValue === 2 ? 0 : (z - cz) / this.scale
|
|
5901
8623
|
]);
|
|
5902
8624
|
this.ctx.device.queue.writeBuffer(buffers.positions, 16 * index, layout);
|
|
@@ -6517,17 +9239,17 @@ class ForceSimulation {
|
|
|
6517
9239
|
const lo = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY];
|
|
6518
9240
|
const hi = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY];
|
|
6519
9241
|
for (let i = 0; i < n; i++) {
|
|
6520
|
-
const
|
|
6521
|
-
const
|
|
9242
|
+
const x = packed[4 * i];
|
|
9243
|
+
const y = packed[4 * i + 1];
|
|
6522
9244
|
const z = packed[4 * i + 2];
|
|
6523
|
-
sx +=
|
|
6524
|
-
sy +=
|
|
9245
|
+
sx += x;
|
|
9246
|
+
sy += y;
|
|
6525
9247
|
sz += z;
|
|
6526
|
-
lo[0] = Math.min(lo[0],
|
|
6527
|
-
lo[1] = Math.min(lo[1],
|
|
9248
|
+
lo[0] = Math.min(lo[0], x);
|
|
9249
|
+
lo[1] = Math.min(lo[1], y);
|
|
6528
9250
|
lo[2] = Math.min(lo[2], z);
|
|
6529
|
-
hi[0] = Math.max(hi[0],
|
|
6530
|
-
hi[1] = Math.max(hi[1],
|
|
9251
|
+
hi[0] = Math.max(hi[0], x);
|
|
9252
|
+
hi[1] = Math.max(hi[1], y);
|
|
6531
9253
|
hi[2] = Math.max(hi[2], z);
|
|
6532
9254
|
}
|
|
6533
9255
|
const mx = sx / n;
|
|
@@ -6788,14 +9510,14 @@ function invalid$1(argument, value, expected, message) {
|
|
|
6788
9510
|
}
|
|
6789
9511
|
function checkMass(mass, source) {
|
|
6790
9512
|
for (let i = 0; i < mass.length; i++) {
|
|
6791
|
-
const
|
|
6792
|
-
if (!Number.isFinite(
|
|
9513
|
+
const v = mass[i];
|
|
9514
|
+
if (!Number.isFinite(v) || v <= 0) {
|
|
6793
9515
|
throw new WebGpuGraphError(
|
|
6794
9516
|
"E_INVALID_ARGUMENT",
|
|
6795
|
-
`${source}: mass[${i}] = ${
|
|
9517
|
+
`${source}: mass[${i}] = ${v} is not a finite number > 0`,
|
|
6796
9518
|
{
|
|
6797
9519
|
argument: "nodeMass",
|
|
6798
|
-
value:
|
|
9520
|
+
value: v,
|
|
6799
9521
|
index: i,
|
|
6800
9522
|
expected: "finite masses > 0"
|
|
6801
9523
|
}
|
|
@@ -7061,13 +9783,13 @@ function pickCenter(given, fallback) {
|
|
|
7061
9783
|
if (length !== 2 && length !== 3) {
|
|
7062
9784
|
throw invalid("center", given, expected);
|
|
7063
9785
|
}
|
|
7064
|
-
const
|
|
7065
|
-
const
|
|
9786
|
+
const x = given[0];
|
|
9787
|
+
const y = given[1];
|
|
7066
9788
|
const z = length === 3 ? given[2] : 0;
|
|
7067
|
-
if (typeof
|
|
9789
|
+
if (typeof x !== "number" || typeof y !== "number" || typeof z !== "number" || !Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) {
|
|
7068
9790
|
throw invalid("center", given, expected);
|
|
7069
9791
|
}
|
|
7070
|
-
return [
|
|
9792
|
+
return [x, y, z];
|
|
7071
9793
|
}
|
|
7072
9794
|
function pickSeed(given, fallback) {
|
|
7073
9795
|
if (given === void 0) {
|
|
@@ -7669,9 +10391,9 @@ function resolveForceAtlas2Options(options, previous) {
|
|
|
7669
10391
|
}
|
|
7670
10392
|
const resolved = {
|
|
7671
10393
|
maxIter: pickNumber("maxIter", o.maxIter, base.maxIter, isPositiveInteger, "an integer >= 1"),
|
|
7672
|
-
jitterTolerance: pickNumber("jitterTolerance", o.jitterTolerance, base.jitterTolerance, (
|
|
7673
|
-
scalingRatio: pickNumber("scalingRatio", o.scalingRatio, base.scalingRatio, (
|
|
7674
|
-
gravity: pickNumber("gravity", o.gravity, base.gravity, (
|
|
10394
|
+
jitterTolerance: pickNumber("jitterTolerance", o.jitterTolerance, base.jitterTolerance, (v) => v > 0, "> 0"),
|
|
10395
|
+
scalingRatio: pickNumber("scalingRatio", o.scalingRatio, base.scalingRatio, (v) => v > 0, "> 0"),
|
|
10396
|
+
gravity: pickNumber("gravity", o.gravity, base.gravity, (v) => v >= 0, ">= 0"),
|
|
7675
10397
|
strongGravity: pickBoolean("strongGravity", o.strongGravity, base.strongGravity),
|
|
7676
10398
|
distributedAction: pickBoolean("distributedAction", o.distributedAction, base.distributedAction),
|
|
7677
10399
|
linlog: pickBoolean("linlog", o.linlog, base.linlog),
|
|
@@ -7680,10 +10402,10 @@ function resolveForceAtlas2Options(options, previous) {
|
|
|
7680
10402
|
weight: o.weight === void 0 ? base.weight : o.weight,
|
|
7681
10403
|
dissuadeHubs: pickBoolean("dissuadeHubs", o.dissuadeHubs, base.dissuadeHubs),
|
|
7682
10404
|
dim: pickDim(o.dim, base.dim),
|
|
7683
|
-
scale: pickNumber("scale", o.scale, base.scale, (
|
|
10405
|
+
scale: pickNumber("scale", o.scale, base.scale, (v) => v > 0, "> 0"),
|
|
7684
10406
|
center: pickCenter(o.center, base.center),
|
|
7685
10407
|
seed: pickSeed(o.seed, base.seed),
|
|
7686
|
-
settleThreshold: pickNumber("settleThreshold", o.settleThreshold, base.settleThreshold, (
|
|
10408
|
+
settleThreshold: pickNumber("settleThreshold", o.settleThreshold, base.settleThreshold, (v) => v >= 0, ">= 0"),
|
|
7687
10409
|
settleWindow: pickNumber(
|
|
7688
10410
|
"settleWindow",
|
|
7689
10411
|
o.settleWindow,
|
|
@@ -7695,7 +10417,7 @@ function resolveForceAtlas2Options(options, previous) {
|
|
|
7695
10417
|
"iterationsPerStep",
|
|
7696
10418
|
o.iterationsPerStep,
|
|
7697
10419
|
base.iterationsPerStep,
|
|
7698
|
-
(
|
|
10420
|
+
(v) => isPositiveInteger(v) && v <= MAX_ITERATIONS_PER_STEP,
|
|
7699
10421
|
`an integer in [1, ${MAX_ITERATIONS_PER_STEP}]`
|
|
7700
10422
|
),
|
|
7701
10423
|
maxInFlight: pickNumber("maxInFlight", o.maxInFlight, base.maxInFlight, isPositiveInteger, "an integer >= 1")
|
|
@@ -7725,7 +10447,7 @@ function resolveLayoutTuning(tuning) {
|
|
|
7725
10447
|
"nearMax",
|
|
7726
10448
|
t.nearMax,
|
|
7727
10449
|
LAYOUT_TUNING_DEFAULTS.nearMax,
|
|
7728
|
-
(
|
|
10450
|
+
(v) => isPositiveInteger(v) && v >= 2,
|
|
7729
10451
|
"an integer >= 2"
|
|
7730
10452
|
),
|
|
7731
10453
|
deterministic: pickBoolean("deterministic", t.deterministic, LAYOUT_TUNING_DEFAULTS.deterministic),
|
|
@@ -7747,7 +10469,7 @@ function resolveLayoutTuning(tuning) {
|
|
|
7747
10469
|
"extentFactor",
|
|
7748
10470
|
t.extentFactor,
|
|
7749
10471
|
LAYOUT_TUNING_DEFAULTS.extentFactor,
|
|
7750
|
-
(
|
|
10472
|
+
(v) => v > 0,
|
|
7751
10473
|
"> 0"
|
|
7752
10474
|
),
|
|
7753
10475
|
compat
|
|
@@ -8374,7 +11096,7 @@ function resolveK(given, fallback) {
|
|
|
8374
11096
|
if (given === null || given === 0 || Number.isNaN(given)) {
|
|
8375
11097
|
return null;
|
|
8376
11098
|
}
|
|
8377
|
-
return pickNumber("k", given, 1, (
|
|
11099
|
+
return pickNumber("k", given, 1, (v) => v > 0, "> 0 or null");
|
|
8378
11100
|
}
|
|
8379
11101
|
function pickCooling(given, fallback) {
|
|
8380
11102
|
if (given === void 0) {
|
|
@@ -8407,16 +11129,16 @@ function resolveFruchtermanReingoldOptions(options, previous) {
|
|
|
8407
11129
|
"iterations",
|
|
8408
11130
|
o.iterations,
|
|
8409
11131
|
iterationsFallback,
|
|
8410
|
-
(
|
|
11132
|
+
(v) => Number.isInteger(v) && v >= 0,
|
|
8411
11133
|
"an integer >= 0"
|
|
8412
11134
|
),
|
|
8413
11135
|
cooling,
|
|
8414
11136
|
fixed,
|
|
8415
11137
|
dim: pickDim(o.dim, base.dim),
|
|
8416
|
-
scale: pickNumber("scale", o.scale, base.scale, (
|
|
11138
|
+
scale: pickNumber("scale", o.scale, base.scale, (v) => v > 0, "> 0"),
|
|
8417
11139
|
center: pickCenter(o.center, base.center),
|
|
8418
11140
|
seed: pickSeed(o.seed, base.seed),
|
|
8419
|
-
settleThreshold: pickNumber("settleThreshold", o.settleThreshold, base.settleThreshold, (
|
|
11141
|
+
settleThreshold: pickNumber("settleThreshold", o.settleThreshold, base.settleThreshold, (v) => v >= 0, ">= 0"),
|
|
8420
11142
|
settleWindow: pickNumber(
|
|
8421
11143
|
"settleWindow",
|
|
8422
11144
|
o.settleWindow,
|
|
@@ -8428,7 +11150,7 @@ function resolveFruchtermanReingoldOptions(options, previous) {
|
|
|
8428
11150
|
"iterationsPerStep",
|
|
8429
11151
|
o.iterationsPerStep,
|
|
8430
11152
|
base.iterationsPerStep,
|
|
8431
|
-
(
|
|
11153
|
+
(v) => isPositiveInteger(v) && v <= MAX_ITERATIONS_PER_STEP,
|
|
8432
11154
|
`an integer in [1, ${MAX_ITERATIONS_PER_STEP}]`
|
|
8433
11155
|
),
|
|
8434
11156
|
maxInFlight: pickNumber("maxInFlight", o.maxInFlight, base.maxInFlight, isPositiveInteger, "an integer >= 1")
|
|
@@ -9058,22 +11780,22 @@ function resolveSpringElectricalOptions(options, previous) {
|
|
|
9058
11780
|
);
|
|
9059
11781
|
}
|
|
9060
11782
|
const resolved = {
|
|
9061
|
-
springLength: pickNumber("springLength", o.springLength, base.springLength, (
|
|
11783
|
+
springLength: pickNumber("springLength", o.springLength, base.springLength, (v) => v > 0, "> 0"),
|
|
9062
11784
|
springCoefficient: pickNullable(
|
|
9063
11785
|
"springCoefficient",
|
|
9064
11786
|
o.springCoefficient,
|
|
9065
11787
|
base.springCoefficient,
|
|
9066
|
-
(
|
|
11788
|
+
(v) => v > 0,
|
|
9067
11789
|
"> 0"
|
|
9068
11790
|
),
|
|
9069
11791
|
gravity: pickNullable("gravity", o.gravity, base.gravity, () => true, "a finite number (negative repels)"),
|
|
9070
|
-
dragCoefficient: pickNumber("dragCoefficient", o.dragCoefficient, base.dragCoefficient, (
|
|
9071
|
-
timeStep: pickNumber("timeStep", o.timeStep, base.timeStep, (
|
|
11792
|
+
dragCoefficient: pickNumber("dragCoefficient", o.dragCoefficient, base.dragCoefficient, (v) => v >= 0, ">= 0"),
|
|
11793
|
+
timeStep: pickNumber("timeStep", o.timeStep, base.timeStep, (v) => v > 0, "> 0"),
|
|
9072
11794
|
dim: pickDim(o.dim, base.dim),
|
|
9073
|
-
scale: pickNumber("scale", o.scale, base.scale, (
|
|
11795
|
+
scale: pickNumber("scale", o.scale, base.scale, (v) => v > 0, "> 0"),
|
|
9074
11796
|
center: pickCenter(o.center, base.center),
|
|
9075
11797
|
seed: pickSeed(o.seed, base.seed),
|
|
9076
|
-
settleThreshold: pickNumber("settleThreshold", o.settleThreshold, base.settleThreshold, (
|
|
11798
|
+
settleThreshold: pickNumber("settleThreshold", o.settleThreshold, base.settleThreshold, (v) => v >= 0, ">= 0"),
|
|
9077
11799
|
settleWindow: pickNumber(
|
|
9078
11800
|
"settleWindow",
|
|
9079
11801
|
o.settleWindow,
|
|
@@ -9085,7 +11807,7 @@ function resolveSpringElectricalOptions(options, previous) {
|
|
|
9085
11807
|
"iterationsPerStep",
|
|
9086
11808
|
o.iterationsPerStep,
|
|
9087
11809
|
base.iterationsPerStep,
|
|
9088
|
-
(
|
|
11810
|
+
(v) => isPositiveInteger(v) && v <= MAX_ITERATIONS_PER_STEP,
|
|
9089
11811
|
`an integer in [1, ${MAX_ITERATIONS_PER_STEP}]`
|
|
9090
11812
|
),
|
|
9091
11813
|
maxInFlight: pickNumber("maxInFlight", o.maxInFlight, base.maxInFlight, isPositiveInteger, "an integer >= 1")
|
|
@@ -9718,6 +12440,51 @@ function createAccelerator(ctx, options) {
|
|
|
9718
12440
|
ctx.assertReady();
|
|
9719
12441
|
return await connectedComponents(ctx, gs, o);
|
|
9720
12442
|
},
|
|
12443
|
+
/**
|
|
12444
|
+
* Breadth-first search on the device (spec 8.4; P8-T13). The result is bitwise reproducible (P8 PD-14).
|
|
12445
|
+
* @param gs - the snapshot
|
|
12446
|
+
* @param source - the source node index
|
|
12447
|
+
* @param o - the seam's `BfsOptions` (`maxDepth`)
|
|
12448
|
+
* @returns depth, parent, the level-grouped order, visitedCount, levels and switches (spec 3.3 line 830)
|
|
12449
|
+
*/
|
|
12450
|
+
async breadthFirstSearch(gs, source, o) {
|
|
12451
|
+
ctx.assertReady();
|
|
12452
|
+
return await breadthFirstSearch(ctx, gs, source, o);
|
|
12453
|
+
},
|
|
12454
|
+
/**
|
|
12455
|
+
* Single-source shortest paths on the device (spec 8.4; P8-T13): the near-far queue over f32 distances, or the
|
|
12456
|
+
* breadth-first route when every weight is one.
|
|
12457
|
+
* @param gs - the snapshot
|
|
12458
|
+
* @param source - the source node index
|
|
12459
|
+
* @param o - the seam's `SsspOptions` (`cutoff`, `weights`)
|
|
12460
|
+
* @returns dist, predArc and reachedCount (spec 3.3 line 831)
|
|
12461
|
+
*/
|
|
12462
|
+
async sssp(gs, source, o) {
|
|
12463
|
+
ctx.assertReady();
|
|
12464
|
+
return await sssp(ctx, gs, source, o);
|
|
12465
|
+
},
|
|
12466
|
+
/**
|
|
12467
|
+
* Bellman-Ford on the device with negative-cycle detection (spec 8.4; P8-T13).
|
|
12468
|
+
* @param gs - the snapshot
|
|
12469
|
+
* @param source - the source node index
|
|
12470
|
+
* @param o - the seam's `SsspOptions` (`cutoff`, `weights`)
|
|
12471
|
+
* @returns dist, predArc, reachedCount and hasNegativeCycle (spec 3.3 line 832)
|
|
12472
|
+
*/
|
|
12473
|
+
async bellmanFord(gs, source, o) {
|
|
12474
|
+
ctx.assertReady();
|
|
12475
|
+
return await bellmanFord(ctx, gs, source, o);
|
|
12476
|
+
},
|
|
12477
|
+
/**
|
|
12478
|
+
* Closeness centrality on the device (spec 8.4; P8-T13): the bit-parallel multi-source sweep, or one `sssp`
|
|
12479
|
+
* per source when `weighted`. `maxIterations` / `tolerance` are refused when defined (P8 PD-25).
|
|
12480
|
+
* @param gs - the snapshot
|
|
12481
|
+
* @param o - the seam's placeholder `HitsOptionsLike` (`weighted`)
|
|
12482
|
+
* @returns the f32 scores with `precision: "f32"` (spec 9.7)
|
|
12483
|
+
*/
|
|
12484
|
+
async closenessCentrality(gs, o) {
|
|
12485
|
+
ctx.assertReady();
|
|
12486
|
+
return await closenessCentrality(ctx, gs, o);
|
|
12487
|
+
},
|
|
9721
12488
|
/**
|
|
9722
12489
|
* Destroys every device buffer recorded for the snapshot (spec 4.5); delegates to ctx.release.
|
|
9723
12490
|
* @param s - the snapshot the app is done with
|
|
@@ -9835,12 +12602,15 @@ export {
|
|
|
9835
12602
|
LAYOUT_TUNING_DEFAULTS,
|
|
9836
12603
|
MAX_1D_ITEMS,
|
|
9837
12604
|
MAX_WORKGROUPS_PER_DIM,
|
|
9838
|
-
|
|
12605
|
+
C as PASSTHROUGH_FORMAT_CODES,
|
|
9839
12606
|
SE_DEFAULTS,
|
|
9840
|
-
|
|
9841
|
-
|
|
12607
|
+
D as STORAGE_ALIGN,
|
|
12608
|
+
H as WORKGROUP_SIZE,
|
|
9842
12609
|
WebGpuGraphError,
|
|
12610
|
+
bellmanFord,
|
|
12611
|
+
breadthFirstSearch,
|
|
9843
12612
|
calibrateLayout,
|
|
12613
|
+
closenessCentrality,
|
|
9844
12614
|
connectedComponents,
|
|
9845
12615
|
createAccelerator,
|
|
9846
12616
|
createForceAtlas2,
|
|
@@ -9850,12 +12620,13 @@ export {
|
|
|
9850
12620
|
eigenvectorCentrality,
|
|
9851
12621
|
hasErrorCode,
|
|
9852
12622
|
hits,
|
|
9853
|
-
|
|
12623
|
+
J as isSoftwareAdapter,
|
|
9854
12624
|
isWebGpuGraphError,
|
|
9855
12625
|
katzCentrality,
|
|
9856
12626
|
pageRank,
|
|
9857
12627
|
personalizedPageRank,
|
|
9858
12628
|
seedPositions,
|
|
12629
|
+
sssp,
|
|
9859
12630
|
verifyDevice
|
|
9860
12631
|
};
|
|
9861
12632
|
//# sourceMappingURL=webgpu-graph-algorithms.js.map
|