@graphty/webgpu-graph-algorithms 0.6.3 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/README.md +62 -32
  2. package/dist/browser.js +1 -1
  3. package/dist/chunks/{context-BXqgCifx.js → context-hzGggHeM.js} +68 -24
  4. package/dist/chunks/context-hzGggHeM.js.map +1 -0
  5. package/dist/node.js +1 -1
  6. package/dist/src/accelerator.d.ts +8 -6
  7. package/dist/src/accelerator.d.ts.map +1 -1
  8. package/dist/src/accelerator.js +57 -6
  9. package/dist/src/accelerator.js.map +1 -1
  10. package/dist/src/algorithms/bellman-ford.d.ts +60 -0
  11. package/dist/src/algorithms/bellman-ford.d.ts.map +1 -0
  12. package/dist/src/algorithms/bellman-ford.js +301 -0
  13. package/dist/src/algorithms/bellman-ford.js.map +1 -0
  14. package/dist/src/algorithms/bfs.d.ts +67 -0
  15. package/dist/src/algorithms/bfs.d.ts.map +1 -0
  16. package/dist/src/algorithms/bfs.js +534 -0
  17. package/dist/src/algorithms/bfs.js.map +1 -0
  18. package/dist/src/algorithms/closeness.d.ts +53 -0
  19. package/dist/src/algorithms/closeness.d.ts.map +1 -0
  20. package/dist/src/algorithms/closeness.js +323 -0
  21. package/dist/src/algorithms/closeness.js.map +1 -0
  22. package/dist/src/algorithms/scope.d.ts +3 -1
  23. package/dist/src/algorithms/scope.d.ts.map +1 -1
  24. package/dist/src/algorithms/scope.js +1 -0
  25. package/dist/src/algorithms/scope.js.map +1 -1
  26. package/dist/src/algorithms/sssp.d.ts +72 -0
  27. package/dist/src/algorithms/sssp.d.ts.map +1 -0
  28. package/dist/src/algorithms/sssp.js +586 -0
  29. package/dist/src/algorithms/sssp.js.map +1 -0
  30. package/dist/src/constants.d.ts +10 -0
  31. package/dist/src/constants.d.ts.map +1 -1
  32. package/dist/src/constants.js +10 -0
  33. package/dist/src/constants.js.map +1 -1
  34. package/dist/src/index.d.ts +8 -2
  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 +4 -4
  39. package/dist/src/kernel/prelude.d.ts.map +1 -1
  40. package/dist/src/kernel/prelude.js +39 -5
  41. package/dist/src/kernel/prelude.js.map +1 -1
  42. package/dist/src/kernel/uniform-ring.d.ts +8 -0
  43. package/dist/src/kernel/uniform-ring.d.ts.map +1 -1
  44. package/dist/src/kernel/uniform-ring.js +13 -0
  45. package/dist/src/kernel/uniform-ring.js.map +1 -1
  46. package/dist/src/kernels.d.ts +45 -4
  47. package/dist/src/kernels.d.ts.map +1 -1
  48. package/dist/src/kernels.js +368 -3
  49. package/dist/src/kernels.js.map +1 -1
  50. package/dist/src/primitives/advance.d.ts +63 -0
  51. package/dist/src/primitives/advance.d.ts.map +1 -0
  52. package/dist/src/primitives/advance.js +95 -0
  53. package/dist/src/primitives/advance.js.map +1 -0
  54. package/dist/src/primitives/compact.d.ts +89 -0
  55. package/dist/src/primitives/compact.d.ts.map +1 -0
  56. package/dist/src/primitives/compact.js +233 -0
  57. package/dist/src/primitives/compact.js.map +1 -0
  58. package/dist/src/primitives/core-shape.d.ts +22 -1
  59. package/dist/src/primitives/core-shape.d.ts.map +1 -1
  60. package/dist/src/primitives/core-shape.js +33 -3
  61. package/dist/src/primitives/core-shape.js.map +1 -1
  62. package/dist/src/primitives/frontier.d.ts +151 -0
  63. package/dist/src/primitives/frontier.d.ts.map +1 -0
  64. package/dist/src/primitives/frontier.js +250 -0
  65. package/dist/src/primitives/frontier.js.map +1 -0
  66. package/dist/src/types/accelerator.d.ts +16 -7
  67. package/dist/src/types/accelerator.d.ts.map +1 -1
  68. package/dist/src/types/traversal.d.ts +53 -0
  69. package/dist/src/types/traversal.d.ts.map +1 -0
  70. package/dist/src/types/traversal.js +10 -0
  71. package/dist/src/types/traversal.js.map +1 -0
  72. package/dist/src/wgsl/advance-expand.wgsl.d.ts +19 -0
  73. package/dist/src/wgsl/advance-expand.wgsl.d.ts.map +1 -0
  74. package/dist/src/wgsl/advance-expand.wgsl.js +69 -0
  75. package/dist/src/wgsl/advance-expand.wgsl.js.map +1 -0
  76. package/dist/src/wgsl/bf-relax.wgsl.d.ts +22 -0
  77. package/dist/src/wgsl/bf-relax.wgsl.d.ts.map +1 -0
  78. package/dist/src/wgsl/bf-relax.wgsl.js +58 -0
  79. package/dist/src/wgsl/bf-relax.wgsl.js.map +1 -0
  80. package/dist/src/wgsl/bfs-bitset-build.wgsl.d.ts +15 -0
  81. package/dist/src/wgsl/bfs-bitset-build.wgsl.d.ts.map +1 -0
  82. package/dist/src/wgsl/bfs-bitset-build.wgsl.js +24 -0
  83. package/dist/src/wgsl/bfs-bitset-build.wgsl.js.map +1 -0
  84. package/dist/src/wgsl/bfs-bottom-up.wgsl.d.ts +20 -0
  85. package/dist/src/wgsl/bfs-bottom-up.wgsl.d.ts.map +1 -0
  86. package/dist/src/wgsl/bfs-bottom-up.wgsl.js +67 -0
  87. package/dist/src/wgsl/bfs-bottom-up.wgsl.js.map +1 -0
  88. package/dist/src/wgsl/bfs-contract.wgsl.d.ts +20 -0
  89. package/dist/src/wgsl/bfs-contract.wgsl.d.ts.map +1 -0
  90. package/dist/src/wgsl/bfs-contract.wgsl.js +55 -0
  91. package/dist/src/wgsl/bfs-contract.wgsl.js.map +1 -0
  92. package/dist/src/wgsl/bfs-fused.wgsl.d.ts +25 -0
  93. package/dist/src/wgsl/bfs-fused.wgsl.d.ts.map +1 -0
  94. package/dist/src/wgsl/bfs-fused.wgsl.js +78 -0
  95. package/dist/src/wgsl/bfs-fused.wgsl.js.map +1 -0
  96. package/dist/src/wgsl/bfs-unvisited-flags.wgsl.d.ts +18 -0
  97. package/dist/src/wgsl/bfs-unvisited-flags.wgsl.d.ts.map +1 -0
  98. package/dist/src/wgsl/bfs-unvisited-flags.wgsl.js +42 -0
  99. package/dist/src/wgsl/bfs-unvisited-flags.wgsl.js.map +1 -0
  100. package/dist/src/wgsl/closeness-reduce.wgsl.d.ts +17 -0
  101. package/dist/src/wgsl/closeness-reduce.wgsl.d.ts.map +1 -0
  102. package/dist/src/wgsl/closeness-reduce.wgsl.js +65 -0
  103. package/dist/src/wgsl/closeness-reduce.wgsl.js.map +1 -0
  104. package/dist/src/wgsl/closeness-sweep.wgsl.d.ts +20 -0
  105. package/dist/src/wgsl/closeness-sweep.wgsl.d.ts.map +1 -0
  106. package/dist/src/wgsl/closeness-sweep.wgsl.js +96 -0
  107. package/dist/src/wgsl/closeness-sweep.wgsl.js.map +1 -0
  108. package/dist/src/wgsl/compact-scatter.wgsl.d.ts +9 -0
  109. package/dist/src/wgsl/compact-scatter.wgsl.d.ts.map +1 -0
  110. package/dist/src/wgsl/compact-scatter.wgsl.js +17 -0
  111. package/dist/src/wgsl/compact-scatter.wgsl.js.map +1 -0
  112. package/dist/src/wgsl/dedupe-claim.wgsl.d.ts +10 -0
  113. package/dist/src/wgsl/dedupe-claim.wgsl.d.ts.map +1 -0
  114. package/dist/src/wgsl/dedupe-claim.wgsl.js +19 -0
  115. package/dist/src/wgsl/dedupe-claim.wgsl.js.map +1 -0
  116. package/dist/src/wgsl/dedupe-filter.wgsl.d.ts +12 -0
  117. package/dist/src/wgsl/dedupe-filter.wgsl.d.ts.map +1 -0
  118. package/dist/src/wgsl/dedupe-filter.wgsl.js +46 -0
  119. package/dist/src/wgsl/dedupe-filter.wgsl.js.map +1 -0
  120. package/dist/src/wgsl/frontier-finalize.wgsl.d.ts +53 -0
  121. package/dist/src/wgsl/frontier-finalize.wgsl.d.ts.map +1 -0
  122. package/dist/src/wgsl/frontier-finalize.wgsl.js +164 -0
  123. package/dist/src/wgsl/frontier-finalize.wgsl.js.map +1 -0
  124. package/dist/src/wgsl/sssp-pred.wgsl.d.ts +28 -0
  125. package/dist/src/wgsl/sssp-pred.wgsl.d.ts.map +1 -0
  126. package/dist/src/wgsl/sssp-pred.wgsl.js +80 -0
  127. package/dist/src/wgsl/sssp-pred.wgsl.js.map +1 -0
  128. package/dist/src/wgsl/sssp-relax.wgsl.d.ts +30 -0
  129. package/dist/src/wgsl/sssp-relax.wgsl.d.ts.map +1 -0
  130. package/dist/src/wgsl/sssp-relax.wgsl.js +72 -0
  131. package/dist/src/wgsl/sssp-relax.wgsl.js.map +1 -0
  132. package/dist/webgpu-graph-algorithms.js +3155 -384
  133. package/dist/webgpu-graph-algorithms.js.map +1 -1
  134. package/package.json +5 -4
  135. package/src/accelerator.ts +65 -7
  136. package/src/algorithms/bellman-ford.ts +387 -0
  137. package/src/algorithms/bfs.ts +626 -0
  138. package/src/algorithms/closeness.ts +395 -0
  139. package/src/algorithms/scope.ts +4 -1
  140. package/src/algorithms/sssp.ts +768 -0
  141. package/src/constants.ts +10 -0
  142. package/src/index.ts +14 -1
  143. package/src/kernel/prelude.ts +39 -4
  144. package/src/kernel/uniform-ring.ts +14 -0
  145. package/src/kernels.ts +447 -6
  146. package/src/primitives/advance.ts +131 -0
  147. package/src/primitives/compact.ts +323 -0
  148. package/src/primitives/core-shape.ts +41 -3
  149. package/src/primitives/frontier.ts +372 -0
  150. package/src/types/accelerator.ts +18 -5
  151. package/src/types/traversal.ts +56 -0
  152. package/src/wgsl/advance-expand.wgsl.ts +68 -0
  153. package/src/wgsl/bf-relax.wgsl.ts +57 -0
  154. package/src/wgsl/bfs-bitset-build.wgsl.ts +23 -0
  155. package/src/wgsl/bfs-bottom-up.wgsl.ts +66 -0
  156. package/src/wgsl/bfs-contract.wgsl.ts +54 -0
  157. package/src/wgsl/bfs-fused.wgsl.ts +77 -0
  158. package/src/wgsl/bfs-unvisited-flags.wgsl.ts +41 -0
  159. package/src/wgsl/closeness-reduce.wgsl.ts +64 -0
  160. package/src/wgsl/closeness-sweep.wgsl.ts +95 -0
  161. package/src/wgsl/compact-scatter.wgsl.ts +16 -0
  162. package/src/wgsl/dedupe-claim.wgsl.ts +18 -0
  163. package/src/wgsl/dedupe-filter.wgsl.ts +45 -0
  164. package/src/wgsl/frontier-finalize.wgsl.ts +163 -0
  165. package/src/wgsl/sssp-pred.wgsl.ts +79 -0
  166. package/src/wgsl/sssp-relax.wgsl.ts +71 -0
  167. package/dist/chunks/context-BXqgCifx.js.map +0 -1
