@graphty/webgpu-graph-algorithms 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/README.md +50 -39
  2. package/dist/browser.js +1 -1
  3. package/dist/chunks/{context-E6iKaeuJ.js → context-CRbw2Wyo.js} +178 -19
  4. package/dist/chunks/{context-E6iKaeuJ.js.map → context-CRbw2Wyo.js.map} +1 -1
  5. package/dist/node.js +1 -1
  6. package/dist/src/accelerator.d.ts +12 -7
  7. package/dist/src/accelerator.d.ts.map +1 -1
  8. package/dist/src/accelerator.js +88 -7
  9. package/dist/src/accelerator.js.map +1 -1
  10. package/dist/src/algorithms/components.d.ts +30 -0
  11. package/dist/src/algorithms/components.d.ts.map +1 -0
  12. package/dist/src/algorithms/components.js +300 -0
  13. package/dist/src/algorithms/components.js.map +1 -0
  14. package/dist/src/algorithms/pagerank.d.ts +39 -0
  15. package/dist/src/algorithms/pagerank.d.ts.map +1 -0
  16. package/dist/src/algorithms/pagerank.js +298 -0
  17. package/dist/src/algorithms/pagerank.js.map +1 -0
  18. package/dist/src/algorithms/power-iteration.d.ts +109 -0
  19. package/dist/src/algorithms/power-iteration.d.ts.map +1 -0
  20. package/dist/src/algorithms/power-iteration.js +206 -0
  21. package/dist/src/algorithms/power-iteration.js.map +1 -0
  22. package/dist/src/algorithms/scope.d.ts +26 -0
  23. package/dist/src/algorithms/scope.d.ts.map +1 -0
  24. package/dist/src/algorithms/scope.js +41 -0
  25. package/dist/src/algorithms/scope.js.map +1 -0
  26. package/dist/src/algorithms/spectral.d.ts +50 -0
  27. package/dist/src/algorithms/spectral.d.ts.map +1 -0
  28. package/dist/src/algorithms/spectral.js +247 -0
  29. package/dist/src/algorithms/spectral.js.map +1 -0
  30. package/dist/src/index.d.ts +4 -0
  31. package/dist/src/index.d.ts.map +1 -1
  32. package/dist/src/index.js +4 -0
  33. package/dist/src/index.js.map +1 -1
  34. package/dist/src/kernel/dispatch.d.ts +4 -1
  35. package/dist/src/kernel/dispatch.d.ts.map +1 -1
  36. package/dist/src/kernel/dispatch.js +12 -5
  37. package/dist/src/kernel/dispatch.js.map +1 -1
  38. package/dist/src/kernels.d.ts +20 -4
  39. package/dist/src/kernels.d.ts.map +1 -1
  40. package/dist/src/kernels.js +172 -2
  41. package/dist/src/kernels.js.map +1 -1
  42. package/dist/src/layouts/seed.d.ts +3 -1
  43. package/dist/src/layouts/seed.d.ts.map +1 -1
  44. package/dist/src/layouts/seed.js +3 -1
  45. package/dist/src/layouts/seed.js.map +1 -1
  46. package/dist/src/memory/residency.d.ts.map +1 -1
  47. package/dist/src/memory/residency.js +164 -11
  48. package/dist/src/memory/residency.js.map +1 -1
  49. package/dist/src/primitives/core-shape.d.ts +41 -0
  50. package/dist/src/primitives/core-shape.d.ts.map +1 -0
  51. package/dist/src/primitives/core-shape.js +89 -0
  52. package/dist/src/primitives/core-shape.js.map +1 -0
  53. package/dist/src/primitives/segmented-reduce.d.ts.map +1 -1
  54. package/dist/src/primitives/segmented-reduce.js +4 -30
  55. package/dist/src/primitives/segmented-reduce.js.map +1 -1
  56. package/dist/src/primitives/spmv.d.ts +56 -0
  57. package/dist/src/primitives/spmv.d.ts.map +1 -0
  58. package/dist/src/primitives/spmv.js +101 -0
  59. package/dist/src/primitives/spmv.js.map +1 -0
  60. package/dist/src/types/accelerator.d.ts +24 -31
  61. package/dist/src/types/accelerator.d.ts.map +1 -1
  62. package/dist/src/types/accelerator.js +5 -4
  63. package/dist/src/types/accelerator.js.map +1 -1
  64. package/dist/src/types/algorithms.d.ts +74 -0
  65. package/dist/src/types/algorithms.d.ts.map +1 -0
  66. package/dist/src/types/algorithms.js +18 -0
  67. package/dist/src/types/algorithms.js.map +1 -0
  68. package/dist/src/types/layout.d.ts +2 -2
  69. package/dist/src/types/layout.d.ts.map +1 -1
  70. package/dist/src/types/layout.js +1 -1
  71. package/dist/src/types/options.d.ts +8 -49
  72. package/dist/src/types/options.d.ts.map +1 -1
  73. package/dist/src/types/options.js +5 -3
  74. package/dist/src/types/options.js.map +1 -1
  75. package/dist/src/wgsl/pr-finalize.wgsl.d.ts +11 -0
  76. package/dist/src/wgsl/pr-finalize.wgsl.d.ts.map +1 -0
  77. package/dist/src/wgsl/pr-finalize.wgsl.js +36 -0
  78. package/dist/src/wgsl/pr-finalize.wgsl.js.map +1 -0
  79. package/dist/src/wgsl/pr-scale.wgsl.d.ts +14 -0
  80. package/dist/src/wgsl/pr-scale.wgsl.d.ts.map +1 -0
  81. package/dist/src/wgsl/pr-scale.wgsl.js +48 -0
  82. package/dist/src/wgsl/pr-scale.wgsl.js.map +1 -0
  83. package/dist/src/wgsl/spmv-pull.wgsl.d.ts +15 -0
  84. package/dist/src/wgsl/spmv-pull.wgsl.d.ts.map +1 -0
  85. package/dist/src/wgsl/spmv-pull.wgsl.js +47 -0
  86. package/dist/src/wgsl/spmv-pull.wgsl.js.map +1 -0
  87. package/dist/src/wgsl/wcc-compress.wgsl.d.ts +9 -0
  88. package/dist/src/wgsl/wcc-compress.wgsl.d.ts.map +1 -0
  89. package/dist/src/wgsl/wcc-compress.wgsl.js +26 -0
  90. package/dist/src/wgsl/wcc-compress.wgsl.js.map +1 -0
  91. package/dist/src/wgsl/wcc-link-edges.wgsl.d.ts +13 -0
  92. package/dist/src/wgsl/wcc-link-edges.wgsl.d.ts.map +1 -0
  93. package/dist/src/wgsl/wcc-link-edges.wgsl.js +46 -0
  94. package/dist/src/wgsl/wcc-link-edges.wgsl.js.map +1 -0
  95. package/dist/src/wgsl/wcc-link-sample.wgsl.d.ts +11 -0
  96. package/dist/src/wgsl/wcc-link-sample.wgsl.d.ts.map +1 -0
  97. package/dist/src/wgsl/wcc-link-sample.wgsl.js +45 -0
  98. package/dist/src/wgsl/wcc-link-sample.wgsl.js.map +1 -0
  99. package/dist/src/wgsl/wcc-sample.wgsl.d.ts +10 -0
  100. package/dist/src/wgsl/wcc-sample.wgsl.d.ts.map +1 -0
  101. package/dist/src/wgsl/wcc-sample.wgsl.js +18 -0
  102. package/dist/src/wgsl/wcc-sample.wgsl.js.map +1 -0
  103. package/dist/tsconfig.build.tsbuildinfo +1 -1
  104. package/dist/webgpu-graph-algorithms.js +1550 -29
  105. package/dist/webgpu-graph-algorithms.js.map +1 -1
  106. package/package.json +5 -4
  107. package/src/accelerator.ts +104 -8
  108. package/src/algorithms/components.ts +348 -0
  109. package/src/algorithms/pagerank.ts +343 -0
  110. package/src/algorithms/power-iteration.ts +278 -0
  111. package/src/algorithms/scope.ts +52 -0
  112. package/src/algorithms/spectral.ts +300 -0
  113. package/src/index.ts +20 -1
  114. package/src/kernel/dispatch.ts +12 -5
  115. package/src/kernels.ts +206 -5
  116. package/src/layouts/seed.ts +3 -1
  117. package/src/memory/residency.ts +200 -11
  118. package/src/primitives/core-shape.ts +103 -0
  119. package/src/primitives/segmented-reduce.ts +4 -36
  120. package/src/primitives/spmv.ts +155 -0
  121. package/src/types/accelerator.ts +40 -32
  122. package/src/types/algorithms.ts +83 -0
  123. package/src/types/layout.ts +2 -2
  124. package/src/types/options.ts +22 -53
  125. package/src/wgsl/pr-finalize.wgsl.ts +36 -0
  126. package/src/wgsl/pr-scale.wgsl.ts +48 -0
  127. package/src/wgsl/spmv-pull.wgsl.ts +47 -0
  128. package/src/wgsl/wcc-compress.wgsl.ts +26 -0
  129. package/src/wgsl/wcc-link-edges.wgsl.ts +46 -0
  130. package/src/wgsl/wcc-link-sample.wgsl.ts +45 -0
  131. package/src/wgsl/wcc-sample.wgsl.ts +18 -0
