@graphty/webgpu-graph-algorithms 0.2.1 → 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.
Files changed (115) hide show
  1. package/README.md +45 -36
  2. package/dist/browser.js +1 -1
  3. package/dist/chunks/{context-E6iKaeuJ.js → context-CRbw2Wyo.js} +178 -19
  4. package/dist/chunks/{context-E6iKaeuJ.js.map → context-CRbw2Wyo.js.map} +1 -1
  5. package/dist/node.js +1 -1
  6. package/dist/src/accelerator.d.ts +9 -6
  7. package/dist/src/accelerator.d.ts.map +1 -1
  8. package/dist/src/accelerator.js +85 -6
  9. package/dist/src/accelerator.js.map +1 -1
  10. package/dist/src/algorithms/components.d.ts +30 -0
  11. package/dist/src/algorithms/components.d.ts.map +1 -0
  12. package/dist/src/algorithms/components.js +300 -0
  13. package/dist/src/algorithms/components.js.map +1 -0
  14. package/dist/src/algorithms/pagerank.d.ts +39 -0
  15. package/dist/src/algorithms/pagerank.d.ts.map +1 -0
  16. package/dist/src/algorithms/pagerank.js +298 -0
  17. package/dist/src/algorithms/pagerank.js.map +1 -0
  18. package/dist/src/algorithms/power-iteration.d.ts +109 -0
  19. package/dist/src/algorithms/power-iteration.d.ts.map +1 -0
  20. package/dist/src/algorithms/power-iteration.js +206 -0
  21. package/dist/src/algorithms/power-iteration.js.map +1 -0
  22. package/dist/src/algorithms/scope.d.ts +26 -0
  23. package/dist/src/algorithms/scope.d.ts.map +1 -0
  24. package/dist/src/algorithms/scope.js +41 -0
  25. package/dist/src/algorithms/scope.js.map +1 -0
  26. package/dist/src/algorithms/spectral.d.ts +50 -0
  27. package/dist/src/algorithms/spectral.d.ts.map +1 -0
  28. package/dist/src/algorithms/spectral.js +247 -0
  29. package/dist/src/algorithms/spectral.js.map +1 -0
  30. package/dist/src/index.d.ts +4 -0
  31. package/dist/src/index.d.ts.map +1 -1
  32. package/dist/src/index.js +4 -0
  33. package/dist/src/index.js.map +1 -1
  34. package/dist/src/kernel/dispatch.d.ts +4 -1
  35. package/dist/src/kernel/dispatch.d.ts.map +1 -1
  36. package/dist/src/kernel/dispatch.js +12 -5
  37. package/dist/src/kernel/dispatch.js.map +1 -1
  38. package/dist/src/kernels.d.ts +20 -4
  39. package/dist/src/kernels.d.ts.map +1 -1
  40. package/dist/src/kernels.js +172 -2
  41. package/dist/src/kernels.js.map +1 -1
  42. package/dist/src/memory/residency.d.ts.map +1 -1
  43. package/dist/src/memory/residency.js +164 -11
  44. package/dist/src/memory/residency.js.map +1 -1
  45. package/dist/src/primitives/core-shape.d.ts +41 -0
  46. package/dist/src/primitives/core-shape.d.ts.map +1 -0
  47. package/dist/src/primitives/core-shape.js +89 -0
  48. package/dist/src/primitives/core-shape.js.map +1 -0
  49. package/dist/src/primitives/segmented-reduce.d.ts.map +1 -1
  50. package/dist/src/primitives/segmented-reduce.js +4 -30
  51. package/dist/src/primitives/segmented-reduce.js.map +1 -1
  52. package/dist/src/primitives/spmv.d.ts +56 -0
  53. package/dist/src/primitives/spmv.d.ts.map +1 -0
  54. package/dist/src/primitives/spmv.js +101 -0
  55. package/dist/src/primitives/spmv.js.map +1 -0
  56. package/dist/src/types/accelerator.d.ts +14 -2
  57. package/dist/src/types/accelerator.d.ts.map +1 -1
  58. package/dist/src/types/algorithms.d.ts +73 -0
  59. package/dist/src/types/algorithms.d.ts.map +1 -0
  60. package/dist/src/types/algorithms.js +17 -0
  61. package/dist/src/types/algorithms.js.map +1 -0
  62. package/dist/src/wgsl/pr-finalize.wgsl.d.ts +11 -0
  63. package/dist/src/wgsl/pr-finalize.wgsl.d.ts.map +1 -0
  64. package/dist/src/wgsl/pr-finalize.wgsl.js +36 -0
  65. package/dist/src/wgsl/pr-finalize.wgsl.js.map +1 -0
  66. package/dist/src/wgsl/pr-scale.wgsl.d.ts +14 -0
  67. package/dist/src/wgsl/pr-scale.wgsl.d.ts.map +1 -0
  68. package/dist/src/wgsl/pr-scale.wgsl.js +48 -0
  69. package/dist/src/wgsl/pr-scale.wgsl.js.map +1 -0
  70. package/dist/src/wgsl/spmv-pull.wgsl.d.ts +15 -0
  71. package/dist/src/wgsl/spmv-pull.wgsl.d.ts.map +1 -0
  72. package/dist/src/wgsl/spmv-pull.wgsl.js +47 -0
  73. package/dist/src/wgsl/spmv-pull.wgsl.js.map +1 -0
  74. package/dist/src/wgsl/wcc-compress.wgsl.d.ts +9 -0
  75. package/dist/src/wgsl/wcc-compress.wgsl.d.ts.map +1 -0
  76. package/dist/src/wgsl/wcc-compress.wgsl.js +26 -0
  77. package/dist/src/wgsl/wcc-compress.wgsl.js.map +1 -0
  78. package/dist/src/wgsl/wcc-link-edges.wgsl.d.ts +13 -0
  79. package/dist/src/wgsl/wcc-link-edges.wgsl.d.ts.map +1 -0
  80. package/dist/src/wgsl/wcc-link-edges.wgsl.js +46 -0
  81. package/dist/src/wgsl/wcc-link-edges.wgsl.js.map +1 -0
  82. package/dist/src/wgsl/wcc-link-sample.wgsl.d.ts +11 -0
  83. package/dist/src/wgsl/wcc-link-sample.wgsl.d.ts.map +1 -0
  84. package/dist/src/wgsl/wcc-link-sample.wgsl.js +45 -0
  85. package/dist/src/wgsl/wcc-link-sample.wgsl.js.map +1 -0
  86. package/dist/src/wgsl/wcc-sample.wgsl.d.ts +10 -0
  87. package/dist/src/wgsl/wcc-sample.wgsl.d.ts.map +1 -0
  88. package/dist/src/wgsl/wcc-sample.wgsl.js +18 -0
  89. package/dist/src/wgsl/wcc-sample.wgsl.js.map +1 -0
  90. package/dist/tsconfig.build.tsbuildinfo +1 -1
  91. package/dist/webgpu-graph-algorithms.js +1550 -29
  92. package/dist/webgpu-graph-algorithms.js.map +1 -1
  93. package/package.json +1 -1
  94. package/src/accelerator.ts +101 -7
  95. package/src/algorithms/components.ts +348 -0
  96. package/src/algorithms/pagerank.ts +343 -0
  97. package/src/algorithms/power-iteration.ts +278 -0
  98. package/src/algorithms/scope.ts +52 -0
  99. package/src/algorithms/spectral.ts +300 -0
  100. package/src/index.ts +18 -0
  101. package/src/kernel/dispatch.ts +12 -5
  102. package/src/kernels.ts +206 -5
  103. package/src/memory/residency.ts +200 -11
  104. package/src/primitives/core-shape.ts +103 -0
  105. package/src/primitives/segmented-reduce.ts +4 -36
  106. package/src/primitives/spmv.ts +155 -0
  107. package/src/types/accelerator.ts +28 -2
  108. package/src/types/algorithms.ts +82 -0
  109. package/src/wgsl/pr-finalize.wgsl.ts +36 -0
  110. package/src/wgsl/pr-scale.wgsl.ts +48 -0
  111. package/src/wgsl/spmv-pull.wgsl.ts +47 -0
  112. package/src/wgsl/wcc-compress.wgsl.ts +26 -0
  113. package/src/wgsl/wcc-link-edges.wgsl.ts +46 -0
  114. package/src/wgsl/wcc-link-sample.wgsl.ts +45 -0
  115. package/src/wgsl/wcc-sample.wgsl.ts +18 -0
