@graphty/webgpu-graph-algorithms 0.6.14 → 0.6.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/README.md +52 -52
  2. package/dist/browser.js +1 -1
  3. package/dist/chunks/{context-Bi6AhScG.js → context-VIvatQOo.js} +69 -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 +5 -3
  7. package/dist/src/accelerator.d.ts.map +1 -1
  8. package/dist/src/accelerator.js +101 -5
  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/betweenness.d.ts +70 -0
  15. package/dist/src/algorithms/betweenness.d.ts.map +1 -0
  16. package/dist/src/algorithms/betweenness.js +538 -0
  17. package/dist/src/algorithms/betweenness.js.map +1 -0
  18. package/dist/src/algorithms/closeness.d.ts +15 -5
  19. package/dist/src/algorithms/closeness.d.ts.map +1 -1
  20. package/dist/src/algorithms/closeness.js +112 -26
  21. package/dist/src/algorithms/closeness.js.map +1 -1
  22. package/dist/src/algorithms/components.d.ts +9 -1
  23. package/dist/src/algorithms/components.d.ts.map +1 -1
  24. package/dist/src/algorithms/components.js +2 -2
  25. package/dist/src/algorithms/components.js.map +1 -1
  26. package/dist/src/algorithms/label-propagation.d.ts +31 -0
  27. package/dist/src/algorithms/label-propagation.d.ts.map +1 -0
  28. package/dist/src/algorithms/label-propagation.js +254 -0
  29. package/dist/src/algorithms/label-propagation.js.map +1 -0
  30. package/dist/src/algorithms/simple-symmetric.d.ts +88 -0
  31. package/dist/src/algorithms/simple-symmetric.d.ts.map +1 -0
  32. package/dist/src/algorithms/simple-symmetric.js +347 -0
  33. package/dist/src/algorithms/simple-symmetric.js.map +1 -0
  34. package/dist/src/algorithms/triangles.d.ts +34 -0
  35. package/dist/src/algorithms/triangles.d.ts.map +1 -0
  36. package/dist/src/algorithms/triangles.js +203 -0
  37. package/dist/src/algorithms/triangles.js.map +1 -0
  38. package/dist/src/constants.d.ts +53 -0
  39. package/dist/src/constants.d.ts.map +1 -1
  40. package/dist/src/constants.js +53 -0
  41. package/dist/src/constants.js.map +1 -1
  42. package/dist/src/index.d.ts +12 -3
  43. package/dist/src/index.d.ts.map +1 -1
  44. package/dist/src/index.js +8 -1
  45. package/dist/src/index.js.map +1 -1
  46. package/dist/src/kernel/prelude.d.ts.map +1 -1
  47. package/dist/src/kernel/prelude.js +4 -1
  48. package/dist/src/kernel/prelude.js.map +1 -1
  49. package/dist/src/kernels.d.ts +24 -6
  50. package/dist/src/kernels.d.ts.map +1 -1
  51. package/dist/src/kernels.js +373 -7
  52. package/dist/src/kernels.js.map +1 -1
  53. package/dist/src/memory/residency.js +15 -4
  54. package/dist/src/memory/residency.js.map +1 -1
  55. package/dist/src/primitives/coo-to-csr.d.ts +73 -0
  56. package/dist/src/primitives/coo-to-csr.d.ts.map +1 -0
  57. package/dist/src/primitives/coo-to-csr.js +183 -0
  58. package/dist/src/primitives/coo-to-csr.js.map +1 -0
  59. package/dist/src/primitives/frontier.d.ts +2 -0
  60. package/dist/src/primitives/frontier.d.ts.map +1 -1
  61. package/dist/src/primitives/frontier.js +2 -0
  62. package/dist/src/primitives/frontier.js.map +1 -1
  63. package/dist/src/primitives/group-by-key.d.ts +82 -0
  64. package/dist/src/primitives/group-by-key.d.ts.map +1 -0
  65. package/dist/src/primitives/group-by-key.js +147 -0
  66. package/dist/src/primitives/group-by-key.js.map +1 -0
  67. package/dist/src/types/accelerator.d.ts +19 -7
  68. package/dist/src/types/accelerator.d.ts.map +1 -1
  69. package/dist/src/types/algorithms.d.ts +4 -0
  70. package/dist/src/types/algorithms.d.ts.map +1 -1
  71. package/dist/src/types/all-pairs.d.ts +35 -0
  72. package/dist/src/types/all-pairs.d.ts.map +1 -0
  73. package/dist/src/types/all-pairs.js +8 -0
  74. package/dist/src/types/all-pairs.js.map +1 -0
  75. package/dist/src/types/betweenness.d.ts +35 -0
  76. package/dist/src/types/betweenness.d.ts.map +1 -0
  77. package/dist/src/types/betweenness.js +7 -0
  78. package/dist/src/types/betweenness.js.map +1 -0
  79. package/dist/src/types/community.d.ts +18 -0
  80. package/dist/src/types/community.d.ts.map +1 -0
  81. package/dist/src/types/community.js +5 -0
  82. package/dist/src/types/community.js.map +1 -0
  83. package/dist/src/types/structure.d.ts +27 -0
  84. package/dist/src/types/structure.d.ts.map +1 -0
  85. package/dist/src/types/structure.js +8 -0
  86. package/dist/src/types/structure.js.map +1 -0
  87. package/dist/src/wgsl/apsp-fw.wgsl.d.ts +25 -0
  88. package/dist/src/wgsl/apsp-fw.wgsl.d.ts.map +1 -0
  89. package/dist/src/wgsl/apsp-fw.wgsl.js +113 -0
  90. package/dist/src/wgsl/apsp-fw.wgsl.js.map +1 -0
  91. package/dist/src/wgsl/apsp-init.wgsl.d.ts +12 -0
  92. package/dist/src/wgsl/apsp-init.wgsl.d.ts.map +1 -0
  93. package/dist/src/wgsl/apsp-init.wgsl.js +26 -0
  94. package/dist/src/wgsl/apsp-init.wgsl.js.map +1 -0
  95. package/dist/src/wgsl/bc-backward.wgsl.d.ts +15 -0
  96. package/dist/src/wgsl/bc-backward.wgsl.d.ts.map +1 -0
  97. package/dist/src/wgsl/bc-backward.wgsl.js +34 -0
  98. package/dist/src/wgsl/bc-backward.wgsl.js.map +1 -0
  99. package/dist/src/wgsl/bc-edge-gather.wgsl.d.ts +12 -0
  100. package/dist/src/wgsl/bc-edge-gather.wgsl.d.ts.map +1 -0
  101. package/dist/src/wgsl/bc-edge-gather.wgsl.js +36 -0
  102. package/dist/src/wgsl/bc-edge-gather.wgsl.js.map +1 -0
  103. package/dist/src/wgsl/bc-finalize.wgsl.d.ts +21 -0
  104. package/dist/src/wgsl/bc-finalize.wgsl.d.ts.map +1 -0
  105. package/dist/src/wgsl/bc-finalize.wgsl.js +47 -0
  106. package/dist/src/wgsl/bc-finalize.wgsl.js.map +1 -0
  107. package/dist/src/wgsl/bc-forward-edge.wgsl.d.ts +15 -0
  108. package/dist/src/wgsl/bc-forward-edge.wgsl.d.ts.map +1 -0
  109. package/dist/src/wgsl/bc-forward-edge.wgsl.js +76 -0
  110. package/dist/src/wgsl/bc-forward-edge.wgsl.js.map +1 -0
  111. package/dist/src/wgsl/bc-forward.wgsl.d.ts +23 -0
  112. package/dist/src/wgsl/bc-forward.wgsl.d.ts.map +1 -0
  113. package/dist/src/wgsl/bc-forward.wgsl.js +106 -0
  114. package/dist/src/wgsl/bc-forward.wgsl.js.map +1 -0
  115. package/dist/src/wgsl/bc-gather.wgsl.d.ts +9 -0
  116. package/dist/src/wgsl/bc-gather.wgsl.d.ts.map +1 -0
  117. package/dist/src/wgsl/bc-gather.wgsl.js +20 -0
  118. package/dist/src/wgsl/bc-gather.wgsl.js.map +1 -0
  119. package/dist/src/wgsl/closeness-reduce.wgsl.d.ts +4 -1
  120. package/dist/src/wgsl/closeness-reduce.wgsl.d.ts.map +1 -1
  121. package/dist/src/wgsl/closeness-reduce.wgsl.js +8 -4
  122. package/dist/src/wgsl/closeness-reduce.wgsl.js.map +1 -1
  123. package/dist/src/wgsl/closeness-sweep.wgsl.d.ts +4 -2
  124. package/dist/src/wgsl/closeness-sweep.wgsl.d.ts.map +1 -1
  125. package/dist/src/wgsl/closeness-sweep.wgsl.js +12 -2
  126. package/dist/src/wgsl/closeness-sweep.wgsl.js.map +1 -1
  127. package/dist/src/wgsl/coo-emit.wgsl.d.ts +10 -0
  128. package/dist/src/wgsl/coo-emit.wgsl.d.ts.map +1 -0
  129. package/dist/src/wgsl/coo-emit.wgsl.js +33 -0
  130. package/dist/src/wgsl/coo-emit.wgsl.js.map +1 -0
  131. package/dist/src/wgsl/coo-scatter.wgsl.d.ts +15 -0
  132. package/dist/src/wgsl/coo-scatter.wgsl.d.ts.map +1 -0
  133. package/dist/src/wgsl/coo-scatter.wgsl.js +32 -0
  134. package/dist/src/wgsl/coo-scatter.wgsl.js.map +1 -0
  135. package/dist/src/wgsl/group-by-key-row.wgsl.d.ts +26 -0
  136. package/dist/src/wgsl/group-by-key-row.wgsl.d.ts.map +1 -0
  137. package/dist/src/wgsl/group-by-key-row.wgsl.js +146 -0
  138. package/dist/src/wgsl/group-by-key-row.wgsl.js.map +1 -0
  139. package/dist/src/wgsl/lpa-step.wgsl.d.ts +10 -0
  140. package/dist/src/wgsl/lpa-step.wgsl.d.ts.map +1 -0
  141. package/dist/src/wgsl/lpa-step.wgsl.js +35 -0
  142. package/dist/src/wgsl/lpa-step.wgsl.js.map +1 -0
  143. package/dist/src/wgsl/orient-flags.wgsl.d.ts +9 -0
  144. package/dist/src/wgsl/orient-flags.wgsl.d.ts.map +1 -0
  145. package/dist/src/wgsl/orient-flags.wgsl.js +21 -0
  146. package/dist/src/wgsl/orient-flags.wgsl.js.map +1 -0
  147. package/dist/src/wgsl/run-flags.wgsl.d.ts +8 -0
  148. package/dist/src/wgsl/run-flags.wgsl.d.ts.map +1 -0
  149. package/dist/src/wgsl/run-flags.wgsl.js +18 -0
  150. package/dist/src/wgsl/run-flags.wgsl.js.map +1 -0
  151. package/dist/src/wgsl/tri-intersect.wgsl.d.ts +11 -0
  152. package/dist/src/wgsl/tri-intersect.wgsl.d.ts.map +1 -0
  153. package/dist/src/wgsl/tri-intersect.wgsl.js +64 -0
  154. package/dist/src/wgsl/tri-intersect.wgsl.js.map +1 -0
  155. package/dist/webgpu-graph-algorithms.js +2828 -321
  156. package/dist/webgpu-graph-algorithms.js.map +1 -1
  157. package/package.json +5 -5
  158. package/src/accelerator.ts +130 -7
  159. package/src/algorithms/all-pairs.ts +228 -0
  160. package/src/algorithms/betweenness.ts +739 -0
  161. package/src/algorithms/closeness.ts +124 -32
  162. package/src/algorithms/components.ts +2 -2
  163. package/src/algorithms/label-propagation.ts +280 -0
  164. package/src/algorithms/simple-symmetric.ts +409 -0
  165. package/src/algorithms/triangles.ts +240 -0
  166. package/src/constants.ts +53 -0
  167. package/src/index.ts +20 -1
  168. package/src/kernel/prelude.ts +6 -0
  169. package/src/kernels.ts +411 -10
  170. package/src/memory/residency.ts +15 -4
  171. package/src/primitives/coo-to-csr.ts +251 -0
  172. package/src/primitives/frontier.ts +4 -0
  173. package/src/primitives/group-by-key.ts +209 -0
  174. package/src/types/accelerator.ts +26 -6
  175. package/src/types/algorithms.ts +5 -0
  176. package/src/types/all-pairs.ts +37 -0
  177. package/src/types/betweenness.ts +38 -0
  178. package/src/types/community.ts +18 -0
  179. package/src/types/structure.ts +28 -0
  180. package/src/wgsl/apsp-fw.wgsl.ts +112 -0
  181. package/src/wgsl/apsp-init.wgsl.ts +25 -0
  182. package/src/wgsl/bc-backward.wgsl.ts +33 -0
  183. package/src/wgsl/bc-edge-gather.wgsl.ts +35 -0
  184. package/src/wgsl/bc-finalize.wgsl.ts +46 -0
  185. package/src/wgsl/bc-forward-edge.wgsl.ts +75 -0
  186. package/src/wgsl/bc-forward.wgsl.ts +105 -0
  187. package/src/wgsl/bc-gather.wgsl.ts +19 -0
  188. package/src/wgsl/closeness-reduce.wgsl.ts +8 -4
  189. package/src/wgsl/closeness-sweep.wgsl.ts +12 -2
  190. package/src/wgsl/coo-emit.wgsl.ts +32 -0
  191. package/src/wgsl/coo-scatter.wgsl.ts +31 -0
  192. package/src/wgsl/group-by-key-row.wgsl.ts +145 -0
  193. package/src/wgsl/lpa-step.wgsl.ts +34 -0
  194. package/src/wgsl/orient-flags.wgsl.ts +20 -0
  195. package/src/wgsl/run-flags.wgsl.ts +17 -0
  196. package/src/wgsl/tri-intersect.wgsl.ts +63 -0
  197. package/dist/chunks/context-Bi6AhScG.js.map +0 -1
