@graphty/webgpu-graph-algorithms 0.6.15 → 0.6.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/README.md +5 -5
  2. package/dist/browser.js +1 -1
  3. package/dist/chunks/{context-oXphO3yj.js → context-VIvatQOo.js} +61 -34
  4. package/dist/chunks/context-VIvatQOo.js.map +1 -0
  5. package/dist/node.js +1 -1
  6. package/dist/src/accelerator.d.ts +2 -1
  7. package/dist/src/accelerator.d.ts.map +1 -1
  8. package/dist/src/accelerator.js +53 -1
  9. package/dist/src/accelerator.js.map +1 -1
  10. package/dist/src/algorithms/all-pairs.d.ts +41 -0
  11. package/dist/src/algorithms/all-pairs.d.ts.map +1 -0
  12. package/dist/src/algorithms/all-pairs.js +181 -0
  13. package/dist/src/algorithms/all-pairs.js.map +1 -0
  14. package/dist/src/algorithms/components.d.ts +9 -1
  15. package/dist/src/algorithms/components.d.ts.map +1 -1
  16. package/dist/src/algorithms/components.js +2 -2
  17. package/dist/src/algorithms/components.js.map +1 -1
  18. package/dist/src/algorithms/label-propagation.d.ts +31 -0
  19. package/dist/src/algorithms/label-propagation.d.ts.map +1 -0
  20. package/dist/src/algorithms/label-propagation.js +254 -0
  21. package/dist/src/algorithms/label-propagation.js.map +1 -0
  22. package/dist/src/algorithms/simple-symmetric.d.ts +88 -0
  23. package/dist/src/algorithms/simple-symmetric.d.ts.map +1 -0
  24. package/dist/src/algorithms/simple-symmetric.js +347 -0
  25. package/dist/src/algorithms/simple-symmetric.js.map +1 -0
  26. package/dist/src/algorithms/triangles.d.ts +34 -0
  27. package/dist/src/algorithms/triangles.d.ts.map +1 -0
  28. package/dist/src/algorithms/triangles.js +203 -0
  29. package/dist/src/algorithms/triangles.js.map +1 -0
  30. package/dist/src/constants.d.ts +45 -0
  31. package/dist/src/constants.d.ts.map +1 -1
  32. package/dist/src/constants.js +45 -0
  33. package/dist/src/constants.js.map +1 -1
  34. package/dist/src/index.d.ts +8 -1
  35. package/dist/src/index.d.ts.map +1 -1
  36. package/dist/src/index.js +7 -1
  37. package/dist/src/index.js.map +1 -1
  38. package/dist/src/kernel/prelude.d.ts.map +1 -1
  39. package/dist/src/kernel/prelude.js +4 -1
  40. package/dist/src/kernel/prelude.js.map +1 -1
  41. package/dist/src/kernels.d.ts +16 -4
  42. package/dist/src/kernels.d.ts.map +1 -1
  43. package/dist/src/kernels.js +223 -3
  44. package/dist/src/kernels.js.map +1 -1
  45. package/dist/src/memory/residency.js +15 -4
  46. package/dist/src/memory/residency.js.map +1 -1
  47. package/dist/src/primitives/coo-to-csr.d.ts +73 -0
  48. package/dist/src/primitives/coo-to-csr.d.ts.map +1 -0
  49. package/dist/src/primitives/coo-to-csr.js +183 -0
  50. package/dist/src/primitives/coo-to-csr.js.map +1 -0
  51. package/dist/src/primitives/group-by-key.d.ts +82 -0
  52. package/dist/src/primitives/group-by-key.d.ts.map +1 -0
  53. package/dist/src/primitives/group-by-key.js +147 -0
  54. package/dist/src/primitives/group-by-key.js.map +1 -0
  55. package/dist/src/types/accelerator.d.ts +10 -2
  56. package/dist/src/types/accelerator.d.ts.map +1 -1
  57. package/dist/src/types/all-pairs.d.ts +35 -0
  58. package/dist/src/types/all-pairs.d.ts.map +1 -0
  59. package/dist/src/types/all-pairs.js +8 -0
  60. package/dist/src/types/all-pairs.js.map +1 -0
  61. package/dist/src/types/community.d.ts +18 -0
  62. package/dist/src/types/community.d.ts.map +1 -0
  63. package/dist/src/types/community.js +5 -0
  64. package/dist/src/types/community.js.map +1 -0
  65. package/dist/src/types/structure.d.ts +27 -0
  66. package/dist/src/types/structure.d.ts.map +1 -0
  67. package/dist/src/types/structure.js +8 -0
  68. package/dist/src/types/structure.js.map +1 -0
  69. package/dist/src/wgsl/apsp-fw.wgsl.d.ts +25 -0
  70. package/dist/src/wgsl/apsp-fw.wgsl.d.ts.map +1 -0
  71. package/dist/src/wgsl/apsp-fw.wgsl.js +113 -0
  72. package/dist/src/wgsl/apsp-fw.wgsl.js.map +1 -0
  73. package/dist/src/wgsl/apsp-init.wgsl.d.ts +12 -0
  74. package/dist/src/wgsl/apsp-init.wgsl.d.ts.map +1 -0
  75. package/dist/src/wgsl/apsp-init.wgsl.js +26 -0
  76. package/dist/src/wgsl/apsp-init.wgsl.js.map +1 -0
  77. package/dist/src/wgsl/coo-emit.wgsl.d.ts +10 -0
  78. package/dist/src/wgsl/coo-emit.wgsl.d.ts.map +1 -0
  79. package/dist/src/wgsl/coo-emit.wgsl.js +33 -0
  80. package/dist/src/wgsl/coo-emit.wgsl.js.map +1 -0
  81. package/dist/src/wgsl/coo-scatter.wgsl.d.ts +15 -0
  82. package/dist/src/wgsl/coo-scatter.wgsl.d.ts.map +1 -0
  83. package/dist/src/wgsl/coo-scatter.wgsl.js +32 -0
  84. package/dist/src/wgsl/coo-scatter.wgsl.js.map +1 -0
  85. package/dist/src/wgsl/group-by-key-row.wgsl.d.ts +26 -0
  86. package/dist/src/wgsl/group-by-key-row.wgsl.d.ts.map +1 -0
  87. package/dist/src/wgsl/group-by-key-row.wgsl.js +146 -0
  88. package/dist/src/wgsl/group-by-key-row.wgsl.js.map +1 -0
  89. package/dist/src/wgsl/lpa-step.wgsl.d.ts +10 -0
  90. package/dist/src/wgsl/lpa-step.wgsl.d.ts.map +1 -0
  91. package/dist/src/wgsl/lpa-step.wgsl.js +35 -0
  92. package/dist/src/wgsl/lpa-step.wgsl.js.map +1 -0
  93. package/dist/src/wgsl/orient-flags.wgsl.d.ts +9 -0
  94. package/dist/src/wgsl/orient-flags.wgsl.d.ts.map +1 -0
  95. package/dist/src/wgsl/orient-flags.wgsl.js +21 -0
  96. package/dist/src/wgsl/orient-flags.wgsl.js.map +1 -0
  97. package/dist/src/wgsl/run-flags.wgsl.d.ts +8 -0
  98. package/dist/src/wgsl/run-flags.wgsl.d.ts.map +1 -0
  99. package/dist/src/wgsl/run-flags.wgsl.js +18 -0
  100. package/dist/src/wgsl/run-flags.wgsl.js.map +1 -0
  101. package/dist/src/wgsl/tri-intersect.wgsl.d.ts +11 -0
  102. package/dist/src/wgsl/tri-intersect.wgsl.d.ts.map +1 -0
  103. package/dist/src/wgsl/tri-intersect.wgsl.js +64 -0
  104. package/dist/src/wgsl/tri-intersect.wgsl.js.map +1 -0
  105. package/dist/webgpu-graph-algorithms.js +1819 -181
  106. package/dist/webgpu-graph-algorithms.js.map +1 -1
  107. package/package.json +4 -4
  108. package/src/accelerator.ts +56 -1
  109. package/src/algorithms/all-pairs.ts +228 -0
  110. package/src/algorithms/components.ts +2 -2
  111. package/src/algorithms/label-propagation.ts +280 -0
  112. package/src/algorithms/simple-symmetric.ts +409 -0
  113. package/src/algorithms/triangles.ts +240 -0
  114. package/src/constants.ts +45 -0
  115. package/src/index.ts +12 -1
  116. package/src/kernel/prelude.ts +6 -0
  117. package/src/kernels.ts +248 -6
  118. package/src/memory/residency.ts +15 -4
  119. package/src/primitives/coo-to-csr.ts +251 -0
  120. package/src/primitives/group-by-key.ts +209 -0
  121. package/src/types/accelerator.ts +10 -2
  122. package/src/types/all-pairs.ts +37 -0
  123. package/src/types/community.ts +18 -0
  124. package/src/types/structure.ts +28 -0
  125. package/src/wgsl/apsp-fw.wgsl.ts +112 -0
  126. package/src/wgsl/apsp-init.wgsl.ts +25 -0
  127. package/src/wgsl/coo-emit.wgsl.ts +32 -0
  128. package/src/wgsl/coo-scatter.wgsl.ts +31 -0
  129. package/src/wgsl/group-by-key-row.wgsl.ts +145 -0
  130. package/src/wgsl/lpa-step.wgsl.ts +34 -0
  131. package/src/wgsl/orient-flags.wgsl.ts +20 -0
  132. package/src/wgsl/run-flags.wgsl.ts +17 -0
  133. package/src/wgsl/tri-intersect.wgsl.ts +63 -0
  134. package/dist/chunks/context-oXphO3yj.js.map +0 -1