@@ -0,0 +1,300 @@
1
+ /**
2
+ * HITS, eigenvector centrality and Katz centrality on the device (spec 3.3 lines 794-796, 8.2 lines 2603-2605):
3
+ * three entry points over the ONE power-iteration driver, differing only in the adjacency the pull walks, the
4
+ * normaliser and the coefficients. Eigenvector is the L2-normalised iteration over the forward core; Katz is
5
+ * `alpha * A^T x + beta` over the reverse view with no per-iteration normaliser; HITS alternates the two pulls with
6
+ * sum normalisation, `a(i) = A^T norm(h(i-1))` and `h(i) = A norm(a(i-1))` (the CPU package's recurrence, which
7
+ * design 9.7 makes the parity target), as TWO interleaved chains of the driver -- the reverse-first chain from the
8
+ * hub seed and the forward-first chain from the authority seed -- so a call is still two runs and two readbacks.
9
+ * Every result is normalised ONCE on the host after the readback (sum for HITS, L2 for the other two), matching
10
+ * the f64 oracle; the per-iteration normaliser never leaves the device (PD-10).
11
+ */
12
+
13
+ import { type F32, type GraphSnapshot } from "@graphty/graph-format";
14
+
15
+ import { type GpuContext } from "../context.js";
16
+ import { WebGpuGraphError } from "../errors.js";
17
+ import {
18
+ type EigenvectorOptions,
19
+ type GpuHitsResult,
20
+ type GpuScoresResult,
21
+ type HitsOptions,
22
+ type KatzOptions,
23
+ } from "../types/algorithms.js";
24
+ import { type Binding } from "../types/memory.js";
25
+ import { type GpuRunOptions } from "../types/run.js";
26
+ import { aborted, checkDest, coreOf, type PowerIterationRun, reverseOf, runPowerIteration } from "./power-iteration.js";
27
+
28
+ /** The three options every entry point shares, resolved. */
29
+ interface Resolved {
30
+ readonly n: number;
31
+ readonly maxIterations: number;
32
+ readonly tolerance: number;
33
+ readonly weights: Binding | null | undefined;
34
+ readonly dest: F32 | null;
35
+ }
36
+
37
+ /**
38
+ * Resolves and validates the shared options and the run options' `dest` / `signal`.
39
+ * @param ctx - the context
40
+ * @param s - the snapshot
41
+ * @param options - maxIterations / tolerance / weighted plus dest / signal
42
+ * @param algorithm - the public name, for messages
43
+ * @returns the resolved options
44
+ */
45
+ function resolve(
46
+ ctx: GpuContext,
47
+ s: GraphSnapshot,
48
+ options: (HitsOptions & GpuRunOptions) | undefined,
49
+ algorithm: string,
50
+ ): Resolved {
51
+ ctx.assertReady();
52
+ const n = s.nodeCount;
53
+ const maxIterations = options?.maxIterations ?? 100;
54
+ const tolerance = options?.tolerance ?? 1e-6;
55
+ if (!Number.isInteger(maxIterations) || maxIterations < 1) {
56
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${algorithm}: maxIterations must be a positive integer`, {
57
+ argument: "maxIterations",
58
+ value: maxIterations,
59
+ expected: "a positive integer",
60
+ });
61
+ }
62
+ const dest = checkDest(options?.dest, n, algorithm);
63
+ if (options?.signal?.aborted) {
64
+ throw aborted(algorithm);
65
+ }
66
+ // weighted: false runs UNWEIGHTED on a weighted snapshot: the pull folds 1 per arc (prepareSpmvPull's null)
67
+ return { n, maxIterations, tolerance, weights: options?.weighted === false ? null : undefined, dest };
68
+ }
69
+
70
+ /**
71
+ * A finite coefficient, or E_INVALID_ARGUMENT.
72
+ * @param value - the caller's value, or undefined for the default
73
+ * @param fallback - the default
74
+ * @param argument - the option name
75
+ * @returns the coefficient
76
+ */
77
+ function finite(value: number | undefined, fallback: number, argument: string): number {
78
+ const resolved = value ?? fallback;
79
+ if (!Number.isFinite(resolved)) {
80
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `katzCentrality: ${argument} must be a finite number`, {
81
+ argument,
82
+ value: resolved,
83
+ expected: "a finite number",
84
+ });
85
+ }
86
+ return resolved;
87
+ }
88
+
89
+ /**
90
+ * Copies the raw iterate into its destination divided by its norm (unchanged when the norm is not positive).
91
+ * @param raw - the driver's iterate
92
+ * @param dest - the caller's destination, or null for the raw array itself
93
+ * @param norm - "sum" (L1) or "l2"
94
+ * @returns the normalised scores
95
+ */
96
+ function normalised(raw: F32, dest: F32 | null, norm: "sum" | "l2"): F32 {
97
+ let acc = 0;
98
+ for (const v of raw) {
99
+ acc += norm === "sum" ? Math.abs(v) : v * v;
100
+ }
101
+ const scale = norm === "sum" ? acc : Math.sqrt(acc);
102
+ const out = dest ?? raw;
103
+ for (let v = 0; v < raw.length; v++) {
104
+ out[v] = scale > 0 ? raw[v] / scale : raw[v];
105
+ }
106
+ return out;
107
+ }
108
+
109
+ /**
110
+ * The score result of one run.
111
+ * @param run - the driver's run
112
+ * @param dest - the caller's destination, or null
113
+ * @param norm - the final normaliser
114
+ * @returns the result
115
+ */
116
+ function scoresOf(run: PowerIterationRun, dest: F32 | null, norm: "sum" | "l2"): GpuScoresResult {
117
+ return {
118
+ scores: normalised(run.scores, dest, norm),
119
+ iterations: run.iterations,
120
+ converged: run.converged,
121
+ precision: "f32",
122
+ };
123
+ }
124
+
125
+ /**
126
+ * The n = 0 result: nothing to iterate.
127
+ * @param dest - the caller's destination, or null
128
+ * @param total - the run's progress denominator, reported to onProgress as both done and total
129
+ * @param onProgress - the caller's progress callback
130
+ * @returns the empty result
131
+ */
132
+ function empty(
133
+ dest: F32 | null,
134
+ total: number,
135
+ onProgress: ((done: number, total: number) => void) | undefined,
136
+ ): GpuScoresResult {
137
+ onProgress?.(total, total);
138
+ return { scores: dest ?? new Float32Array(0), iterations: 0, converged: true, precision: "f32" };
139
+ }
140
+
141
+ /**
142
+ * The latest iterate of one kind across the two HITS chains: a chain holds its seed's kind after an even number of
143
+ * iterations m (in `scores`, index m) and the other kind's last iterate one back (in `previous`, index m - 1, never
144
+ * null on an alternating run; the fallback only satisfies the type).
145
+ * @param sameSeed - the chain seeded with the wanted kind
146
+ * @param otherSeed - the chain seeded with the other kind
147
+ * @returns the raw iterate with the higher index
148
+ */
149
+ function latestOfKind(sameSeed: PowerIterationRun, otherSeed: PowerIterationRun): F32 {
150
+ const fromSame = sameSeed.iterationsRun % 2 === 0 ? sameSeed.iterationsRun : sameSeed.iterationsRun - 1;
151
+ const fromOther = otherSeed.iterationsRun % 2 === 1 ? otherSeed.iterationsRun : otherSeed.iterationsRun - 1;
152
+ const chain = fromSame >= fromOther ? sameSeed : otherSeed;
153
+ const index = Math.max(fromSame, fromOther);
154
+ return (index === chain.iterationsRun ? chain.scores : chain.previous) ?? chain.scores;
155
+ }
156
+
157
+ /**
158
+ * Eigenvector centrality on the device (spec 3.3, 8.2): the L2-normalised power iteration over the forward
159
+ * adjacency, converging on `delta < n * tolerance`; the scores are L2-normalised.
160
+ * @param ctx - the context whose device runs the kernels
161
+ * @param s - the snapshot (uploaded through ctx.residency, or found there)
162
+ * @param options - maxIterations 100 / tolerance 1e-6 / weighted true, plus dest / signal / onProgress
163
+ * @returns the scores, iterations, converged and precision
164
+ */
165
+ export async function eigenvectorCentrality(
166
+ ctx: GpuContext,
167
+ s: GraphSnapshot,
168
+ options?: EigenvectorOptions & GpuRunOptions,
169
+ ): Promise<GpuScoresResult> {
170
+ const algorithm = "eigenvectorCentrality";
171
+ const r = resolve(ctx, s, options, algorithm);
172
+ if (r.n === 0) {
173
+ return empty(r.dest, r.maxIterations, options?.onProgress);
174
+ }
175
+ const run = await runPowerIteration(ctx, r.n, {
176
+ normMode: 2,
177
+ adjacency: coreOf(ctx, s, algorithm),
178
+ alternate: null,
179
+ alpha: 1,
180
+ beta: 0,
181
+ uniformP: 0,
182
+ maxIterations: r.maxIterations,
183
+ tolerance: r.tolerance,
184
+ weights: r.weights,
185
+ label: algorithm,
186
+ signal: options?.signal,
187
+ onProgress: options?.onProgress,
188
+ });
189
+ return scoresOf(run, r.dest, "l2");
190
+ }
191
+
192
+ /**
193
+ * Katz centrality on the device (spec 3.3, 8.2): `x = alpha * A^T x + beta` over the reverse adjacency with no
194
+ * per-iteration normaliser; the scores are L2-normalised once at the end.
195
+ * @param ctx - the context whose device runs the kernels
196
+ * @param s - the snapshot (uploaded through ctx.residency, or found there)
197
+ * @param options - alpha 0.1 / beta 1 / maxIterations 100 / tolerance 1e-6 / weighted true, plus dest / signal / onProgress
198
+ * @returns the scores, iterations, converged and precision
199
+ */
200
+ export async function katzCentrality(
201
+ ctx: GpuContext,
202
+ s: GraphSnapshot,
203
+ options?: KatzOptions & GpuRunOptions,
204
+ ): Promise<GpuScoresResult> {
205
+ const algorithm = "katzCentrality";
206
+ const alpha = finite(options?.alpha, 0.1, "alpha");
207
+ const beta = finite(options?.beta, 1, "beta");
208
+ const r = resolve(ctx, s, options, algorithm);
209
+ if (r.n === 0) {
210
+ return empty(r.dest, r.maxIterations, options?.onProgress);
211
+ }
212
+ const run = await runPowerIteration(ctx, r.n, {
213
+ normMode: 4,
214
+ adjacency: reverseOf(ctx, s),
215
+ alternate: null,
216
+ alpha,
217
+ beta,
218
+ uniformP: 1,
219
+ maxIterations: r.maxIterations,
220
+ tolerance: r.tolerance,
221
+ weights: r.weights,
222
+ label: algorithm,
223
+ signal: options?.signal,
224
+ onProgress: options?.onProgress,
225
+ });
226
+ return scoresOf(run, r.dest, "l2");
227
+ }
228
+
229
+ /**
230
+ * HITS on the device (spec 3.3, 8.2): `a(i) = A^T norm(h(i-1))` and `h(i) = A norm(a(i-1))` from uniform seeds,
231
+ * sum-normalised. Those two recurrences interleave into two independent chains, each alternating the reverse and
232
+ * the forward pull, so the driver runs twice: the chain seeded as hubs pulls reverse first and holds h(m) after an
233
+ * even number m of iterations and a(m) after an odd one; the chain seeded as authorities is the mirror image. Each
234
+ * chain stops at its own batch boundary, so the two can end on opposite parities holding the SAME kind; the result
235
+ * takes the latest iterate of each kind across both chains (a chain's last two iterates are one of each kind),
236
+ * which is h(m) and a(m) when both ran m iterations and never two vectors of one kind otherwise.
237
+ * `iterations` is the larger of the two and `converged` their conjunction. `dest`, when given, receives the hubs.
238
+ * Both vectors are sum-normalised.
239
+ * @param ctx - the context whose device runs the kernels
240
+ * @param s - the snapshot (uploaded through ctx.residency, or found there)
241
+ * @param options - maxIterations 100 / tolerance 1e-6 / weighted true, plus dest / signal / onProgress
242
+ * @returns hubs, authorities, iterations, converged and precision
243
+ */
244
+ export async function hits(
245
+ ctx: GpuContext,
246
+ s: GraphSnapshot,
247
+ options?: HitsOptions & GpuRunOptions,
248
+ ): Promise<GpuHitsResult> {
249
+ const algorithm = "hits";
250
+ const r = resolve(ctx, s, options, algorithm);
251
+ const total = 2 * r.maxIterations;
252
+ if (r.n === 0) {
253
+ const { scores, iterations, converged, precision } = empty(r.dest, total, options?.onProgress);
254
+ return { hubs: scores, authorities: new Float32Array(0), iterations, converged, precision };
255
+ }
256
+ const onProgress = options?.onProgress;
257
+ const shared = {
258
+ normMode: 1 as const,
259
+ alpha: 1,
260
+ beta: 0,
261
+ uniformP: 0,
262
+ maxIterations: r.maxIterations,
263
+ tolerance: r.tolerance,
264
+ weights: r.weights,
265
+ signal: options?.signal,
266
+ };
267
+ const forward = coreOf(ctx, s, algorithm);
268
+ const reverse = reverseOf(ctx, s);
269
+ const hubSeeded = await runPowerIteration(ctx, r.n, {
270
+ ...shared,
271
+ adjacency: reverse,
272
+ alternate: forward,
273
+ label: `${algorithm}/hub-seeded`,
274
+ onProgress:
275
+ onProgress === undefined
276
+ ? undefined
277
+ : (done) => {
278
+ onProgress(done, total);
279
+ },
280
+ });
281
+ const authoritySeeded = await runPowerIteration(ctx, r.n, {
282
+ ...shared,
283
+ adjacency: forward,
284
+ alternate: reverse,
285
+ label: `${algorithm}/authority-seeded`,
286
+ onProgress:
287
+ onProgress === undefined
288
+ ? undefined
289
+ : (done) => {
290
+ onProgress(r.maxIterations + done, total);
291
+ },
292
+ });
293
+ return {
294
+ hubs: normalised(latestOfKind(hubSeeded, authoritySeeded), r.dest, "sum"),
295
+ authorities: normalised(latestOfKind(authoritySeeded, hubSeeded), null, "sum"),
296
+ iterations: Math.max(hubSeeded.iterations, authoritySeeded.iterations),
297
+ converged: hubSeeded.converged && authoritySeeded.converged,
298
+ precision: "f32",
299
+ };
300
+ }
package/src/index.ts CHANGED
@@ -40,6 +40,11 @@ export type { PassTiming, Profiler } from "./kernel/profiler.js";
40
40
  // ==================== algorithms (P1: the walking-skeleton diagnostic, spec 3.3)
41
41
  export { degree } from "./algorithms/degree.js";
42
42
 
43
+ // ==================== algorithms (P7: the SpMV family and WCC, spec 8.2, 8.3)
44
+ export { connectedComponents } from "./algorithms/components.js";
45
+ export { pageRank, personalizedPageRank } from "./algorithms/pagerank.js";
46
+ export { eigenvectorCentrality, hits, katzCentrality } from "./algorithms/spectral.js";
47
+
43
48
  // ==================== layouts and the accelerator (P3)
44
49
  export { createAccelerator } from "./accelerator.js";
45
50
  export { createForceAtlas2 } from "./layouts/forceatlas2.js";
@@ -67,6 +72,19 @@ export type {
67
72
  SsspResultLike,
68
73
  } from "./types/accelerator.js";
69
74
 
75
+ // ==================== types: the P7 algorithm results and option records (spec 3.3 lines 815-828, 9.7)
76
+ export type {
77
+ ComponentsOptions,
78
+ EigenvectorOptions,
79
+ GpuHitsResult,
80
+ GpuLabelResult,
81
+ GpuPageRankResult,
82
+ GpuScoresResult,
83
+ HitsOptions,
84
+ KatzOptions,
85
+ PageRankOptions,
86
+ } from "./types/algorithms.js";
87
+
70
88
  // ==================== types: context and capabilities (P0 / P1)
71
89
  export type {
72
90
  AdapterInfoLike,
@@ -123,7 +123,9 @@ export function plan2d(groups: number, caps: PlanCaps): DispatchPlan {
123
123
  }
124
124
 
125
125
  /**
126
- * P7 (spec 5.2): groups = min(groups, maxGroups ?? (caps.software ? 64 : 4096)) with the kernel looping by stride. P1-P3: throws E_UNSUPPORTED { feature: "planGridStride" } (lead f).
126
+ * P7 (spec 5.2): groups = min(ceil(items / wg), maxGroups ?? (caps.software ? 64 : 4096), the per-dimension limit) with the kernel looping by `stride = groups * wg`; items 0 -> { x: 0, stride: null }.
127
+ * The cap is the ONE performance default in src/ that may read `caps.software` (spec 2.4, 5.2): a grid-stride map is
128
+ * order-independent, so the result never depends on it and the same body serves both adapters.
127
129
  * PLAN DECISION: `maxGroups?` is spelled `?: number` rather than the contract's `?: number | undefined` because the
128
130
  * root ESLint rule no-duplicate-type-constituents rejects the explicit undefined on an optional parameter (the call
129
131
  * signature is identical).
@@ -131,12 +133,17 @@ export function plan2d(groups: number, caps: PlanCaps): DispatchPlan {
131
133
  * @param wg - the workgroup size
132
134
  * @param caps - the capability table
133
135
  * @param maxGroups - the group cap
136
+ * @returns the plan
134
137
  */
135
138
  export function planGridStride(items: number, wg: number, caps: PlanCaps, maxGroups?: number): DispatchPlan {
136
- throw new WebGpuGraphError("E_UNSUPPORTED", "planGridStride lands with the grid-stride kernels of P7 (spec 5.2)", {
137
- feature: "planGridStride",
138
- hint: `items ${items}, wg ${wg}, maxGroups ${maxGroups ?? (caps.software ? 64 : 4096)}`,
139
- });
139
+ assertCount("items", items);
140
+ assertWorkgroupSize(wg);
141
+ if (items === 0) {
142
+ return { x: 0, y: 1, z: 1, items, stride: null };
143
+ }
144
+ const cap = Math.min(maxGroups ?? (caps.software ? 64 : 4096), perDimension(caps));
145
+ const groups = Math.min(Math.ceil(items / wg), cap);
146
+ return { x: groups, y: 1, z: 1, items, stride: groups * wg };
140
147
  }
141
148
 
142
149
  /**
package/src/kernels.ts CHANGED
@@ -6,7 +6,9 @@
6
6
  * test and PipelineCache.warm() iterate this table. P1-T4 lands degree, reduce, fill, fa2-repulsion-exact (K3)
7
7
  * and fa2-speed-finalize (K4) together with every generated block of contract 3.10.2; P2-T2 lands
8
8
  * segmented-reduce; P3-T2 adds fa2-stats-finalize (K1), fa2-attraction (K2), fa2-integrate (K5) and
9
- * fa2-to-scene. This file is the only importer of src/wgsl/** (spec 3.2; test/layers.test.ts).
9
+ * fa2-to-scene; M8b-T3 adds the seven P7 entries: spmv-pull, pr-scale, pr-finalize, wcc-link-sample,
10
+ * wcc-link-edges, wcc-compress and wcc-sample. This file is the only importer of src/wgsl/** (spec 3.2;
11
+ * test/layers.test.ts).
10
12
  */
11
13
 
12
14
  import { STATE_HEADER_BYTES } from "./constants.js";
@@ -23,10 +25,17 @@ import { fa2SpeedFinalizeWgsl } from "./wgsl/fa2-speed-finalize.wgsl.js";
23
25
  import { fa2StatsFinalizeWgsl } from "./wgsl/fa2-stats-finalize.wgsl.js";
24
26
  import { fa2ToSceneWgsl } from "./wgsl/fa2-to-scene.wgsl.js";
25
27
  import { fillWgsl } from "./wgsl/fill.wgsl.js";
28
+ import { prFinalizeWgsl } from "./wgsl/pr-finalize.wgsl.js";
29
+ import { prScaleWgsl } from "./wgsl/pr-scale.wgsl.js";
26
30
  import { reduceWgsl } from "./wgsl/reduce.wgsl.js";
27
31
  import { segmentedReduceWgsl } from "./wgsl/segmented-reduce.wgsl.js";
32
+ import { spmvPullWgsl } from "./wgsl/spmv-pull.wgsl.js";
33
+ import { wccCompressWgsl } from "./wgsl/wcc-compress.wgsl.js";
34
+ import { wccLinkEdgesWgsl } from "./wgsl/wcc-link-edges.wgsl.js";
35
+ import { wccLinkSampleWgsl } from "./wgsl/wcc-link-sample.wgsl.js";
36
+ import { wccSampleWgsl } from "./wgsl/wcc-sample.wgsl.js";
28
37
 
29
- /** Every module id of P1-P3 (P4+ ids are appended, never renamed). */
38
+ /** Every module id of P1-P3 and P7 (later ids are appended, never renamed). */
30
39
  export type KernelId =
31
40
  | "degree"
32
41
  | "reduce"
@@ -37,7 +46,14 @@ export type KernelId =
37
46
  | "fa2-repulsion-exact"
38
47
  | "fa2-speed-finalize"
39
48
  | "fa2-integrate"
40
- | "fa2-to-scene";
49
+ | "fa2-to-scene"
50
+ | "spmv-pull"
51
+ | "pr-scale"
52
+ | "pr-finalize"
53
+ | "wcc-link-sample"
54
+ | "wcc-link-edges"
55
+ | "wcc-compress"
56
+ | "wcc-sample";
41
57
 
42
58
  /** One registry entry: everything of a WgslModuleSpec except the per-variant overrides and snippets. */
43
59
  export interface KernelEntry {
@@ -52,7 +68,7 @@ export interface KernelEntry {
52
68
  /** The snippet marker names the body carries (segmented-reduce: ["VALUE"]). */
53
69
  readonly snippetSlots: readonly string[];
54
70
  /** The phase the entry landed in (documentation and the compile-matrix filter). */
55
- readonly phase: "P1" | "P2" | "P3";
71
+ readonly phase: "P1" | "P2" | "P3" | "P7";
56
72
  }
57
73
 
58
74
  // ---- the generated blocks (spec 5.3; contract 3.10.2): field order = byte order, offsets in the JSDoc
@@ -169,6 +185,64 @@ export const FA2_PARTIAL: UniformBlock = UniformBlock.define(
169
185
  { layout: "storage" },
170
186
  );
171
187
 
188
+ /** `SpmvParams` (uniform, 32 B; spec 8.2): `n` @0 rows of the pull, the bound arc window `[arcBase, arcEnd)` @4 / @8 (0 and arcCount when not windowed), the grid-stride step `stride` @12, `alpha` @16, `beta` @20 (the `1 - alpha` term), `uniformP` @24 (the uniform personalization mass `1 / n`, 0 for a pure SpMV), `pad0` @28. */
189
+ export const SPMV_PARAMS: UniformBlock = UniformBlock.define("SpmvParams", [
190
+ ["n", "u32"],
191
+ ["arcBase", "u32"],
192
+ ["arcEnd", "u32"],
193
+ ["stride", "u32"],
194
+ ["alpha", "f32"],
195
+ ["beta", "f32"],
196
+ ["uniformP", "f32"],
197
+ ["pad0", "u32"],
198
+ ]);
199
+
200
+ /** `PrParams` (uniform, 32 B; spec 8.2): `n` @0, `groups` @4 (the per-workgroup partial count the finalize folds), `iteration` @8 (1-based), `trackConvergence` @12 (1 records firstConverged), `convergeThreshold` @16 (`tolerance * n`, the design's `delta < tol * n`), `pad0` @20, `pad1` @24, `pad2` @28. */
201
+ export const PR_PARAMS: UniformBlock = UniformBlock.define("PrParams", [
202
+ ["n", "u32"],
203
+ ["groups", "u32"],
204
+ ["iteration", "u32"],
205
+ ["trackConvergence", "u32"],
206
+ ["convergeThreshold", "f32"],
207
+ ["pad0", "u32"],
208
+ ["pad1", "u32"],
209
+ ["pad2", "u32"],
210
+ ]);
211
+
212
+ /**
213
+ * `PrPartial` (storage record, 32 B; spec 8.2). Element 0 is the HEADER, whose first 16 bytes are exactly the four
214
+ * fields the design names -- `danglingMass` @0, `delta` @4, `firstConvergedIteration` @8, `iteration` @12 -- plus
215
+ * `norm` @16, which M8b adds so HITS / eigenvector / Katz keep their normaliser on the device (PD-10). Element
216
+ * `1 + g` is workgroup g's partial: it uses `danglingMass`, `delta` and `norm` as three sums and leaves the two u32
217
+ * fields zero.
218
+ */
219
+ export const PR_PARTIAL: UniformBlock = UniformBlock.define(
220
+ "PrPartial",
221
+ [
222
+ ["danglingMass", "f32"],
223
+ ["delta", "f32"],
224
+ ["firstConverged", "u32"],
225
+ ["iteration", "u32"],
226
+ ["norm", "f32"],
227
+ ["pad0", "f32"],
228
+ ["pad1", "f32"],
229
+ ["pad2", "f32"],
230
+ ],
231
+ { layout: "storage" },
232
+ );
233
+
234
+ /** `WccParams` (uniform, 32 B; spec 8.3): `n` @0, `items` @4 (rows for a sample round, edges for an edge round, 1024 for the sampler), `stride` @8, `r` @12 (the neighbour index of the sampled round, and the sampler's seed), `flagIndex` @16 (the changed word inside `comp`, PD-4), `giant` @20 (`U32_MAX` before the sample), `maxSteps` @24, `pad0` @28. */
235
+ export const WCC_PARAMS: UniformBlock = UniformBlock.define("WccParams", [
236
+ ["n", "u32"],
237
+ ["items", "u32"],
238
+ ["stride", "u32"],
239
+ ["r", "u32"],
240
+ ["flagIndex", "u32"],
241
+ ["giant", "u32"],
242
+ ["maxSteps", "u32"],
243
+ ["pad0", "u32"],
244
+ ]);
245
+
172
246
  // ---- the entries (contract 3.10.1; group 0 = graph, 1 = state, 2 = params, 3 = cold)
173
247
 
174
248
  /**
@@ -381,13 +455,133 @@ const FA2_TO_SCENE: KernelEntry = {
381
455
  phase: "P3",
382
456
  };
383
457
 
458
+ /** `spmv-pull` (spec 8.10): the grid-stride pull SpMV over the reverse adjacency; HAS_PERSONALIZATION / USE_DANGLING plus the standard USE_PERM / HAS_WEIGHTS; 8 storage bindings (the design's count). */
459
+ const SPMV_PULL: KernelEntry = {
460
+ id: "spmv-pull",
461
+ body: spmvPullWgsl,
462
+ entryPoint: "spmv_pull",
463
+ bindings: GRAPH_SLOTS.concat(
464
+ decl(1, 0, "xNorm", "storage-ro", "array<f32>"),
465
+ decl(1, 1, "rankOut", "storage", "array<f32>"),
466
+ decl(1, 2, "personalization", "storage-ro", "array<f32>"),
467
+ decl(1, 3, "partials", "storage-ro", "array<PrPartial>"),
468
+ decl(2, 0, "P", "uniform", "SpmvParams"),
469
+ ),
470
+ overrideDecls: [
471
+ { name: "HAS_PERSONALIZATION", type: "bool", default: false },
472
+ { name: "USE_DANGLING", type: "bool", default: false },
473
+ ],
474
+ uniforms: [SPMV_PARAMS, PR_PARTIAL],
475
+ needs: [],
476
+ snippetSlots: [],
477
+ phase: "P7",
478
+ };
479
+
480
+ /** `pr-scale` (spec 8.2 dispatch (a), 8.10): the per-node normaliser and the per-workgroup partials; NORM_MODE 0 PageRank / 1 L1 pass / 2 L2 pass / 3 scale / 4 identity; 5 storage bindings; calls the reduction helpers. */
481
+ const PR_SCALE: KernelEntry = {
482
+ id: "pr-scale",
483
+ body: prScaleWgsl,
484
+ entryPoint: "pr_scale",
485
+ bindings: [
486
+ decl(1, 0, "rankIn", "storage-ro", "array<f32>"),
487
+ decl(1, 1, "rankPrev", "storage-ro", "array<f32>"),
488
+ decl(1, 2, "outWeightSum", "storage-ro", "array<f32>"),
489
+ decl(1, 3, "xNorm", "storage", "array<f32>"),
490
+ decl(1, 4, "partials", "storage", "array<PrPartial>"),
491
+ decl(2, 0, "P", "uniform", "PrParams"),
492
+ ],
493
+ overrideDecls: [{ name: "NORM_MODE", type: "u32", default: 0 }],
494
+ uniforms: [PR_PARAMS, PR_PARTIAL],
495
+ needs: ["subgroups"],
496
+ snippetSlots: [],
497
+ phase: "P7",
498
+ };
499
+
500
+ /** `pr-finalize` (spec 8.2 dispatch (b), 8.10): the one-workgroup fold of the partials into the header at partials[0] and the firstConverged record; NORM_MODE 2 takes the square root; 1 storage binding; calls the reduction helpers. */
501
+ const PR_FINALIZE: KernelEntry = {
502
+ id: "pr-finalize",
503
+ body: prFinalizeWgsl,
504
+ entryPoint: "pr_finalize",
505
+ bindings: [decl(1, 0, "partials", "storage", "array<PrPartial>"), decl(2, 0, "P", "uniform", "PrParams")],
506
+ overrideDecls: [{ name: "NORM_MODE", type: "u32", default: 0 }],
507
+ uniforms: [PR_PARAMS, PR_PARTIAL],
508
+ needs: ["subgroups"],
509
+ snippetSlots: [],
510
+ phase: "P7",
511
+ };
512
+
513
+ /** `wcc-link-sample` (spec 8.3): Afforest's sampled link round over the CSR rows (every vertex links its r-th neighbour); the standard USE_PERM / HAS_WEIGHTS only; 5 storage bindings (the four graph slots and the atomic `comp`, whose word at P.flagIndex is the changed flag, PD-4). */
514
+ const WCC_LINK_SAMPLE: KernelEntry = {
515
+ id: "wcc-link-sample",
516
+ body: wccLinkSampleWgsl,
517
+ entryPoint: "wcc_link_sample",
518
+ bindings: GRAPH_SLOTS.concat(
519
+ decl(1, 0, "comp", "storage", "array<atomic<u32>>"),
520
+ decl(2, 0, "P", "uniform", "WccParams"),
521
+ ),
522
+ overrideDecls: [],
523
+ uniforms: [WCC_PARAMS],
524
+ needs: [],
525
+ snippetSlots: [],
526
+ phase: "P7",
527
+ };
528
+
529
+ /** `wcc-link-edges` (spec 8.3, 8.10): Afforest's each-edge-once link round over the edge list; no overrides; 3 storage bindings (the design's count: edgeSrc, edgeDst and the atomic `comp` carrying the changed flag). */
530
+ const WCC_LINK_EDGES: KernelEntry = {
531
+ id: "wcc-link-edges",
532
+ body: wccLinkEdgesWgsl,
533
+ entryPoint: "wcc_link_edges",
534
+ bindings: [
535
+ decl(1, 0, "edgeSrc", "storage-ro", "array<u32>"),
536
+ decl(1, 1, "edgeDst", "storage-ro", "array<u32>"),
537
+ decl(1, 2, "comp", "storage", "array<atomic<u32>>"),
538
+ decl(2, 0, "P", "uniform", "WccParams"),
539
+ ],
540
+ overrideDecls: [],
541
+ uniforms: [WCC_PARAMS],
542
+ needs: [],
543
+ snippetSlots: [],
544
+ phase: "P7",
545
+ };
546
+
547
+ /** `wcc-compress` (spec 8.3, 8.10): Afforest's bounded pointer-jumping compress; no overrides; 1 storage binding. */
548
+ const WCC_COMPRESS: KernelEntry = {
549
+ id: "wcc-compress",
550
+ body: wccCompressWgsl,
551
+ entryPoint: "wcc_compress",
552
+ bindings: [decl(1, 0, "comp", "storage", "array<atomic<u32>>"), decl(2, 0, "P", "uniform", "WccParams")],
553
+ overrideDecls: [],
554
+ uniforms: [WCC_PARAMS],
555
+ needs: [],
556
+ snippetSlots: [],
557
+ phase: "P7",
558
+ };
559
+
560
+ /** `wcc-sample` (spec 8.3, 8.10): the component-label sample of P.items pseudo-random vertices the host takes the mode of (PD-12); no overrides; 2 storage bindings. */
561
+ const WCC_SAMPLE: KernelEntry = {
562
+ id: "wcc-sample",
563
+ body: wccSampleWgsl,
564
+ entryPoint: "wcc_sample",
565
+ bindings: [
566
+ decl(1, 0, "comp", "storage", "array<atomic<u32>>"),
567
+ decl(1, 1, "hist", "storage", "array<u32>"),
568
+ decl(2, 0, "P", "uniform", "WccParams"),
569
+ ],
570
+ overrideDecls: [],
571
+ uniforms: [WCC_PARAMS],
572
+ needs: [],
573
+ snippetSlots: [],
574
+ phase: "P7",
575
+ };
576
+
384
577
  /**
385
578
  * The entries by id, in dispatch order. PLAN DECISION: `KernelId` is declared in full (contract 3.10) while the
386
579
  * entries landed phase by phase, so the table is built as a Partial record and exported below through the
387
580
  * contract's `Readonly<Record<KernelId, KernelEntry>>` type by one assertion; the runtime membership check of
388
581
  * `entryOf` is the E_INVALID_ARGUMENT the contract documents for a JS caller's unknown id. P1-T4 landed the five
389
582
  * P1 entries, P2-T2 `"segmented-reduce"`, and P3-T2 `"fa2-stats-finalize"`, `"fa2-attraction"`, `"fa2-integrate"`
390
- * and `"fa2-to-scene"`, so every member of `KernelId` is present and the assertion is exact.
583
+ * and `"fa2-to-scene"`; M8b-T3 landed the seven P7 entries, so every member of `KernelId` is present and the
584
+ * assertion is exact.
391
585
  */
392
586
  const REGISTRY: Readonly<Partial<Record<KernelId, KernelEntry>>> = Object.freeze({
393
587
  degree: DEGREE,
@@ -400,6 +594,13 @@ const REGISTRY: Readonly<Partial<Record<KernelId, KernelEntry>>> = Object.freeze
400
594
  "fa2-speed-finalize": FA2_SPEED_FINALIZE,
401
595
  "fa2-integrate": FA2_INTEGRATE,
402
596
  "fa2-to-scene": FA2_TO_SCENE,
597
+ "spmv-pull": SPMV_PULL,
598
+ "pr-scale": PR_SCALE,
599
+ "pr-finalize": PR_FINALIZE,
600
+ "wcc-link-sample": WCC_LINK_SAMPLE,
601
+ "wcc-link-edges": WCC_LINK_EDGES,
602
+ "wcc-compress": WCC_COMPRESS,
603
+ "wcc-sample": WCC_SAMPLE,
403
604
  });
404
605
 
405
606
  /** THE registry (spec 3.5): every entry, keyed by id. */