@@ -0,0 +1,768 @@
1
+ /**
2
+ * Weighted single-source shortest paths on the device (design 8.4, 3.3 line 808, 9.7; P8-T9, the P8 plan's PD-9 /
3
+ * PD-10 / PD-11 / PD-14 / PD-19 / PD-20 / PD-22 / PD-27 / DEP-P8-E): Davidson's near-far queue over the `Frontier`
4
+ * of P8-T4 with the `dedupe` of P8-T3, every per-round decision made ON THE DEVICE. `dist` is `array<atomic<u32>>`
5
+ * holding the f32 bit patterns of the distances (`F32_INF_BITS` = unreached), claimed by `atomicMin`, which is
6
+ * exact on non-negative floats (PD-9): the settled value is the minimum of a fixed set of f32 sums, so it is
7
+ * bitwise reproducible, bitwise identical across adapters, and bitwise equal to the f32 Dijkstra oracle.
8
+ *
9
+ * A round is eight recorded dispatches: `frontier-finalize` role 2 (the round boundary: a non-empty raw near half
10
+ * is deduped into `nearIn` for a near round; an empty one with a non-empty far half raises the threshold by the
11
+ * delta and dedupes the far half into `farIn` for a pass-through; both empty is `done`), `dedupe-claim` and
12
+ * `dedupe-filter` over each half (direct grid-stride dispatches; role 2 writes the chosen half's raw count into that
13
+ * dedupe's count word -- `edgeCount` for the near half, `edgeCountUnclamped` for the far one, two words SSSP borrows
14
+ * -- and 0 into the other's, so the half not chosen is a no-op), role 3 (restarts the raw half the round consumed),
15
+ * and `sssp-relax` twice (role 0 over `nearIn` sized by `frontierCount`, role 1 over `farIn` sized by `farCount`;
16
+ * the block's `path` word, 5 a near round and 6 a far one, makes the other role's dispatch a no-op). Nothing in a
17
+ * round is an indirect dispatch (2026-09-25). The far pile is re-bucketed by the
18
+ * relax kernel's pass-through, not by `compact` (PD-20): a far entry whose settled distance fell below the previous
19
+ * threshold was relaxed in the near band already and is dropped, the rest go back to near or far against the raised
20
+ * threshold. The near pile is ONE pile (no sub-partitions). The host records `MAX_LEVELS_PER_SUBMIT` rounds per
21
+ * submit and reads four bytes, the `done` word: 1 finished, 2 a raw half above its capacity (E_TOO_LARGE
22
+ * `sssp.pile`: the far half accumulates one entry per improving relaxation beyond the threshold across a bucket's
23
+ * near rounds and is deduped only at the split, so the refusal is reachable on a graph whose vertices improve many
24
+ * times inside one bucket), 3 the f32 threshold absorbed the delta (E_UNSUPPORTED `sssp.thresholdAbsorbed`). The
25
+ * rounds are at most `n + ceil(sum / delta) + 1` (each near round settles the pile's minimum, one pass-through per
26
+ * bucket), so more submits than that is E_VALIDATION, never a hang.
27
+ *
28
+ * The two routings (PD-22) are decided by the RUN's weight vector: `options.weights` when given (`arcCount` long or
29
+ * E_INVALID_ARGUMENT; narrowed to `Float32Array` when it is not one -- a `U32` or `I32` value above 2^24, or the
30
+ * bits of an `F64` value below the f32 ulp, is rounded silently, the package-wide f32 caveat and not a refusal --
31
+ * uploaded into a lease buffer and bound in the graph group's weights slot with `HAS_WEIGHTS` set even on an
32
+ * unweighted snapshot), else the snapshot's column, else none. A run whose vector is all ones, or has none, is the
33
+ * unit-weight BFS of P8-T6 with a depth cap derived from `cutoff` (`dv <= cutoff` on unit weights is
34
+ * `depth <= floor(cutoff)`), its `predArc` from `sssp-pred` in depth mode writing arc indices, `reachedCount` the
35
+ * visited count -- not a fallback, a better algorithm for the input. A negative weight is E_UNSUPPORTED
36
+ * `sssp.negativeWeights` (use bellmanFord), a non-finite one E_UNSUPPORTED `sssp.nonFiniteWeights` (the bit-pattern
37
+ * order is undefined for NaN); `cutoff: NaN` is E_INVALID_ARGUMENT, because the CPU port's `dv <= NaN` relaxes
38
+ * nothing while the kernel's `nd > NaN` guard would relax everything (PD-19). All host-side, before any device work.
39
+ * The delta is `SSSP_DELTA_FACTOR x avgWeight / avgDegree` over the vector in use (1 for an all-zero vector), seeded
40
+ * into the counters block as `deltaBits` and `thresholdBits`.
41
+ *
42
+ * `predArc` is a second pass over the settled distances (PD-11), never packed into the distance atomic: `sssp-pred`
43
+ * in `MODE 0` follows PD-27's plateau rule -- the smallest TIGHT arc whose source sits one key step below `v`, the
44
+ * key `(dist, hops)` with `hops` the plateau depth from a root -- through three roles over one buffer (the arcs,
45
+ * the hop counts, the changed word and the orphan word), so the chain strictly decreases the key, ends at the
46
+ * source, and is bitwise reproducible. A hop batch of `MAX_LEVELS_PER_SUBMIT` passes that still changed something
47
+ * is recorded again without the roots pass; a reached node the key never reached is a kernel bug (E_VALIDATION),
48
+ * never a result. The tuning entry `ssspWithTuning` (PD-26's shape) is what the tests drive; nothing public
49
+ * exposes it.
50
+ *
51
+ * The host-side helpers of the two routings (`resolveWeights`, `normaliseCutoff`, `checkDest`, `assertSource`,
52
+ * `unitWeightRoute`) and the predecessor pass (`predecessorPass`, both PD-27 keys) are shared with
53
+ * `bellman-ford.ts` (P8-T10), which is the same seam type over the same `SsspOptions`; they take the algorithm's
54
+ * name for their messages and are `@internal`.
55
+ */
56
+
57
+ import { type F32, type GraphSnapshot, INVALID_INDEX, type NumericVector, type U32 } from "@graphty/graph-format";
58
+
59
+ import { F32_INF_BITS, MAX_LEVELS_PER_SUBMIT, SSSP_DELTA_FACTOR } from "../constants.js";
60
+ import { type GpuContext } from "../context.js";
61
+ import { WebGpuGraphError } from "../errors.js";
62
+ import { CommandBatch } from "../kernel/batch.js";
63
+ import { plan1d, planGridStride } from "../kernel/dispatch.js";
64
+ import { type Kernel } from "../kernel/kernel.js";
65
+ import { type UniformValues } from "../kernel/struct-block.js";
66
+ import {
67
+ FILL_PARAMS,
68
+ FRONTIER_COUNTERS,
69
+ FRONTIER_PARAMS,
70
+ graphBindings,
71
+ graphOverrides,
72
+ kernelSpec,
73
+ } from "../kernels.js";
74
+ import { type DedupeRecord, prepareCompact } from "../primitives/compact.js";
75
+ import { assertWholeCore } from "../primitives/core-shape.js";
76
+ import { prepareFrontier, W } from "../primitives/frontier.js";
77
+ import { assertDeviceComputes } from "../primitives/verify.js";
78
+ import { type SsspOptions } from "../types/accelerator.js";
79
+ import { type Binding } from "../types/memory.js";
80
+ import { type GpuRunOptions } from "../types/run.js";
81
+ import { type GpuSsspResult } from "../types/traversal.js";
82
+ import { bfsWithTuning } from "./bfs.js";
83
+ import { type AlgorithmScope, algorithmScope } from "./scope.js";
84
+
85
+ const ALGORITHM = "sssp";
86
+
87
+ /** The arc-window alignment of the raw halves: the far half's byte offset is `4 x cap`, a multiple of 256, so `dedupe` can bind it alone. */
88
+ const HALF_ALIGN = 64;
89
+
90
+ /**
91
+ * Params slots of the ring, COUNTED (`UniformRing.reserve` wraps silently): per round `frontier-finalize` twice and
92
+ * the two dedupe records (4), plus the two relax records shared by every round of a submit (the near and the far
93
+ * role read the same fields), so `4 x MAX_LEVELS_PER_SUBMIT + 2` for a round submit; the predecessor batch is the
94
+ * roots pass, `MAX_LEVELS_PER_SUBMIT` hop passes, one `fill` and the predecessor pass (35) and the setup batch two
95
+ * `fill`s; they flush in their own submits but must fit the same ring.
96
+ */
97
+ const RING_SLOTS = 4 * MAX_LEVELS_PER_SUBMIT + 40;
98
+
99
+ /**
100
+ * The knobs the tests need and nothing public offers (PD-26's shape): the submit cadence, the near-far delta and the
101
+ * inspect seam.
102
+ * @internal
103
+ */
104
+ export interface SsspTuning {
105
+ /** Rounds recorded per submit (default `MAX_LEVELS_PER_SUBMIT`); 1 hands `onRound` the block after every round. */
106
+ readonly roundsPerSubmit?: number | undefined;
107
+ /** The near-far delta (a finite positive number) in place of the derived `SSSP_DELTA_FACTOR x avgWeight / avgDegree`. */
108
+ readonly delta?: number | undefined;
109
+ /** The inspect seam: after every SUBMIT, the index of the last round recorded and the whole counters block as the submit left it. */
110
+ readonly onRound?: ((round: number, counters: UniformValues) => void) | undefined;
111
+ }
112
+
113
+ /**
114
+ * The weight vector a run uses and what the host scan found in it.
115
+ * @internal
116
+ */
117
+ export interface WeightVector {
118
+ /** The per-arc weights (the snapshot's column, or the override narrowed to f32). */
119
+ readonly values: F32;
120
+ /** Null when the snapshot's own column is used (the core's binding serves); else the override to upload. */
121
+ readonly override: F32 | null;
122
+ readonly allOne: boolean;
123
+ readonly nonNegative: boolean;
124
+ readonly finite: boolean;
125
+ /** The f64 sum of the vector (the delta's numerator and the round bound's). */
126
+ readonly sum: number;
127
+ }
128
+
129
+ /**
130
+ * The f32 bit pattern of a number (the form the counters block and `FrontierParams` carry).
131
+ * @internal
132
+ * @param value - the number (rounded to f32 first)
133
+ * @returns the pattern
134
+ */
135
+ export function bitsOf(value: number): number {
136
+ return new Uint32Array(Float32Array.of(value).buffer)[0];
137
+ }
138
+
139
+ /**
140
+ * Whole-buffer binding of a scratch buffer over its first `size` bytes.
141
+ * @internal
142
+ * @param buffer - the buffer
143
+ * @param size - the bound byte length
144
+ * @returns the binding
145
+ */
146
+ export function bindingOf(buffer: GPUBuffer, size: number): Binding {
147
+ return { buffer, offset: 0, size, window: null };
148
+ }
149
+
150
+ /**
151
+ * The E_ABORTED error of a signal.
152
+ * @internal
153
+ * @param algorithm - the algorithm's name (the message prefix)
154
+ * @param batchId - the last submitted batch, when one exists
155
+ * @returns the error
156
+ */
157
+ export function aborted(algorithm: string, batchId?: number): WebGpuGraphError {
158
+ return new WebGpuGraphError(
159
+ "E_ABORTED",
160
+ `${algorithm}: the signal was aborted`,
161
+ batchId === undefined ? {} : { batchId },
162
+ );
163
+ }
164
+
165
+ /**
166
+ * E_INVALID_ARGUMENT unless `source` is an integer in `[0, n)`.
167
+ * @internal
168
+ * @param algorithm - the algorithm's name (the message prefix)
169
+ * @param source - the caller's source node index
170
+ * @param n - the node count
171
+ */
172
+ export function assertSource(algorithm: string, source: number, n: number): void {
173
+ if (!Number.isInteger(source) || source < 0 || source >= n) {
174
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${algorithm}: source ${source} is outside [0, ${n})`, {
175
+ argument: "source",
176
+ value: source,
177
+ expected: `an integer in [0, ${n})`,
178
+ });
179
+ }
180
+ }
181
+
182
+ /**
183
+ * One scalar word of a decoded block (every FrontierCounters field is a u32, so anything else is a decoder bug).
184
+ * @param block - the decoded block
185
+ * @param name - the field
186
+ * @returns the word
187
+ */
188
+ function wordOf(block: UniformValues, name: string): number {
189
+ const value = block[name];
190
+ if (typeof value !== "number") {
191
+ throw new WebGpuGraphError("E_VALIDATION", `${ALGORITHM}: counters.${name} did not decode to a number`, {
192
+ label: `${ALGORITHM}/counters`,
193
+ message: `the field ${name} did not decode to a number`,
194
+ });
195
+ }
196
+ return value;
197
+ }
198
+
199
+ /**
200
+ * Validates `options.dest` for a distance result of `n` elements.
201
+ * @internal
202
+ * @param algorithm - the algorithm's name (the message prefix)
203
+ * @param dest - the caller's destination array, if any
204
+ * @param n - the node count
205
+ * @returns the destination as an F32, or null when none was given
206
+ */
207
+ export function checkDest(algorithm: string, dest: Float32Array | Uint32Array | undefined, n: number): F32 | null {
208
+ if (dest === undefined) {
209
+ return null;
210
+ }
211
+ if (dest instanceof Float32Array && dest.length === n && dest.buffer instanceof ArrayBuffer) {
212
+ return dest as F32;
213
+ }
214
+ throw new WebGpuGraphError(
215
+ "E_INVALID_ARGUMENT",
216
+ `${algorithm}: dest must be a Float32Array of length ${n} over an ArrayBuffer`,
217
+ {
218
+ argument: "dest",
219
+ value: `${dest.constructor.name}(${dest.length})`,
220
+ expected: `Float32Array(${n}) over an ArrayBuffer`,
221
+ },
222
+ );
223
+ }
224
+
225
+ /**
226
+ * The CPU port's `cutoff` before any device work: absent is no cap; `NaN` is refused (PD-19: `dv <= NaN` relaxes
227
+ * nothing on the CPU while the kernel's `nd > NaN` guard would relax everything, a silently inverted option);
228
+ * anything else is a finite or infinite number.
229
+ * @internal
230
+ * @param algorithm - the algorithm's name (the message prefix)
231
+ * @param cutoff - the caller's option
232
+ * @returns the cap
233
+ */
234
+ export function normaliseCutoff(algorithm: string, cutoff: number | undefined): number {
235
+ if (cutoff === undefined) {
236
+ return Infinity;
237
+ }
238
+ if (Number.isNaN(cutoff)) {
239
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${algorithm}: cutoff must not be NaN`, {
240
+ argument: "cutoff",
241
+ value: cutoff,
242
+ expected: "a number (the CPU port and the kernel would disagree on NaN)",
243
+ });
244
+ }
245
+ return cutoff;
246
+ }
247
+
248
+ /**
249
+ * The weight vector of the run (PD-22): the override, validated and narrowed, else the snapshot's column, else
250
+ * null; scanned once on the host for the routing flags and the sum.
251
+ * @internal
252
+ * @param algorithm - the algorithm's name (the message prefix)
253
+ * @param s - the snapshot
254
+ * @param weights - the caller's override, if any
255
+ * @returns the vector and its flags, or null when the run has no weights at all
256
+ */
257
+ export function resolveWeights(
258
+ algorithm: string,
259
+ s: GraphSnapshot,
260
+ weights: NumericVector | undefined,
261
+ ): WeightVector | null {
262
+ if (weights === undefined) {
263
+ if (s.weights === null) {
264
+ return null;
265
+ }
266
+ let sum = 0;
267
+ for (const w of s.weights) {
268
+ sum += w;
269
+ }
270
+ return {
271
+ values: s.weights,
272
+ override: null,
273
+ allOne: s.flags.allWeightsOne,
274
+ nonNegative: s.flags.nonNegativeWeights,
275
+ finite: s.flags.finiteWeights,
276
+ sum,
277
+ };
278
+ }
279
+ if (weights.length !== s.arcCount) {
280
+ throw new WebGpuGraphError(
281
+ "E_INVALID_ARGUMENT",
282
+ `${algorithm}: weights has ${weights.length} entries, the snapshot ${s.arcCount} arcs`,
283
+ { argument: "weights", value: weights.length, expected: s.arcCount },
284
+ );
285
+ }
286
+ // the f32 narrowing: a U32 / I32 value above 2^24 or the bits of an F64 value below the f32 ulp round silently
287
+ const values: F32 = weights instanceof Float32Array ? weights : Float32Array.from(weights);
288
+ let allOne = true;
289
+ let nonNegative = true;
290
+ let finite = true;
291
+ let sum = 0;
292
+ for (const w of values) {
293
+ if (w !== 1) {
294
+ allOne = false;
295
+ }
296
+ if (w < 0) {
297
+ nonNegative = false;
298
+ }
299
+ if (!Number.isFinite(w)) {
300
+ finite = false;
301
+ }
302
+ sum += w;
303
+ }
304
+ return { values, override: values, allOne, nonNegative, finite, sum };
305
+ }
306
+
307
+ /**
308
+ * The unit-weight route (PD-22): the BFS with `cutoff` as a depth cap (`dv <= cutoff` on unit weights is
309
+ * `depth <= floor(cutoff)`; no cap or `+Infinity` is no cap, a negative cutoff the source alone), its `parent` in
310
+ * arc form, its depths as f32 distances.
311
+ * @internal
312
+ * @param ctx - the context
313
+ * @param s - the snapshot
314
+ * @param source - the source node index
315
+ * @param cutoff - the normalised cutoff
316
+ * @param dest - the caller's destination, if any
317
+ * @param options - the run options
318
+ * @returns the result
319
+ */
320
+ export async function unitWeightRoute(
321
+ ctx: GpuContext,
322
+ s: GraphSnapshot,
323
+ source: number,
324
+ cutoff: number,
325
+ dest: F32 | null,
326
+ options: GpuRunOptions | undefined,
327
+ ): Promise<GpuSsspResult> {
328
+ const maxDepth = cutoff === Infinity ? undefined : Math.floor(cutoff);
329
+ const bfs = await bfsWithTuning(
330
+ ctx,
331
+ s,
332
+ source,
333
+ { maxDepth, signal: options?.signal, onProgress: options?.onProgress },
334
+ { predKind: 0 },
335
+ );
336
+ const dist = dest ?? new Float32Array(s.nodeCount);
337
+ for (let v = 0; v < s.nodeCount; v++) {
338
+ const d = bfs.depth[v];
339
+ dist[v] = d === INVALID_INDEX ? Infinity : d;
340
+ }
341
+ return { dist, predArc: bfs.parent, reachedCount: bfs.visitedCount };
342
+ }
343
+
344
+ /**
345
+ * The words of PD-27's `pred` buffer for `n` nodes: the arcs at 0, the hop counts at `hb = roundUp(n, 64)`, the
346
+ * changed word at `2 hb` and the orphan word after it, in a 64-word tail.
347
+ * @internal
348
+ * @param n - the node count
349
+ * @returns the word count (`2 x hb + 64`)
350
+ */
351
+ export function predBufferWords(n: number): number {
352
+ return 2 * Math.ceil(n / 64) * 64 + 64;
353
+ }
354
+
355
+ /**
356
+ * What the predecessor pass needs from its driver.
357
+ * @internal
358
+ */
359
+ export interface PredecessorPassInput {
360
+ /** The algorithm's name (the batch labels and the E_VALIDATION label). */
361
+ readonly algorithm: string;
362
+ readonly ctx: GpuContext;
363
+ readonly scope: AlgorithmScope;
364
+ /** `sssp-pred` compiled in `MODE 0` with the run's graph overrides. */
365
+ readonly predKernel: Kernel;
366
+ /** Records one `fill` of `count` words of `dst` with `value` (the driver's own closure over its `fill` kernel). */
367
+ readonly recordFill: (pass: GPUComputePassEncoder, dst: Binding, count: number, value: number) => void;
368
+ /** The graph group with the run's weights in the weights slot. */
369
+ readonly graph: Readonly<Record<"rowPtr" | "colIdx" | "weights" | "perm", Binding>>;
370
+ /** The settled distances (f32 bit patterns, `F32_INF_BITS` unreached). */
371
+ readonly dist: Binding;
372
+ /** The `predBufferWords(n)`-word pred buffer, every word `INVALID_INDEX` (the driver's setup fill). */
373
+ readonly pred: Binding;
374
+ readonly n: number;
375
+ readonly arcCount: number;
376
+ readonly source: number;
377
+ /** PD-27's key: 0 the plateau rule (`sssp`; a roots pass first), 1 the tight-subgraph rule (`bellmanFord`; the source is the only root). */
378
+ readonly mode: 0 | 1;
379
+ }
380
+
381
+ /**
382
+ * What the predecessor pass read back, in one batch with the arcs.
383
+ * @internal
384
+ */
385
+ export interface PredecessorPassOutput {
386
+ /** The settled distances (the dist buffer), a view of the batch's copy. */
387
+ readonly dist: Float32Array;
388
+ /** `pred[0, n)`: the arcs. */
389
+ readonly predArc: U32;
390
+ /** The orphan word: reached non-source nodes the key never reached. */
391
+ readonly orphans: number;
392
+ }
393
+
394
+ /**
395
+ * The predecessor pass (PD-11, PD-27): the source is the one seeded root and the two flag words are zeroed by
396
+ * `queue.writeBuffer`; then ONE batch of the roots pass (the plateau rule only), `MAX_LEVELS_PER_SUBMIT` hop passes
397
+ * with `P.iteration` counting inside the batch, the arcs re-filled with `INVALID_INDEX` and the predecessor pass,
398
+ * with the readbacks of `dist`, `pred[0, n)` and the two flag words; recorded again without the roots pass while the
399
+ * last hop pass still changed something (a plateau, or a tight subgraph, `k` hops deep needs `ceil((k + 1) / 32)`
400
+ * batches). More than `ceil((n + 1) / MAX_LEVELS_PER_SUBMIT) + 1` batches is E_VALIDATION, because a hop count is at
401
+ * most `n`. The orphan word is returned, not judged: `sssp` treats it as a kernel bug and `bellmanFord` as the rounded
402
+ * cycle PD-27 names.
403
+ * @internal
404
+ * @param input - the driver's buffers, kernels and key rule
405
+ * @returns the distances, the arcs and the orphan count
406
+ */
407
+ export async function predecessorPass(input: PredecessorPassInput): Promise<PredecessorPassOutput> {
408
+ const { algorithm, ctx, scope, predKernel, recordFill, graph, dist, pred, n, arcCount, source, mode } = input;
409
+ const wg = ctx.workgroupSize;
410
+ const { queue } = ctx.device;
411
+ const bytes = 4 * n;
412
+ const hb = Math.ceil(n / 64) * 64;
413
+ const predArcs: Binding = { buffer: pred.buffer, offset: pred.offset, size: bytes, window: null };
414
+ queue.writeBuffer(pred.buffer, pred.offset + 4 * (hb + source), Uint32Array.of(0));
415
+ queue.writeBuffer(pred.buffer, pred.offset + 4 * 2 * hb, new Uint32Array(2));
416
+ const predPlan = planGridStride(n, wg, ctx.caps);
417
+ const predFields = {
418
+ wg,
419
+ n,
420
+ arcBase: 0,
421
+ arcEnd: arcCount,
422
+ predKind: 0,
423
+ source,
424
+ stride: predPlan.stride ?? n,
425
+ mode,
426
+ };
427
+ const maxBatches = Math.ceil((n + 1) / MAX_LEVELS_PER_SUBMIT) + 1;
428
+ let batches = 0;
429
+ for (;;) {
430
+ const batch = new CommandBatch(ctx, `${algorithm}/pred`);
431
+ const pass = batch.pass("pred");
432
+ const recordRole = (role: number, iteration: number): void => {
433
+ const params = scope.params(FRONTIER_PARAMS, { ...predFields, role, iteration });
434
+ const bound = predKernel.bind({ ...graph, dist, pred, P: params.binding });
435
+ predKernel.dispatch(pass, bound, predPlan, [params.offset]);
436
+ };
437
+ if (batches === 0 && mode === 0) {
438
+ recordRole(0, 0);
439
+ }
440
+ for (let iteration = 0; iteration < MAX_LEVELS_PER_SUBMIT; iteration++) {
441
+ recordRole(1, iteration);
442
+ }
443
+ recordFill(pass, predArcs, n, INVALID_INDEX);
444
+ recordRole(2, 0);
445
+ batch.endPass();
446
+ const distRequest = batch.readback(dist.buffer, dist.offset, bytes);
447
+ const predRequest = batch.readback(pred.buffer, pred.offset, bytes);
448
+ const flagsRequest = batch.readback(pred.buffer, pred.offset + 4 * 2 * hb, 8);
449
+ scope.flush();
450
+ const back = await batch.submit().readback;
451
+ ctx.assertReady();
452
+ batches += 1;
453
+ const [changed, orphans] = new Uint32Array(back, flagsRequest.offset, 2);
454
+ if (changed < MAX_LEVELS_PER_SUBMIT) {
455
+ return {
456
+ dist: new Float32Array(back, distRequest.offset, n),
457
+ predArc: new Uint32Array(back, predRequest.offset, n).slice(),
458
+ orphans,
459
+ };
460
+ }
461
+ if (batches > maxBatches) {
462
+ throw new WebGpuGraphError(
463
+ "E_VALIDATION",
464
+ `${algorithm}: the hop passes still changed something after ${batches} batches (a hop count is at most ${n})`,
465
+ { label: `${algorithm}/pred`, message: `the hop passes did not converge in ${batches} batches` },
466
+ );
467
+ }
468
+ queue.writeBuffer(pred.buffer, pred.offset + 4 * 2 * hb, new Uint32Array(2));
469
+ }
470
+ }
471
+
472
+ /**
473
+ * Single-source shortest paths with the test knobs of PD-26's shape; `sssp` is this with an empty tuning.
474
+ * @internal
475
+ * @param ctx - the context whose device runs the kernels
476
+ * @param s - the snapshot (uploaded through ctx.residency, or found there)
477
+ * @param source - the source node index
478
+ * @param options - `cutoff` and `weights`, plus dest / signal / onProgress
479
+ * @param tuning - the knobs
480
+ * @returns the distances, the predecessor arcs and the reached count
481
+ */
482
+ export async function ssspWithTuning(
483
+ ctx: GpuContext,
484
+ s: GraphSnapshot,
485
+ source: number,
486
+ options: (SsspOptions & GpuRunOptions) | undefined,
487
+ tuning: SsspTuning,
488
+ ): Promise<GpuSsspResult> {
489
+ ctx.assertReady();
490
+ await assertDeviceComputes(ctx);
491
+ const n = s.nodeCount;
492
+ assertSource(ALGORITHM, source, n);
493
+ const roundsPerSubmit = tuning.roundsPerSubmit ?? MAX_LEVELS_PER_SUBMIT;
494
+ if (!Number.isInteger(roundsPerSubmit) || roundsPerSubmit < 1 || roundsPerSubmit > MAX_LEVELS_PER_SUBMIT) {
495
+ throw new WebGpuGraphError(
496
+ "E_INVALID_ARGUMENT",
497
+ `${ALGORITHM}: roundsPerSubmit must be an integer in [1, ${MAX_LEVELS_PER_SUBMIT}]`,
498
+ {
499
+ argument: "roundsPerSubmit",
500
+ value: roundsPerSubmit,
501
+ expected: `an integer in [1, ${MAX_LEVELS_PER_SUBMIT}]`,
502
+ },
503
+ );
504
+ }
505
+ if (tuning.delta !== undefined && !(Number.isFinite(tuning.delta) && tuning.delta > 0)) {
506
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `${ALGORITHM}: delta must be a finite positive number`, {
507
+ argument: "delta",
508
+ value: tuning.delta,
509
+ expected: "a finite positive number",
510
+ });
511
+ }
512
+ const dest = checkDest(ALGORITHM, options?.dest, n);
513
+ const vector = resolveWeights(ALGORITHM, s, options?.weights);
514
+ const cutoff = normaliseCutoff(ALGORITHM, options?.cutoff);
515
+ if (options?.signal?.aborted) {
516
+ throw aborted(ALGORITHM);
517
+ }
518
+ if (vector === null || vector.allOne) {
519
+ return unitWeightRoute(ctx, s, source, cutoff, dest, options);
520
+ }
521
+ if (!vector.nonNegative) {
522
+ throw new WebGpuGraphError("E_UNSUPPORTED", `${ALGORITHM}: a negative weight has no shortest path here`, {
523
+ feature: "sssp.negativeWeights",
524
+ hint: "use bellmanFord",
525
+ });
526
+ }
527
+ if (!vector.finite) {
528
+ throw new WebGpuGraphError("E_UNSUPPORTED", `${ALGORITHM}: a NaN or infinite weight has no bit-pattern order`, {
529
+ feature: "sssp.nonFiniteWeights",
530
+ });
531
+ }
532
+ const { arcCount } = s;
533
+ const core = ctx.residency.core(s);
534
+ const limit = ctx.caps.limits.maxStorageBufferBindingSize;
535
+ assertWholeCore(core, arcCount, limit, ALGORITHM);
536
+ // the raw halves: cap entries each, the far half at word cap so its byte offset is 256-aligned; one binding
537
+ // of both halves is what the relax kernel sees, so the whole buffer is held to the binding limit (DEP-P8-E)
538
+ const cap = Math.ceil(Math.max(1, arcCount) / HALF_ALIGN) * HALF_ALIGN;
539
+ if (8 * cap > limit) {
540
+ throw new WebGpuGraphError(
541
+ "E_TOO_LARGE",
542
+ `${ALGORITHM}: the near-far queue of ${cap} entries per half needs ${8 * cap} bytes, above the ${limit}-byte binding limit (the relax is never windowed)`,
543
+ { needed: 8 * cap, limit, path: "sssp.queue", algorithm: ALGORITHM },
544
+ );
545
+ }
546
+ // PD-22: the delta from the vector in use; an all-zero vector would land every relaxation on the far pile
547
+ const delta = Math.fround(
548
+ tuning.delta ?? (vector.sum === 0 ? 1 : (SSSP_DELTA_FACTOR * (vector.sum / arcCount)) / (arcCount / n)),
549
+ );
550
+ const deltaBits = bitsOf(delta);
551
+ const maxRounds = n + Math.ceil(vector.sum / delta) + 1;
552
+ const maxSubmits = Math.ceil((maxRounds + 1) / roundsPerSubmit) + 1;
553
+ const scope = algorithmScope(ctx, ALGORITHM, RING_SLOTS);
554
+ try {
555
+ const wg = ctx.workgroupSize;
556
+ const bytes = 4 * n;
557
+ const dist = bindingOf(scope.scratch(bytes, "dist"), bytes);
558
+ const owner = bindingOf(scope.scratch(bytes, "owner"), bytes);
559
+ const queueOut = bindingOf(scope.scratch(8 * cap, "queue-out"), 8 * cap);
560
+ const nearHalf: Binding = { buffer: queueOut.buffer, offset: 0, size: 4 * cap, window: null };
561
+ const farHalf: Binding = { buffer: queueOut.buffer, offset: 4 * cap, size: 4 * cap, window: null };
562
+ // the pred buffer of PD-27: the arcs at 0, the hop counts at hb, the changed word at 2 hb, the orphan word after it
563
+ const predWords = predBufferWords(n);
564
+ const pred = bindingOf(scope.scratch(4 * predWords, "pred"), 4 * predWords);
565
+ const { queue } = ctx.device;
566
+ let weightsBinding: Binding | undefined;
567
+ if (vector.override !== null) {
568
+ const uploaded = scope.scratch(4 * arcCount, "weights");
569
+ queue.writeBuffer(uploaded, 0, vector.override);
570
+ weightsBinding = bindingOf(uploaded, 4 * arcCount);
571
+ }
572
+ await ctx.allocator.check();
573
+ const planner = await prepareFrontier(scope, n, arcCount, cap);
574
+ const compact = await prepareCompact(scope);
575
+ const overrides = graphOverrides(core, null, weightsBinding);
576
+ const relax = await ctx.pipelines.kernel(kernelSpec("sssp-relax", overrides));
577
+ const predKernel = await ctx.pipelines.kernel(kernelSpec("sssp-pred", { ...overrides, MODE: 0 }));
578
+ const fill = await ctx.pipelines.kernel(kernelSpec("fill"));
579
+ const graph = graphBindings(core, null, weightsBinding);
580
+ const { frontier } = planner;
581
+ const { counters } = frontier;
582
+ const nearIn = frontier.vertices[0];
583
+ const farIn = frontier.vertices[1];
584
+ const recordFill = (pass: GPUComputePassEncoder, dst: Binding, count: number, value: number): void => {
585
+ const params = scope.params(FILL_PARAMS, { count, value, mode: 0, pad0: 0 });
586
+ fill.dispatch(pass, fill.bind({ dst, P: params.binding }), plan1d(count, wg, ctx.caps), [params.offset]);
587
+ };
588
+ const submit = (batch: CommandBatch): ReturnType<CommandBatch["submit"]> => {
589
+ scope.flush();
590
+ return batch.submit();
591
+ };
592
+
593
+ // setup: dist = +Inf everywhere and the pred buffer INVALID_INDEX; then the source at 0, the seeded block
594
+ // (the source in the RAW near half so the first boundary dedupes a one-entry pile like every later one)
595
+ const setup = new CommandBatch(ctx, `${ALGORITHM}/setup`);
596
+ const setupPass = setup.pass("fill");
597
+ recordFill(setupPass, dist, n, F32_INF_BITS);
598
+ recordFill(setupPass, pred, predWords, INVALID_INDEX);
599
+ setup.endPass();
600
+ await submit(setup).readback;
601
+ ctx.assertReady();
602
+ queue.writeBuffer(dist.buffer, dist.offset + 4 * source, Uint32Array.of(0));
603
+ frontier.reset(queue, source, { nextFrontierCount: 1, thresholdBits: deltaBits, deltaBits });
604
+ queue.writeBuffer(queueOut.buffer, queueOut.offset, Uint32Array.of(source));
605
+
606
+ // the rounds: MAX_LEVELS_PER_SUBMIT per submit, four bytes back
607
+ const nearDedupe: DedupeRecord = {
608
+ queue: nearHalf,
609
+ count: cap,
610
+ countIndex: W.edgeCount, // role 2 writes the raw near count here when it chooses a near round, 0 otherwise
611
+ counters,
612
+ owner,
613
+ out: nearIn,
614
+ outCount: counters,
615
+ outIndex: W.frontierCount,
616
+ };
617
+ const farDedupe: DedupeRecord = {
618
+ queue: farHalf,
619
+ count: cap,
620
+ countIndex: W.edgeCountUnclamped, // and the raw far count here on a far round
621
+ counters,
622
+ owner,
623
+ out: farIn,
624
+ outCount: counters,
625
+ outIndex: W.farCount,
626
+ };
627
+ const relaxPlan = planGridStride(n, wg, ctx.caps); // a pile holds at most n entries; the kernel loops to the count word
628
+ const relaxFields = {
629
+ wg,
630
+ n,
631
+ edgeCapacity: cap,
632
+ arcBase: 0,
633
+ arcEnd: arcCount,
634
+ cutoffBits: bitsOf(cutoff),
635
+ source,
636
+ stride: relaxPlan.stride ?? wg,
637
+ };
638
+ let roundsRecorded = 0;
639
+ let submits = 0;
640
+ for (;;) {
641
+ const batch = new CommandBatch(ctx, `${ALGORITHM}/rounds`);
642
+ const pass = batch.pass("sssp");
643
+ const near = scope.params(FRONTIER_PARAMS, { ...relaxFields, role: 0 });
644
+ const far = scope.params(FRONTIER_PARAMS, { ...relaxFields, role: 1 });
645
+ const boundNear = relax.bind({ ...graph, dist, counters, queueIn: nearIn, queueOut, P: near.binding });
646
+ const boundFar = relax.bind({ ...graph, dist, counters, queueIn: farIn, queueOut, P: far.binding });
647
+ for (let round = 0; round < roundsPerSubmit; round++) {
648
+ // every dispatch of a round is DIRECT: role 2 writes the chosen half's raw count into the dedupe's count
649
+ // word (the other's is 0) and the path word the relax roles gate on, so the half not chosen is a no-op
650
+ planner.recordFinalize(pass, 2, round, {});
651
+ compact.recordDedupe(pass, nearDedupe);
652
+ compact.recordDedupe(pass, farDedupe);
653
+ planner.recordFinalize(pass, 3, round, {});
654
+ relax.dispatch(pass, boundNear, relaxPlan, [near.offset]);
655
+ relax.dispatch(pass, boundFar, relaxPlan, [far.offset]);
656
+ }
657
+ batch.endPass();
658
+ const doneRequest = batch.readback(counters.buffer, counters.offset + 4 * W.done, 4);
659
+ const inspect =
660
+ tuning.onRound === undefined
661
+ ? null
662
+ : batch.readback(counters.buffer, counters.offset, FRONTIER_COUNTERS.byteLength);
663
+ const submitted = submit(batch);
664
+ const back = await submitted.readback;
665
+ roundsRecorded += roundsPerSubmit;
666
+ submits += 1;
667
+ ctx.assertReady();
668
+ if (options?.signal?.aborted) {
669
+ throw aborted(ALGORITHM, submitted.id);
670
+ }
671
+ options?.onProgress?.(Math.min(roundsRecorded, maxRounds), maxRounds);
672
+ if (inspect !== null && tuning.onRound !== undefined) {
673
+ tuning.onRound(roundsRecorded - 1, FRONTIER_COUNTERS.read(new DataView(back), inspect.offset));
674
+ }
675
+ const done = new Uint32Array(back, doneRequest.offset, 1)[0];
676
+ if (done === 1) {
677
+ break;
678
+ }
679
+ if (done !== 0) {
680
+ const block = FRONTIER_COUNTERS.read(
681
+ new DataView(
682
+ await ctx.readback.read(
683
+ counters.buffer,
684
+ FRONTIER_COUNTERS.byteLength,
685
+ undefined,
686
+ counters.offset,
687
+ ),
688
+ ),
689
+ );
690
+ if (done === 2) {
691
+ const needed = Math.max(wordOf(block, "nextFrontierCount"), wordOf(block, "nextFarCount"));
692
+ throw new WebGpuGraphError(
693
+ "E_TOO_LARGE",
694
+ `${ALGORITHM}: a raw pile of ${needed} entries overflowed its ${cap}-entry half`,
695
+ { needed, limit: cap, path: "sssp.pile", algorithm: ALGORITHM },
696
+ );
697
+ }
698
+ throw new WebGpuGraphError(
699
+ "E_UNSUPPORTED",
700
+ `${ALGORITHM}: the f32 threshold ${wordOf(block, "thresholdBits")} absorbed the delta ${deltaBits} (as bit patterns); the far pile can no longer be bucketed`,
701
+ { feature: "sssp.thresholdAbsorbed", hint: "the distances outgrew the delta's f32 precision" },
702
+ );
703
+ }
704
+ if (submits > maxSubmits) {
705
+ throw new WebGpuGraphError(
706
+ "E_VALIDATION",
707
+ `${ALGORITHM}: the done flag never rose in ${submits} submits (at most ${maxRounds} rounds)`,
708
+ { label: `${ALGORITHM}/rounds`, message: `the done flag never rose in ${submits} submits` },
709
+ );
710
+ }
711
+ }
712
+
713
+ // the predecessor pass (PD-11, PD-27) under the plateau rule
714
+ const passed = await predecessorPass({
715
+ algorithm: ALGORITHM,
716
+ ctx,
717
+ scope,
718
+ predKernel,
719
+ recordFill,
720
+ graph,
721
+ dist,
722
+ pred,
723
+ n,
724
+ arcCount,
725
+ source,
726
+ mode: 0,
727
+ });
728
+ if (passed.orphans !== 0) {
729
+ // (2) of PD-27: every reached non-source node has an admitted in-arc under non-negative weights
730
+ throw new WebGpuGraphError(
731
+ "E_VALIDATION",
732
+ `${ALGORITHM}: ${passed.orphans} reached node(s) the predecessor key never reached (a kernel bug)`,
733
+ { label: `${ALGORITHM}/pred`, message: `${passed.orphans} orphan(s) in the predecessor pass` },
734
+ );
735
+ }
736
+ const distOut = dest ?? new Float32Array(n);
737
+ distOut.set(passed.dist);
738
+ let reachedCount = 0;
739
+ for (const d of distOut) {
740
+ if (d !== Infinity) {
741
+ reachedCount += 1;
742
+ }
743
+ }
744
+ return { dist: distOut, predArc: passed.predArc, reachedCount };
745
+ } finally {
746
+ scope.dispose();
747
+ }
748
+ }
749
+
750
+ /**
751
+ * Single-source shortest paths on the device (spec 3.3 line 808, design 8.4, 9.7): `dist` in f32, bitwise equal to
752
+ * the f32 Dijkstra (PD-9), `predArc` a tight arc one PD-27 key step below each reached node (the chain always ends
753
+ * at the source), `reachedCount`; `cutoff` as the CPU port reads it (`dv <= cutoff`), `weights` a per-arc override
754
+ * of the snapshot's column for this run; a run with unit weights is the BFS (PD-22).
755
+ * @param ctx - the context whose device runs the kernels
756
+ * @param s - the snapshot (uploaded through ctx.residency, or found there)
757
+ * @param source - the source node index (E_INVALID_ARGUMENT outside `[0, n)`)
758
+ * @param options - `cutoff` and `weights`, plus dest (a Float32Array of length n for `dist`) / signal / onProgress
759
+ * @returns the distances, the predecessor arcs and the reached count
760
+ */
761
+ export function sssp(
762
+ ctx: GpuContext,
763
+ s: GraphSnapshot,
764
+ source: number,
765
+ options?: SsspOptions & GpuRunOptions,
766
+ ): Promise<GpuSsspResult> {
767
+ return ssspWithTuning(ctx, s, source, options, {});
768
+ }