@@ -0,0 +1,112 @@
1
+ /**
2
+ * The `apsp-fw` kernel body (design 8.7): one phase of one round of blocked Floyd-Warshall over `APSP_TILE x
3
+ * APSP_TILE` blocks of the row-major `n x n` matrix `dist`. Round `r` (`P.round`) runs three dispatches in order,
4
+ * the `PHASE` override choosing which:
5
+ *
6
+ * - PHASE 0, one workgroup: the pivot block `(r, r)`, staged in `tileA` and updated in place.
7
+ * - PHASE 1, `2 (B - 1)` workgroups (`B` = `P.blocks`): every other block of block row `r` (the first `B - 1`
8
+ * workgroups) and of block column `r` (the rest). The pivot block is staged in `tileA`, the block itself in
9
+ * `tileB`, updated in place.
10
+ * - PHASE 2, `(B - 1)^2` workgroups: every block `(i, j)` off the pivot row and column. Its two operands are the
11
+ * pivot-COLUMN block `(i, r)` in `tileA` and the pivot-ROW block `(r, j)` in `tileB`; its own cells are read,
12
+ * minimised over the 32 steps and written by one lane each, never shared, so they stay in registers.
13
+ *
14
+ * In-place updates are race-free because a cell is written only when the candidate is STRICTLY smaller: the cells
15
+ * every lane reads in step `k` (column `k` and row `k` of the tile) would be updated with `d + d[k][k]`, and the
16
+ * diagonal is `0` (weights are non-negative, which the driver checks) or `+Infinity` outside the matrix, so they are
17
+ * never written in step `k`. Every barrier is reached in uniform control flow: the early return keys on the workgroup
18
+ * id and uniforms only, the per-lane loops hold no barrier, and the lanes of an edge tile load `+Infinity` for a cell
19
+ * outside `n x n` (it offers no path; read from `P.infBits` because Tint refuses `+Infinity` as a constant) and
20
+ * store nothing there -- the matrix is exactly `n * n`, never padded, so the
21
+ * ceiling is `floor(sqrt(maxStorageBufferBindingSize / 4))`. Body only (spec 3.5, D9); the text is normative: the
22
+ * sabotage rows of test/helpers/sabotage.ts are textual edits of it.
23
+ */
24
+ export const apspFwWgsl = /* wgsl */ `
25
+ var<workgroup> tileA: array<f32, APSP_TILE * APSP_TILE>;
26
+ var<workgroup> tileB: array<f32, APSP_TILE * APSP_TILE>;
27
+
28
+ fn tile_row(block: vec2<u32>, c: u32) -> u32 { return block.x * APSP_TILE + c / APSP_TILE; }
29
+ fn tile_col(block: vec2<u32>, c: u32) -> u32 { return block.y * APSP_TILE + c % APSP_TILE; }
30
+
31
+ fn load_cell(block: vec2<u32>, c: u32) -> f32 {
32
+ let i = tile_row(block, c);
33
+ let j = tile_col(block, c);
34
+ if (i < P.n && j < P.n) { return dist[i * P.n + j]; }
35
+ return bitcast<f32>(P.infBits); // outside the matrix: no path through it
36
+ }
37
+
38
+ fn store_cell(block: vec2<u32>, c: u32, v: f32) {
39
+ let i = tile_row(block, c);
40
+ let j = tile_col(block, c);
41
+ if (i >= P.n || j >= P.n) { return; } // an edge tile stores nothing outside n x n
42
+ dist[i * P.n + j] = v;
43
+ }
44
+
45
+ @compute @workgroup_size(WG)
46
+ fn apsp_fw(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
47
+ let g = group_id(wid);
48
+ let r = P.round;
49
+ let others = max(P.blocks, 2u) - 1u; // the blocks of a strip, the pivot excluded
50
+ let cells = APSP_TILE * APSP_TILE;
51
+ var count = 1u;
52
+ var own = vec2<u32>(r, r); // the block this workgroup updates
53
+ var a = vec2<u32>(r, r); // staged in tileA
54
+ var b = vec2<u32>(r, r); // staged in tileB
55
+ if (PHASE == 1u) {
56
+ count = 2u * others;
57
+ let s = g % others;
58
+ let o = select(s, s + 1u, s >= r); // a strip skips the pivot block
59
+ own = select(vec2<u32>(o, r), vec2<u32>(r, o), g < others); // block row r first, then block column r
60
+ b = own;
61
+ }
62
+ if (PHASE == 2u) {
63
+ count = others * others;
64
+ let i = g / others;
65
+ let j = g % others;
66
+ own = vec2<u32>(select(i, i + 1u, i >= r), select(j, j + 1u, j >= r));
67
+ a = vec2<u32>(own.x, r); // the pivot-column block (i, r)
68
+ b = vec2<u32>(r, own.y); // the pivot-row block (r, j)
69
+ }
70
+ if (g >= count) { return; } // uniform: the workgroup id and uniforms only
71
+
72
+ for (var c = lid.x; c < cells; c = c + WG) {
73
+ tileA[c] = load_cell(a, c);
74
+ if (PHASE != 0u) { tileB[c] = load_cell(b, c); }
75
+ }
76
+ workgroupBarrier(); // every staged cell is visible
77
+
78
+ if (PHASE == 2u) {
79
+ for (var c = lid.x; c < cells; c = c + WG) {
80
+ let x = c / APSP_TILE;
81
+ let y = c % APSP_TILE;
82
+ var v = load_cell(own, c);
83
+ for (var kr = 0u; kr < APSP_TILE; kr = kr + 1u) {
84
+ v = min(v, tileA[x * APSP_TILE + kr] + tileB[kr * APSP_TILE + y]);
85
+ }
86
+ store_cell(own, c, v);
87
+ }
88
+ return;
89
+ }
90
+
91
+ let pivotRow = PHASE == 1u && own.x == r; // block row r reads d[x][k] from the pivot
92
+ for (var k = 0u; k < APSP_TILE; k = k + 1u) {
93
+ for (var c = lid.x; c < cells; c = c + WG) {
94
+ let x = c / APSP_TILE;
95
+ let y = c % APSP_TILE;
96
+ if (PHASE == 0u) {
97
+ let via = tileA[x * APSP_TILE + k] + tileA[k * APSP_TILE + y];
98
+ if (via < tileA[c]) { tileA[c] = via; }
99
+ } else {
100
+ let left = select(tileB[x * APSP_TILE + k], tileA[x * APSP_TILE + k], pivotRow);
101
+ let right = select(tileA[k * APSP_TILE + y], tileB[k * APSP_TILE + y], pivotRow);
102
+ let via = left + right;
103
+ if (via < tileB[c]) { tileB[c] = via; }
104
+ }
105
+ }
106
+ workgroupBarrier(); // step k is complete before step k + 1 reads
107
+ }
108
+ for (var c = lid.x; c < cells; c = c + WG) {
109
+ store_cell(own, c, select(tileB[c], tileA[c], PHASE == 0u));
110
+ }
111
+ }
112
+ `;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The `apsp-init` kernel body (design 8.7): writes the arcs into the `n x n` distance matrix after `fill` has set
3
+ * every entry to `+Infinity`. One lane per ROW `u`: it walks `u`'s arcs and keeps the cheapest weight per target
4
+ * (`min`), so parallel arcs collapse to the cheapest one, then writes the diagonal zero LAST, so a self-loop never
5
+ * displaces it. Every arc of row `u` lands in row `u` of the matrix and only this lane writes that row, so there is
6
+ * no race and no atomic. Unweighted (`HAS_WEIGHTS` false) every arc costs 1. A directed snapshot writes its one
7
+ * direction; an undirected one stores both arcs of every edge, so both halves are written. The driver refuses
8
+ * negative and non-finite weights before any dispatch. Body only (spec 3.5, D9); the text is normative: the
9
+ * sabotage rows of test/helpers/sabotage.ts are textual edits of it.
10
+ */
11
+ export const apspInitWgsl = /* wgsl */ `
12
+ @compute @workgroup_size(WG)
13
+ fn apsp_init(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
14
+ let u = linear_id(wid, lid.x);
15
+ if (u >= P.n) { return; }
16
+ let rowBase = u * P.n;
17
+ let end = rowPtr[u + 1u];
18
+ for (var a = rowPtr[u]; a < end; a = a + 1u) {
19
+ let v = colIdx[a];
20
+ let w = select(1.0, weights[a], HAS_WEIGHTS);
21
+ dist[rowBase + v] = min(dist[rowBase + v], w); // parallel arcs collapse to the cheapest
22
+ }
23
+ dist[rowBase + u] = 0.0; // last: a self-loop never displaces the zero
24
+ }
25
+ `;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The `coo-emit` kernel body (design 6 row 10, 8.6; the first step of the simple symmetric graph build): position `i`
3
+ * takes arc `a` -- `i` itself, or `order[i]` under INDEXED, which is how a sorted permutation is materialised -- and
4
+ * writes that arc's source, target and weight. Arc `2e` is logical edge `e` as declared and arc `2e + 1` its reverse,
5
+ * so every edge lands in both directions and the result is symmetric whatever the snapshot's directedness. A
6
+ * self-loop is dropped: both of its arcs are written as `INVALID_INDEX`, which sorts after every node index and which
7
+ * `run-flags` never marks. Weights are 1 without WEIGHTED. Body only (spec 3.5, D9); normative text.
8
+ */
9
+ export const cooEmitWgsl = /* wgsl */ `
10
+ @compute @workgroup_size(WG)
11
+ fn coo_emit(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
12
+ let i = linear_id(wid, lid.x);
13
+ if (i >= P.count) { return; } // no barrier follows
14
+ var a = i;
15
+ if (INDEXED) { a = order[i]; } // the arc this position takes
16
+ let e = a / 2u; // arc 2e is edge e as declared, arc 2e + 1 its reverse
17
+ let forward = (a % 2u) == 0u;
18
+ let u = edgeSrc[e];
19
+ let v = edgeDst[e];
20
+ var w = 1.0;
21
+ if (WEIGHTED) { w = edgeWeight[e]; }
22
+ if (u == v) { // a self-loop is dropped: both arcs sort last and never open a run
23
+ outSrc[i] = INVALID_INDEX;
24
+ outDst[i] = INVALID_INDEX;
25
+ outWeight[i] = 0.0;
26
+ return;
27
+ }
28
+ outSrc[i] = select(v, u, forward);
29
+ outDst[i] = select(u, v, forward);
30
+ outWeight[i] = w;
31
+ }
32
+ `;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The `coo-scatter` kernel body (design 6 row 10): the last step of `cooToCsr`, after the histogram of the sources
3
+ * and its exclusive scan into `rowPtr`. Two modes under SORTED_INPUT.
4
+ *
5
+ * SORTED_INPUT false is the design's cursor scatter: an arc reserves its slot with `atomicAdd` on its row's cursor
6
+ * (`cursors`, zeroed by the caller), so slots are handed out in race order and a row is NOT sorted by target even when
7
+ * the input was.
8
+ *
9
+ * SORTED_INPUT true takes arcs already ordered by source: arc `i` then sits at its own index minus its row's start
10
+ * inside the row, no cursor and no atomic, so the write is a pure function of the input and the input order survives
11
+ * into every row. The precondition is checked, not trusted: an arc whose source is below its predecessor's raises
12
+ * `cursors[0]`, the word the driver reads back and refuses. Body only (spec 3.5, D9); normative text.
13
+ */
14
+ export const cooScatterWgsl = /* wgsl */ `
15
+ @compute @workgroup_size(WG)
16
+ fn coo_scatter(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
17
+ let i = linear_id(wid, lid.x);
18
+ if (i >= P.count) { return; } // no barrier follows
19
+ let s = src[i];
20
+ var slot = 0u;
21
+ if (SORTED_INPUT) {
22
+ if (i > 0u && s < src[i - 1u]) { atomicStore(&cursors[0], 1u); } // unsorted input: the flag the driver refuses
23
+ let within = i - rowPtr[s]; // the arc's place in its row
24
+ slot = rowPtr[s] + within;
25
+ } else {
26
+ slot = rowPtr[s] + atomicAdd(&cursors[s], 1u); // race order: the row is not sorted in this mode
27
+ }
28
+ colIdx[slot] = dst[i];
29
+ if (WEIGHTED) { outWeight[slot] = weight[i]; }
30
+ }
31
+ `;
@@ -0,0 +1,145 @@
1
+ /**
2
+ * The `group-by-key-row` kernel body (design 8.6; the per-row group-by-key): for every listed row `v`, the arcs of
3
+ * the row are grouped by the key of their target (`keyIn[colIdx[a]]`), the weights are summed per key, and the key
4
+ * with the largest sum wins, ties going to the LOWEST key -- which makes the answer independent of the order the arcs
5
+ * are visited in, and so bitwise reproducible. `bestKey[v]` gets the key (`INVALID_INDEX` for an empty row) and
6
+ * `bestScore[v]` the summed weight.
7
+ *
8
+ * The sums are u32 fixed point, because WGSL has no float atomic: every weight is scaled by `2^s`, a power of two
9
+ * chosen from the exponents of the row's largest weight and of its degree alone so that `maxWeight x degree x 2^s`
10
+ * lies in [2^28, 2^30), and rounded to the nearest integer, halves up. No step rounds a float: the scale needs no
11
+ * product, scaling by a power of two is exact, and so are the integer part and the fraction of the scaled weight.
12
+ * That matters because WGSL lets `x + y` and `x * y` round to EITHER neighbour of an inexact result, so a rounded
13
+ * step could differ between devices; as written, both tiers, every device and any CPU reference compute identical
14
+ * integers. A weight smaller than `2^-s / 2` contributes nothing, and a negative weight counts as zero. Without
15
+ * WEIGHTED every weight is 1.
16
+ *
17
+ * TIER 0: one thread per row (`rows[P.rowsBase + i]`), a pairwise scan in registers -- for short rows, and at most
18
+ * GROUP_ROW_THREAD_LIMIT arcs, since llvmpipe stops an invocation's loops after 65,535 steps. Any other TIER: one
19
+ * workgroup per row (`rows[P.rowsBase + g]`) over its own open-addressing region of `GROUP_HASH_LOAD_FACTOR x degree`
20
+ * slot pairs (key, sum) starting at word `rows[P.basesBase + g]` of `hashRegion`, cleared by the workgroup itself;
21
+ * a key is claimed by a bounded compare-exchange loop with linear probing, and a lane that exhausts its bound raises
22
+ * `hashRegion[0]`, which the driver reads and refuses. Every barrier is reached in uniform control flow: the only
23
+ * early return keys on the workgroup id and a uniform. Body only (spec 3.5, D9); normative text.
24
+ */
25
+ export const groupByKeyRowWgsl = /* wgsl */ `
26
+ var<workgroup> shMax: array<f32, WG>;
27
+ var<workgroup> shKey: array<u32, WG>;
28
+ var<workgroup> shSum: array<u32, WG>;
29
+
30
+ fn weight_of(a: u32) -> f32 {
31
+ if (WEIGHTED) { return weights[a]; }
32
+ return 1.0;
33
+ }
34
+ fn scale_of(maxW: f32, d: u32) -> f32 { // 2^s, maxW x d x 2^s in [2^28, 2^30), from exponents alone
35
+ let em = i32((bitcast<u32>(maxW) >> 23u) & 255u) - 127;
36
+ let ed = i32(firstLeadingBit(max(d, 1u)));
37
+ let s = clamp(28 - em - ed, -126, 126);
38
+ return bitcast<f32>(u32(s + 127) << 23u);
39
+ }
40
+ fn inverse_of(scale: f32) -> f32 { // 2^-s, exact
41
+ let s = i32((bitcast<u32>(scale) >> 23u) & 255u) - 127;
42
+ return bitcast<f32>(u32(127 - s) << 23u);
43
+ }
44
+ fn quantize(w: f32, scale: f32) -> u32 { // nearest integer, halves up; every step exact
45
+ let x = max(w * scale, 0.0);
46
+ let i = u32(x);
47
+ return select(i, i + 1u, x - f32(i) >= 0.5);
48
+ }
49
+ fn better(sum: u32, key: u32, bestSum: u32, bestKey0: u32) -> bool { return sum > bestSum || (sum == bestSum && key < bestKey0); }
50
+
51
+ fn row_thread(v: u32) {
52
+ let lo = rowPtr[v];
53
+ let hi = rowPtr[v + 1u];
54
+ var maxW = 0.0;
55
+ for (var a = lo; a < hi; a = a + 1u) { maxW = max(maxW, weight_of(a)); }
56
+ let scale = scale_of(maxW, hi - lo);
57
+ var bk = INVALID_INDEX;
58
+ var bs = 0u;
59
+ for (var a = lo; a < hi; a = a + 1u) {
60
+ let k = keyIn[colIdx[a]];
61
+ var seen = false;
62
+ for (var b = lo; b < a; b = b + 1u) { if (keyIn[colIdx[b]] == k) { seen = true; break; } }
63
+ if (seen) { continue; } // this key was summed at its first arc
64
+ var sum = 0u;
65
+ for (var b = a; b < hi; b = b + 1u) { if (keyIn[colIdx[b]] == k) { sum = sum + quantize(weight_of(b), scale); } }
66
+ if (better(sum, k, bs, bk)) { bk = k; bs = sum; }
67
+ }
68
+ bestKey[v] = bk;
69
+ bestScore[v] = f32(bs) * inverse_of(scale);
70
+ }
71
+
72
+ fn row_hash(g: u32, lid: u32) {
73
+ let v = rows[P.rowsBase + g];
74
+ let base = rows[P.basesBase + g];
75
+ let lo = rowPtr[v];
76
+ let hi = rowPtr[v + 1u];
77
+ let cap = GROUP_HASH_LOAD_FACTOR * (hi - lo);
78
+ var m = 0.0;
79
+ for (var a = lo + lid; a < hi; a = a + WG) { m = max(m, weight_of(a)); }
80
+ shMax[lid] = m;
81
+ workgroupBarrier();
82
+ for (var s = WG / 2u; s > 0u; s = s / 2u) {
83
+ if (lid < s) { shMax[lid] = max(shMax[lid], shMax[lid + s]); }
84
+ workgroupBarrier();
85
+ }
86
+ let scale = scale_of(shMax[0], hi - lo);
87
+ for (var j = lid; j < cap; j = j + WG) { // the region is this workgroup's alone: clear it
88
+ atomicStore(&hashRegion[base + 2u * j], INVALID_INDEX);
89
+ atomicStore(&hashRegion[base + 2u * j + 1u], 0u);
90
+ }
91
+ storageBarrier();
92
+ var exhausted = false;
93
+ for (var a = lo + lid; a < hi; a = a + WG) {
94
+ let k = keyIn[colIdx[a]];
95
+ let q = quantize(weight_of(a), scale);
96
+ var slot = lowbias32(k) % cap;
97
+ var steps = 0u;
98
+ loop {
99
+ if (steps >= cap + 64u) { exhausted = true; break; } // bounded: a spurious compare-exchange failure is legal (WGSL 17.8.5)
100
+ steps = steps + 1u;
101
+ let r = atomicCompareExchangeWeak(&hashRegion[base + 2u * slot], INVALID_INDEX, k);
102
+ if (r.exchanged || r.old_value == k) { atomicAdd(&hashRegion[base + 2u * slot + 1u], q); break; }
103
+ if (r.old_value == INVALID_INDEX) { continue; } // a spurious failure: the same slot again
104
+ slot = (slot + 1u) % cap; // linear probing
105
+ }
106
+ }
107
+ if (exhausted) { atomicStore(&hashRegion[0], 1u); }
108
+ storageBarrier();
109
+ var bk = INVALID_INDEX;
110
+ var bs = 0u;
111
+ for (var j = lid; j < cap; j = j + WG) {
112
+ let k = atomicLoad(&hashRegion[base + 2u * j]);
113
+ if (k != INVALID_INDEX) {
114
+ let sum = atomicLoad(&hashRegion[base + 2u * j + 1u]);
115
+ if (better(sum, k, bs, bk)) { bk = k; bs = sum; }
116
+ }
117
+ }
118
+ shKey[lid] = bk;
119
+ shSum[lid] = bs;
120
+ workgroupBarrier();
121
+ for (var s = WG / 2u; s > 0u; s = s / 2u) {
122
+ if (lid < s && better(shSum[lid + s], shKey[lid + s], shSum[lid], shKey[lid])) {
123
+ shKey[lid] = shKey[lid + s];
124
+ shSum[lid] = shSum[lid + s];
125
+ }
126
+ workgroupBarrier();
127
+ }
128
+ if (lid == 0u) {
129
+ bestKey[v] = shKey[0];
130
+ bestScore[v] = f32(shSum[0]) * inverse_of(scale);
131
+ }
132
+ }
133
+
134
+ @compute @workgroup_size(WG)
135
+ fn group_by_key_row(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
136
+ if (TIER == 0u) {
137
+ let i = linear_id(wid, lid.x);
138
+ if (i < P.count) { row_thread(rows[P.rowsBase + i]); }
139
+ return; // TIER is an override: uniform
140
+ }
141
+ let g = group_id(wid);
142
+ if (g >= P.count) { return; } // uniform: the workgroup id and a uniform
143
+ row_hash(g, lid.x);
144
+ }
145
+ `;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The `lpa-step` kernel body (design 8.6; label propagation): after `group-by-key-row` has written every vertex's
3
+ * best neighbour label into `bestKey`, a vertex adopts it only when the move goes the way the pass allows -- down to
4
+ * a lower label on a pass whose `P.direction` is 0, up to a higher one when it is 1. That is cuGraph's swap-avoidance
5
+ * rule: two neighbours can no longer trade labels forever, because on any one pass only one of the two moves is
6
+ * legal. Updates are synchronous (`labelsIn` read, `labelsOut` written) and the moves of a workgroup are added to
7
+ * `counters[P.counterIndex]` with ONE atomic per workgroup. Body only (spec 3.5, D9); normative text.
8
+ */
9
+ export const lpaStepWgsl = /* wgsl */ `
10
+ var<workgroup> moved: atomic<u32>;
11
+
12
+ @compute @workgroup_size(WG)
13
+ fn lpa_step(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
14
+ if (lid.x == 0u) { atomicStore(&moved, 0u); }
15
+ workgroupBarrier();
16
+ let v = linear_id(wid, lid.x);
17
+ if (v < P.n) {
18
+ let cur = labelsIn[v];
19
+ let best = bestKey[v];
20
+ var next = cur;
21
+ if (best != INVALID_INDEX && best != cur) {
22
+ let down = best < cur;
23
+ if (down == (P.direction == 0u)) { next = best; } // the alternating direction rule
24
+ }
25
+ labelsOut[v] = next;
26
+ if (next != cur) { atomicAdd(&moved, 1u); }
27
+ }
28
+ workgroupBarrier(); // every lane, unconditionally
29
+ if (lid.x == 0u) {
30
+ let m = atomicLoad(&moved);
31
+ if (m > 0u) { atomicAdd(&counters[P.counterIndex], m); } // one atomic per workgroup
32
+ }
33
+ }
34
+ `;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The `orient-flags` kernel body (design 8.5; triangle counting): over the arcs of the simple symmetric graph, arc
3
+ * `(u, v)` is kept when `(degree(v), v)` is above `(degree(u), u)` lexicographically. The order is strict and total,
4
+ * so exactly one arc of every undirected edge survives and every triangle is found once, at its lowest-ranked vertex;
5
+ * the rows stay sorted by target because a compaction preserves order. `src` holds each arc's source. Body only
6
+ * (spec 3.5, D9); normative text.
7
+ */
8
+ export const orientFlagsWgsl = /* wgsl */ `
9
+ @compute @workgroup_size(WG)
10
+ fn orient_flags(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
11
+ let a = linear_id(wid, lid.x);
12
+ if (a >= P.count) { return; } // no barrier follows
13
+ let u = src[a];
14
+ let v = colIdx[a];
15
+ let du = rowPtr[u + 1u] - rowPtr[u];
16
+ let dv = rowPtr[v + 1u] - rowPtr[v];
17
+ let keep = dv > du || (dv == du && v > u); // (degree, id) of the target above the source's
18
+ flags[a] = select(0u, 1u, keep);
19
+ }
20
+ `;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The `run-flags` kernel body (design 6 row 10; the simple symmetric graph build): over arcs sorted by the pair
3
+ * (`keysA`, `keysB`), `flags[i]` is 1 exactly when arc `i` opens a run of equal pairs -- the first arc, or one whose
4
+ * pair differs from its predecessor's -- and is not a dropped arc (`keysA[i] == INVALID_INDEX`). An exclusive scan
5
+ * of the flags numbers the runs, which is how parallel arcs merge into one. Body only (spec 3.5, D9); normative text.
6
+ */
7
+ export const runFlagsWgsl = /* wgsl */ `
8
+ @compute @workgroup_size(WG)
9
+ fn run_flags(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
10
+ let i = linear_id(wid, lid.x);
11
+ if (i >= P.count) { return; } // no barrier follows
12
+ let a = keysA[i];
13
+ var first = a != INVALID_INDEX; // a dropped arc never opens a run
14
+ if (i > 0u) { first = first && (a != keysA[i - 1u] || keysB[i] != keysB[i - 1u]); }
15
+ flags[i] = select(0u, 1u, first);
16
+ }
17
+ `;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The `tri-intersect` kernel body (design 8.5): one invocation per ORIENTED arc `(u, v)`; every common target `w` of
3
+ * the oriented rows of `u` and `v` closes a triangle, found exactly once. The rows are sorted by target, so they are
4
+ * intersected by a merge, or -- when one row is more than TRIANGLE_BINARY_SEARCH_RATIO times the other -- by a binary
5
+ * search of each element of the shorter row in the longer. SEARCH forces a path: 0 chooses, 1 always merges, 2
6
+ * always searches (the tier-agreement tests compare the two). `counts` gains one per triangle at `w` as it is found
7
+ * and the arc's triangle count at `u` and `v` once at the end: u32 atomics, so the counts are exact and
8
+ * order-independent. Body only (spec 3.5, D9); normative text.
9
+ */
10
+ export const triIntersectWgsl = /* wgsl */ `
11
+ fn contains(lo0: u32, hi0: u32, x: u32) -> bool { // binary search of x in colIdx[lo0, hi0), sorted ascending
12
+ var lo = lo0;
13
+ var hi = hi0;
14
+ loop {
15
+ if (lo >= hi) { break; }
16
+ let mid = lo + (hi - lo) / 2u;
17
+ let y = colIdx[mid];
18
+ if (y == x) { return true; }
19
+ if (y < x) { lo = mid + 1u; } else { hi = mid; }
20
+ }
21
+ return false;
22
+ }
23
+
24
+ @compute @workgroup_size(WG)
25
+ fn tri_intersect(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
26
+ let a = linear_id(wid, lid.x);
27
+ if (a >= P.count) { return; } // no barrier follows
28
+ let u = src[a];
29
+ let v = colIdx[a];
30
+ let uLo = rowPtr[u];
31
+ let uHi = rowPtr[u + 1u];
32
+ let vLo = rowPtr[v];
33
+ let vHi = rowPtr[v + 1u];
34
+ let du = uHi - uLo;
35
+ let dv = vHi - vLo;
36
+ var search = SEARCH == 2u;
37
+ if (SEARCH == 0u) { search = du / TRIANGLE_BINARY_SEARCH_RATIO > dv || dv / TRIANGLE_BINARY_SEARCH_RATIO > du; }
38
+ var found = 0u;
39
+ if (search) {
40
+ var sLo = uLo;
41
+ var sHi = uHi;
42
+ var lLo = vLo;
43
+ var lHi = vHi;
44
+ if (du > dv) { sLo = vLo; sHi = vHi; lLo = uLo; lHi = uHi; }
45
+ for (var k = sLo; k < sHi; k = k + 1u) {
46
+ let w = colIdx[k];
47
+ if (contains(lLo, lHi, w)) { atomicAdd(&counts[w], 1u); found = found + 1u; }
48
+ }
49
+ } else {
50
+ var i = uLo;
51
+ var j = vLo;
52
+ loop {
53
+ if (i >= uHi || j >= vHi) { break; }
54
+ let x = colIdx[i];
55
+ let y = colIdx[j];
56
+ if (x == y) { atomicAdd(&counts[x], 1u); found = found + 1u; i = i + 1u; j = j + 1u; }
57
+ else if (x < y) { i = i + 1u; }
58
+ else { j = j + 1u; }
59
+ }
60
+ }
61
+ if (found > 0u) { atomicAdd(&counts[u], found); atomicAdd(&counts[v], found); }
62
+ }
63
+ `;