@@ -20,6 +20,7 @@ import {
20
20
  type TypedArrayData,
21
21
  } from "@graphty/graph-format";
22
22
 
23
+ import { STORAGE_ALIGN } from "../constants.js";
23
24
  import { type AllocationTracker } from "../device/error-scope.js";
24
25
  import { BufferUsage } from "../device/webgpu-constants.js";
25
26
  import { WebGpuGraphError } from "../errors.js";
@@ -140,6 +141,16 @@ interface ResidencyRecord {
140
141
  readonly segments: Readonly<Record<CoreArrayName, ArenaSegmentBinding | null>>;
141
142
  } | null;
142
143
  readonly bindings: Map<CoreArrayName, Binding>;
144
+ /** Memo of the P7 views by `name` or `name + ":packed"`, so a second view() call uploads nothing (spec 4.3). */
145
+ readonly views: Map<string, ViewBinding>;
146
+ /**
147
+ * One fresh marker object per PACKED view, keyed the same way as `views`. It is the `key` argument of
148
+ * upload(): a packed view must never be keyed on one of the snapshot's own arrays, because upload()
149
+ * memoises on the key object and does NOT compare byte lengths, and `s.reverse()` is cached
150
+ * (graph-format `graph-snapshot.ts:600-601`) so the packed and the unpacked builder see the same
151
+ * `rev.rowPtr` object.
152
+ */
153
+ readonly packKeys: Map<string, object>;
143
154
  released: boolean;