@@ -23,6 +23,11 @@
23
23
  * seam's placeholder keys and an exact traversal has neither, so a defined value is REFUSED before any device work
24
24
  * (`E_UNSUPPORTED { option }`, the package's rule for an option it does not implement, PD-25); `undefined` is legal.
25
25
  * `iterations` reports the source batches run (the sources, on the weighted route), `converged` is always true.
26
+ * A SAMPLED run (`sources`, issue #426; undirected snapshots only) seeds its batches from the listed sources (the
27
+ * reduce's role 2 reads the list the host wrote after the per-node sums in `perSource`), and the sweep also adds each
28
+ * claim's distance into a per-node sum (`perNode`), read back with every submit and folded on the host in f64 into
29
+ * `1 / sum` per NODE, where the exact run folds per SOURCE: on an undirected graph the distance from a source to a node
30
+ * is the distance from the node to the source, which is what the CPU port's sampled closeness sums.
26
31
  *
27
32
  * Cost, stated so nobody is surprised: closeness is O(n x m) on any device -- at 1M nodes it is 31,250 batches of a
28
33
  * full multi-source traversal, minutes on the card, and no target in design 10.4 asks for less. `compact.record`
@@ -54,8 +59,8 @@ import { assertWholeCore } from "../primitives/core-shape.js";
54
59
  import { W } from "../primitives/frontier.js";
55
60
  import { type ReduceScope } from "../primitives/reduce.js";
56
61
  import { assertDeviceComputes } from "../primitives/verify.js";
57
- import { type HitsOptionsLike } from "../types/accelerator.js";
58
- import { type GpuScoresResult } from "../types/algorithms.js";
62
+ import { type ClosenessAcceleratorOptions, type HitsOptionsLike } from "../types/accelerator.js";
63
+ import { type GpuClosenessResult } from "../types/algorithms.js";
59
64
  import { type Binding } from "../types/memory.js";
60
65
  import { type GpuRunOptions } from "../types/run.js";
61
66
  import { algorithmScope } from "./scope.js";
@@ -116,10 +121,12 @@ function reusingScratch(scope: ReduceScope): ReduceScope {
116
121
  }
117
122
 
118
123
  /**
119
- * The weighted route: one `sssp` per source, the sums reduced on the host.
124
+ * The weighted route: one `sssp` per source, the sums reduced on the host. With `sources` (a sampled run on an
125
+ * undirected snapshot) each search adds its distances into the sums of the nodes it reaches instead of its own.
120
126
  * @param ctx - the context
121
127
  * @param s - the snapshot
122
128
  * @param scores - the destination
129
+ * @param sources - a sampled run's sources, or null for every node
123
130
  * @param options - the run options
124
131
  * @returns the result
125
132
  */
@@ -127,25 +134,40 @@ async function weightedRoute(
127
134
  ctx: GpuContext,
128
135
  s: GraphSnapshot,
129
136
  scores: F32,
137
+ sources: readonly number[] | null,
130
138
  options: GpuRunOptions | undefined,
131
- ): Promise<GpuScoresResult> {
139
+ ): Promise<GpuClosenessResult> {
132
140
  const n = s.nodeCount;
133
- for (let source = 0; source < n; source++) {
141
+ const count = sources?.length ?? n;
142
+ const totals = sources === null ? null : new Float64Array(n);
143
+ for (let i = 0; i < count; i++) {
134
144
  if (options?.signal?.aborted) {
135
145
  throw aborted(ALGORITHM);
136
146
  }
147
+ const source = sources === null ? i : sources[i];
137
148
  const { dist } = await sssp(ctx, s, source, { signal: options?.signal });
138
149
  let sum = 0;
139
150
  for (let v = 0; v < n; v++) {
140
151
  const d = dist[v];
141
152
  if (v !== source && d !== Infinity) {
142
- sum += d;
153
+ if (totals === null) {
154
+ sum += d;
155
+ } else {
156
+ totals[v] += d;
157
+ }
143
158
  }
144
159
  }
145
- scores[source] = sum === 0 ? 0 : 1 / sum;
146
- options?.onProgress?.(source + 1, n);
160
+ if (totals === null) {
161
+ scores[source] = sum === 0 ? 0 : 1 / sum;
162
+ }
163
+ options?.onProgress?.(i + 1, count);
164
+ }
165
+ if (totals !== null) {
166
+ totals.forEach((sum, v) => {
167
+ scores[v] = sum === 0 ? 0 : 1 / sum;
168
+ });
147
169
  }
148
- return { scores, iterations: n, converged: true, precision: "f32" };
170
+ return { scores, iterations: count, converged: true, precision: "f32", sourcesUsed: count };
149
171
  }
150
172
 
151
173
  /**
@@ -153,6 +175,7 @@ async function weightedRoute(
153
175
  * @param ctx - the context
154
176
  * @param s - the snapshot
155
177
  * @param scores - the destination
178
+ * @param sources - a sampled run's sources, or null for every node
156
179
  * @param levelsPerSubmit - the submit cadence
157
180
  * @param options - the run options
158
181
  * @param tuning - the knobs
@@ -162,13 +185,15 @@ async function sweepRoute(
162
185
  ctx: GpuContext,
163
186
  s: GraphSnapshot,
164
187
  scores: F32,
188
+ sources: readonly number[] | null,
165
189
  levelsPerSubmit: number,
166
190
  options: GpuRunOptions | undefined,
167
191
  tuning: ClosenessTuning,
168
- ): Promise<GpuScoresResult> {
192
+ ): Promise<GpuClosenessResult> {
169
193
  const n = s.nodeCount;
170
- if (n === 0) {
171
- return { scores, iterations: 0, converged: true, precision: "f32" };
194
+ const seedCount = sources?.length ?? n;
195
+ if (seedCount === 0) {
196
+ return { scores, iterations: 0, converged: true, precision: "f32", sourcesUsed: 0 };
172
197
  }
173
198
  const core = ctx.residency.core(s);
174
199
  assertWholeCore(core, s.arcCount, ctx.caps.limits.maxStorageBufferBindingSize, ALGORITHM);
@@ -195,7 +220,18 @@ async function sweepRoute(
195
220
  FRONTIER_COUNTERS.byteLength,
196
221
  );
197
222
  const perSourceBytes = 4 * PER_SOURCE_WORDS;
198
- const perSource = bindingOf(scope.scratch(perSourceBytes, "per-source"), perSourceBytes);
223
+ // a sampled run appends the per-node distance sums (bitsBase words) and then its source list
224
+ const zeroedWords = PER_SOURCE_WORDS + (sources === null ? 0 : bitsBase);
225
+ const perSourceAll = 4 * (zeroedWords + (sources === null ? 0 : sources.length));
226
+ const perSource = bindingOf(scope.scratch(perSourceAll, "per-source"), perSourceAll);
227
+ if (sources !== null) {
228
+ ctx.device.queue.writeBuffer(
229
+ perSource.buffer,
230
+ perSource.offset + 4 * zeroedWords,
231
+ Uint32Array.from(sources),
232
+ );
233
+ }
234
+ const totals = sources === null ? null : new Float64Array(n);
199
235
  await ctx.allocator.check();
200
236
  const compact = await prepareCompact(reusingScratch(scope));
201
237
  const sweep = await ctx.pipelines.kernel(kernelSpec("closeness-sweep", graphOverrides(core, null)));
@@ -222,7 +258,7 @@ async function sweepRoute(
222
258
  ctx.assertReady();
223
259
 
224
260
  let batches = 0;
225
- for (let batchStart = 0; batchStart < n; batchStart += SOURCES_PER_BATCH) {
261
+ for (let batchStart = 0; batchStart < seedCount; batchStart += SOURCES_PER_BATCH) {
226
262
  let level = 0;
227
263
  for (let first = true; ; first = false) {
228
264
  const batch = new CommandBatch(ctx, `${ALGORITHM}/levels`);
@@ -231,8 +267,13 @@ async function sweepRoute(
231
267
  // the batch's seed: the four regions and the block zeroed, then role 1 (the sources' bits, their
232
268
  // flags, counters[0] = k, level = U32_MAX)
233
269
  recordFill(pass, bits, 4 * bitsBase, 0);
234
- recordFill(pass, perSource, PER_SOURCE_WORDS, 0);
235
- const seed = scope.params(FRONTIER_PARAMS, { role: 1, n, bitsBase, source: batchStart });
270
+ recordFill(pass, perSource, zeroedWords, 0);
271
+ const seed = scope.params(FRONTIER_PARAMS, {
272
+ role: sources === null ? 1 : 2,
273
+ n: seedCount,
274
+ bitsBase,
275
+ source: batchStart,
276
+ });
236
277
  reduce.dispatch(pass, reduce.bind({ counters, perSource, bits, P: seed.binding }), onePlan, [
237
278
  seed.offset,
238
279
  ]);
@@ -253,6 +294,7 @@ async function sweepRoute(
253
294
  arcEnd: s.arcCount,
254
295
  mode,
255
296
  stride: sweepPlan.stride ?? wg,
297
+ perNode: sources === null ? 0 : 1,
256
298
  });
257
299
  return {
258
300
  bound: sweep.bind({ ...graph, frontierList, counters, bits, perSource, P: params.binding }),
@@ -277,6 +319,8 @@ async function sweepRoute(
277
319
  batch.endPass();
278
320
  const doneRequest = batch.readback(counters.buffer, counters.offset + 4 * W.done, 4);
279
321
  const blockRequest = batch.readback(perSource.buffer, perSource.offset, perSourceBytes);
322
+ const nodeRequest =
323
+ totals === null ? null : batch.readback(perSource.buffer, perSource.offset + perSourceBytes, 4 * n);
280
324
  const submitted = submit(batch);
281
325
  const back = await submitted.readback;
282
326
  ctx.assertReady();
@@ -285,10 +329,18 @@ async function sweepRoute(
285
329
  }
286
330
  if (new Uint32Array(back, doneRequest.offset, 1)[0] !== 0) {
287
331
  const block = new Uint32Array(back, blockRequest.offset, PER_SOURCE_WORDS);
288
- const count = Math.min(SOURCES_PER_BATCH, n - batchStart);
289
- for (let i = 0; i < count; i++) {
290
- const sum = block[3 * SOURCES_PER_BATCH + i] * 2 ** 32 + block[2 * SOURCES_PER_BATCH + i];
291
- scores[batchStart + i] = sum === 0 ? 0 : 1 / sum;
332
+ if (totals === null || nodeRequest === null) {
333
+ const count = Math.min(SOURCES_PER_BATCH, n - batchStart);
334
+ for (let i = 0; i < count; i++) {
335
+ const sum = block[3 * SOURCES_PER_BATCH + i] * 2 ** 32 + block[2 * SOURCES_PER_BATCH + i];
336
+ scores[batchStart + i] = sum === 0 ? 0 : 1 / sum;
337
+ }
338
+ } else {
339
+ // at most 32 (n - 1) per node per batch, so a u32 word never wraps below 134M nodes
340
+ const sums = new Uint32Array(back, nodeRequest.offset, n);
341
+ for (let v = 0; v < n; v++) {
342
+ totals[v] += sums[v];
343
+ }
292
344
  }
293
345
  tuning.onBatch?.(batchStart, block.slice());
294
346
  break;
@@ -303,29 +355,63 @@ async function sweepRoute(
303
355
  }
304
356
  }
305
357
  batches += 1;
306
- options?.onProgress?.(Math.min(batchStart + SOURCES_PER_BATCH, n), n);
358
+ options?.onProgress?.(Math.min(batchStart + SOURCES_PER_BATCH, seedCount), seedCount);
307
359
  }
308
- return { scores, iterations: batches, converged: true, precision: "f32" };
360
+ totals?.forEach((sum, v) => {
361
+ scores[v] = sum === 0 ? 0 : 1 / sum;
362
+ });
363
+ return { scores, iterations: batches, converged: true, precision: "f32", sourcesUsed: seedCount };
309
364
  } finally {
310
365
  scope.dispose();
311
366
  }
312
367
  }
313
368
 
369
+ /**
370
+ * A sampled run's sources, checked: node indices of `s`, on an undirected snapshot only.
371
+ * @param s - the snapshot
372
+ * @param sources - the caller's list, or undefined for every node
373
+ * @returns the list, or null for every node
374
+ * @throws WebGpuGraphError E_INVALID_ARGUMENT for an index outside the snapshot, E_UNSUPPORTED on a directed snapshot
375
+ */
376
+ function checkSources(s: GraphSnapshot, sources: readonly number[] | undefined): readonly number[] | null {
377
+ if (sources === undefined) {
378
+ return null;
379
+ }
380
+ if (s.directed) {
381
+ // a search FROM a source measures distance to the nodes it reaches, which is the distance FROM them to the
382
+ // source only when every edge runs both ways
383
+ throw new WebGpuGraphError("E_UNSUPPORTED", `${ALGORITHM}: sampled sources need an undirected snapshot`, {
384
+ feature: "closenessCentrality.directedSources",
385
+ hint: "run the CPU port, which searches the in-arcs",
386
+ });
387
+ }
388
+ for (const v of sources) {
389
+ if (!Number.isInteger(v) || v < 0 || v >= s.nodeCount) {
390
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${ALGORITHM}: a source is not a node index`, {
391
+ argument: "sources",
392
+ value: v,
393
+ expected: `an integer in [0, ${s.nodeCount})`,
394
+ });
395
+ }
396
+ }
397
+ return sources;
398
+ }
399
+
314
400
  /**
315
401
  * Closeness with the test knobs of PD-26's shape; `closenessCentrality` is this with an empty tuning.
316
402
  * @internal
317
403
  * @param ctx - the context whose device runs the kernels
318
404
  * @param s - the snapshot (uploaded through ctx.residency, or found there)
319
- * @param options - the seam's `HitsOptionsLike` (`weighted` honoured, the other two refused when defined), plus dest / signal / onProgress
405
+ * @param options - `weighted` and a sampled run's `sources` honoured, the placeholder `maxIterations` / `tolerance` refused when defined, plus dest / signal / onProgress
320
406
  * @param tuning - the knobs
321
- * @returns the scores, the batches run, `converged: true` and `precision: "f32"`
407
+ * @returns the scores, the batches run, `converged: true`, `precision: "f32"` and `sourcesUsed`
322
408
  */
323
409
  export async function closenessWithTuning(
324
410
  ctx: GpuContext,
325
411
  s: GraphSnapshot,
326
- options: (HitsOptionsLike & GpuRunOptions) | undefined,
412
+ options: (ClosenessAcceleratorOptions & HitsOptionsLike & GpuRunOptions) | undefined,
327
413
  tuning: ClosenessTuning,
328
- ): Promise<GpuScoresResult> {
414
+ ): Promise<GpuClosenessResult> {
329
415
  ctx.assertReady();
330
416
  await assertDeviceComputes(ctx);
331
417
  for (const key of ["maxIterations", "tolerance"] as const) {
@@ -337,6 +423,7 @@ export async function closenessWithTuning(
337
423
  }
338
424
  }
339
425
  const n = s.nodeCount;
426
+ const sources = checkSources(s, options?.sources);
340
427
  const levelsPerSubmit = tuning.levelsPerSubmit ?? MAX_LEVELS_PER_SUBMIT;
341
428
  if (!Number.isInteger(levelsPerSubmit) || levelsPerSubmit < 1 || levelsPerSubmit > MAX_LEVELS_PER_SUBMIT) {
342
429
  throw new WebGpuGraphError(
@@ -370,9 +457,9 @@ export async function closenessWithTuning(
370
457
  feature: "closenessCentrality.nonFiniteWeights",
371
458
  });
372
459
  }
373
- return weightedRoute(ctx, s, scores, options);
460
+ return weightedRoute(ctx, s, scores, sources, options);
374
461
  }
375
- return sweepRoute(ctx, s, scores, levelsPerSubmit, options, tuning);
462
+ return sweepRoute(ctx, s, scores, sources, levelsPerSubmit, options, tuning);
376
463
  }
377
464
 
378
465
  /**
@@ -381,15 +468,20 @@ export async function closenessWithTuning(
381
468
  * `closenessCentrality`, unweighted by one bit-parallel multi-source search per 32 sources, weighted by one `sssp`
382
469
  * per source; `weighted` defaults to the snapshot's flag, `maxIterations` / `tolerance` are refused when defined
383
470
  * (PD-25). `iterations` is the source batches run and `converged` is always true.
471
+ *
472
+ * SAMPLED (`sources`, node indices, duplicates run twice; undirected snapshots only, E_UNSUPPORTED
473
+ * `closenessCentrality.directedSources` otherwise): the batches seed the listed sources instead of every node, and
474
+ * each node's score is `1 / sum` of its distances to the sources that reach it (itself excluded), `0` when none does:
475
+ * the sampled score of the CPU port, unscaled. `sourcesUsed` is the list's length (`n` exact).
384
476
  * @param ctx - the context whose device runs the kernels
385
477
  * @param s - the snapshot (uploaded through ctx.residency, or found there)
386
- * @param options - the seam's `HitsOptionsLike`, plus dest (a Float32Array of length n for `scores`) / signal / onProgress
387
- * @returns the scores, the batches run, `converged: true` and `precision: "f32"`
478
+ * @param options - `weighted`, `sources`, plus dest (a Float32Array of length n for `scores`) / signal / onProgress
479
+ * @returns the scores, the batches run, `converged: true`, `precision: "f32"` and `sourcesUsed`
388
480
  */
389
481
  export function closenessCentrality(
390
482
  ctx: GpuContext,
391
483
  s: GraphSnapshot,
392
- options?: HitsOptionsLike & GpuRunOptions,
393
- ): Promise<GpuScoresResult> {
484
+ options?: ClosenessAcceleratorOptions & HitsOptionsLike & GpuRunOptions,
485
+ ): Promise<GpuClosenessResult> {
394
486
  return closenessWithTuning(ctx, s, options, {});
395
487
  }
@@ -92,12 +92,12 @@ function bindingOf(buffer: GPUBuffer, size: number): Binding {
92
92
 
93
93
  /**
94
94
  * The result object over a label array whose labels are all `< n`: `count` blocks, `groups()` built lazily once in
95
- * first-seen label order (which for renumbered labels is index order).
95
+ * first-seen label order (which for renumbered labels is index order). Shared with label propagation.
96
96
  * @param labels - the labels
97
97
  * @param count - the block count
98
98
  * @returns the result
99
99
  */
100
- function labelResult(labels: U32, count: number): GpuLabelResult {
100
+ export function labelResult(labels: U32, count: number): GpuLabelResult {
101
101
  let groups: U32[] | null = null;
102
102
  return {
103
103
  labels,
@@ -0,0 +1,280 @@
1
+ /**
2
+ * Label propagation (design 8.6, 3.3 line 807; the P11 plan's P11-T6) over the simple symmetric graph of the
3
+ * snapshot (`buildSimpleSymmetric`). Every vertex starts with its own index as its label; each pass,
4
+ * `group-by-key-row` finds every vertex's weighted mode of its neighbours' labels (the lowest label on a tie) and
5
+ * `lpa-step` adopts it synchronously -- but only in the pass's direction: down to a lower label on even passes, up to
6
+ * a higher one on odd passes (cuGraph's swap-avoidance rule), which is what stops two neighbours trading labels
7
+ * forever. A pass that moves nothing in either direction after one that moved nothing in the other is a fixed point,
8
+ * and the run stops there or at `maxIterations` passes.
9
+ *
10
+ * Passes are recorded LABEL_PROP_PASSES_PER_SUBMIT to a submit with one readback of their move counts, because a
11
+ * readback per pass costs more than the passes on a small graph; the passes after the fixed point inside the last
12
+ * submit change nothing, so the labels are those of the fixed point. The first submit is the graph build's second.
13
+ * The labels are renumbered in first-seen order on the host (`renumberPartition`), as connected components' are.
14
+ *
15
+ * The result is bitwise reproducible on one device and between devices: the grouping is order-independent and every
16
+ * sum is an integer.
17
+ */
18
+
19
+ import { type GraphSnapshot, renumberPartition, type U32 } from "@graphty/graph-format";
20
+
21
+ import { LABEL_PROP_PASSES_PER_SUBMIT } from "../constants.js";
22
+ import { type GpuContext } from "../context.js";
23
+ import { WebGpuGraphError } from "../errors.js";
24
+ import { CommandBatch } from "../kernel/batch.js";
25
+ import { plan1d } from "../kernel/dispatch.js";
26
+ import { FILL_PARAMS, kernelSpec, LPA_PARAMS } from "../kernels.js";
27
+ import { planGroupRows, prepareGroupByKeyRow } from "../primitives/group-by-key.js";
28
+ import { assertDeviceComputes } from "../primitives/verify.js";
29
+ import { type GpuLabelResult } from "../types/algorithms.js";
30
+ import { type LabelPropagationOptions } from "../types/community.js";
31
+ import { type Binding } from "../types/memory.js";
32
+ import { type GpuRunOptions } from "../types/run.js";
33
+ import { labelResult } from "./components.js";
34
+ import { algorithmScope } from "./scope.js";
35
+ import { assertBindable, assertBuildSorted, buildSimpleSymmetric } from "./simple-symmetric.js";
36
+
37
+ const ALGORITHM = "labelPropagation";
38
+ /** The default pass cap, as in `@graphty/algorithms`' labelPropagation. */
39
+ const DEFAULT_MAX_ITERATIONS = 100;
40
+ /** Params slots of the largest batch: the graph build's second submit plus one batch of passes. */
41
+ const RING_SLOTS = 1024;
42
+
43
+ /**
44
+ * Validates `options.dest` for a label result of `n` elements.
45
+ * @param dest - the caller's destination array, if any
46
+ * @param n - the node count
47
+ * @returns the destination as a U32, or null when none was given
48
+ */
49
+ function checkDest(dest: Float32Array | Uint32Array | undefined, n: number): U32 | null {
50
+ if (dest === undefined) {
51
+ return null;
52
+ }
53
+ if (dest instanceof Uint32Array && dest.length === n && dest.buffer instanceof ArrayBuffer) {
54
+ return dest as U32;
55
+ }
56
+ throw new WebGpuGraphError(
57
+ "E_INVALID_ARGUMENT",
58
+ `${ALGORITHM}: dest must be a Uint32Array of length ${n} over an ArrayBuffer`,
59
+ {
60
+ argument: "dest",
61
+ value: `${dest.constructor.name}(${dest.length})`,
62
+ expected: `Uint32Array(${n}) over an ArrayBuffer`,
63
+ },
64
+ );
65
+ }
66
+
67
+ /**
68
+ * The pass cap.
69
+ * @param value - options.maxIterations
70
+ * @returns the cap
71
+ */
72
+ function maxIterationsOf(value: number | undefined): number {
73
+ if (value === undefined) {
74
+ return DEFAULT_MAX_ITERATIONS;
75
+ }
76
+ if (!Number.isSafeInteger(value) || value < 0) {
77
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${ALGORITHM}: maxIterations must be a non-negative integer`, {
78
+ argument: "maxIterations",
79
+ value,
80
+ expected: "a non-negative integer",
81
+ });
82
+ }
83
+ return value;
84
+ }
85
+
86
+ /**
87
+ * An upper bound of every vertex's number of distinct neighbours in the simple symmetric graph: its arcs in both
88
+ * directions, which the merge of parallel edges and the drop of self-loops can only shrink.
89
+ * @param s - the snapshot
90
+ * @returns one bound per vertex
91
+ */
92
+ function neighbourBound(s: GraphSnapshot): Uint32Array {
93
+ const out = s.outDegree();
94
+ if (!s.directed) {
95
+ return out;
96
+ }
97
+ const inDegree = s.inDegree();
98
+ const bound = new Uint32Array(s.nodeCount);
99
+ for (let v = 0; v < bound.length; v++) {
100
+ bound[v] = out[v] + inDegree[v];
101
+ }
102
+ return bound;
103
+ }
104
+
105
+ /**
106
+ * The labels as a result: every label must be a node index (a device bug otherwise), then renumbered first-seen.
107
+ * @param raw - the labels the device produced
108
+ * @param dest - the caller's destination, if any
109
+ * @returns the result
110
+ */
111
+ function resultOf(raw: U32, dest: U32 | null): GpuLabelResult {
112
+ const n = raw.length;
113
+ for (let v = 0; v < n; v++) {
114
+ if (raw[v] >= n) {
115
+ throw new WebGpuGraphError("E_VALIDATION", `${ALGORITHM}: labels[${v}] = ${raw[v]} is not a node index`, {
116
+ label: `${ALGORITHM}/labels`,
117
+ message: `the device produced a label outside [0, ${n})`,
118
+ });
119
+ }
120
+ }
121
+ const { labels, count } = renumberPartition(raw, dest ?? undefined);
122
+ return labelResult(labels, count);
123
+ }
124
+
125
+ /**
126
+ * The identity labelling: every vertex its own community.
127
+ * @param n - the node count
128
+ * @param dest - the caller's destination, if any
129
+ * @returns the result
130
+ */
131
+ function identityResult(n: number, dest: U32 | null): GpuLabelResult {
132
+ const labels = dest ?? new Uint32Array(n);
133
+ for (let v = 0; v < n; v++) {
134
+ labels[v] = v;
135
+ }
136
+ return labelResult(labels, n);
137
+ }
138
+
139
+ /**
140
+ * Label propagation on the device (see the file header).
141
+ * @param ctx - the context whose device runs the kernels
142
+ * @param s - the snapshot (its edge list is uploaded through ctx.residency, or found there)
143
+ * @param options - maxIterations (default 100), weighted (default true), plus dest / signal / onProgress
144
+ * @returns the labels, dense in first-seen order, the community count and groups()
145
+ */
146
+ export async function labelPropagation(
147
+ ctx: GpuContext,
148
+ s: GraphSnapshot,
149
+ options?: LabelPropagationOptions & GpuRunOptions,
150
+ ): Promise<GpuLabelResult> {
151
+ ctx.assertReady();
152
+ await assertDeviceComputes(ctx);
153
+ const n = s.nodeCount;
154
+ const dest = checkDest(options?.dest, n);
155
+ const maxIterations = maxIterationsOf(options?.maxIterations);
156
+ const weighted = options?.weighted !== false;
157
+ if (options?.signal?.aborted) {
158
+ throw new WebGpuGraphError("E_ABORTED", `${ALGORITHM}: the signal was aborted before any work started`, {});
159
+ }
160
+ if (n === 0 || s.edgeCount === 0 || maxIterations === 0) {
161
+ options?.onProgress?.(1, 1);
162
+ return identityResult(n, dest);
163
+ }
164
+ // the hash region of the workgroup tier is bound whole: refuse a graph it outgrows before any device work
165
+ const plan = planGroupRows(neighbourBound(s));
166
+ assertBindable(ctx, 4 * plan.regionWords, "the group-by hash region", ALGORITHM);
167
+ const scope = algorithmScope(ctx, ALGORITHM, RING_SLOTS);
168
+ try {
169
+ const build = await buildSimpleSymmetric(ctx, s, scope, weighted, ALGORITHM);
170
+ const { graph } = build;
171
+ if (graph.colIdx === null) {
172
+ // only self-loops: no vertex has a neighbour, so nothing ever moves
173
+ scope.flush();
174
+ const bytes = await build.batch.submit().readback;
175
+ ctx.assertReady();
176
+ assertBuildSorted(bytes, build, ALGORITHM);
177
+ options?.onProgress?.(1, 1);
178
+ return identityResult(n, dest);
179
+ }
180
+ const wg = ctx.workgroupSize;
181
+ const words = (count: number, label: string): Binding => {
182
+ const size = 4 * Math.max(1, count);
183
+ return { buffer: scope.scratch(size, label), offset: 0, size, window: null };
184
+ };
185
+ const rows = words(plan.words.length, "rows");
186
+ const region = words(plan.regionWords, "hashRegion");
187
+ const labelsA = words(n, "labelsA");
188
+ const labelsB = words(n, "labelsB");
189
+ const bestKey = words(n, "bestKey");
190
+ const bestScore = words(n, "bestScore");
191
+ const counters = words(LABEL_PROP_PASSES_PER_SUBMIT, "counters");
192
+ await ctx.allocator.check();
193
+ const { queue } = ctx.device;
194
+ queue.writeBuffer(rows.buffer, 0, plan.words);
195
+ queue.writeBuffer(region.buffer, 0, new Uint32Array(1));
196
+ const groupBy = await prepareGroupByKeyRow(scope);
197
+ const step = await ctx.pipelines.kernel(kernelSpec("lpa-step"));
198
+ const fill = await ctx.pipelines.kernel(kernelSpec("fill"));
199
+ const nodePlan = plan1d(n, wg, ctx.caps);
200
+
201
+ let { batch } = build;
202
+ let first = true;
203
+ let cur = labelsA;
204
+ let next = labelsB;
205
+ let done = 0;
206
+ let previousLast = -1;
207
+ for (;;) {
208
+ const k = Math.min(LABEL_PROP_PASSES_PER_SUBMIT, maxIterations - done);
209
+ queue.writeBuffer(counters.buffer, 0, new Uint32Array(k));
210
+ const pass = batch.pass("passes");
211
+ if (first) {
212
+ const iota = scope.params(FILL_PARAMS, { count: n, value: 0, mode: 1, pad0: 0 });
213
+ fill.dispatch(pass, fill.bind({ dst: labelsA, P: iota.binding }), nodePlan, [iota.offset]);
214
+ }
215
+ for (let i = 0; i < k; i++) {
216
+ groupBy.record(pass, {
217
+ rowPtr: graph.rowPtr,
218
+ colIdx: graph.colIdx,
219
+ weights: weighted ? graph.weights : null,
220
+ keyIn: cur,
221
+ plan,
222
+ rows,
223
+ hashRegion: region,
224
+ bestKey,
225
+ bestScore,
226
+ });
227
+ const params = scope.params(LPA_PARAMS, {
228
+ n,
229
+ direction: (done + i) % 2,
230
+ counterIndex: i,
231
+ pad0: 0,
232
+ });
233
+ step.dispatch(
234
+ pass,
235
+ step.bind({ labelsIn: cur, bestKey, labelsOut: next, counters, P: params.binding }),
236
+ nodePlan,
237
+ [params.offset],
238
+ );
239
+ [cur, next] = [next, cur];
240
+ }
241
+ batch.endPass();
242
+ const movesRequest = batch.readback(counters.buffer, 0, 4 * k);
243
+ const exhaustedRequest = batch.readback(region.buffer, 0, 4);
244
+ scope.flush();
245
+ const submitted = batch.submit();
246
+ const bytes = await submitted.readback;
247
+ ctx.assertReady();
248
+ if (first) {
249
+ assertBuildSorted(bytes, build, ALGORITHM);
250
+ first = false;
251
+ }
252
+ if (new Uint32Array(bytes, exhaustedRequest.offset, 1)[0] !== 0) {
253
+ throw new WebGpuGraphError("E_VALIDATION", `${ALGORITHM}: a hash probe exhausted its bound`, {
254
+ label: `${ALGORITHM}/group-by-key`,
255
+ message: "a compare-exchange loop of the workgroup tier ran out of steps",
256
+ });
257
+ }
258
+ const moves = new Uint32Array(bytes, movesRequest.offset, k);
259
+ done += k;
260
+ const lastTwo = k >= 2 ? moves[k - 2] + moves[k - 1] : previousLast + moves[0];
261
+ previousLast = moves[k - 1];
262
+ if (options?.signal?.aborted) {
263
+ throw new WebGpuGraphError("E_ABORTED", `${ALGORITHM}: the signal was aborted`, {
264
+ batchId: submitted.id,
265
+ });
266
+ }
267
+ options?.onProgress?.(done, maxIterations);
268
+ if (lastTwo === 0 || done >= maxIterations) {
269
+ break;
270
+ }
271
+ batch = new CommandBatch(ctx, `${ALGORITHM}/passes`);
272
+ }
273
+ const raw = new Uint32Array(n);
274
+ await ctx.readback.read(cur.buffer, 4 * n, raw);
275
+ ctx.assertReady();
276
+ return resultOf(raw, dest);
277
+ } finally {
278
+ scope.dispose();
279
+ }
280
+ }