@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.
- package/README.md +62 -32
- package/dist/browser.js +1 -1
- package/dist/chunks/{context-BXqgCifx.js → context-hzGggHeM.js} +68 -24
- package/dist/chunks/context-hzGggHeM.js.map +1 -0
- package/dist/node.js +1 -1
- package/dist/src/accelerator.d.ts +8 -6
- package/dist/src/accelerator.d.ts.map +1 -1
- package/dist/src/accelerator.js +57 -6
- package/dist/src/accelerator.js.map +1 -1
- package/dist/src/algorithms/bellman-ford.d.ts +60 -0
- package/dist/src/algorithms/bellman-ford.d.ts.map +1 -0
- package/dist/src/algorithms/bellman-ford.js +301 -0
- package/dist/src/algorithms/bellman-ford.js.map +1 -0
- package/dist/src/algorithms/bfs.d.ts +67 -0
- package/dist/src/algorithms/bfs.d.ts.map +1 -0
- package/dist/src/algorithms/bfs.js +534 -0
- package/dist/src/algorithms/bfs.js.map +1 -0
- package/dist/src/algorithms/closeness.d.ts +53 -0
- package/dist/src/algorithms/closeness.d.ts.map +1 -0
- package/dist/src/algorithms/closeness.js +323 -0
- package/dist/src/algorithms/closeness.js.map +1 -0
- package/dist/src/algorithms/scope.d.ts +3 -1
- package/dist/src/algorithms/scope.d.ts.map +1 -1
- package/dist/src/algorithms/scope.js +1 -0
- package/dist/src/algorithms/scope.js.map +1 -1
- package/dist/src/algorithms/sssp.d.ts +72 -0
- package/dist/src/algorithms/sssp.d.ts.map +1 -0
- package/dist/src/algorithms/sssp.js +586 -0
- package/dist/src/algorithms/sssp.js.map +1 -0
- package/dist/src/constants.d.ts +10 -0
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +10 -0
- package/dist/src/constants.js.map +1 -1
- package/dist/src/index.d.ts +8 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/kernel/prelude.d.ts +4 -4
- package/dist/src/kernel/prelude.d.ts.map +1 -1
- package/dist/src/kernel/prelude.js +39 -5
- package/dist/src/kernel/prelude.js.map +1 -1
- package/dist/src/kernel/uniform-ring.d.ts +8 -0
- package/dist/src/kernel/uniform-ring.d.ts.map +1 -1
- package/dist/src/kernel/uniform-ring.js +13 -0
- package/dist/src/kernel/uniform-ring.js.map +1 -1
- package/dist/src/kernels.d.ts +45 -4
- package/dist/src/kernels.d.ts.map +1 -1
- package/dist/src/kernels.js +368 -3
- package/dist/src/kernels.js.map +1 -1
- package/dist/src/primitives/advance.d.ts +63 -0
- package/dist/src/primitives/advance.d.ts.map +1 -0
- package/dist/src/primitives/advance.js +95 -0
- package/dist/src/primitives/advance.js.map +1 -0
- package/dist/src/primitives/compact.d.ts +89 -0
- package/dist/src/primitives/compact.d.ts.map +1 -0
- package/dist/src/primitives/compact.js +233 -0
- package/dist/src/primitives/compact.js.map +1 -0
- package/dist/src/primitives/core-shape.d.ts +22 -1
- package/dist/src/primitives/core-shape.d.ts.map +1 -1
- package/dist/src/primitives/core-shape.js +33 -3
- package/dist/src/primitives/core-shape.js.map +1 -1
- package/dist/src/primitives/frontier.d.ts +151 -0
- package/dist/src/primitives/frontier.d.ts.map +1 -0
- package/dist/src/primitives/frontier.js +250 -0
- package/dist/src/primitives/frontier.js.map +1 -0
- package/dist/src/types/accelerator.d.ts +16 -7
- package/dist/src/types/accelerator.d.ts.map +1 -1
- package/dist/src/types/traversal.d.ts +53 -0
- package/dist/src/types/traversal.d.ts.map +1 -0
- package/dist/src/types/traversal.js +10 -0
- package/dist/src/types/traversal.js.map +1 -0
- package/dist/src/wgsl/advance-expand.wgsl.d.ts +19 -0
- package/dist/src/wgsl/advance-expand.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/advance-expand.wgsl.js +69 -0
- package/dist/src/wgsl/advance-expand.wgsl.js.map +1 -0
- package/dist/src/wgsl/bf-relax.wgsl.d.ts +22 -0
- package/dist/src/wgsl/bf-relax.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/bf-relax.wgsl.js +58 -0
- package/dist/src/wgsl/bf-relax.wgsl.js.map +1 -0
- package/dist/src/wgsl/bfs-bitset-build.wgsl.d.ts +15 -0
- package/dist/src/wgsl/bfs-bitset-build.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/bfs-bitset-build.wgsl.js +24 -0
- package/dist/src/wgsl/bfs-bitset-build.wgsl.js.map +1 -0
- package/dist/src/wgsl/bfs-bottom-up.wgsl.d.ts +20 -0
- package/dist/src/wgsl/bfs-bottom-up.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/bfs-bottom-up.wgsl.js +67 -0
- package/dist/src/wgsl/bfs-bottom-up.wgsl.js.map +1 -0
- package/dist/src/wgsl/bfs-contract.wgsl.d.ts +20 -0
- package/dist/src/wgsl/bfs-contract.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/bfs-contract.wgsl.js +55 -0
- package/dist/src/wgsl/bfs-contract.wgsl.js.map +1 -0
- package/dist/src/wgsl/bfs-fused.wgsl.d.ts +25 -0
- package/dist/src/wgsl/bfs-fused.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/bfs-fused.wgsl.js +78 -0
- package/dist/src/wgsl/bfs-fused.wgsl.js.map +1 -0
- package/dist/src/wgsl/bfs-unvisited-flags.wgsl.d.ts +18 -0
- package/dist/src/wgsl/bfs-unvisited-flags.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/bfs-unvisited-flags.wgsl.js +42 -0
- package/dist/src/wgsl/bfs-unvisited-flags.wgsl.js.map +1 -0
- package/dist/src/wgsl/closeness-reduce.wgsl.d.ts +17 -0
- package/dist/src/wgsl/closeness-reduce.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/closeness-reduce.wgsl.js +65 -0
- package/dist/src/wgsl/closeness-reduce.wgsl.js.map +1 -0
- package/dist/src/wgsl/closeness-sweep.wgsl.d.ts +20 -0
- package/dist/src/wgsl/closeness-sweep.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/closeness-sweep.wgsl.js +96 -0
- package/dist/src/wgsl/closeness-sweep.wgsl.js.map +1 -0
- package/dist/src/wgsl/compact-scatter.wgsl.d.ts +9 -0
- package/dist/src/wgsl/compact-scatter.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/compact-scatter.wgsl.js +17 -0
- package/dist/src/wgsl/compact-scatter.wgsl.js.map +1 -0
- package/dist/src/wgsl/dedupe-claim.wgsl.d.ts +10 -0
- package/dist/src/wgsl/dedupe-claim.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/dedupe-claim.wgsl.js +19 -0
- package/dist/src/wgsl/dedupe-claim.wgsl.js.map +1 -0
- package/dist/src/wgsl/dedupe-filter.wgsl.d.ts +12 -0
- package/dist/src/wgsl/dedupe-filter.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/dedupe-filter.wgsl.js +46 -0
- package/dist/src/wgsl/dedupe-filter.wgsl.js.map +1 -0
- package/dist/src/wgsl/frontier-finalize.wgsl.d.ts +53 -0
- package/dist/src/wgsl/frontier-finalize.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/frontier-finalize.wgsl.js +164 -0
- package/dist/src/wgsl/frontier-finalize.wgsl.js.map +1 -0
- package/dist/src/wgsl/sssp-pred.wgsl.d.ts +28 -0
- package/dist/src/wgsl/sssp-pred.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/sssp-pred.wgsl.js +80 -0
- package/dist/src/wgsl/sssp-pred.wgsl.js.map +1 -0
- package/dist/src/wgsl/sssp-relax.wgsl.d.ts +30 -0
- package/dist/src/wgsl/sssp-relax.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/sssp-relax.wgsl.js +72 -0
- package/dist/src/wgsl/sssp-relax.wgsl.js.map +1 -0
- package/dist/webgpu-graph-algorithms.js +3155 -384
- package/dist/webgpu-graph-algorithms.js.map +1 -1
- package/package.json +5 -4
- package/src/accelerator.ts +65 -7
- package/src/algorithms/bellman-ford.ts +387 -0
- package/src/algorithms/bfs.ts +626 -0
- package/src/algorithms/closeness.ts +395 -0
- package/src/algorithms/scope.ts +4 -1
- package/src/algorithms/sssp.ts +768 -0
- package/src/constants.ts +10 -0
- package/src/index.ts +14 -1
- package/src/kernel/prelude.ts +39 -4
- package/src/kernel/uniform-ring.ts +14 -0
- package/src/kernels.ts +447 -6
- package/src/primitives/advance.ts +131 -0
- package/src/primitives/compact.ts +323 -0
- package/src/primitives/core-shape.ts +41 -3
- package/src/primitives/frontier.ts +372 -0
- package/src/types/accelerator.ts +18 -5
- package/src/types/traversal.ts +56 -0
- package/src/wgsl/advance-expand.wgsl.ts +68 -0
- package/src/wgsl/bf-relax.wgsl.ts +57 -0
- package/src/wgsl/bfs-bitset-build.wgsl.ts +23 -0
- package/src/wgsl/bfs-bottom-up.wgsl.ts +66 -0
- package/src/wgsl/bfs-contract.wgsl.ts +54 -0
- package/src/wgsl/bfs-fused.wgsl.ts +77 -0
- package/src/wgsl/bfs-unvisited-flags.wgsl.ts +41 -0
- package/src/wgsl/closeness-reduce.wgsl.ts +64 -0
- package/src/wgsl/closeness-sweep.wgsl.ts +95 -0
- package/src/wgsl/compact-scatter.wgsl.ts +16 -0
- package/src/wgsl/dedupe-claim.wgsl.ts +18 -0
- package/src/wgsl/dedupe-filter.wgsl.ts +45 -0
- package/src/wgsl/frontier-finalize.wgsl.ts +163 -0
- package/src/wgsl/sssp-pred.wgsl.ts +79 -0
- package/src/wgsl/sssp-relax.wgsl.ts +71 -0
- 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
|
+
}
|