144
155
  }
145
156
 
@@ -345,13 +356,15 @@ export class GraphResidency {
345
356
  }
346
357
 
347
358
  /**
348
- * Uploads (or finds) a view; P1-P3 support outDegree / inDegree / degreeOrder / reverseDegreeOrder; the others
349
- * -> E_UNSUPPORTED until P7. packViews is accepted and ignored until P7 (a `true` request is E_UNSUPPORTED
350
- * { option: "packViews" }, PLAN DECISION 9).
359
+ * Uploads (or finds) a view: outDegree / inDegree / degreeOrder / reverseDegreeOrder upload one array each;
360
+ * reverse and edgeList (P7) upload their arrays perArray, never into the arena, and are memoised per record so
361
+ * a second call uploads nothing (spec 4.3). coo and mate -> E_UNSUPPORTED until P11. packViews concatenates a
362
+ * reverse or edgeList view into ONE buffer at STORAGE_ALIGN offsets; on any other view `true` is E_UNSUPPORTED
363
+ * { option: "packViews" }.
351
364
  * @param s - the snapshot
352
365
  * @param name - the view
353
366
  * @param options - view options
354
- * @param options.packViews - P7's packed reverse upload; `true` is E_UNSUPPORTED until then
367
+ * @param options.packViews - pack the reverse / edgeList arrays into one buffer (a no-op on an undirected reverse)
355
368
  * @returns the view binding
356
369
  */
