@graphty/webgpu-graph-algorithms 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -25
- package/dist/browser.js +1 -1
- package/dist/chunks/{context-E6iKaeuJ.js → context-CRbw2Wyo.js} +178 -19
- package/dist/chunks/{context-E6iKaeuJ.js.map → context-CRbw2Wyo.js.map} +1 -1
- package/dist/node.js +1 -1
- package/dist/src/accelerator.d.ts +9 -6
- package/dist/src/accelerator.d.ts.map +1 -1
- package/dist/src/accelerator.js +85 -6
- package/dist/src/accelerator.js.map +1 -1
- package/dist/src/algorithms/components.d.ts +30 -0
- package/dist/src/algorithms/components.d.ts.map +1 -0
- package/dist/src/algorithms/components.js +300 -0
- package/dist/src/algorithms/components.js.map +1 -0
- package/dist/src/algorithms/pagerank.d.ts +39 -0
- package/dist/src/algorithms/pagerank.d.ts.map +1 -0
- package/dist/src/algorithms/pagerank.js +298 -0
- package/dist/src/algorithms/pagerank.js.map +1 -0
- package/dist/src/algorithms/power-iteration.d.ts +109 -0
- package/dist/src/algorithms/power-iteration.d.ts.map +1 -0
- package/dist/src/algorithms/power-iteration.js +206 -0
- package/dist/src/algorithms/power-iteration.js.map +1 -0
- package/dist/src/algorithms/scope.d.ts +26 -0
- package/dist/src/algorithms/scope.d.ts.map +1 -0
- package/dist/src/algorithms/scope.js +41 -0
- package/dist/src/algorithms/scope.js.map +1 -0
- package/dist/src/algorithms/spectral.d.ts +50 -0
- package/dist/src/algorithms/spectral.d.ts.map +1 -0
- package/dist/src/algorithms/spectral.js +247 -0
- package/dist/src/algorithms/spectral.js.map +1 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/kernel/dispatch.d.ts +4 -1
- package/dist/src/kernel/dispatch.d.ts.map +1 -1
- package/dist/src/kernel/dispatch.js +12 -5
- package/dist/src/kernel/dispatch.js.map +1 -1
- package/dist/src/kernels.d.ts +20 -4
- package/dist/src/kernels.d.ts.map +1 -1
- package/dist/src/kernels.js +172 -2
- package/dist/src/kernels.js.map +1 -1
- package/dist/src/memory/residency.d.ts.map +1 -1
- package/dist/src/memory/residency.js +164 -11
- package/dist/src/memory/residency.js.map +1 -1
- package/dist/src/primitives/core-shape.d.ts +41 -0
- package/dist/src/primitives/core-shape.d.ts.map +1 -0
- package/dist/src/primitives/core-shape.js +89 -0
- package/dist/src/primitives/core-shape.js.map +1 -0
- package/dist/src/primitives/segmented-reduce.d.ts.map +1 -1
- package/dist/src/primitives/segmented-reduce.js +4 -30
- package/dist/src/primitives/segmented-reduce.js.map +1 -1
- package/dist/src/primitives/spmv.d.ts +56 -0
- package/dist/src/primitives/spmv.d.ts.map +1 -0
- package/dist/src/primitives/spmv.js +101 -0
- package/dist/src/primitives/spmv.js.map +1 -0
- package/dist/src/types/accelerator.d.ts +14 -2
- package/dist/src/types/accelerator.d.ts.map +1 -1
- package/dist/src/types/algorithms.d.ts +73 -0
- package/dist/src/types/algorithms.d.ts.map +1 -0
- package/dist/src/types/algorithms.js +17 -0
- package/dist/src/types/algorithms.js.map +1 -0
- package/dist/src/wgsl/pr-finalize.wgsl.d.ts +11 -0
- package/dist/src/wgsl/pr-finalize.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/pr-finalize.wgsl.js +36 -0
- package/dist/src/wgsl/pr-finalize.wgsl.js.map +1 -0
- package/dist/src/wgsl/pr-scale.wgsl.d.ts +14 -0
- package/dist/src/wgsl/pr-scale.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/pr-scale.wgsl.js +48 -0
- package/dist/src/wgsl/pr-scale.wgsl.js.map +1 -0
- package/dist/src/wgsl/spmv-pull.wgsl.d.ts +15 -0
- package/dist/src/wgsl/spmv-pull.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/spmv-pull.wgsl.js +47 -0
- package/dist/src/wgsl/spmv-pull.wgsl.js.map +1 -0
- package/dist/src/wgsl/wcc-compress.wgsl.d.ts +9 -0
- package/dist/src/wgsl/wcc-compress.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/wcc-compress.wgsl.js +26 -0
- package/dist/src/wgsl/wcc-compress.wgsl.js.map +1 -0
- package/dist/src/wgsl/wcc-link-edges.wgsl.d.ts +13 -0
- package/dist/src/wgsl/wcc-link-edges.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/wcc-link-edges.wgsl.js +46 -0
- package/dist/src/wgsl/wcc-link-edges.wgsl.js.map +1 -0
- package/dist/src/wgsl/wcc-link-sample.wgsl.d.ts +11 -0
- package/dist/src/wgsl/wcc-link-sample.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/wcc-link-sample.wgsl.js +45 -0
- package/dist/src/wgsl/wcc-link-sample.wgsl.js.map +1 -0
- package/dist/src/wgsl/wcc-sample.wgsl.d.ts +10 -0
- package/dist/src/wgsl/wcc-sample.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/wcc-sample.wgsl.js +18 -0
- package/dist/src/wgsl/wcc-sample.wgsl.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/webgpu-graph-algorithms.js +1550 -29
- package/dist/webgpu-graph-algorithms.js.map +1 -1
- package/package.json +3 -3
- package/src/accelerator.ts +101 -7
- package/src/algorithms/components.ts +348 -0
- package/src/algorithms/pagerank.ts +343 -0
- package/src/algorithms/power-iteration.ts +278 -0
- package/src/algorithms/scope.ts +52 -0
- package/src/algorithms/spectral.ts +300 -0
- package/src/index.ts +18 -0
- package/src/kernel/dispatch.ts +12 -5
- package/src/kernels.ts +206 -5
- package/src/memory/residency.ts +200 -11
- package/src/primitives/core-shape.ts +103 -0
- package/src/primitives/segmented-reduce.ts +4 -36
- package/src/primitives/spmv.ts +155 -0
- package/src/types/accelerator.ts +28 -2
- package/src/types/algorithms.ts +82 -0
- package/src/wgsl/pr-finalize.wgsl.ts +36 -0
- package/src/wgsl/pr-scale.wgsl.ts +48 -0
- package/src/wgsl/spmv-pull.wgsl.ts +47 -0
- package/src/wgsl/wcc-compress.wgsl.ts +26 -0
- package/src/wgsl/wcc-link-edges.wgsl.ts +46 -0
- package/src/wgsl/wcc-link-sample.wgsl.ts +45 -0
- package/src/wgsl/wcc-sample.wgsl.ts +18 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { W as WebGpuGraphError, M as MAX_WORKGROUPS_PER_DIM, a as WGSL_RESERVED_WORDS, S as STATE_HEADER_BYTES, B as BufferUsage, i as isWebGpuGraphError, d as deviceLostError, U as UNIFORM_SLOT_BYTES, F as FA2_DEFAULTS,
|
|
2
|
-
import { A, E, G,
|
|
3
|
-
import { makeMask, maskTest, expandEdges } from "@graphty/graph-format";
|
|
1
|
+
import { W as WebGpuGraphError, M as MAX_WORKGROUPS_PER_DIM, a as WGSL_RESERVED_WORDS, S as STATE_HEADER_BYTES, B as BufferUsage, i as isWebGpuGraphError, d as deviceLostError, U as UNIFORM_SLOT_BYTES, b as U32_MAX, F as FA2_DEFAULTS, c as MAX_ITERATIONS_PER_STEP, e as MAX_1D_ITEMS, h as hasErrorCode, f as FA2_FLAG_FIRST, P as PARTIAL_BYTES, L as LAYOUT_TUNING_DEFAULTS, T as TRACE_RECORD_BYTES } from "./chunks/context-CRbw2Wyo.js";
|
|
2
|
+
import { A, E, G, g, j, k, l } from "./chunks/context-CRbw2Wyo.js";
|
|
3
|
+
import { renumberPartition, makeMask, maskTest, expandEdges } from "@graphty/graph-format";
|
|
4
4
|
function perDimension(caps) {
|
|
5
5
|
return Math.min(MAX_WORKGROUPS_PER_DIM, caps.limits.maxComputeWorkgroupsPerDimension);
|
|
6
6
|
}
|
|
@@ -55,6 +55,19 @@ function plan1d(items, wg, caps) {
|
|
|
55
55
|
assertWorkgroupSize(wg);
|
|
56
56
|
return grid(Math.ceil(items / wg), items, caps);
|
|
57
57
|
}
|
|
58
|
+
function planGridStride(items, wg, caps, maxGroups) {
|
|
59
|
+
assertCount("items", items);
|
|
60
|
+
assertWorkgroupSize(wg);
|
|
61
|
+
if (items === 0) {
|
|
62
|
+
return { x: 0, y: 1, z: 1, items, stride: null };
|
|
63
|
+
}
|
|
64
|
+
const cap = Math.min(caps.software ? 64 : 4096, perDimension(caps));
|
|
65
|
+
const groups = Math.min(Math.ceil(items / wg), cap);
|
|
66
|
+
return { x: groups, y: 1, z: 1, items, stride: groups * wg };
|
|
67
|
+
}
|
|
68
|
+
function groupsOf(plan) {
|
|
69
|
+
return plan.x * plan.y;
|
|
70
|
+
}
|
|
58
71
|
const TYPE_INFO = Object.freeze({
|
|
59
72
|
u32: { size: 4, align: 4, width: 1, scalar: "u32" },
|
|
60
73
|
i32: { size: 4, align: 4, width: 1, scalar: "i32" },
|
|
@@ -247,7 +260,7 @@ ${lines.join("\n")}
|
|
|
247
260
|
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `UniformBlock ${this.name}: unknown field "${key}"`, {
|
|
248
261
|
argument: "values",
|
|
249
262
|
value: key,
|
|
250
|
-
expected: this.laidOut.map((
|
|
263
|
+
expected: this.laidOut.map((f) => f.name).join(", ")
|
|
251
264
|
});
|
|
252
265
|
}
|
|
253
266
|
}
|
|
@@ -337,7 +350,7 @@ ${lines.join("\n")}
|
|
|
337
350
|
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `UniformBlock ${this.name}: unknown field "${field}"`, {
|
|
338
351
|
argument: "field",
|
|
339
352
|
value: field,
|
|
340
|
-
expected: this.laidOut.map((
|
|
353
|
+
expected: this.laidOut.map((f) => f.name).join(", ")
|
|
341
354
|
});
|
|
342
355
|
}
|
|
343
356
|
return layout;
|
|
@@ -674,6 +687,73 @@ fn fill(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid
|
|
|
674
687
|
}
|
|
675
688
|
`
|
|
676
689
|
);
|
|
690
|
+
const prFinalizeWgsl = (
|
|
691
|
+
/* wgsl */
|
|
692
|
+
`
|
|
693
|
+
@compute @workgroup_size(WG)
|
|
694
|
+
fn pr_finalize(@builtin(local_invocation_id) lid: vec3<u32>) {
|
|
695
|
+
var d = 0.0;
|
|
696
|
+
var e = 0.0;
|
|
697
|
+
var m = 0.0;
|
|
698
|
+
for (var g = lid.x; g < P.groups; g = g + WG) {
|
|
699
|
+
d = d + partials[1u + g].danglingMass;
|
|
700
|
+
e = e + partials[1u + g].delta;
|
|
701
|
+
m = m + partials[1u + g].norm;
|
|
702
|
+
}
|
|
703
|
+
let folded = wg_reduce_vec4(vec4f(d, e, m, 0.0), lid.x, 0u);
|
|
704
|
+
if (lid.x == 0u) {
|
|
705
|
+
partials[0].danglingMass = folded.x;
|
|
706
|
+
partials[0].delta = folded.y;
|
|
707
|
+
var norm = folded.z;
|
|
708
|
+
if (NORM_MODE == 2u) { norm = sqrt(max(0.0, folded.z)); }
|
|
709
|
+
partials[0].norm = norm;
|
|
710
|
+
partials[0].iteration = P.iteration;
|
|
711
|
+
let unset = partials[0].firstConverged == U32_MAX;
|
|
712
|
+
if (P.trackConvergence == 1u && P.iteration >= 2u && folded.y < P.convergeThreshold && unset) {
|
|
713
|
+
partials[0].firstConverged = P.iteration - 1u;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
`
|
|
718
|
+
);
|
|
719
|
+
const prScaleWgsl = (
|
|
720
|
+
/* wgsl */
|
|
721
|
+
`
|
|
722
|
+
@compute @workgroup_size(WG)
|
|
723
|
+
fn pr_scale(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
|
|
724
|
+
let u = linear_id(wid, lid.x);
|
|
725
|
+
let inRange = u < P.n;
|
|
726
|
+
var x = 0.0;
|
|
727
|
+
var prev = 0.0;
|
|
728
|
+
if (inRange) { x = rankIn[u]; prev = rankPrev[u]; }
|
|
729
|
+
var dangling = 0.0;
|
|
730
|
+
var delta = 0.0;
|
|
731
|
+
var normTerm = 0.0;
|
|
732
|
+
if (inRange) {
|
|
733
|
+
delta = abs(x - prev);
|
|
734
|
+
if (NORM_MODE == 0u) {
|
|
735
|
+
let divisor = outWeightSum[u];
|
|
736
|
+
if (divisor <= 0.0) { dangling = x; xNorm[u] = 0.0; } else { xNorm[u] = x / divisor; }
|
|
737
|
+
}
|
|
738
|
+
if (NORM_MODE == 1u) { normTerm = abs(x); }
|
|
739
|
+
if (NORM_MODE == 2u) { normTerm = x * x; }
|
|
740
|
+
if (NORM_MODE == 3u) {
|
|
741
|
+
var scale = partials[0].norm;
|
|
742
|
+
if (scale <= 0.0) { scale = 1.0; }
|
|
743
|
+
xNorm[u] = x / scale;
|
|
744
|
+
}
|
|
745
|
+
if (NORM_MODE == 4u) { xNorm[u] = x; }
|
|
746
|
+
}
|
|
747
|
+
let folded = wg_reduce_vec4(vec4f(dangling, delta, normTerm, 0.0), lid.x, 0u);
|
|
748
|
+
if (lid.x == 0u) {
|
|
749
|
+
let slot = 1u + group_id(wid);
|
|
750
|
+
partials[slot].danglingMass = folded.x;
|
|
751
|
+
partials[slot].delta = folded.y;
|
|
752
|
+
partials[slot].norm = folded.z;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
`
|
|
756
|
+
);
|
|
677
757
|
const reduceWgsl = (
|
|
678
758
|
/* wgsl */
|
|
679
759
|
`
|
|
@@ -757,6 +837,150 @@ fn segmented_reduce(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invoca
|
|
|
757
837
|
}
|
|
758
838
|
`
|
|
759
839
|
);
|
|
840
|
+
const spmvPullWgsl = (
|
|
841
|
+
/* wgsl */
|
|
842
|
+
`
|
|
843
|
+
@compute @workgroup_size(WG)
|
|
844
|
+
fn spmv_pull(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
|
|
845
|
+
var dangling = 0.0;
|
|
846
|
+
if (USE_DANGLING) { dangling = partials[0].danglingMass; }
|
|
847
|
+
let first = linear_id(wid, lid.x);
|
|
848
|
+
for (var row = first; row < P.n; row = row + P.stride) {
|
|
849
|
+
let v = select(row, perm[row], USE_PERM);
|
|
850
|
+
let a0 = max(rowPtr[v], P.arcBase);
|
|
851
|
+
let a1 = min(rowPtr[v + 1u], P.arcEnd);
|
|
852
|
+
var acc = 0.0;
|
|
853
|
+
var chunk = 0.0;
|
|
854
|
+
var inChunk = 0u;
|
|
855
|
+
for (var arc = a0; arc < a1; arc = arc + 1u) {
|
|
856
|
+
let nbr = colIdx[arc - P.arcBase]; // \`target\` is a WGSL reserved word (spec 16.2)
|
|
857
|
+
var weight = 1.0;
|
|
858
|
+
if (HAS_WEIGHTS) { weight = weights[arc - P.arcBase]; }
|
|
859
|
+
// two-level sum: 64 terms into chunk, chunk into acc (see the header; no compensation, no select)
|
|
860
|
+
chunk = chunk + (weight * xNorm[nbr]);
|
|
861
|
+
inChunk = inChunk + 1u;
|
|
862
|
+
if (inChunk == 64u) {
|
|
863
|
+
acc = acc + chunk;
|
|
864
|
+
chunk = 0.0;
|
|
865
|
+
inChunk = 0u;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
acc = acc + chunk;
|
|
869
|
+
var pv = P.uniformP;
|
|
870
|
+
if (HAS_PERSONALIZATION) { pv = personalization[v]; }
|
|
871
|
+
rankOut[v] = (P.beta * pv) + (P.alpha * (acc + (dangling * pv)));
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
`
|
|
875
|
+
);
|
|
876
|
+
const wccCompressWgsl = (
|
|
877
|
+
/* wgsl */
|
|
878
|
+
`
|
|
879
|
+
@compute @workgroup_size(WG)
|
|
880
|
+
fn wcc_compress(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
|
|
881
|
+
let first = linear_id(wid, lid.x);
|
|
882
|
+
for (var v = first; v < P.items; v = v + P.stride) {
|
|
883
|
+
var root = atomicLoad(&comp[v]);
|
|
884
|
+
var steps = 0u;
|
|
885
|
+
loop {
|
|
886
|
+
let parent = atomicLoad(&comp[root]);
|
|
887
|
+
if (parent == root) { break; }
|
|
888
|
+
if (steps >= P.maxSteps) { break; }
|
|
889
|
+
steps = steps + 1u;
|
|
890
|
+
root = parent;
|
|
891
|
+
}
|
|
892
|
+
atomicStore(&comp[v], root);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
`
|
|
896
|
+
);
|
|
897
|
+
const wccLinkEdgesWgsl = (
|
|
898
|
+
/* wgsl */
|
|
899
|
+
`
|
|
900
|
+
fn link_pair(a: u32, b: u32) {
|
|
901
|
+
var p1 = atomicLoad(&comp[a]);
|
|
902
|
+
var p2 = atomicLoad(&comp[b]);
|
|
903
|
+
var steps = 0u;
|
|
904
|
+
loop {
|
|
905
|
+
if (p1 == p2) { break; }
|
|
906
|
+
if (steps >= P.maxSteps) { atomicStore(&comp[P.flagIndex], 1u); break; }
|
|
907
|
+
steps = steps + 1u;
|
|
908
|
+
let hi = max(p1, p2);
|
|
909
|
+
let lo = min(p1, p2);
|
|
910
|
+
let pHigh = atomicLoad(&comp[hi]);
|
|
911
|
+
if (pHigh == lo) { break; }
|
|
912
|
+
if (pHigh == hi) {
|
|
913
|
+
let swapped = atomicCompareExchangeWeak(&comp[hi], hi, lo);
|
|
914
|
+
if (swapped.exchanged) { atomicStore(&comp[P.flagIndex], 1u); break; }
|
|
915
|
+
}
|
|
916
|
+
p1 = atomicLoad(&comp[atomicLoad(&comp[hi])]);
|
|
917
|
+
p2 = atomicLoad(&comp[lo]);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
@compute @workgroup_size(WG)
|
|
922
|
+
fn wcc_link_edges(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
|
|
923
|
+
let first = linear_id(wid, lid.x);
|
|
924
|
+
for (var e = first; e < P.items; e = e + P.stride) {
|
|
925
|
+
let u = edgeSrc[e];
|
|
926
|
+
let v = edgeDst[e];
|
|
927
|
+
if (u == v) { continue; }
|
|
928
|
+
if (atomicLoad(&comp[u]) == P.giant && atomicLoad(&comp[v]) == P.giant) { continue; }
|
|
929
|
+
link_pair(u, v);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
`
|
|
933
|
+
);
|
|
934
|
+
const wccLinkSampleWgsl = (
|
|
935
|
+
/* wgsl */
|
|
936
|
+
`
|
|
937
|
+
fn link_pair(a: u32, b: u32) {
|
|
938
|
+
var p1 = atomicLoad(&comp[a]);
|
|
939
|
+
var p2 = atomicLoad(&comp[b]);
|
|
940
|
+
var steps = 0u;
|
|
941
|
+
loop {
|
|
942
|
+
if (p1 == p2) { break; }
|
|
943
|
+
if (steps >= P.maxSteps) { atomicStore(&comp[P.flagIndex], 1u); break; }
|
|
944
|
+
steps = steps + 1u;
|
|
945
|
+
let hi = max(p1, p2);
|
|
946
|
+
let lo = min(p1, p2);
|
|
947
|
+
let pHigh = atomicLoad(&comp[hi]);
|
|
948
|
+
if (pHigh == lo) { break; }
|
|
949
|
+
if (pHigh == hi) {
|
|
950
|
+
let swapped = atomicCompareExchangeWeak(&comp[hi], hi, lo);
|
|
951
|
+
if (swapped.exchanged) { atomicStore(&comp[P.flagIndex], 1u); break; }
|
|
952
|
+
}
|
|
953
|
+
p1 = atomicLoad(&comp[atomicLoad(&comp[hi])]);
|
|
954
|
+
p2 = atomicLoad(&comp[lo]);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
@compute @workgroup_size(WG)
|
|
959
|
+
fn wcc_link_sample(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
|
|
960
|
+
let first = linear_id(wid, lid.x);
|
|
961
|
+
for (var row = first; row < P.items; row = row + P.stride) {
|
|
962
|
+
let v = select(row, perm[row], USE_PERM);
|
|
963
|
+
let a0 = rowPtr[v];
|
|
964
|
+
let a1 = rowPtr[v + 1u];
|
|
965
|
+
if (a0 + P.r < a1) {
|
|
966
|
+
link_pair(v, colIdx[a0 + P.r]);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
`
|
|
971
|
+
);
|
|
972
|
+
const wccSampleWgsl = (
|
|
973
|
+
/* wgsl */
|
|
974
|
+
`
|
|
975
|
+
@compute @workgroup_size(WG)
|
|
976
|
+
fn wcc_sample(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
|
|
977
|
+
let i = linear_id(wid, lid.x);
|
|
978
|
+
if (i >= P.items) { return; }
|
|
979
|
+
let v = lowbias32(i + P.r) % P.n;
|
|
980
|
+
hist[i] = atomicLoad(&comp[v]);
|
|
981
|
+
}
|
|
982
|
+
`
|
|
983
|
+
);
|
|
760
984
|
const RANGE_PARAMS = UniformBlock.define("RangeParams", [
|
|
761
985
|
["start", "u32"],
|
|
762
986
|
["end", "u32"],
|
|
@@ -855,6 +1079,50 @@ const FA2_PARTIAL = UniformBlock.define(
|
|
|
855
1079
|
],
|
|
856
1080
|
{ layout: "storage" }
|
|
857
1081
|
);
|
|
1082
|
+
const SPMV_PARAMS = UniformBlock.define("SpmvParams", [
|
|
1083
|
+
["n", "u32"],
|
|
1084
|
+
["arcBase", "u32"],
|
|
1085
|
+
["arcEnd", "u32"],
|
|
1086
|
+
["stride", "u32"],
|
|
1087
|
+
["alpha", "f32"],
|
|
1088
|
+
["beta", "f32"],
|
|
1089
|
+
["uniformP", "f32"],
|
|
1090
|
+
["pad0", "u32"]
|
|
1091
|
+
]);
|
|
1092
|
+
const PR_PARAMS = UniformBlock.define("PrParams", [
|
|
1093
|
+
["n", "u32"],
|
|
1094
|
+
["groups", "u32"],
|
|
1095
|
+
["iteration", "u32"],
|
|
1096
|
+
["trackConvergence", "u32"],
|
|
1097
|
+
["convergeThreshold", "f32"],
|
|
1098
|
+
["pad0", "u32"],
|
|
1099
|
+
["pad1", "u32"],
|
|
1100
|
+
["pad2", "u32"]
|
|
1101
|
+
]);
|
|
1102
|
+
const PR_PARTIAL = UniformBlock.define(
|
|
1103
|
+
"PrPartial",
|
|
1104
|
+
[
|
|
1105
|
+
["danglingMass", "f32"],
|
|
1106
|
+
["delta", "f32"],
|
|
1107
|
+
["firstConverged", "u32"],
|
|
1108
|
+
["iteration", "u32"],
|
|
1109
|
+
["norm", "f32"],
|
|
1110
|
+
["pad0", "f32"],
|
|
1111
|
+
["pad1", "f32"],
|
|
1112
|
+
["pad2", "f32"]
|
|
1113
|
+
],
|
|
1114
|
+
{ layout: "storage" }
|
|
1115
|
+
);
|
|
1116
|
+
const WCC_PARAMS = UniformBlock.define("WccParams", [
|
|
1117
|
+
["n", "u32"],
|
|
1118
|
+
["items", "u32"],
|
|
1119
|
+
["stride", "u32"],
|
|
1120
|
+
["r", "u32"],
|
|
1121
|
+
["flagIndex", "u32"],
|
|
1122
|
+
["giant", "u32"],
|
|
1123
|
+
["maxSteps", "u32"],
|
|
1124
|
+
["pad0", "u32"]
|
|
1125
|
+
]);
|
|
858
1126
|
function decl(group, binding, name, kind, wgslType) {
|
|
859
1127
|
return { group, binding, name, kind, wgslType };
|
|
860
1128
|
}
|
|
@@ -1027,6 +1295,111 @@ const FA2_TO_SCENE = {
|
|
|
1027
1295
|
snippetSlots: [],
|
|
1028
1296
|
phase: "P3"
|
|
1029
1297
|
};
|
|
1298
|
+
const SPMV_PULL = {
|
|
1299
|
+
id: "spmv-pull",
|
|
1300
|
+
body: spmvPullWgsl,
|
|
1301
|
+
entryPoint: "spmv_pull",
|
|
1302
|
+
bindings: GRAPH_SLOTS.concat(
|
|
1303
|
+
decl(1, 0, "xNorm", "storage-ro", "array<f32>"),
|
|
1304
|
+
decl(1, 1, "rankOut", "storage", "array<f32>"),
|
|
1305
|
+
decl(1, 2, "personalization", "storage-ro", "array<f32>"),
|
|
1306
|
+
decl(1, 3, "partials", "storage-ro", "array<PrPartial>"),
|
|
1307
|
+
decl(2, 0, "P", "uniform", "SpmvParams")
|
|
1308
|
+
),
|
|
1309
|
+
overrideDecls: [
|
|
1310
|
+
{ name: "HAS_PERSONALIZATION", type: "bool", default: false },
|
|
1311
|
+
{ name: "USE_DANGLING", type: "bool", default: false }
|
|
1312
|
+
],
|
|
1313
|
+
uniforms: [SPMV_PARAMS, PR_PARTIAL],
|
|
1314
|
+
needs: [],
|
|
1315
|
+
snippetSlots: [],
|
|
1316
|
+
phase: "P7"
|
|
1317
|
+
};
|
|
1318
|
+
const PR_SCALE = {
|
|
1319
|
+
id: "pr-scale",
|
|
1320
|
+
body: prScaleWgsl,
|
|
1321
|
+
entryPoint: "pr_scale",
|
|
1322
|
+
bindings: [
|
|
1323
|
+
decl(1, 0, "rankIn", "storage-ro", "array<f32>"),
|
|
1324
|
+
decl(1, 1, "rankPrev", "storage-ro", "array<f32>"),
|
|
1325
|
+
decl(1, 2, "outWeightSum", "storage-ro", "array<f32>"),
|
|
1326
|
+
decl(1, 3, "xNorm", "storage", "array<f32>"),
|
|
1327
|
+
decl(1, 4, "partials", "storage", "array<PrPartial>"),
|
|
1328
|
+
decl(2, 0, "P", "uniform", "PrParams")
|
|
1329
|
+
],
|
|
1330
|
+
overrideDecls: [{ name: "NORM_MODE", type: "u32", default: 0 }],
|
|
1331
|
+
uniforms: [PR_PARAMS, PR_PARTIAL],
|
|
1332
|
+
needs: ["subgroups"],
|
|
1333
|
+
snippetSlots: [],
|
|
1334
|
+
phase: "P7"
|
|
1335
|
+
};
|
|
1336
|
+
const PR_FINALIZE = {
|
|
1337
|
+
id: "pr-finalize",
|
|
1338
|
+
body: prFinalizeWgsl,
|
|
1339
|
+
entryPoint: "pr_finalize",
|
|
1340
|
+
bindings: [decl(1, 0, "partials", "storage", "array<PrPartial>"), decl(2, 0, "P", "uniform", "PrParams")],
|
|
1341
|
+
overrideDecls: [{ name: "NORM_MODE", type: "u32", default: 0 }],
|
|
1342
|
+
uniforms: [PR_PARAMS, PR_PARTIAL],
|
|
1343
|
+
needs: ["subgroups"],
|
|
1344
|
+
snippetSlots: [],
|
|
1345
|
+
phase: "P7"
|
|
1346
|
+
};
|
|
1347
|
+
const WCC_LINK_SAMPLE = {
|
|
1348
|
+
id: "wcc-link-sample",
|
|
1349
|
+
body: wccLinkSampleWgsl,
|
|
1350
|
+
entryPoint: "wcc_link_sample",
|
|
1351
|
+
bindings: GRAPH_SLOTS.concat(
|
|
1352
|
+
decl(1, 0, "comp", "storage", "array<atomic<u32>>"),
|
|
1353
|
+
decl(2, 0, "P", "uniform", "WccParams")
|
|
1354
|
+
),
|
|
1355
|
+
overrideDecls: [],
|
|
1356
|
+
uniforms: [WCC_PARAMS],
|
|
1357
|
+
needs: [],
|
|
1358
|
+
snippetSlots: [],
|
|
1359
|
+
phase: "P7"
|
|
1360
|
+
};
|
|
1361
|
+
const WCC_LINK_EDGES = {
|
|
1362
|
+
id: "wcc-link-edges",
|
|
1363
|
+
body: wccLinkEdgesWgsl,
|
|
1364
|
+
entryPoint: "wcc_link_edges",
|
|
1365
|
+
bindings: [
|
|
1366
|
+
decl(1, 0, "edgeSrc", "storage-ro", "array<u32>"),
|
|
1367
|
+
decl(1, 1, "edgeDst", "storage-ro", "array<u32>"),
|
|
1368
|
+
decl(1, 2, "comp", "storage", "array<atomic<u32>>"),
|
|
1369
|
+
decl(2, 0, "P", "uniform", "WccParams")
|
|
1370
|
+
],
|
|
1371
|
+
overrideDecls: [],
|
|
1372
|
+
uniforms: [WCC_PARAMS],
|
|
1373
|
+
needs: [],
|
|
1374
|
+
snippetSlots: [],
|
|
1375
|
+
phase: "P7"
|
|
1376
|
+
};
|
|
1377
|
+
const WCC_COMPRESS = {
|
|
1378
|
+
id: "wcc-compress",
|
|
1379
|
+
body: wccCompressWgsl,
|
|
1380
|
+
entryPoint: "wcc_compress",
|
|
1381
|
+
bindings: [decl(1, 0, "comp", "storage", "array<atomic<u32>>"), decl(2, 0, "P", "uniform", "WccParams")],
|
|
1382
|
+
overrideDecls: [],
|
|
1383
|
+
uniforms: [WCC_PARAMS],
|
|
1384
|
+
needs: [],
|
|
1385
|
+
snippetSlots: [],
|
|
1386
|
+
phase: "P7"
|
|
1387
|
+
};
|
|
1388
|
+
const WCC_SAMPLE = {
|
|
1389
|
+
id: "wcc-sample",
|
|
1390
|
+
body: wccSampleWgsl,
|
|
1391
|
+
entryPoint: "wcc_sample",
|
|
1392
|
+
bindings: [
|
|
1393
|
+
decl(1, 0, "comp", "storage", "array<atomic<u32>>"),
|
|
1394
|
+
decl(1, 1, "hist", "storage", "array<u32>"),
|
|
1395
|
+
decl(2, 0, "P", "uniform", "WccParams")
|
|
1396
|
+
],
|
|
1397
|
+
overrideDecls: [],
|
|
1398
|
+
uniforms: [WCC_PARAMS],
|
|
1399
|
+
needs: [],
|
|
1400
|
+
snippetSlots: [],
|
|
1401
|
+
phase: "P7"
|
|
1402
|
+
};
|
|
1030
1403
|
const REGISTRY = Object.freeze({
|
|
1031
1404
|
degree: DEGREE,
|
|
1032
1405
|
reduce: REDUCE,
|
|
@@ -1037,7 +1410,14 @@ const REGISTRY = Object.freeze({
|
|
|
1037
1410
|
"fa2-repulsion-exact": FA2_REPULSION_EXACT,
|
|
1038
1411
|
"fa2-speed-finalize": FA2_SPEED_FINALIZE,
|
|
1039
1412
|
"fa2-integrate": FA2_INTEGRATE,
|
|
1040
|
-
"fa2-to-scene": FA2_TO_SCENE
|
|
1413
|
+
"fa2-to-scene": FA2_TO_SCENE,
|
|
1414
|
+
"spmv-pull": SPMV_PULL,
|
|
1415
|
+
"pr-scale": PR_SCALE,
|
|
1416
|
+
"pr-finalize": PR_FINALIZE,
|
|
1417
|
+
"wcc-link-sample": WCC_LINK_SAMPLE,
|
|
1418
|
+
"wcc-link-edges": WCC_LINK_EDGES,
|
|
1419
|
+
"wcc-compress": WCC_COMPRESS,
|
|
1420
|
+
"wcc-sample": WCC_SAMPLE
|
|
1041
1421
|
});
|
|
1042
1422
|
const bodyOverrides = /* @__PURE__ */ new Map();
|
|
1043
1423
|
function entryOf(id) {
|
|
@@ -1078,7 +1458,7 @@ function graphOverrides(core, perm, weights) {
|
|
|
1078
1458
|
const resolved = weights === void 0 ? core.weights : weights;
|
|
1079
1459
|
return { USE_PERM: perm !== null, HAS_WEIGHTS: resolved !== null };
|
|
1080
1460
|
}
|
|
1081
|
-
function checkDest(dest, n) {
|
|
1461
|
+
function checkDest$3(dest, n) {
|
|
1082
1462
|
if (dest === void 0) {
|
|
1083
1463
|
return null;
|
|
1084
1464
|
}
|
|
@@ -1095,7 +1475,7 @@ function checkDest(dest, n) {
|
|
|
1095
1475
|
}
|
|
1096
1476
|
);
|
|
1097
1477
|
}
|
|
1098
|
-
function coreOf(ctx, s) {
|
|
1478
|
+
function coreOf$3(ctx, s) {
|
|
1099
1479
|
try {
|
|
1100
1480
|
return ctx.residency.core(s);
|
|
1101
1481
|
} catch (error) {
|
|
@@ -1112,7 +1492,7 @@ function coreOf(ctx, s) {
|
|
|
1112
1492
|
async function degree(ctx, s, options) {
|
|
1113
1493
|
ctx.assertReady();
|
|
1114
1494
|
const n = s.nodeCount;
|
|
1115
|
-
const dest = checkDest(options?.dest, n);
|
|
1495
|
+
const dest = checkDest$3(options?.dest, n);
|
|
1116
1496
|
if (options?.signal?.aborted) {
|
|
1117
1497
|
throw new WebGpuGraphError("E_ABORTED", "degree: the signal was aborted before any work started", {});
|
|
1118
1498
|
}
|
|
@@ -1120,7 +1500,7 @@ async function degree(ctx, s, options) {
|
|
|
1120
1500
|
options?.onProgress?.(1, 1);
|
|
1121
1501
|
return dest ?? new Uint32Array(0);
|
|
1122
1502
|
}
|
|
1123
|
-
const core = coreOf(ctx, s);
|
|
1503
|
+
const core = coreOf$3(ctx, s);
|
|
1124
1504
|
if (s.arcCount === 0) {
|
|
1125
1505
|
const zeros = dest ?? new Uint32Array(n);
|
|
1126
1506
|
zeros.fill(0);
|
|
@@ -1165,8 +1545,8 @@ async function degree(ctx, s, options) {
|
|
|
1165
1545
|
const LOSS_GRACE_MS = 2e3;
|
|
1166
1546
|
async function settled(pending, graceMs) {
|
|
1167
1547
|
let timer;
|
|
1168
|
-
const grace = new Promise((
|
|
1169
|
-
timer = setTimeout(
|
|
1548
|
+
const grace = new Promise((resolve2) => {
|
|
1549
|
+
timer = setTimeout(resolve2, graceMs);
|
|
1170
1550
|
});
|
|
1171
1551
|
await Promise.race([
|
|
1172
1552
|
pending.then(
|
|
@@ -1393,9 +1773,9 @@ class CommandBatch {
|
|
|
1393
1773
|
}
|
|
1394
1774
|
}
|
|
1395
1775
|
let timer;
|
|
1396
|
-
const grace = new Promise((
|
|
1776
|
+
const grace = new Promise((resolve2) => {
|
|
1397
1777
|
timer = setTimeout(() => {
|
|
1398
|
-
|
|
1778
|
+
resolve2(null);
|
|
1399
1779
|
}, LOSS_GRACE_MS);
|
|
1400
1780
|
});
|
|
1401
1781
|
const late = await Promise.race([
|
|
@@ -1638,6 +2018,1068 @@ class UniformRing {
|
|
|
1638
2018
|
}
|
|
1639
2019
|
}
|
|
1640
2020
|
}
|
|
2021
|
+
function algorithmScope(ctx, label, slots) {
|
|
2022
|
+
const lease = ctx.pool.lease();
|
|
2023
|
+
const ring = new UniformRing(ctx.device, ctx.allocator, slots, `${label}/ring`);
|
|
2024
|
+
return {
|
|
2025
|
+
device: ctx.device,
|
|
2026
|
+
caps: ctx.caps,
|
|
2027
|
+
pipelines: ctx.pipelines,
|
|
2028
|
+
pool: ctx.pool,
|
|
2029
|
+
workgroupSize: ctx.workgroupSize,
|
|
2030
|
+
scratch: (byteLength, scratchLabel) => lease.storage(byteLength, `${label}/${scratchLabel}`),
|
|
2031
|
+
params(block, values) {
|
|
2032
|
+
const slot = ring.reserve(1);
|
|
2033
|
+
ring.write(slot, block, values);
|
|
2034
|
+
return { binding: ring.binding(block), offset: ring.offsetOf(slot) };
|
|
2035
|
+
},
|
|
2036
|
+
flush: () => {
|
|
2037
|
+
ring.flush();
|
|
2038
|
+
},
|
|
2039
|
+
dispose() {
|
|
2040
|
+
ring.destroy();
|
|
2041
|
+
lease.release();
|
|
2042
|
+
}
|
|
2043
|
+
};
|
|
2044
|
+
}
|
|
2045
|
+
const ALGORITHM = "connectedComponents";
|
|
2046
|
+
const ROUNDS_PER_BATCH = 4;
|
|
2047
|
+
const MAX_WCC_ROUNDS = 64;
|
|
2048
|
+
const SAMPLE_SIZE = 1024;
|
|
2049
|
+
const MAX_STEPS = 1024;
|
|
2050
|
+
const RING_SLOTS$2 = 2 * ROUNDS_PER_BATCH;
|
|
2051
|
+
function checkDest$2(dest, n) {
|
|
2052
|
+
if (dest === void 0) {
|
|
2053
|
+
return null;
|
|
2054
|
+
}
|
|
2055
|
+
if (dest instanceof Uint32Array && dest.length === n && dest.buffer instanceof ArrayBuffer) {
|
|
2056
|
+
return dest;
|
|
2057
|
+
}
|
|
2058
|
+
throw new WebGpuGraphError(
|
|
2059
|
+
"E_INVALID_ARGUMENT",
|
|
2060
|
+
`${ALGORITHM}: dest must be a Uint32Array of length ${n} over an ArrayBuffer`,
|
|
2061
|
+
{
|
|
2062
|
+
argument: "dest",
|
|
2063
|
+
value: `${dest.constructor.name}(${dest.length})`,
|
|
2064
|
+
expected: `Uint32Array(${n}) over an ArrayBuffer`
|
|
2065
|
+
}
|
|
2066
|
+
);
|
|
2067
|
+
}
|
|
2068
|
+
function coreOf$2(ctx, s) {
|
|
2069
|
+
try {
|
|
2070
|
+
return ctx.residency.core(s);
|
|
2071
|
+
} catch (error) {
|
|
2072
|
+
if (isWebGpuGraphError(error) && error.code === "E_TOO_LARGE" && error.details.path === "windowed") {
|
|
2073
|
+
throw new WebGpuGraphError(
|
|
2074
|
+
"E_TOO_LARGE",
|
|
2075
|
+
`${ALGORITHM}: the arc arrays need a windowed upload, which P1-P3 plan but do not execute`,
|
|
2076
|
+
{ ...error.details, algorithm: ALGORITHM }
|
|
2077
|
+
);
|
|
2078
|
+
}
|
|
2079
|
+
throw error;
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
function bindingOf$1(buffer, size) {
|
|
2083
|
+
return { buffer, offset: 0, size, window: null };
|
|
2084
|
+
}
|
|
2085
|
+
function labelResult(labels, count) {
|
|
2086
|
+
let groups = null;
|
|
2087
|
+
return {
|
|
2088
|
+
labels,
|
|
2089
|
+
count,
|
|
2090
|
+
groups() {
|
|
2091
|
+
if (groups !== null) {
|
|
2092
|
+
return groups;
|
|
2093
|
+
}
|
|
2094
|
+
const n = labels.length;
|
|
2095
|
+
const dense = new Uint32Array(n).fill(U32_MAX);
|
|
2096
|
+
const sizes = new Uint32Array(count);
|
|
2097
|
+
let next = 0;
|
|
2098
|
+
for (let v = 0; v < n; v++) {
|
|
2099
|
+
let k2 = dense[labels[v]];
|
|
2100
|
+
if (k2 === U32_MAX) {
|
|
2101
|
+
k2 = next++;
|
|
2102
|
+
dense[labels[v]] = k2;
|
|
2103
|
+
}
|
|
2104
|
+
sizes[k2]++;
|
|
2105
|
+
}
|
|
2106
|
+
const built = Array.from(sizes, (size) => new Uint32Array(size));
|
|
2107
|
+
const filled = new Uint32Array(count);
|
|
2108
|
+
for (let v = 0; v < n; v++) {
|
|
2109
|
+
const k2 = dense[labels[v]];
|
|
2110
|
+
built[k2][filled[k2]++] = v;
|
|
2111
|
+
}
|
|
2112
|
+
groups = built;
|
|
2113
|
+
return built;
|
|
2114
|
+
}
|
|
2115
|
+
};
|
|
2116
|
+
}
|
|
2117
|
+
function modeOf(sample) {
|
|
2118
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2119
|
+
let best = sample[0];
|
|
2120
|
+
let bestCount = 0;
|
|
2121
|
+
for (const label of sample) {
|
|
2122
|
+
const c = (counts.get(label) ?? 0) + 1;
|
|
2123
|
+
counts.set(label, c);
|
|
2124
|
+
if (c > bestCount) {
|
|
2125
|
+
best = label;
|
|
2126
|
+
bestCount = c;
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
return best;
|
|
2130
|
+
}
|
|
2131
|
+
function checkLabels(raw) {
|
|
2132
|
+
const n = raw.length;
|
|
2133
|
+
const seen = new Uint8Array(n);
|
|
2134
|
+
let count = 0;
|
|
2135
|
+
for (let v = 0; v < n; v++) {
|
|
2136
|
+
const label = raw[v];
|
|
2137
|
+
if (label >= n) {
|
|
2138
|
+
throw new WebGpuGraphError("E_VALIDATION", `${ALGORITHM}: labels[${v}] = ${label} is not a node index`, {
|
|
2139
|
+
label: `${ALGORITHM}/labels`,
|
|
2140
|
+
message: `the device produced a label outside [0, ${n})`
|
|
2141
|
+
});
|
|
2142
|
+
}
|
|
2143
|
+
if (seen[label] === 0) {
|
|
2144
|
+
seen[label] = 1;
|
|
2145
|
+
count++;
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
return count;
|
|
2149
|
+
}
|
|
2150
|
+
async function connectedComponents(ctx, s, options) {
|
|
2151
|
+
ctx.assertReady();
|
|
2152
|
+
const n = s.nodeCount;
|
|
2153
|
+
const renumber = options?.renumber !== false;
|
|
2154
|
+
const dest = checkDest$2(options?.dest, n);
|
|
2155
|
+
if (options?.signal?.aborted) {
|
|
2156
|
+
throw new WebGpuGraphError("E_ABORTED", `${ALGORITHM}: the signal was aborted before any work started`, {});
|
|
2157
|
+
}
|
|
2158
|
+
if (n === 0) {
|
|
2159
|
+
options?.onProgress?.(1, 1);
|
|
2160
|
+
return labelResult(dest ?? new Uint32Array(0), 0);
|
|
2161
|
+
}
|
|
2162
|
+
const core = coreOf$2(ctx, s);
|
|
2163
|
+
if (s.arcCount === 0) {
|
|
2164
|
+
const labels = dest ?? new Uint32Array(n);
|
|
2165
|
+
for (let v = 0; v < n; v++) {
|
|
2166
|
+
labels[v] = v;
|
|
2167
|
+
}
|
|
2168
|
+
options?.onProgress?.(1, 1);
|
|
2169
|
+
return labelResult(labels, n);
|
|
2170
|
+
}
|
|
2171
|
+
const edges = ctx.residency.view(s, "edgeList");
|
|
2172
|
+
const edgeCount = edges.scalars.edgeCount[0];
|
|
2173
|
+
const scope = algorithmScope(ctx, ALGORITHM, RING_SLOTS$2);
|
|
2174
|
+
try {
|
|
2175
|
+
const compBytes = 4 * (n + 1);
|
|
2176
|
+
const comp = scope.scratch(compBytes, "comp");
|
|
2177
|
+
const items = Math.min(SAMPLE_SIZE, n);
|
|
2178
|
+
const hist = scope.scratch(4 * items, "hist");
|
|
2179
|
+
await ctx.allocator.check();
|
|
2180
|
+
const fill = await ctx.pipelines.kernel(kernelSpec("fill", {}));
|
|
2181
|
+
const linkSample = await ctx.pipelines.kernel(kernelSpec("wcc-link-sample", graphOverrides(core, null)));
|
|
2182
|
+
const linkEdges = await ctx.pipelines.kernel(kernelSpec("wcc-link-edges", {}));
|
|
2183
|
+
const compress = await ctx.pipelines.kernel(kernelSpec("wcc-compress", {}));
|
|
2184
|
+
const sample = await ctx.pipelines.kernel(kernelSpec("wcc-sample", {}));
|
|
2185
|
+
const { queue } = ctx.device;
|
|
2186
|
+
const compBinding = bindingOf$1(comp, compBytes);
|
|
2187
|
+
const histBinding = bindingOf$1(hist, 4 * items);
|
|
2188
|
+
const rowPlan = planGridStride(n, ctx.workgroupSize, ctx.caps);
|
|
2189
|
+
const edgePlan = planGridStride(edgeCount, ctx.workgroupSize, ctx.caps);
|
|
2190
|
+
const flagIndex = n;
|
|
2191
|
+
const zero = new Uint32Array(1);
|
|
2192
|
+
const wccParams = (fields) => scope.params(WCC_PARAMS, { n, ...fields, flagIndex, maxSteps: MAX_STEPS, pad0: 0 });
|
|
2193
|
+
const recordCompress = (pass2) => {
|
|
2194
|
+
const params = wccParams({ items: n, stride: rowPlan.stride ?? n, r: 0, giant: U32_MAX });
|
|
2195
|
+
compress.dispatch(pass2, compress.bind({ comp: compBinding, P: params.binding }), rowPlan, [params.offset]);
|
|
2196
|
+
};
|
|
2197
|
+
const submit = (batch) => {
|
|
2198
|
+
scope.flush();
|
|
2199
|
+
return batch.submit();
|
|
2200
|
+
};
|
|
2201
|
+
queue.writeBuffer(comp, 4 * flagIndex, zero);
|
|
2202
|
+
const setup = new CommandBatch(ctx, `${ALGORITHM}/setup`);
|
|
2203
|
+
let pass = setup.pass("sample-rounds");
|
|
2204
|
+
const fillParams = scope.params(FILL_PARAMS, { count: n, value: 0, mode: 1, pad0: 0 });
|
|
2205
|
+
fill.dispatch(
|
|
2206
|
+
pass,
|
|
2207
|
+
fill.bind({ dst: bindingOf$1(comp, 4 * n), P: fillParams.binding }),
|
|
2208
|
+
plan1d(n, ctx.workgroupSize, ctx.caps),
|
|
2209
|
+
[fillParams.offset]
|
|
2210
|
+
);
|
|
2211
|
+
const graph = graphBindings(core, null);
|
|
2212
|
+
for (let r = 0; r < 2; r++) {
|
|
2213
|
+
const params = wccParams({ items: n, stride: rowPlan.stride ?? n, r, giant: U32_MAX });
|
|
2214
|
+
const bound = linkSample.bind({ ...graph, comp: compBinding, P: params.binding });
|
|
2215
|
+
linkSample.dispatch(pass, bound, rowPlan, [params.offset]);
|
|
2216
|
+
}
|
|
2217
|
+
recordCompress(pass);
|
|
2218
|
+
setup.endPass();
|
|
2219
|
+
await submit(setup).readback;
|
|
2220
|
+
ctx.assertReady();
|
|
2221
|
+
const sampler = new CommandBatch(ctx, `${ALGORITHM}/sample`);
|
|
2222
|
+
pass = sampler.pass("sample");
|
|
2223
|
+
const sampleParams = wccParams({ items, stride: 0, r: 0, giant: U32_MAX });
|
|
2224
|
+
sample.dispatch(
|
|
2225
|
+
pass,
|
|
2226
|
+
sample.bind({ comp: compBinding, hist: histBinding, P: sampleParams.binding }),
|
|
2227
|
+
plan1d(items, ctx.workgroupSize, ctx.caps),
|
|
2228
|
+
[sampleParams.offset]
|
|
2229
|
+
);
|
|
2230
|
+
sampler.endPass();
|
|
2231
|
+
const histRequest = sampler.readback(hist, 0, 4 * items);
|
|
2232
|
+
const histBytes = await submit(sampler).readback;
|
|
2233
|
+
ctx.assertReady();
|
|
2234
|
+
const giant = modeOf(new Uint32Array(histBytes, histRequest.offset, items));
|
|
2235
|
+
const edgeBindings = { edgeSrc: edges.bindings.src, edgeDst: edges.bindings.dst, comp: compBinding };
|
|
2236
|
+
let rounds = 0;
|
|
2237
|
+
for (; ; ) {
|
|
2238
|
+
queue.writeBuffer(comp, 4 * flagIndex, zero);
|
|
2239
|
+
const batch = new CommandBatch(ctx, `${ALGORITHM}/rounds`);
|
|
2240
|
+
pass = batch.pass("edge-rounds");
|
|
2241
|
+
for (let i = 0; i < ROUNDS_PER_BATCH; i++) {
|
|
2242
|
+
const params = wccParams({ items: edgeCount, stride: edgePlan.stride ?? edgeCount, r: 0, giant });
|
|
2243
|
+
const bound = linkEdges.bind({ ...edgeBindings, P: params.binding });
|
|
2244
|
+
linkEdges.dispatch(pass, bound, edgePlan, [params.offset]);
|
|
2245
|
+
recordCompress(pass);
|
|
2246
|
+
}
|
|
2247
|
+
batch.endPass();
|
|
2248
|
+
const flagRequest = batch.readback(comp, 4 * flagIndex, 4);
|
|
2249
|
+
const submitted = submit(batch);
|
|
2250
|
+
const back = await submitted.readback;
|
|
2251
|
+
rounds += ROUNDS_PER_BATCH;
|
|
2252
|
+
ctx.assertReady();
|
|
2253
|
+
if (options?.signal?.aborted) {
|
|
2254
|
+
throw new WebGpuGraphError("E_ABORTED", `${ALGORITHM}: the signal was aborted`, { batchId: submitted.id });
|
|
2255
|
+
}
|
|
2256
|
+
if (new Uint32Array(back, flagRequest.offset, 1)[0] === 0) {
|
|
2257
|
+
break;
|
|
2258
|
+
}
|
|
2259
|
+
if (rounds >= MAX_WCC_ROUNDS) {
|
|
2260
|
+
throw new WebGpuGraphError(
|
|
2261
|
+
"E_VALIDATION",
|
|
2262
|
+
`${ALGORITHM}: the changed flag never settled in ${MAX_WCC_ROUNDS} rounds`,
|
|
2263
|
+
{ label: ALGORITHM, message: `the changed flag never settled in ${MAX_WCC_ROUNDS} rounds` }
|
|
2264
|
+
);
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
const final = new CommandBatch(ctx, `${ALGORITHM}/final`);
|
|
2268
|
+
recordCompress(final.pass("compress"));
|
|
2269
|
+
final.endPass();
|
|
2270
|
+
await submit(final).readback;
|
|
2271
|
+
ctx.assertReady();
|
|
2272
|
+
const raw = !renumber && dest !== null ? dest : new Uint32Array(n);
|
|
2273
|
+
await ctx.readback.read(comp, 4 * n, raw);
|
|
2274
|
+
ctx.assertReady();
|
|
2275
|
+
const distinct = checkLabels(raw);
|
|
2276
|
+
options?.onProgress?.(1, 1);
|
|
2277
|
+
if (!renumber) {
|
|
2278
|
+
return labelResult(raw, distinct);
|
|
2279
|
+
}
|
|
2280
|
+
const { labels, count } = renumberPartition(raw, dest ?? void 0);
|
|
2281
|
+
return labelResult(labels, count);
|
|
2282
|
+
} finally {
|
|
2283
|
+
scope.dispose();
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
function rowCountOf(core, primitive) {
|
|
2287
|
+
const bytes = core.rowPtr.size;
|
|
2288
|
+
if (bytes < 4 || bytes % 4 !== 0) {
|
|
2289
|
+
throw new WebGpuGraphError(
|
|
2290
|
+
"E_INVALID_ARGUMENT",
|
|
2291
|
+
`${primitive}: a rowPtr binding of ${bytes} bytes is not 4(n + 1)`,
|
|
2292
|
+
{
|
|
2293
|
+
argument: "core.rowPtr",
|
|
2294
|
+
value: bytes,
|
|
2295
|
+
expected: "a positive multiple of 4"
|
|
2296
|
+
}
|
|
2297
|
+
);
|
|
2298
|
+
}
|
|
2299
|
+
return bytes / 4 - 1;
|
|
2300
|
+
}
|
|
2301
|
+
function arcCountOf(core) {
|
|
2302
|
+
return core.colIdx === null ? 0 : core.colIdx.size / 4;
|
|
2303
|
+
}
|
|
2304
|
+
function assertNotWindowed(core, primitive) {
|
|
2305
|
+
if (core.plan === "windowed" || core.windows !== null) {
|
|
2306
|
+
throw new WebGpuGraphError("E_UNSUPPORTED", `${primitive}: windowed cores are executed at P4`, {
|
|
2307
|
+
feature: `${primitive}.windowed`
|
|
2308
|
+
});
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
function coreOfView(v, arcCount) {
|
|
2312
|
+
const { rowPtr } = v.bindings;
|
|
2313
|
+
if (rowPtr === void 0) {
|
|
2314
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `the ${v.view} view has no rowPtr binding`, {
|
|
2315
|
+
argument: "view",
|
|
2316
|
+
value: v.view,
|
|
2317
|
+
expected: "a view with a rowPtr binding (reverse)"
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
const colIdx = v.bindings.colIdx ?? null;
|
|
2321
|
+
const weights = v.bindings.weights ?? null;
|
|
2322
|
+
const bound = colIdx === null ? 0 : colIdx.size / 4;
|
|
2323
|
+
if (bound !== arcCount) {
|
|
2324
|
+
throw new WebGpuGraphError(
|
|
2325
|
+
"E_INVALID_ARGUMENT",
|
|
2326
|
+
`the ${v.view} view binds ${bound} arcs but its arcCount scalar says ${arcCount}`,
|
|
2327
|
+
{
|
|
2328
|
+
argument: "arcCount",
|
|
2329
|
+
value: arcCount,
|
|
2330
|
+
expected: bound
|
|
2331
|
+
}
|
|
2332
|
+
);
|
|
2333
|
+
}
|
|
2334
|
+
return Object.freeze({
|
|
2335
|
+
serial: -1,
|
|
2336
|
+
plan: "perArray",
|
|
2337
|
+
rowPtr,
|
|
2338
|
+
colIdx,
|
|
2339
|
+
weights,
|
|
2340
|
+
arcToEdge: null,
|
|
2341
|
+
edgeToArc: null,
|
|
2342
|
+
windows: null,
|
|
2343
|
+
hasWeights: weights !== null
|
|
2344
|
+
});
|
|
2345
|
+
}
|
|
2346
|
+
const VALUE_SNIPPET_VOCABULARY = /* @__PURE__ */ new Set(["row", "arc", "nbr", "weight", "v"]);
|
|
2347
|
+
const VALUE_SNIPPET_WGSL_WORDS = /* @__PURE__ */ new Set([
|
|
2348
|
+
"if",
|
|
2349
|
+
"else",
|
|
2350
|
+
"let",
|
|
2351
|
+
"var",
|
|
2352
|
+
"const",
|
|
2353
|
+
"true",
|
|
2354
|
+
"false",
|
|
2355
|
+
"f32",
|
|
2356
|
+
"u32",
|
|
2357
|
+
"i32",
|
|
2358
|
+
"bool",
|
|
2359
|
+
"abs",
|
|
2360
|
+
"ceil",
|
|
2361
|
+
"clamp",
|
|
2362
|
+
"exp",
|
|
2363
|
+
"exp2",
|
|
2364
|
+
"floor",
|
|
2365
|
+
"fract",
|
|
2366
|
+
"inverseSqrt",
|
|
2367
|
+
"log",
|
|
2368
|
+
"log2",
|
|
2369
|
+
"max",
|
|
2370
|
+
"min",
|
|
2371
|
+
"mix",
|
|
2372
|
+
"pow",
|
|
2373
|
+
"round",
|
|
2374
|
+
"select",
|
|
2375
|
+
"sign",
|
|
2376
|
+
"sqrt",
|
|
2377
|
+
"step",
|
|
2378
|
+
"trunc"
|
|
2379
|
+
]);
|
|
2380
|
+
function opCode(op) {
|
|
2381
|
+
switch (op) {
|
|
2382
|
+
case "sum":
|
|
2383
|
+
return 0;
|
|
2384
|
+
case "min":
|
|
2385
|
+
return 1;
|
|
2386
|
+
case "max":
|
|
2387
|
+
return 2;
|
|
2388
|
+
default:
|
|
2389
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `segmentedReduce: unknown op ${String(op)}`, {
|
|
2390
|
+
argument: "op",
|
|
2391
|
+
value: op,
|
|
2392
|
+
expected: "sum | min | max"
|
|
2393
|
+
});
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
function stripComments(text) {
|
|
2397
|
+
return text.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/\/\/[^\n]*/g, " ");
|
|
2398
|
+
}
|
|
2399
|
+
function validateValueSnippet(snippet) {
|
|
2400
|
+
const code = stripComments(snippet);
|
|
2401
|
+
if (!/\bv\s*[-+*/]?=(?!=)/.test(code)) {
|
|
2402
|
+
throw new WebGpuGraphError("E_SHADER_COMPILE", "segmentedReduce: the VALUE snippet never assigns v", {
|
|
2403
|
+
id: "segmented-reduce",
|
|
2404
|
+
stage: "compose",
|
|
2405
|
+
slot: "VALUE"
|
|
2406
|
+
});
|
|
2407
|
+
}
|
|
2408
|
+
const tokens = code.match(/[A-Za-z_][A-Za-z0-9_]*|[0-9][0-9A-Za-z_.]*/g) ?? [];
|
|
2409
|
+
for (const token of tokens) {
|
|
2410
|
+
if (/^[0-9]/.test(token) || VALUE_SNIPPET_VOCABULARY.has(token) || VALUE_SNIPPET_WGSL_WORDS.has(token)) {
|
|
2411
|
+
continue;
|
|
2412
|
+
}
|
|
2413
|
+
throw new WebGpuGraphError(
|
|
2414
|
+
"E_SHADER_COMPILE",
|
|
2415
|
+
`segmentedReduce: the VALUE snippet names "${token}"; the only identifiers allowed are row, arc, nbr, weight and v`,
|
|
2416
|
+
{ id: "segmented-reduce", stage: "compose", slot: "VALUE", identifier: token }
|
|
2417
|
+
);
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
class ThreadPerRowPlanner {
|
|
2421
|
+
/**
|
|
2422
|
+
* Wraps a compiled thread-per-row pipeline with the pattern it was compiled for.
|
|
2423
|
+
* @param scope - the scope the pipeline was prepared in
|
|
2424
|
+
* @param kernel - the compiled kernel
|
|
2425
|
+
* @param hasWeights - the HAS_WEIGHTS the pipeline was compiled with
|
|
2426
|
+
* @param accumulate - whether record() combines into out instead of overwriting
|
|
2427
|
+
*/
|
|
2428
|
+
constructor(scope, kernel, hasWeights, accumulate) {
|
|
2429
|
+
this.scope = scope;
|
|
2430
|
+
this.kernel = kernel;
|
|
2431
|
+
this.hasWeights = hasWeights;
|
|
2432
|
+
this.accumulate = accumulate;
|
|
2433
|
+
}
|
|
2434
|
+
/**
|
|
2435
|
+
* Records the dispatch: rows [0, n), arcs [0, arcCount), plan1d(n); nothing for n = 0 (no zero-length binding is
|
|
2436
|
+
* ever created).
|
|
2437
|
+
* @param pass - the pass to record into
|
|
2438
|
+
* @param core - a core with the SAME weights pattern as the one prepared (any snapshot)
|
|
2439
|
+
* @param out - at least 4n bytes of f32
|
|
2440
|
+
*/
|
|
2441
|
+
record(pass, core, out) {
|
|
2442
|
+
assertNotWindowed(core, "segmentedReduce");
|
|
2443
|
+
if (core.weights !== null !== this.hasWeights) {
|
|
2444
|
+
throw new WebGpuGraphError(
|
|
2445
|
+
"E_INVALID_ARGUMENT",
|
|
2446
|
+
"segmentedReduce: the core's weights pattern differs from the one prepared",
|
|
2447
|
+
{
|
|
2448
|
+
argument: "core",
|
|
2449
|
+
value: core.weights !== null,
|
|
2450
|
+
expected: this.hasWeights
|
|
2451
|
+
}
|
|
2452
|
+
);
|
|
2453
|
+
}
|
|
2454
|
+
const n = rowCountOf(core, "segmentedReduce");
|
|
2455
|
+
if (n === 0) {
|
|
2456
|
+
return;
|
|
2457
|
+
}
|
|
2458
|
+
const arcCount = core.colIdx === null ? 0 : core.colIdx.size / 4;
|
|
2459
|
+
if (out.size < 4 * n) {
|
|
2460
|
+
throw new WebGpuGraphError(
|
|
2461
|
+
"E_INVALID_ARGUMENT",
|
|
2462
|
+
`segmentedReduce: out holds ${out.size} bytes, ${4 * n} needed`,
|
|
2463
|
+
{
|
|
2464
|
+
argument: "out",
|
|
2465
|
+
value: out.size,
|
|
2466
|
+
expected: `>= ${4 * n}`
|
|
2467
|
+
}
|
|
2468
|
+
);
|
|
2469
|
+
}
|
|
2470
|
+
const params = this.scope.params(RANGE_PARAMS, {
|
|
2471
|
+
start: 0,
|
|
2472
|
+
end: n,
|
|
2473
|
+
arcBase: 0,
|
|
2474
|
+
arcEnd: arcCount,
|
|
2475
|
+
accumulate: this.accumulate ? 1 : 0,
|
|
2476
|
+
n
|
|
2477
|
+
});
|
|
2478
|
+
const bound = this.kernel.bind({ ...graphBindings(core, null), out, P: params.binding });
|
|
2479
|
+
this.kernel.dispatch(pass, bound, plan1d(n, this.scope.workgroupSize, this.scope.caps), [params.offset]);
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
async function prepareSegmentedReduce(scope, core, options) {
|
|
2483
|
+
assertNotWindowed(core, "segmentedReduce");
|
|
2484
|
+
const op = opCode(options.op);
|
|
2485
|
+
validateValueSnippet(options.valueSnippet);
|
|
2486
|
+
const overrides = { ...graphOverrides(core, null), OP: op, TIER: 0 };
|
|
2487
|
+
const spec = kernelSpec("segmented-reduce", overrides, { VALUE: options.valueSnippet });
|
|
2488
|
+
const kernel = await scope.pipelines.kernel(spec);
|
|
2489
|
+
return new ThreadPerRowPlanner(scope, kernel, core.weights !== null, options.accumulate === true);
|
|
2490
|
+
}
|
|
2491
|
+
class SpmvPullPlannerImpl {
|
|
2492
|
+
/**
|
|
2493
|
+
* Wraps a compiled pipeline with the weights choice it was compiled for.
|
|
2494
|
+
* @param scope - the scope the pipeline was prepared in
|
|
2495
|
+
* @param kernel - the compiled kernel
|
|
2496
|
+
* @param weights - the weights option the pipeline's HAS_WEIGHTS was derived from; record() binds the same way
|
|
2497
|
+
*/
|
|
2498
|
+
constructor(scope, kernel, weights) {
|
|
2499
|
+
this.dispatches = 0;
|
|
2500
|
+
this.scope = scope;
|
|
2501
|
+
this.kernel = kernel;
|
|
2502
|
+
this.weights = weights;
|
|
2503
|
+
}
|
|
2504
|
+
/**
|
|
2505
|
+
* Dispatches the last record() issued.
|
|
2506
|
+
* @returns 1, or 0 when the last record covered no rows
|
|
2507
|
+
*/
|
|
2508
|
+
get lastDispatches() {
|
|
2509
|
+
return this.dispatches;
|
|
2510
|
+
}
|
|
2511
|
+
/**
|
|
2512
|
+
* Records the dispatch: rows [0, n), arcs [0, arcCount), planGridStride(n); nothing for n = 0 (no zero-length
|
|
2513
|
+
* binding is ever created). `personalization ?? xNorm` follows the group-0 dummy rule: both slots are
|
|
2514
|
+
* storage-ro, so the aliasing check of Kernel.bind does not fire, and HAS_PERSONALIZATION false never reads it.
|
|
2515
|
+
* @param pass - the pass to record into
|
|
2516
|
+
* @param rev - the reverse core (any snapshot with the weights pattern the planner was prepared for)
|
|
2517
|
+
* @param resources - xNorm, rankOut, personalization, partials
|
|
2518
|
+
* @param coefficients - alpha, beta, uniformP
|
|
2519
|
+
*/
|
|
2520
|
+
record(pass, rev, resources, coefficients) {
|
|
2521
|
+
const n = rowCountOf(rev, "spmvPull");
|
|
2522
|
+
const plan = planGridStride(n, this.scope.workgroupSize, this.scope.caps);
|
|
2523
|
+
if (plan.x === 0) {
|
|
2524
|
+
this.dispatches = 0;
|
|
2525
|
+
return;
|
|
2526
|
+
}
|
|
2527
|
+
const params = this.scope.params(SPMV_PARAMS, {
|
|
2528
|
+
n,
|
|
2529
|
+
arcBase: 0,
|
|
2530
|
+
arcEnd: arcCountOf(rev),
|
|
2531
|
+
stride: plan.stride ?? n,
|
|
2532
|
+
alpha: coefficients.alpha,
|
|
2533
|
+
beta: coefficients.beta,
|
|
2534
|
+
uniformP: coefficients.uniformP,
|
|
2535
|
+
pad0: 0
|
|
2536
|
+
});
|
|
2537
|
+
const bound = this.kernel.bind({
|
|
2538
|
+
...graphBindings(rev, null, this.weights),
|
|
2539
|
+
xNorm: resources.xNorm,
|
|
2540
|
+
rankOut: resources.rankOut,
|
|
2541
|
+
personalization: resources.personalization ?? resources.xNorm,
|
|
2542
|
+
partials: resources.partials,
|
|
2543
|
+
P: params.binding
|
|
2544
|
+
});
|
|
2545
|
+
this.kernel.dispatch(pass, bound, plan, [params.offset]);
|
|
2546
|
+
this.dispatches = 1;
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
async function prepareSpmvPull(scope, rev, options) {
|
|
2550
|
+
if (options.tiers !== null) {
|
|
2551
|
+
throw new WebGpuGraphError("E_UNSUPPORTED", "spmvPull: the in-degree tiers land at P4; pass tiers: null", {
|
|
2552
|
+
feature: "spmvPull.tiers"
|
|
2553
|
+
});
|
|
2554
|
+
}
|
|
2555
|
+
assertNotWindowed(rev, "spmvPull");
|
|
2556
|
+
const spec = kernelSpec("spmv-pull", {
|
|
2557
|
+
...graphOverrides(rev, null, options.weights),
|
|
2558
|
+
HAS_PERSONALIZATION: options.personalization,
|
|
2559
|
+
USE_DANGLING: options.dangling
|
|
2560
|
+
});
|
|
2561
|
+
const kernel = await scope.pipelines.kernel(spec);
|
|
2562
|
+
return new SpmvPullPlannerImpl(scope, kernel, options.weights);
|
|
2563
|
+
}
|
|
2564
|
+
const PR_BATCH = 8;
|
|
2565
|
+
const RING_SLOTS$1 = 2 * PR_BATCH + 1;
|
|
2566
|
+
function checkDest$1(dest, n, algorithm) {
|
|
2567
|
+
if (dest === void 0) {
|
|
2568
|
+
return null;
|
|
2569
|
+
}
|
|
2570
|
+
if (dest instanceof Float32Array && dest.length === n && dest.buffer instanceof ArrayBuffer) {
|
|
2571
|
+
return dest;
|
|
2572
|
+
}
|
|
2573
|
+
throw new WebGpuGraphError(
|
|
2574
|
+
"E_INVALID_ARGUMENT",
|
|
2575
|
+
`${algorithm}: dest must be a Float32Array of length ${n} over an ArrayBuffer`,
|
|
2576
|
+
{
|
|
2577
|
+
argument: "dest",
|
|
2578
|
+
value: `${dest.constructor.name}(${dest.length})`,
|
|
2579
|
+
expected: `Float32Array(${n}) over an ArrayBuffer`
|
|
2580
|
+
}
|
|
2581
|
+
);
|
|
2582
|
+
}
|
|
2583
|
+
function coreOf$1(ctx, s, algorithm) {
|
|
2584
|
+
try {
|
|
2585
|
+
return ctx.residency.core(s);
|
|
2586
|
+
} catch (error) {
|
|
2587
|
+
if (isWebGpuGraphError(error) && error.code === "E_TOO_LARGE" && error.details.path === "windowed") {
|
|
2588
|
+
throw new WebGpuGraphError(
|
|
2589
|
+
"E_TOO_LARGE",
|
|
2590
|
+
`${algorithm}: the arc arrays need a windowed upload, which P1-P3 plan but do not execute`,
|
|
2591
|
+
{ ...error.details, algorithm }
|
|
2592
|
+
);
|
|
2593
|
+
}
|
|
2594
|
+
throw error;
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
function bindingOf(buffer, size) {
|
|
2598
|
+
return { buffer, offset: 0, size, window: null };
|
|
2599
|
+
}
|
|
2600
|
+
function aborted$1(algorithm, batchId) {
|
|
2601
|
+
return new WebGpuGraphError(
|
|
2602
|
+
"E_ABORTED",
|
|
2603
|
+
`${algorithm}: the signal was aborted`,
|
|
2604
|
+
batchId === void 0 ? {} : { batchId }
|
|
2605
|
+
);
|
|
2606
|
+
}
|
|
2607
|
+
async function run(ctx, s, personalization, options, algorithm) {
|
|
2608
|
+
ctx.assertReady();
|
|
2609
|
+
const n = s.nodeCount;
|
|
2610
|
+
const alpha = options?.dampingFactor ?? 0.85;
|
|
2611
|
+
const maxIterations = options?.maxIterations ?? 100;
|
|
2612
|
+
const tolerance = options?.tolerance ?? 1e-6;
|
|
2613
|
+
const useWeights = options?.weighted !== false;
|
|
2614
|
+
if (!Number.isInteger(maxIterations) || maxIterations < 1) {
|
|
2615
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${algorithm}: maxIterations must be a positive integer`, {
|
|
2616
|
+
argument: "maxIterations",
|
|
2617
|
+
value: maxIterations,
|
|
2618
|
+
expected: "a positive integer"
|
|
2619
|
+
});
|
|
2620
|
+
}
|
|
2621
|
+
const dest = checkDest$1(options?.dest, n, algorithm);
|
|
2622
|
+
if (options?.signal?.aborted) {
|
|
2623
|
+
throw aborted$1(algorithm);
|
|
2624
|
+
}
|
|
2625
|
+
if (n === 0) {
|
|
2626
|
+
options?.onProgress?.(maxIterations, maxIterations);
|
|
2627
|
+
return { scores: dest ?? new Float32Array(0), iterations: 0, converged: true, danglingMass: 0, precision: "f32" };
|
|
2628
|
+
}
|
|
2629
|
+
const core = coreOf$1(ctx, s, algorithm);
|
|
2630
|
+
if (s.arcCount === 0) {
|
|
2631
|
+
const scores = dest ?? new Float32Array(n);
|
|
2632
|
+
if (personalization === null) {
|
|
2633
|
+
scores.fill(1 / n);
|
|
2634
|
+
} else {
|
|
2635
|
+
scores.set(personalization);
|
|
2636
|
+
}
|
|
2637
|
+
options?.onProgress?.(maxIterations, maxIterations);
|
|
2638
|
+
return { scores, iterations: 0, converged: true, danglingMass: 1, precision: "f32" };
|
|
2639
|
+
}
|
|
2640
|
+
const view = ctx.residency.view(s, "reverse");
|
|
2641
|
+
const rev = coreOfView(view, view.scalars.arcCount[0]);
|
|
2642
|
+
const weights = useWeights ? void 0 : null;
|
|
2643
|
+
const weightedCore = useWeights ? core : { ...core, weights: null, hasWeights: false };
|
|
2644
|
+
const weightedRev = useWeights ? rev : { ...rev, weights: null, hasWeights: false };
|
|
2645
|
+
const scope = algorithmScope(ctx, algorithm, RING_SLOTS$1);
|
|
2646
|
+
let uploaded = null;
|
|
2647
|
+
try {
|
|
2648
|
+
const bytes = 4 * n;
|
|
2649
|
+
const rankA = scope.scratch(bytes, "rankA");
|
|
2650
|
+
const rankB = scope.scratch(bytes, "rankB");
|
|
2651
|
+
const xNorm = scope.scratch(bytes, "xNorm");
|
|
2652
|
+
const outWeightSum = scope.scratch(bytes, "outWeightSum");
|
|
2653
|
+
const scalePlan = plan1d(n, ctx.workgroupSize, ctx.caps);
|
|
2654
|
+
const groups = groupsOf(scalePlan);
|
|
2655
|
+
const partialsBytes = PR_PARTIAL.byteLength * (1 + groups);
|
|
2656
|
+
const partials = scope.scratch(partialsBytes, "partials");
|
|
2657
|
+
if (personalization !== null) {
|
|
2658
|
+
uploaded = ctx.residency.array(personalization, `${algorithm}/personalization`);
|
|
2659
|
+
}
|
|
2660
|
+
await ctx.allocator.check();
|
|
2661
|
+
const normaliser = await prepareSegmentedReduce(scope, weightedCore, {
|
|
2662
|
+
op: "sum",
|
|
2663
|
+
valueSnippet: "v = weight;",
|
|
2664
|
+
tiers: null
|
|
2665
|
+
});
|
|
2666
|
+
const pull = await prepareSpmvPull(scope, weightedRev, {
|
|
2667
|
+
personalization: personalization !== null,
|
|
2668
|
+
dangling: true,
|
|
2669
|
+
weights,
|
|
2670
|
+
tiers: null
|
|
2671
|
+
});
|
|
2672
|
+
const scale = await ctx.pipelines.kernel(kernelSpec("pr-scale", { NORM_MODE: 0 }));
|
|
2673
|
+
const finalize = await ctx.pipelines.kernel(kernelSpec("pr-finalize", { NORM_MODE: 0 }));
|
|
2674
|
+
const finalizePlan = plan1d(1, ctx.workgroupSize, ctx.caps);
|
|
2675
|
+
const { queue } = ctx.device;
|
|
2676
|
+
queue.writeBuffer(rankA, 0, new Float32Array(n).fill(1 / n));
|
|
2677
|
+
queue.writeBuffer(rankB, 0, new Float32Array(n));
|
|
2678
|
+
const header = new ArrayBuffer(PR_PARTIAL.byteLength);
|
|
2679
|
+
PR_PARTIAL.write(new DataView(header), { firstConverged: U32_MAX, iteration: 0 });
|
|
2680
|
+
queue.writeBuffer(partials, 0, header);
|
|
2681
|
+
const rank = [bindingOf(rankA, bytes), bindingOf(rankB, bytes)];
|
|
2682
|
+
const xNormBinding = bindingOf(xNorm, bytes);
|
|
2683
|
+
const outWeightSumBinding = bindingOf(outWeightSum, bytes);
|
|
2684
|
+
const partialsBinding = bindingOf(partials, partialsBytes);
|
|
2685
|
+
const coefficients = { alpha, beta: 1 - alpha, uniformP: 1 / n };
|
|
2686
|
+
let cur = 0;
|
|
2687
|
+
let iterationsRun = 0;
|
|
2688
|
+
for (; ; ) {
|
|
2689
|
+
const k2 = Math.min(PR_BATCH, maxIterations - iterationsRun);
|
|
2690
|
+
const batch = new CommandBatch(ctx, algorithm);
|
|
2691
|
+
const pass = batch.pass("iterations");
|
|
2692
|
+
if (iterationsRun === 0) {
|
|
2693
|
+
normaliser.record(pass, weightedCore, outWeightSumBinding);
|
|
2694
|
+
}
|
|
2695
|
+
for (let i = 0; i < k2; i++) {
|
|
2696
|
+
const params = scope.params(PR_PARAMS, {
|
|
2697
|
+
n,
|
|
2698
|
+
groups,
|
|
2699
|
+
iteration: iterationsRun + i + 1,
|
|
2700
|
+
trackConvergence: 1,
|
|
2701
|
+
convergeThreshold: tolerance * n
|
|
2702
|
+
});
|
|
2703
|
+
const other = 1 - cur;
|
|
2704
|
+
const scaleBound = scale.bind({
|
|
2705
|
+
rankIn: rank[cur],
|
|
2706
|
+
rankPrev: rank[other],
|
|
2707
|
+
outWeightSum: outWeightSumBinding,
|
|
2708
|
+
xNorm: xNormBinding,
|
|
2709
|
+
partials: partialsBinding,
|
|
2710
|
+
P: params.binding
|
|
2711
|
+
});
|
|
2712
|
+
scale.dispatch(pass, scaleBound, scalePlan, [params.offset]);
|
|
2713
|
+
const finalizeBound = finalize.bind({ partials: partialsBinding, P: params.binding });
|
|
2714
|
+
finalize.dispatch(pass, finalizeBound, finalizePlan, [params.offset]);
|
|
2715
|
+
pull.record(
|
|
2716
|
+
pass,
|
|
2717
|
+
weightedRev,
|
|
2718
|
+
{
|
|
2719
|
+
xNorm: xNormBinding,
|
|
2720
|
+
rankOut: rank[other],
|
|
2721
|
+
personalization: uploaded?.binding ?? null,
|
|
2722
|
+
partials: partialsBinding
|
|
2723
|
+
},
|
|
2724
|
+
coefficients
|
|
2725
|
+
);
|
|
2726
|
+
cur = other;
|
|
2727
|
+
}
|
|
2728
|
+
batch.endPass();
|
|
2729
|
+
const headerRequest = batch.readback(partials, 0, PR_PARTIAL.byteLength);
|
|
2730
|
+
const scoresRequest = batch.readback(rank[cur].buffer, 0, bytes);
|
|
2731
|
+
scope.flush();
|
|
2732
|
+
const submitted = batch.submit();
|
|
2733
|
+
const back = await submitted.readback;
|
|
2734
|
+
iterationsRun += k2;
|
|
2735
|
+
ctx.assertReady();
|
|
2736
|
+
if (options?.signal?.aborted) {
|
|
2737
|
+
throw aborted$1(algorithm, submitted.id);
|
|
2738
|
+
}
|
|
2739
|
+
options?.onProgress?.(iterationsRun, maxIterations);
|
|
2740
|
+
const folded = PR_PARTIAL.read(new DataView(back), headerRequest.offset);
|
|
2741
|
+
const firstConverged = folded.firstConverged;
|
|
2742
|
+
const converged = firstConverged !== U32_MAX;
|
|
2743
|
+
if (converged || iterationsRun >= maxIterations) {
|
|
2744
|
+
const scores = dest ?? new Float32Array(n);
|
|
2745
|
+
scores.set(new Float32Array(back, scoresRequest.offset, n));
|
|
2746
|
+
if (converged && iterationsRun < maxIterations) {
|
|
2747
|
+
options?.onProgress?.(maxIterations, maxIterations);
|
|
2748
|
+
}
|
|
2749
|
+
return {
|
|
2750
|
+
scores,
|
|
2751
|
+
iterations: converged ? firstConverged : iterationsRun,
|
|
2752
|
+
converged,
|
|
2753
|
+
danglingMass: folded.danglingMass,
|
|
2754
|
+
precision: "f32"
|
|
2755
|
+
};
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
} finally {
|
|
2759
|
+
uploaded?.destroy();
|
|
2760
|
+
scope.dispose();
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
function pageRank(ctx, s, options) {
|
|
2764
|
+
return run(ctx, s, null, options, "pageRank");
|
|
2765
|
+
}
|
|
2766
|
+
async function personalizedPageRank(ctx, s, personalization, options) {
|
|
2767
|
+
const n = s.nodeCount;
|
|
2768
|
+
const invalid2 = (value, expected) => new WebGpuGraphError("E_INVALID_ARGUMENT", `personalizedPageRank: personalization must be ${expected}`, {
|
|
2769
|
+
argument: "personalization",
|
|
2770
|
+
value,
|
|
2771
|
+
expected
|
|
2772
|
+
});
|
|
2773
|
+
if (!(personalization instanceof Float32Array) || personalization.length !== n) {
|
|
2774
|
+
throw invalid2(`${personalization.constructor.name}(${personalization.length})`, `a Float32Array of length ${n}`);
|
|
2775
|
+
}
|
|
2776
|
+
let total = 0;
|
|
2777
|
+
for (let v = 0; v < n; v++) {
|
|
2778
|
+
const mass = personalization[v];
|
|
2779
|
+
if (!Number.isFinite(mass) || mass < 0) {
|
|
2780
|
+
throw invalid2(mass, "finite and non-negative in every entry");
|
|
2781
|
+
}
|
|
2782
|
+
total += mass;
|
|
2783
|
+
}
|
|
2784
|
+
if (n > 0 && !(total > 0)) {
|
|
2785
|
+
throw invalid2(total, "a vector whose entries sum to a positive number");
|
|
2786
|
+
}
|
|
2787
|
+
const normalised2 = new Float32Array(n);
|
|
2788
|
+
for (let v = 0; v < n; v++) {
|
|
2789
|
+
normalised2[v] = personalization[v] / total;
|
|
2790
|
+
}
|
|
2791
|
+
return run(ctx, s, normalised2, options, "personalizedPageRank");
|
|
2792
|
+
}
|
|
2793
|
+
const BATCH = 8;
|
|
2794
|
+
const RING_SLOTS = 4 * BATCH + 8;
|
|
2795
|
+
function checkDest(dest, n, algorithm) {
|
|
2796
|
+
if (dest === void 0) {
|
|
2797
|
+
return null;
|
|
2798
|
+
}
|
|
2799
|
+
if (dest instanceof Float32Array && dest.length === n && dest.buffer instanceof ArrayBuffer) {
|
|
2800
|
+
return dest;
|
|
2801
|
+
}
|
|
2802
|
+
throw new WebGpuGraphError(
|
|
2803
|
+
"E_INVALID_ARGUMENT",
|
|
2804
|
+
`${algorithm}: dest must be a Float32Array of length ${n} over an ArrayBuffer`,
|
|
2805
|
+
{
|
|
2806
|
+
argument: "dest",
|
|
2807
|
+
value: `${dest.constructor.name}(${dest.length})`,
|
|
2808
|
+
expected: `Float32Array(${n}) over an ArrayBuffer`
|
|
2809
|
+
}
|
|
2810
|
+
);
|
|
2811
|
+
}
|
|
2812
|
+
function coreOf(ctx, s, algorithm) {
|
|
2813
|
+
try {
|
|
2814
|
+
return ctx.residency.core(s);
|
|
2815
|
+
} catch (error) {
|
|
2816
|
+
if (isWebGpuGraphError(error) && error.code === "E_TOO_LARGE" && error.details.path === "windowed") {
|
|
2817
|
+
throw new WebGpuGraphError(
|
|
2818
|
+
"E_TOO_LARGE",
|
|
2819
|
+
`${algorithm}: the arc arrays need a windowed upload, which P1-P3 plan but do not execute`,
|
|
2820
|
+
{ ...error.details, algorithm }
|
|
2821
|
+
);
|
|
2822
|
+
}
|
|
2823
|
+
throw error;
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
function reverseOf(ctx, s) {
|
|
2827
|
+
const view = ctx.residency.view(s, "reverse");
|
|
2828
|
+
return coreOfView(view, view.scalars.arcCount[0]);
|
|
2829
|
+
}
|
|
2830
|
+
function aborted(algorithm, batchId) {
|
|
2831
|
+
return new WebGpuGraphError(
|
|
2832
|
+
"E_ABORTED",
|
|
2833
|
+
`${algorithm}: the signal was aborted`,
|
|
2834
|
+
batchId === void 0 ? {} : { batchId }
|
|
2835
|
+
);
|
|
2836
|
+
}
|
|
2837
|
+
function whole(buffer, size) {
|
|
2838
|
+
return { buffer, offset: 0, size, window: null };
|
|
2839
|
+
}
|
|
2840
|
+
async function runPowerIteration(ctx, n, config) {
|
|
2841
|
+
const scope = algorithmScope(ctx, config.label, RING_SLOTS);
|
|
2842
|
+
try {
|
|
2843
|
+
const bytes = 4 * n;
|
|
2844
|
+
const ring = (config.alternate === null ? ["rankA", "rankB"] : ["rankA", "rankB", "rankC"]).map(
|
|
2845
|
+
(label) => whole(scope.scratch(bytes, label), bytes)
|
|
2846
|
+
);
|
|
2847
|
+
const xNorm = whole(scope.scratch(bytes, "xNorm"), bytes);
|
|
2848
|
+
const scalePlan = plan1d(n, ctx.workgroupSize, ctx.caps);
|
|
2849
|
+
const groups = groupsOf(scalePlan);
|
|
2850
|
+
const partialsBytes = PR_PARTIAL.byteLength * (1 + groups);
|
|
2851
|
+
const partialsBuffer = scope.scratch(partialsBytes, "partials");
|
|
2852
|
+
const partials = whole(partialsBuffer, partialsBytes);
|
|
2853
|
+
await ctx.allocator.check();
|
|
2854
|
+
const pullOptions = { personalization: false, dangling: false, weights: config.weights, tiers: null };
|
|
2855
|
+
const pulls = [{ core: config.adjacency, pull: await prepareSpmvPull(scope, config.adjacency, pullOptions) }];
|
|
2856
|
+
if (config.alternate !== null) {
|
|
2857
|
+
pulls.push({ core: config.alternate, pull: await prepareSpmvPull(scope, config.alternate, pullOptions) });
|
|
2858
|
+
}
|
|
2859
|
+
const scaleNorm = await ctx.pipelines.kernel(kernelSpec("pr-scale", { NORM_MODE: config.normMode }));
|
|
2860
|
+
const scaleApply = config.normMode === 4 ? null : await ctx.pipelines.kernel(kernelSpec("pr-scale", { NORM_MODE: 3 }));
|
|
2861
|
+
const finalize = await ctx.pipelines.kernel(kernelSpec("pr-finalize", { NORM_MODE: config.normMode }));
|
|
2862
|
+
const finalizePlan = plan1d(1, ctx.workgroupSize, ctx.caps);
|
|
2863
|
+
const { queue } = ctx.device;
|
|
2864
|
+
queue.writeBuffer(ring[0].buffer, 0, new Float32Array(n).fill(1 / n));
|
|
2865
|
+
for (const slot of ring.slice(1)) {
|
|
2866
|
+
queue.writeBuffer(slot.buffer, 0, new Float32Array(n));
|
|
2867
|
+
}
|
|
2868
|
+
const header = new ArrayBuffer(PR_PARTIAL.byteLength);
|
|
2869
|
+
PR_PARTIAL.write(new DataView(header), { firstConverged: U32_MAX, iteration: 0 });
|
|
2870
|
+
queue.writeBuffer(partialsBuffer, 0, header);
|
|
2871
|
+
const coefficients = { alpha: config.alpha, beta: config.beta, uniformP: config.uniformP };
|
|
2872
|
+
let iterationsRun = 0;
|
|
2873
|
+
for (; ; ) {
|
|
2874
|
+
const k2 = Math.min(BATCH, config.maxIterations - iterationsRun);
|
|
2875
|
+
const batch = new CommandBatch(ctx, config.label);
|
|
2876
|
+
const pass = batch.pass("iterations");
|
|
2877
|
+
for (let i = 0; i < k2; i++) {
|
|
2878
|
+
const iteration = iterationsRun + i + 1;
|
|
2879
|
+
const params = scope.params(PR_PARAMS, {
|
|
2880
|
+
n,
|
|
2881
|
+
groups,
|
|
2882
|
+
iteration,
|
|
2883
|
+
trackConvergence: 1,
|
|
2884
|
+
convergeThreshold: config.tolerance * n
|
|
2885
|
+
});
|
|
2886
|
+
const rankIn = ring[(iteration - 1) % ring.length];
|
|
2887
|
+
const rankOut = ring[iteration % ring.length];
|
|
2888
|
+
const { core, pull } = pulls[(iteration - 1) % pulls.length];
|
|
2889
|
+
const scaleBindings = { rankIn, rankPrev: rankOut, outWeightSum: rankIn, xNorm, partials, P: params.binding };
|
|
2890
|
+
scaleNorm.dispatch(pass, scaleNorm.bind(scaleBindings), scalePlan, [params.offset]);
|
|
2891
|
+
finalize.dispatch(pass, finalize.bind({ partials, P: params.binding }), finalizePlan, [params.offset]);
|
|
2892
|
+
if (scaleApply !== null) {
|
|
2893
|
+
scaleApply.dispatch(pass, scaleApply.bind(scaleBindings), scalePlan, [params.offset]);
|
|
2894
|
+
}
|
|
2895
|
+
pull.record(pass, core, { xNorm, rankOut, personalization: null, partials }, coefficients);
|
|
2896
|
+
}
|
|
2897
|
+
batch.endPass();
|
|
2898
|
+
const headerRequest = batch.readback(partialsBuffer, 0, PR_PARTIAL.byteLength);
|
|
2899
|
+
const scoresRequest = batch.readback(ring[(iterationsRun + k2) % ring.length].buffer, 0, bytes);
|
|
2900
|
+
const previousRequest = config.alternate === null ? null : batch.readback(ring[(iterationsRun + k2 - 1) % ring.length].buffer, 0, bytes);
|
|
2901
|
+
scope.flush();
|
|
2902
|
+
const submitted = batch.submit();
|
|
2903
|
+
const back = await submitted.readback;
|
|
2904
|
+
iterationsRun += k2;
|
|
2905
|
+
ctx.assertReady();
|
|
2906
|
+
if (config.signal?.aborted === true) {
|
|
2907
|
+
throw aborted(config.label, submitted.id);
|
|
2908
|
+
}
|
|
2909
|
+
config.onProgress?.(iterationsRun, config.maxIterations);
|
|
2910
|
+
const folded = PR_PARTIAL.read(new DataView(back), headerRequest.offset);
|
|
2911
|
+
const firstConverged = folded.firstConverged;
|
|
2912
|
+
const converged = firstConverged !== U32_MAX;
|
|
2913
|
+
if (converged || iterationsRun >= config.maxIterations) {
|
|
2914
|
+
if (converged && iterationsRun < config.maxIterations) {
|
|
2915
|
+
config.onProgress?.(config.maxIterations, config.maxIterations);
|
|
2916
|
+
}
|
|
2917
|
+
return {
|
|
2918
|
+
scores: new Float32Array(back, scoresRequest.offset, n).slice(),
|
|
2919
|
+
previous: previousRequest === null ? null : new Float32Array(back, previousRequest.offset, n).slice(),
|
|
2920
|
+
iterations: converged ? firstConverged : iterationsRun,
|
|
2921
|
+
converged,
|
|
2922
|
+
iterationsRun
|
|
2923
|
+
};
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
} finally {
|
|
2927
|
+
scope.dispose();
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
function resolve(ctx, s, options, algorithm) {
|
|
2931
|
+
ctx.assertReady();
|
|
2932
|
+
const n = s.nodeCount;
|
|
2933
|
+
const maxIterations = options?.maxIterations ?? 100;
|
|
2934
|
+
const tolerance = options?.tolerance ?? 1e-6;
|
|
2935
|
+
if (!Number.isInteger(maxIterations) || maxIterations < 1) {
|
|
2936
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${algorithm}: maxIterations must be a positive integer`, {
|
|
2937
|
+
argument: "maxIterations",
|
|
2938
|
+
value: maxIterations,
|
|
2939
|
+
expected: "a positive integer"
|
|
2940
|
+
});
|
|
2941
|
+
}
|
|
2942
|
+
const dest = checkDest(options?.dest, n, algorithm);
|
|
2943
|
+
if (options?.signal?.aborted) {
|
|
2944
|
+
throw aborted(algorithm);
|
|
2945
|
+
}
|
|
2946
|
+
return { n, maxIterations, tolerance, weights: options?.weighted === false ? null : void 0, dest };
|
|
2947
|
+
}
|
|
2948
|
+
function finite(value, fallback, argument) {
|
|
2949
|
+
const resolved = value ?? fallback;
|
|
2950
|
+
if (!Number.isFinite(resolved)) {
|
|
2951
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", `katzCentrality: ${argument} must be a finite number`, {
|
|
2952
|
+
argument,
|
|
2953
|
+
value: resolved,
|
|
2954
|
+
expected: "a finite number"
|
|
2955
|
+
});
|
|
2956
|
+
}
|
|
2957
|
+
return resolved;
|
|
2958
|
+
}
|
|
2959
|
+
function normalised(raw, dest, norm) {
|
|
2960
|
+
let acc = 0;
|
|
2961
|
+
for (const v of raw) {
|
|
2962
|
+
acc += norm === "sum" ? Math.abs(v) : v * v;
|
|
2963
|
+
}
|
|
2964
|
+
const scale = norm === "sum" ? acc : Math.sqrt(acc);
|
|
2965
|
+
const out = dest ?? raw;
|
|
2966
|
+
for (let v = 0; v < raw.length; v++) {
|
|
2967
|
+
out[v] = scale > 0 ? raw[v] / scale : raw[v];
|
|
2968
|
+
}
|
|
2969
|
+
return out;
|
|
2970
|
+
}
|
|
2971
|
+
function scoresOf(run2, dest, norm) {
|
|
2972
|
+
return {
|
|
2973
|
+
scores: normalised(run2.scores, dest, norm),
|
|
2974
|
+
iterations: run2.iterations,
|
|
2975
|
+
converged: run2.converged,
|
|
2976
|
+
precision: "f32"
|
|
2977
|
+
};
|
|
2978
|
+
}
|
|
2979
|
+
function empty(dest, total, onProgress) {
|
|
2980
|
+
onProgress?.(total, total);
|
|
2981
|
+
return { scores: dest ?? new Float32Array(0), iterations: 0, converged: true, precision: "f32" };
|
|
2982
|
+
}
|
|
2983
|
+
function latestOfKind(sameSeed, otherSeed) {
|
|
2984
|
+
const fromSame = sameSeed.iterationsRun % 2 === 0 ? sameSeed.iterationsRun : sameSeed.iterationsRun - 1;
|
|
2985
|
+
const fromOther = otherSeed.iterationsRun % 2 === 1 ? otherSeed.iterationsRun : otherSeed.iterationsRun - 1;
|
|
2986
|
+
const chain = fromSame >= fromOther ? sameSeed : otherSeed;
|
|
2987
|
+
const index = Math.max(fromSame, fromOther);
|
|
2988
|
+
return (index === chain.iterationsRun ? chain.scores : chain.previous) ?? chain.scores;
|
|
2989
|
+
}
|
|
2990
|
+
async function eigenvectorCentrality(ctx, s, options) {
|
|
2991
|
+
const algorithm = "eigenvectorCentrality";
|
|
2992
|
+
const r = resolve(ctx, s, options, algorithm);
|
|
2993
|
+
if (r.n === 0) {
|
|
2994
|
+
return empty(r.dest, r.maxIterations, options?.onProgress);
|
|
2995
|
+
}
|
|
2996
|
+
const run2 = await runPowerIteration(ctx, r.n, {
|
|
2997
|
+
normMode: 2,
|
|
2998
|
+
adjacency: coreOf(ctx, s, algorithm),
|
|
2999
|
+
alternate: null,
|
|
3000
|
+
alpha: 1,
|
|
3001
|
+
beta: 0,
|
|
3002
|
+
uniformP: 0,
|
|
3003
|
+
maxIterations: r.maxIterations,
|
|
3004
|
+
tolerance: r.tolerance,
|
|
3005
|
+
weights: r.weights,
|
|
3006
|
+
label: algorithm,
|
|
3007
|
+
signal: options?.signal,
|
|
3008
|
+
onProgress: options?.onProgress
|
|
3009
|
+
});
|
|
3010
|
+
return scoresOf(run2, r.dest, "l2");
|
|
3011
|
+
}
|
|
3012
|
+
async function katzCentrality(ctx, s, options) {
|
|
3013
|
+
const algorithm = "katzCentrality";
|
|
3014
|
+
const alpha = finite(options?.alpha, 0.1, "alpha");
|
|
3015
|
+
const beta = finite(options?.beta, 1, "beta");
|
|
3016
|
+
const r = resolve(ctx, s, options, algorithm);
|
|
3017
|
+
if (r.n === 0) {
|
|
3018
|
+
return empty(r.dest, r.maxIterations, options?.onProgress);
|
|
3019
|
+
}
|
|
3020
|
+
const run2 = await runPowerIteration(ctx, r.n, {
|
|
3021
|
+
normMode: 4,
|
|
3022
|
+
adjacency: reverseOf(ctx, s),
|
|
3023
|
+
alternate: null,
|
|
3024
|
+
alpha,
|
|
3025
|
+
beta,
|
|
3026
|
+
uniformP: 1,
|
|
3027
|
+
maxIterations: r.maxIterations,
|
|
3028
|
+
tolerance: r.tolerance,
|
|
3029
|
+
weights: r.weights,
|
|
3030
|
+
label: algorithm,
|
|
3031
|
+
signal: options?.signal,
|
|
3032
|
+
onProgress: options?.onProgress
|
|
3033
|
+
});
|
|
3034
|
+
return scoresOf(run2, r.dest, "l2");
|
|
3035
|
+
}
|
|
3036
|
+
async function hits(ctx, s, options) {
|
|
3037
|
+
const algorithm = "hits";
|
|
3038
|
+
const r = resolve(ctx, s, options, algorithm);
|
|
3039
|
+
const total = 2 * r.maxIterations;
|
|
3040
|
+
if (r.n === 0) {
|
|
3041
|
+
const { scores, iterations, converged, precision } = empty(r.dest, total, options?.onProgress);
|
|
3042
|
+
return { hubs: scores, authorities: new Float32Array(0), iterations, converged, precision };
|
|
3043
|
+
}
|
|
3044
|
+
const onProgress = options?.onProgress;
|
|
3045
|
+
const shared = {
|
|
3046
|
+
normMode: 1,
|
|
3047
|
+
alpha: 1,
|
|
3048
|
+
beta: 0,
|
|
3049
|
+
uniformP: 0,
|
|
3050
|
+
maxIterations: r.maxIterations,
|
|
3051
|
+
tolerance: r.tolerance,
|
|
3052
|
+
weights: r.weights,
|
|
3053
|
+
signal: options?.signal
|
|
3054
|
+
};
|
|
3055
|
+
const forward = coreOf(ctx, s, algorithm);
|
|
3056
|
+
const reverse = reverseOf(ctx, s);
|
|
3057
|
+
const hubSeeded = await runPowerIteration(ctx, r.n, {
|
|
3058
|
+
...shared,
|
|
3059
|
+
adjacency: reverse,
|
|
3060
|
+
alternate: forward,
|
|
3061
|
+
label: `${algorithm}/hub-seeded`,
|
|
3062
|
+
onProgress: onProgress === void 0 ? void 0 : (done) => {
|
|
3063
|
+
onProgress(done, total);
|
|
3064
|
+
}
|
|
3065
|
+
});
|
|
3066
|
+
const authoritySeeded = await runPowerIteration(ctx, r.n, {
|
|
3067
|
+
...shared,
|
|
3068
|
+
adjacency: forward,
|
|
3069
|
+
alternate: reverse,
|
|
3070
|
+
label: `${algorithm}/authority-seeded`,
|
|
3071
|
+
onProgress: onProgress === void 0 ? void 0 : (done) => {
|
|
3072
|
+
onProgress(r.maxIterations + done, total);
|
|
3073
|
+
}
|
|
3074
|
+
});
|
|
3075
|
+
return {
|
|
3076
|
+
hubs: normalised(latestOfKind(hubSeeded, authoritySeeded), r.dest, "sum"),
|
|
3077
|
+
authorities: normalised(latestOfKind(authoritySeeded, hubSeeded), null, "sum"),
|
|
3078
|
+
iterations: Math.max(hubSeeded.iterations, authoritySeeded.iterations),
|
|
3079
|
+
converged: hubSeeded.converged && authoritySeeded.converged,
|
|
3080
|
+
precision: "f32"
|
|
3081
|
+
};
|
|
3082
|
+
}
|
|
1641
3083
|
const LCG_M = 34359738337;
|
|
1642
3084
|
const LCG_A = 185852;
|
|
1643
3085
|
const LCG_C = 1;
|
|
@@ -1710,7 +3152,7 @@ function seedPositions(s, positions, seed, dim, scale, center, range) {
|
|
|
1710
3152
|
let finiteRows = 0;
|
|
1711
3153
|
let unseededRows = 0;
|
|
1712
3154
|
for (let i = 0; i < n; i++) {
|
|
1713
|
-
let
|
|
3155
|
+
let finite2 = true;
|
|
1714
3156
|
for (let axis = 0; axis < dim; axis++) {
|
|
1715
3157
|
const v = positions[3 * i + axis];
|
|
1716
3158
|
if (Number.isFinite(v)) {
|
|
@@ -1721,10 +3163,10 @@ function seedPositions(s, positions, seed, dim, scale, center, range) {
|
|
|
1721
3163
|
hi[axis] = v;
|
|
1722
3164
|
}
|
|
1723
3165
|
} else {
|
|
1724
|
-
|
|
3166
|
+
finite2 = false;
|
|
1725
3167
|
}
|
|
1726
3168
|
}
|
|
1727
|
-
if (
|
|
3169
|
+
if (finite2) {
|
|
1728
3170
|
finiteRows++;
|
|
1729
3171
|
} else {
|
|
1730
3172
|
unseededRows++;
|
|
@@ -1837,7 +3279,7 @@ function fieldTypeOf(block, field) {
|
|
|
1837
3279
|
throw invalidArgument(
|
|
1838
3280
|
"field",
|
|
1839
3281
|
field,
|
|
1840
|
-
block.fields.map((
|
|
3282
|
+
block.fields.map((f) => f[0]),
|
|
1841
3283
|
`block ${block.name} has no field ${field}`
|
|
1842
3284
|
);
|
|
1843
3285
|
}
|
|
@@ -1950,7 +3392,7 @@ class HeaderWriter {
|
|
|
1950
3392
|
throw invalidArgument(
|
|
1951
3393
|
"field",
|
|
1952
3394
|
field,
|
|
1953
|
-
this.block.fields.map((
|
|
3395
|
+
this.block.fields.map((f) => f[0]),
|
|
1954
3396
|
`state block has no field ${field}`
|
|
1955
3397
|
);
|
|
1956
3398
|
}
|
|
@@ -2002,8 +3444,8 @@ class HeaderWriter {
|
|
|
2002
3444
|
function createPending(k2, generation) {
|
|
2003
3445
|
let resolveFn = () => void 0;
|
|
2004
3446
|
let rejectFn = () => void 0;
|
|
2005
|
-
const promise = new Promise((
|
|
2006
|
-
resolveFn =
|
|
3447
|
+
const promise = new Promise((resolve2, reject) => {
|
|
3448
|
+
resolveFn = resolve2;
|
|
2007
3449
|
rejectFn = reject;
|
|
2008
3450
|
});
|
|
2009
3451
|
return {
|
|
@@ -2034,7 +3476,7 @@ class ForceSimulation {
|
|
|
2034
3476
|
* @param tuning - the resolved GPU tuning
|
|
2035
3477
|
* @param resolve - how setParams merges a patch into the current record
|
|
2036
3478
|
*/
|
|
2037
|
-
constructor(ctx, model, options, tuning,
|
|
3479
|
+
constructor(ctx, model, options, tuning, resolve2) {
|
|
2038
3480
|
this.overrideList = /* @__PURE__ */ new Map();
|
|
2039
3481
|
this.pending = [];
|
|
2040
3482
|
this.stateValue = "created";
|
|
@@ -2109,7 +3551,7 @@ class ForceSimulation {
|
|
|
2109
3551
|
this.ctx = ctx;
|
|
2110
3552
|
this.model = model;
|
|
2111
3553
|
this.tuning = tuning;
|
|
2112
|
-
this.resolveOptions =
|
|
3554
|
+
this.resolveOptions = resolve2;
|
|
2113
3555
|
this.optionsValue = options;
|
|
2114
3556
|
this.maxInFlight = maxInFlight;
|
|
2115
3557
|
this.dimValue = dim;
|
|
@@ -3299,8 +4741,8 @@ class ForceSimulation {
|
|
|
3299
4741
|
*/
|
|
3300
4742
|
startBind(resources, overrides) {
|
|
3301
4743
|
const generation = this.generationValue;
|
|
3302
|
-
const
|
|
3303
|
-
const promise = this.ready.catch(() => void 0).then(
|
|
4744
|
+
const run2 = () => this.compileAndBind(resources, overrides, generation);
|
|
4745
|
+
const promise = this.ready.catch(() => void 0).then(run2);
|
|
3304
4746
|
void promise.catch(() => void 0);
|
|
3305
4747
|
this.ready = promise;
|
|
3306
4748
|
}
|
|
@@ -4416,6 +5858,79 @@ function createAccelerator(ctx, options) {
|
|
|
4416
5858
|
ctx.assertReady();
|
|
4417
5859
|
return createForceAtlas2(ctx, { ...o, ...frozen.layout });
|
|
4418
5860
|
},
|
|
5861
|
+
/**
|
|
5862
|
+
* PageRank on the device (spec 8.2; contract 3.14).
|
|
5863
|
+
* @param gs - the snapshot
|
|
5864
|
+
* @param o - the CPU option record (spec 9.2 PageRankOptions)
|
|
5865
|
+
* @returns the f32 scores with `precision: "f32"` (spec 9.7)
|
|
5866
|
+
*/
|
|
5867
|
+
async pageRank(gs, o) {
|
|
5868
|
+
ctx.assertReady();
|
|
5869
|
+
return await pageRank(ctx, gs, o);
|
|
5870
|
+
},
|
|
5871
|
+
/**
|
|
5872
|
+
* Personalized PageRank on the device (spec 8.2; contract 3.14). The mirror admits an f64 personalization
|
|
5873
|
+
* (spec 9.2 `F32 | F64`); the kernel reads f32, so an f64 vector is narrowed on the host first.
|
|
5874
|
+
* @param gs - the snapshot
|
|
5875
|
+
* @param personalization - one finite non-negative mass per node, not all zero
|
|
5876
|
+
* @param o - the CPU option record (spec 9.2 PageRankOptions)
|
|
5877
|
+
* @returns the f32 scores with `precision: "f32"` (spec 9.7)
|
|
5878
|
+
*/
|
|
5879
|
+
async personalizedPageRank(gs, personalization, o) {
|
|
5880
|
+
ctx.assertReady();
|
|
5881
|
+
const mass = personalization instanceof Float32Array ? personalization : Float32Array.from(personalization);
|
|
5882
|
+
return await personalizedPageRank(ctx, gs, mass, o);
|
|
5883
|
+
},
|
|
5884
|
+
/**
|
|
5885
|
+
* HITS hubs and authorities on the device (spec 8.2; contract 3.14).
|
|
5886
|
+
* @param gs - the snapshot
|
|
5887
|
+
* @param o - the CPU option record (spec 9.2 HitsOptions)
|
|
5888
|
+
* @returns hubs and authorities with `precision: "f32"` (spec 9.7)
|
|
5889
|
+
*/
|
|
5890
|
+
async hits(gs, o) {
|
|
5891
|
+
ctx.assertReady();
|
|
5892
|
+
return await hits(ctx, gs, o);
|
|
5893
|
+
},
|
|
5894
|
+
/**
|
|
5895
|
+
* Eigenvector centrality on the device (spec 8.2; contract 3.14).
|
|
5896
|
+
* @param gs - the snapshot
|
|
5897
|
+
* @param o - the CPU option record (spec 9.2 EigenvectorOptions)
|
|
5898
|
+
* @returns the f32 scores with `precision: "f32"` (spec 9.7)
|
|
5899
|
+
*/
|
|
5900
|
+
async eigenvectorCentrality(gs, o) {
|
|
5901
|
+
ctx.assertReady();
|
|
5902
|
+
return await eigenvectorCentrality(ctx, gs, o);
|
|
5903
|
+
},
|
|
5904
|
+
/**
|
|
5905
|
+
* Katz centrality on the device (spec 8.2; contract 3.14).
|
|
5906
|
+
* @param gs - the snapshot
|
|
5907
|
+
* @param o - the CPU option record (spec 9.2 KatzOptions)
|
|
5908
|
+
* @returns the f32 scores with `precision: "f32"` (spec 9.7)
|
|
5909
|
+
*/
|
|
5910
|
+
async katzCentrality(gs, o) {
|
|
5911
|
+
ctx.assertReady();
|
|
5912
|
+
return await katzCentrality(ctx, gs, o);
|
|
5913
|
+
},
|
|
5914
|
+
/**
|
|
5915
|
+
* Weakly connected components on the device (spec 8.3; contract 3.14): WCC semantics on directed input.
|
|
5916
|
+
* @param gs - the snapshot
|
|
5917
|
+
* @param o - `renumber` (default true); the mirror passes none
|
|
5918
|
+
* @returns the labels, the count and groups() (spec 9.7 LabelResultLike)
|
|
5919
|
+
*/
|
|
5920
|
+
async connectedComponents(gs, o) {
|
|
5921
|
+
ctx.assertReady();
|
|
5922
|
+
return await connectedComponents(ctx, gs, o);
|
|
5923
|
+
},
|
|
5924
|
+
/**
|
|
5925
|
+
* The same algorithm as `connectedComponents` under the mirror's other name (spec 3.3, 9.2).
|
|
5926
|
+
* @param gs - the snapshot
|
|
5927
|
+
* @param o - `renumber` (default true); the mirror passes none
|
|
5928
|
+
* @returns the labels, the count and groups() (spec 9.7 LabelResultLike)
|
|
5929
|
+
*/
|
|
5930
|
+
async weaklyConnectedComponents(gs, o) {
|
|
5931
|
+
ctx.assertReady();
|
|
5932
|
+
return await connectedComponents(ctx, gs, o);
|
|
5933
|
+
},
|
|
4419
5934
|
/**
|
|
4420
5935
|
* Destroys every device buffer recorded for the snapshot (spec 4.5); delegates to ctx.release.
|
|
4421
5936
|
* @param s - the snapshot the app is done with
|
|
@@ -4439,16 +5954,22 @@ export {
|
|
|
4439
5954
|
LAYOUT_TUNING_DEFAULTS,
|
|
4440
5955
|
MAX_1D_ITEMS,
|
|
4441
5956
|
MAX_WORKGROUPS_PER_DIM,
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
5957
|
+
g as PASSTHROUGH_FORMAT_CODES,
|
|
5958
|
+
j as STORAGE_ALIGN,
|
|
5959
|
+
k as WORKGROUP_SIZE,
|
|
4445
5960
|
WebGpuGraphError,
|
|
5961
|
+
connectedComponents,
|
|
4446
5962
|
createAccelerator,
|
|
4447
5963
|
createForceAtlas2,
|
|
4448
5964
|
degree,
|
|
5965
|
+
eigenvectorCentrality,
|
|
4449
5966
|
hasErrorCode,
|
|
4450
|
-
|
|
5967
|
+
hits,
|
|
5968
|
+
l as isSoftwareAdapter,
|
|
4451
5969
|
isWebGpuGraphError,
|
|
5970
|
+
katzCentrality,
|
|
5971
|
+
pageRank,
|
|
5972
|
+
personalizedPageRank,
|
|
4452
5973
|
seedPositions
|
|
4453
5974
|
};
|
|
4454
5975
|
//# sourceMappingURL=webgpu-graph-algorithms.js.map
|