357
370
  view(
@@ -361,10 +374,25 @@ export class GraphResidency {
361
374
  ): ViewBinding {
362
375
  this.assertLive();
363
376
  assertAttached(s);
364
- if (options?.packViews === true) {
365
- throw new WebGpuGraphError("E_UNSUPPORTED", "packViews is not supported before the P7 view uploads", {
377
+ const packed = options?.packViews === true;
378
+ if (name === "reverse" || name === "edgeList") {
379
+ this.assertNotReleased(s);
380
+ this.assertNonEmpty(s);
381
+ const memoKey = packed ? `${name}:packed` : name;
382
+ const record = this.ensureRecord(s);
383
+ const memo = record.views.get(memoKey);
384
+ if (memo !== undefined) {
385
+ return memo;
386
+ }
387
+ const built =
388
+ name === "reverse" ? this.buildReverse(s, record, packed) : this.buildEdgeList(s, record, packed);
389
+ record.views.set(memoKey, built);
390
+ return built;
391
+ }
392
+ if (packed) {
393
+ throw new WebGpuGraphError("E_UNSUPPORTED", "packViews applies to the reverse and edgeList views only", {
366
394
  option: "packViews",
367
- hint: "omit packViews; the four reverse arrays are uploaded separately",
395
+ hint: `the ${name} view uploads one array`,
368
396
  });
369
397
  }
370
398
  let array: TypedArrayData;
@@ -401,13 +429,11 @@ export class GraphResidency {
401
429
  scalars = { segmentOffsets: Array.from(order.segmentOffsets) };
402
430
  break;
403
431
  }
404
- case "reverse":
405
432
  case "coo":
406
- case "edgeList":
407
433
  case "mate":
408
- throw new WebGpuGraphError("E_UNSUPPORTED", `the ${name} view is not uploaded before P7`, {
434
+ throw new WebGpuGraphError("E_UNSUPPORTED", `the ${name} view is not uploaded before P11`, {
409
435
  feature: `view:${name}`,
410
- hint: "P1-P3 upload outDegree, inDegree, degreeOrder and reverseDegreeOrder",
436
+ hint: "outDegree, inDegree, degreeOrder, reverseDegreeOrder, reverse and edgeList are uploaded",
411
437
  });
412
438
  default:
413
439
  throw invalid("name", name, "a view name");
@@ -422,6 +448,165 @@ export class GraphResidency {
422
448
  });
423
449
  }
424
450
 
451
+
452
+ /**
453
+ * The reverse adjacency's bindings. On an UNDIRECTED snapshot graph-format invariant I7 makes reverse() return
454
+ * the FORWARD arrays, so the core bindings ARE the reverse bindings and nothing is uploaded -- delegating to
455
+ * core() rather than re-uploading the arrays is what makes that true on the arena plan as well, where the
456
+ * per-array WeakMap key resolves to the whole-arena resident (spec 4.3 lines 1182-1186). `fwdArc` is never
457
+ * uploaded (spec 13 row P7). `packed` is ignored when undirected: there is nothing to pack.
458
+ * @param s - the snapshot
459
+ * @param record - its residency record
460
+ * @param packed - concatenate the arrays into one buffer (directed only)
461
+ * @returns the view binding
462
+ */
463
+ private buildReverse(s: GraphSnapshot, record: ResidencyRecord, packed: boolean): ViewBinding {
464
+ const rev = s.reverse();
465
+ if (!s.directed) {
466
+ const core = this.core(s, ["rowPtr", "colIdx", "weights"]);
467
+ const bindings: Record<string, Binding> = { rowPtr: core.rowPtr };
468
+ if (core.colIdx !== null) {
469
+ bindings.colIdx = core.colIdx;
470
+ }
471
+ if (core.weights !== null) {
472
+ bindings.weights = core.weights;
473
+ }
474
+ return Object.freeze({
475
+ view: "reverse",
476
+ bindings: Object.freeze(bindings),
477
+ scalars: Object.freeze({ arcCount: [rev.arcCount], directed: [0] }),
478
+ });
479
+ }
480
+ const arrays: [string, TypedArrayData][] = [
481
+ ["rowPtr", rev.rowPtr],
482
+ ["colIdx", rev.colIdx],
483
+ ];
484
+ if (rev.weights !== null) {
485
+ arrays.push(["weights", rev.weights]);
486
+ }
487
+ const bindings = packed
488
+ ? this.packArrays(
489
+ record,
490
+ arrays,
491
+ this.packKey(record, "reverse:packed"),
492
+ `residency:view:${record.serial}:reverse:packed`,
493
+ )
494
+ : this.separateArrays(record, arrays, `residency:view:${record.serial}:reverse`);
495
+ return Object.freeze({
496
+ view: "reverse",
497
+ bindings,
498
+ scalars: Object.freeze({ arcCount: [rev.arcCount], directed: [1] }),
499
+ });
500
+ }
501
+
502
+ /**
503
+ * The each-edge-once binding an edge-parallel kernel uses on directed and undirected snapshots alike (spec 8.1
504
+ * row 2); `arc` is not uploaded (no kernel of P7 reads it).
505
+ * @param s - the snapshot
506
+ * @param record - its residency record
507
+ * @param packed - concatenate into one buffer
508
+ * @returns the view binding
509
+ */
510
+ private buildEdgeList(s: GraphSnapshot, record: ResidencyRecord, packed: boolean): ViewBinding {
511
+ const list = s.edgeList();
512
+ const arrays: [string, TypedArrayData][] = [
513
+ ["src", list.src],
514
+ ["dst", list.dst],
515
+ ];
516
+ if (list.weights !== null) {
517
+ arrays.push(["weights", list.weights]);
518
+ }
519
+ const bindings = packed
520
+ ? this.packArrays(
521
+ record,
522
+ arrays,
523
+ this.packKey(record, "edgeList:packed"),
524
+ `residency:view:${record.serial}:edgeList:packed`,
525
+ )
526
+ : this.separateArrays(record, arrays, `residency:view:${record.serial}:edgeList`);
527
+ return Object.freeze({
528
+ view: "edgeList",
529
+ bindings,
530
+ scalars: Object.freeze({ edgeCount: [s.edgeCount] }),
531
+ });
532
+ }
533
+
534
+ /**
535
+ * One resident per array (spec 4.3: views upload in perArray mode, never into the arena).
536
+ * @param record - the owning record
537
+ * @param arrays - the named arrays
538
+ * @param label - the buffer label prefix
539
+ * @returns the bindings by name
540
+ */
541
+ private separateArrays(
542
+ record: ResidencyRecord,
543
+ arrays: readonly (readonly [string, TypedArrayData])[],
544
+ label: string,
545
+ ): Readonly<Record<string, Binding>> {
546
+ const bindings: Record<string, Binding> = {};
547
+ for (const [name, array] of arrays) {
548
+ const resident = this.upload(record, array, array, `${label}:${name}`);
549
+ bindings[name] = { buffer: resident.buffer, offset: 0, size: resident.byteLength, window: null };
550
+ }
551
+ return Object.freeze(bindings);
552
+ }
553
+
554
+ /**
555
+ * The arrays concatenated into ONE buffer at STORAGE_ALIGN-aligned offsets (spec 4.3 packViews): one
556
+ * createBuffer and one writeBuffer instead of three of each, which is what the option buys. The resident is
557
+ * keyed on `key`, a marker object owned by the record (packKey), NOT on one of the snapshot's arrays: upload()
558
+ * returns an existing resident whenever the key matches and the serial matches, without comparing byte
559
+ * lengths, so keying the packed buffer on `rev.rowPtr` would make the packed and the unpacked view of one
560
+ * snapshot collide -- whichever was built second would get the other's buffer. The record still owns the
561
+ * resident, so release(s) destroys it with the rest. Offsets are STORAGE_ALIGN-aligned because Kernel.bind
562
+ * rejects any other offset synchronously (E_INVALID_ARGUMENT { argument: "offset" }).
563
+ * @param record - the owning record
564
+ * @param arrays - the named arrays, in buffer order
565
+ * @param key - the marker object the resident is keyed on
566
+ * @param label - the buffer label
567
+ * @returns the bindings by name, all into the one buffer
568
+ */
569
+ private packArrays(
570
+ record: ResidencyRecord,
571
+ arrays: readonly (readonly [string, TypedArrayData])[],
572
+ key: object,
573
+ label: string,
574
+ ): Readonly<Record<string, Binding>> {
575
+ const offsets: number[] = [];
576
+ let total = 0;
577
+ for (const [, array] of arrays) {
578
+ offsets.push(total);
579
+ total += Math.ceil(array.byteLength / STORAGE_ALIGN) * STORAGE_ALIGN;
580
+ }
581
+ const staging = new Uint8Array(total);
582
+ arrays.forEach(([, array], i) => {
583
+ staging.set(new Uint8Array(array.buffer, array.byteOffset, array.byteLength), offsets[i]);
584
+ });
585
+ const resident = this.upload(record, key, staging, label);
586
+ const bindings: Record<string, Binding> = {};
587
+ arrays.forEach(([name, array], i) => {
588
+ bindings[name] = { buffer: resident.buffer, offset: offsets[i], size: array.byteLength, window: null };
589
+ });
590
+ return Object.freeze(bindings);
591
+ }
592
+
593
+ /**
594
+ * The upload key of a packed view: a marker object allocated once per record and memo name, never one of the
595
+ * snapshot's arrays.
596
+ * @param record - the owning record
597
+ * @param memoKey - the `views` memo key of this packed view
598
+ * @returns the stable marker object
599
+ */
600
+ private packKey(record: ResidencyRecord, memoKey: string): object {
601
+ const existing = record.packKeys.get(memoKey);
602
+ if (existing !== undefined) {
603
+ return existing;
604
+ }
605
+ const created = {};
606
+ record.packKeys.set(memoKey, created);
607
+ return created;
608
+ }
609
+
425
610
  /**
426
611
  * gpuView(name) + column.version; re-uploads in place when the version changed and the byte length did not
427
612
  * (spec 4.3). CONTRACT DECISION: `owner` is required so release(owner) can find the buffer (a table has no
@@ -679,6 +864,8 @@ export class GraphResidency {
679
864
  private forget(record: ResidencyRecord): void {
680
865
  record.released = true;
681
866
  record.bindings.clear();
867
+ record.views.clear();
868
+ record.packKeys.clear();
682
869
  record.arena = null;
683
870
  record.plan = null;
684
871
  this.bySerial.delete(record.serial);
@@ -705,6 +892,8 @@ export class GraphResidency {
705
892
  plan: null,
706
893
  arena: null,
707
894
  bindings: new Map<CoreArrayName, Binding>(),
895
+ views: new Map<string, ViewBinding>(),
896
+ packKeys: new Map<string, object>(),
708
897
  released: false,
709
898
  };
710
899
  this.bySerial.set(s.serial, record);
@@ -0,0 +1,103 @@
1
+ /**
2
+ * The shape helpers every core-walking primitive shares (spec 4.1): the row count and the arc count a CoreBinding
3
+ * implies, the windowed rejection of P4, and the ViewBinding -> CoreBinding adapter the P7 pull kernels need.
4
+ * Moved out of segmented-reduce.ts by M8b-T4 so spmv.ts can use them without duplicating them; the `primitive`
5
+ * argument keeps each caller's error details byte-identical to what they were when the helpers were private.
6
+ */
7
+
8
+ import { WebGpuGraphError } from "../errors.js";
9
+ import { type CoreBinding, type ViewBinding } from "../memory/residency.js";
10
+ import { type Binding } from "../types/memory.js";
11
+
12
+ /**
13
+ * The row count of a core from its rowPtr binding (4(n + 1) bytes).
14
+ * @param core - the core
15
+ * @param primitive - the caller's name, used in the message and the detail
16
+ * @returns n
17
+ */
18
+ export function rowCountOf(core: CoreBinding, primitive: string): number {
19
+ const bytes = core.rowPtr.size;
20
+ if (bytes < 4 || bytes % 4 !== 0) {
21
+ throw new WebGpuGraphError(
22
+ "E_INVALID_ARGUMENT",
23
+ `${primitive}: a rowPtr binding of ${bytes} bytes is not 4(n + 1)`,
24
+ {
25
+ argument: "core.rowPtr",
26
+ value: bytes,
27
+ expected: "a positive multiple of 4",
28
+ },
29
+ );
30
+ }
31
+ return bytes / 4 - 1;
32
+ }
33
+
34
+ /**
35
+ * The arc count a core implies: the colIdx binding is 4 bytes per arc, and a null colIdx is an arc-less snapshot.
36
+ * @param core - the core
37
+ * @returns the arc count
38
+ */
39
+ export function arcCountOf(core: CoreBinding): number {
40
+ return core.colIdx === null ? 0 : core.colIdx.size / 4;
41
+ }
42
+
43
+ /**
44
+ * Rejects a windowed core (executed at P4).
45
+ * @param core - the core
46
+ * @param primitive - the caller's name, used in the message and the feature detail
47
+ */
48
+ export function assertNotWindowed(core: CoreBinding, primitive: string): void {
49
+ if (core.plan === "windowed" || core.windows !== null) {
50
+ throw new WebGpuGraphError("E_UNSUPPORTED", `${primitive}: windowed cores are executed at P4`, {
51
+ feature: `${primitive}.windowed`,
52
+ });
53
+ }
54
+ }
55
+
56
+ /**
57
+ * The CoreBinding shape of a residency VIEW (spec 4.3). residency.view() returns a ViewBinding -- a name -> Binding
58
+ * record -- while graphBindings / graphOverrides and every core-walking primitive read rowPtr / colIdx / weights as
59
+ * top-level fields (src/kernels.ts graphBindings / graphOverrides). This is the only adapter between the two in the
60
+ * package. `plan` is "perArray" because views always upload per array (spec 4.3 lines 1182-1186) and `windows` is
61
+ * null because a view is never windowed, which is what makes assertNotWindowed pass for a view. `serial` is -1: a
62
+ * view is not a core and no caller of this function reads serial (nothing in src/primitives or src/algorithms
63
+ * reads CoreBinding.serial).
64
+ * @param v - the view binding, from residency.view(s, "reverse") or view(s, "edgeList")
65
+ * @param arcCount - the arc count of the view, from v.scalars.arcCount[0]; it must agree with the colIdx binding,
66
+ * which is what record() derives the arc window from (E_INVALID_ARGUMENT { argument: "arcCount" } otherwise)
67
+ * @returns the equivalent CoreBinding
68
+ */
69
+ export function coreOfView(v: ViewBinding, arcCount: number): CoreBinding {
70
+ const { rowPtr }: { readonly rowPtr?: Binding | undefined } = v.bindings;
71
+ if (rowPtr === undefined) {
72
+ throw new WebGpuGraphError("E_INVALID_ARGUMENT", `the ${v.view} view has no rowPtr binding`, {
73
+ argument: "view",
74
+ value: v.view,
75
+ expected: "a view with a rowPtr binding (reverse)",
76
+ });
77
+ }
78
+ const colIdx = v.bindings.colIdx ?? null;
79
+ const weights = v.bindings.weights ?? null;
80
+ const bound = colIdx === null ? 0 : colIdx.size / 4;
81
+ if (bound !== arcCount) {
82
+ throw new WebGpuGraphError(
83
+ "E_INVALID_ARGUMENT",
84
+ `the ${v.view} view binds ${bound} arcs but its arcCount scalar says ${arcCount}`,
85
+ {
86
+ argument: "arcCount",
87
+ value: arcCount,
88
+ expected: bound,
89
+ },
90
+ );
91
+ }
92
+ return Object.freeze({
93
+ serial: -1,
94
+ plan: "perArray" as const,
95
+ rowPtr,
96
+ colIdx,
97
+ weights,
98
+ arcToEdge: null,
99
+ edgeToArc: null,
100
+ windows: null,
101
+ hasWeights: weights !== null,
102
+ });
103
+ }
@@ -14,6 +14,7 @@ import { type Kernel } from "../kernel/kernel.js";
14
14
  import { graphBindings, graphOverrides, kernelSpec, RANGE_PARAMS } from "../kernels.js";
15
15
  import { type CoreBinding } from "../memory/residency.js";
16
16
  import { type Binding } from "../types/memory.js";
17
+ import { assertNotWindowed, rowCountOf } from "./core-shape.js";
17
18
  import { type ReduceOp, type ReduceScope } from "./reduce.js";
18
19
 
19
20
  /** The degree tiers of degreeOrder(): the permutation binding and the CPU-side segmentOffsets [0, hiEnd, midEnd, lowEnd, n]. */
@@ -140,39 +141,6 @@ function validateValueSnippet(snippet: string): void {
140
141
  }
141
142
  }
142
143
 
143
- /**
144
- * The row count of a core from its rowPtr binding (4(n + 1) bytes).
145
- * @param core - the core
146
- * @returns n
147
- */
148
- function rowCountOf(core: CoreBinding): number {
149
- const bytes = core.rowPtr.size;
150
- if (bytes < 4 || bytes % 4 !== 0) {
151
- throw new WebGpuGraphError(
152
- "E_INVALID_ARGUMENT",
153
- `segmentedReduce: a rowPtr binding of ${bytes} bytes is not 4(n + 1)`,
154
- {
155
- argument: "core.rowPtr",
156
- value: bytes,
157
- expected: "a positive multiple of 4",
158
- },
159
- );
160
- }
161
- return bytes / 4 - 1;
162
- }
163
-
164
- /**
165
- * Rejects a windowed core (executed at P4).
166
- * @param core - the core
167
- */
168
- function assertNotWindowed(core: CoreBinding): void {
169
- if (core.plan === "windowed" || core.windows !== null) {
170
- throw new WebGpuGraphError("E_UNSUPPORTED", "segmentedReduce: windowed cores are executed at P4", {
171
- feature: "segmentedReduce.windowed",
172
- });
173
- }
174
- }
175
-
176
144
  /** The thread-per-row planner: ONE `segmented-reduce` dispatch with TIER 0 over every row. */
177
145
  class ThreadPerRowPlanner implements SegmentedReducePlanner {
178
146
  private readonly scope: ReduceScope;
@@ -202,7 +170,7 @@ class ThreadPerRowPlanner implements SegmentedReducePlanner {
202
170
  * @param out - at least 4n bytes of f32
203
171
  */
204
172
  record(pass: GPUComputePassEncoder, core: CoreBinding, out: Binding): void {
205
- assertNotWindowed(core);
173
+ assertNotWindowed(core, "segmentedReduce");
206
174
  if ((core.weights !== null) !== this.hasWeights) {
207
175
  throw new WebGpuGraphError(
208
176
  "E_INVALID_ARGUMENT",
@@ -214,7 +182,7 @@ class ThreadPerRowPlanner implements SegmentedReducePlanner {
214
182
  },
215
183
  );
216
184
  }
217
- const n = rowCountOf(core);
185
+ const n = rowCountOf(core, "segmentedReduce");
218
186
  if (n === 0) {
219
187
  return;
220
188
  }
@@ -260,7 +228,7 @@ export async function prepareSegmentedReduce(
260
228
  feature: "segmentedReduce.tiers",
261
229
  });
262
230
  }
263
- assertNotWindowed(core);
231
+ assertNotWindowed(core, "segmentedReduce");
264
232
  const op = opCode(options.op);
265
233
  validateValueSnippet(options.valueSnippet);
266
234
  const overrides = { ...graphOverrides(core, null), OP: op, TIER: 0 };
@@ -0,0 +1,155 @@
1
+ /**
2
+ * The pull SpMV primitive of spec 6 row 9 / 8.2 in its thread-per-row tier (P7; M8b plan PD-1 / PD-2): one
3
+ * grid-stride dispatch of the `spmv-pull` module over the rows of a REVERSE adjacency, each invocation folding
4
+ * `weight * xNorm[nbr]` over its row's in-arcs (Kahan-compensated, f32) and writing
5
+ * `rankOut[v] = beta * pv + alpha * (sum + danglingMass * pv)`, where `pv` is `personalization[v]` under
6
+ * HAS_PERSONALIZATION and the uniform `P.uniformP` otherwise, and `danglingMass` is `partials[0].danglingMass`
7
+ * under USE_DANGLING. PageRank sets alpha to the damping, beta to 1 - alpha and USE_DANGLING; HITS and eigenvector
8
+ * set alpha 1, beta 0, uniformP 0; Katz sets alpha to the attenuation, beta to its constant and uniformP 1.
9
+ *
10
+ * It is its own registry entry rather than a `segmentedReduce` VALUE snippet (PD-1): the snippet vocabulary is
11
+ * `row, arc, nbr, weight, v` and cannot read `xNorm[nbr]`, and the design's binding table gives the kernel eight
12
+ * storage bindings of its own. The in-degree tiers of `reverseDegreeOrder()` land at P4 (PD-2): `tiers !== null` is
13
+ * E_UNSUPPORTED and USE_PERM is always false (the perm slot carries the rowPtr dummy of graphBindings). The row and
14
+ * arc counts come from the core's binding sizes exactly as segmentedReduce derives them.
15
+ */
16
+
17
+ import { WebGpuGraphError } from "../errors.js";
18
+ import { planGridStride } from "../kernel/dispatch.js";
19
+ import { type Kernel } from "../kernel/kernel.js";
20
+ import { graphBindings, graphOverrides, kernelSpec, SPMV_PARAMS } from "../kernels.js";
21
+ import { type CoreBinding } from "../memory/residency.js";
22
+ import { type Binding } from "../types/memory.js";
23
+ import { arcCountOf, assertNotWindowed, rowCountOf } from "./core-shape.js";
24
+ import { type ReduceScope } from "./reduce.js";
25
+ import { type DegreeTiers } from "./segmented-reduce.js";
26
+
27
+ /** The group-1 bindings of one pull: the pre-scaled input, the output, the personalization (null binds xNorm as the dummy) and the PrPartial block whose header carries danglingMass. */
28
+ export interface SpmvResources {
29
+ readonly xNorm: Binding;
30
+ readonly rankOut: Binding;
31
+ readonly personalization: Binding | null;
32
+ readonly partials: Binding;
33
+ }
34
+
35
+ /** The scalar coefficients of one pull (SpmvParams alpha / beta / uniformP). */
36
+ export interface SpmvCoefficients {
37
+ readonly alpha: number;
38
+ readonly beta: number;
39
+ readonly uniformP: number;
40
+ }
41
+
42
+ /** Options of prepareSpmvPull: the two variant flags, the weights binding and the tiers (must be null at P7). */
43
+ export interface SpmvPullOptions {
44
+ readonly personalization: boolean;
45
+ readonly dangling: boolean;
46
+ /** The weights binding to fold with: `undefined` takes the core's, `null` runs UNWEIGHTED on a weighted core. */
47
+ readonly weights?: Binding | null | undefined;
48
+ readonly tiers: DegreeTiers | null;
49
+ }
50
+
51
+ /** A prepared pull: records ONE grid-stride dispatch over the rows of a reverse core into a pass. */
52
+ export interface SpmvPullPlanner {
53
+ /** Records the dispatch over rows [0, n) of `rev` writing rankOut[v] (f32) per row; nothing for n = 0. */
54
+ record(pass: GPUComputePassEncoder, rev: CoreBinding, resources: SpmvResources, coefficients: SpmvCoefficients): void;
55
+ /** Dispatches the last record() issued (1, or 0 for n = 0). */
56
+ readonly lastDispatches: number;
57
+ }
58
+
59
+ /** The thread-per-row planner: ONE `spmv-pull` dispatch, grid-stride over every row. */
60
+ class SpmvPullPlannerImpl implements SpmvPullPlanner {
61
+ private readonly scope: ReduceScope;
62
+ private readonly kernel: Kernel;
63
+ private readonly weights: Binding | null | undefined;
64
+ private dispatches = 0;
65
+
66
+ /**
67
+ * Wraps a compiled pipeline with the weights choice it was compiled for.
68
+ * @param scope - the scope the pipeline was prepared in
69
+ * @param kernel - the compiled kernel
70
+ * @param weights - the weights option the pipeline's HAS_WEIGHTS was derived from; record() binds the same way
71
+ */
72
+ constructor(scope: ReduceScope, kernel: Kernel, weights: Binding | null | undefined) {
73
+ this.scope = scope;
74
+ this.kernel = kernel;
75
+ this.weights = weights;
76
+ }
77
+
78
+ /**
79
+ * Dispatches the last record() issued.
80
+ * @returns 1, or 0 when the last record covered no rows
81
+ */
82
+ get lastDispatches(): number {
83
+ return this.dispatches;
84
+ }
85
+
86
+ /**
87
+ * Records the dispatch: rows [0, n), arcs [0, arcCount), planGridStride(n); nothing for n = 0 (no zero-length
88
+ * binding is ever created). `personalization ?? xNorm` follows the group-0 dummy rule: both slots are
89
+ * storage-ro, so the aliasing check of Kernel.bind does not fire, and HAS_PERSONALIZATION false never reads it.
90
+ * @param pass - the pass to record into
91
+ * @param rev - the reverse core (any snapshot with the weights pattern the planner was prepared for)
92
+ * @param resources - xNorm, rankOut, personalization, partials
93
+ * @param coefficients - alpha, beta, uniformP
94
+ */
95
+ record(
96
+ pass: GPUComputePassEncoder,
97
+ rev: CoreBinding,
98
+ resources: SpmvResources,
99
+ coefficients: SpmvCoefficients,
100
+ ): void {
101
+ const n = rowCountOf(rev, "spmvPull");
102
+ const plan = planGridStride(n, this.scope.workgroupSize, this.scope.caps);
103
+ if (plan.x === 0) {
104
+ this.dispatches = 0;
105
+ return;
106
+ }
107
+ const params = this.scope.params(SPMV_PARAMS, {
108
+ n,
109
+ arcBase: 0,
110
+ arcEnd: arcCountOf(rev),
111
+ stride: plan.stride ?? n,
112
+ alpha: coefficients.alpha,
113
+ beta: coefficients.beta,
114
+ uniformP: coefficients.uniformP,
115
+ pad0: 0,
116
+ });
117
+ const bound = this.kernel.bind({
118
+ ...graphBindings(rev, null, this.weights),
119
+ xNorm: resources.xNorm,
120
+ rankOut: resources.rankOut,
121
+ personalization: resources.personalization ?? resources.xNorm,
122
+ partials: resources.partials,
123
+ P: params.binding,
124
+ });
125
+ this.kernel.dispatch(pass, bound, plan, [params.offset]);
126
+ this.dispatches = 1;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Prepares the thread-per-row pull pipeline for a reverse core's weights pattern and the two variant flags.
132
+ * @param scope - the reduce scope (pipelines, params writer)
133
+ * @param rev - the reverse core whose weights pattern selects HAS_WEIGHTS (USE_PERM is false: no tiers at P7)
134
+ * @param options - personalization, dangling, weights, tiers (must be null)
135
+ * @returns the planner
136
+ */
137
+ export async function prepareSpmvPull(
138
+ scope: ReduceScope,
139
+ rev: CoreBinding,
140
+ options: SpmvPullOptions,
141
+ ): Promise<SpmvPullPlanner> {
142
+ if (options.tiers !== null) {
143
+ throw new WebGpuGraphError("E_UNSUPPORTED", "spmvPull: the in-degree tiers land at P4; pass tiers: null", {
144
+ feature: "spmvPull.tiers",
145
+ });
146
+ }
147
+ assertNotWindowed(rev, "spmvPull");
148
+ const spec = kernelSpec("spmv-pull", {
149
+ ...graphOverrides(rev, null, options.weights),
150
+ HAS_PERSONALIZATION: options.personalization,
151
+ USE_DANGLING: options.dangling,
152
+ });
153
+ const kernel = await scope.pipelines.kernel(spec);
154
+ return new SpmvPullPlannerImpl(scope, kernel, options.weights);
155
+ }