@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.
- package/README.md +50 -39
- package/dist/browser.js +1 -1
- package/dist/chunks/{context-E6iKaeuJ.js → context-CRbw2Wyo.js} +178 -19
- package/dist/chunks/{context-E6iKaeuJ.js.map → context-CRbw2Wyo.js.map} +1 -1
- package/dist/node.js +1 -1
- package/dist/src/accelerator.d.ts +12 -7
- package/dist/src/accelerator.d.ts.map +1 -1
- package/dist/src/accelerator.js +88 -7
- package/dist/src/accelerator.js.map +1 -1
- package/dist/src/algorithms/components.d.ts +30 -0
- package/dist/src/algorithms/components.d.ts.map +1 -0
- package/dist/src/algorithms/components.js +300 -0
- package/dist/src/algorithms/components.js.map +1 -0
- package/dist/src/algorithms/pagerank.d.ts +39 -0
- package/dist/src/algorithms/pagerank.d.ts.map +1 -0
- package/dist/src/algorithms/pagerank.js +298 -0
- package/dist/src/algorithms/pagerank.js.map +1 -0
- package/dist/src/algorithms/power-iteration.d.ts +109 -0
- package/dist/src/algorithms/power-iteration.d.ts.map +1 -0
- package/dist/src/algorithms/power-iteration.js +206 -0
- package/dist/src/algorithms/power-iteration.js.map +1 -0
- package/dist/src/algorithms/scope.d.ts +26 -0
- package/dist/src/algorithms/scope.d.ts.map +1 -0
- package/dist/src/algorithms/scope.js +41 -0
- package/dist/src/algorithms/scope.js.map +1 -0
- package/dist/src/algorithms/spectral.d.ts +50 -0
- package/dist/src/algorithms/spectral.d.ts.map +1 -0
- package/dist/src/algorithms/spectral.js +247 -0
- package/dist/src/algorithms/spectral.js.map +1 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/kernel/dispatch.d.ts +4 -1
- package/dist/src/kernel/dispatch.d.ts.map +1 -1
- package/dist/src/kernel/dispatch.js +12 -5
- package/dist/src/kernel/dispatch.js.map +1 -1
- package/dist/src/kernels.d.ts +20 -4
- package/dist/src/kernels.d.ts.map +1 -1
- package/dist/src/kernels.js +172 -2
- package/dist/src/kernels.js.map +1 -1
- package/dist/src/layouts/seed.d.ts +3 -1
- package/dist/src/layouts/seed.d.ts.map +1 -1
- package/dist/src/layouts/seed.js +3 -1
- package/dist/src/layouts/seed.js.map +1 -1
- package/dist/src/memory/residency.d.ts.map +1 -1
- package/dist/src/memory/residency.js +164 -11
- package/dist/src/memory/residency.js.map +1 -1
- package/dist/src/primitives/core-shape.d.ts +41 -0
- package/dist/src/primitives/core-shape.d.ts.map +1 -0
- package/dist/src/primitives/core-shape.js +89 -0
- package/dist/src/primitives/core-shape.js.map +1 -0
- package/dist/src/primitives/segmented-reduce.d.ts.map +1 -1
- package/dist/src/primitives/segmented-reduce.js +4 -30
- package/dist/src/primitives/segmented-reduce.js.map +1 -1
- package/dist/src/primitives/spmv.d.ts +56 -0
- package/dist/src/primitives/spmv.d.ts.map +1 -0
- package/dist/src/primitives/spmv.js +101 -0
- package/dist/src/primitives/spmv.js.map +1 -0
- package/dist/src/types/accelerator.d.ts +24 -31
- package/dist/src/types/accelerator.d.ts.map +1 -1
- package/dist/src/types/accelerator.js +5 -4
- package/dist/src/types/accelerator.js.map +1 -1
- package/dist/src/types/algorithms.d.ts +74 -0
- package/dist/src/types/algorithms.d.ts.map +1 -0
- package/dist/src/types/algorithms.js +18 -0
- package/dist/src/types/algorithms.js.map +1 -0
- package/dist/src/types/layout.d.ts +2 -2
- package/dist/src/types/layout.d.ts.map +1 -1
- package/dist/src/types/layout.js +1 -1
- package/dist/src/types/options.d.ts +8 -49
- package/dist/src/types/options.d.ts.map +1 -1
- package/dist/src/types/options.js +5 -3
- package/dist/src/types/options.js.map +1 -1
- package/dist/src/wgsl/pr-finalize.wgsl.d.ts +11 -0
- package/dist/src/wgsl/pr-finalize.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/pr-finalize.wgsl.js +36 -0
- package/dist/src/wgsl/pr-finalize.wgsl.js.map +1 -0
- package/dist/src/wgsl/pr-scale.wgsl.d.ts +14 -0
- package/dist/src/wgsl/pr-scale.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/pr-scale.wgsl.js +48 -0
- package/dist/src/wgsl/pr-scale.wgsl.js.map +1 -0
- package/dist/src/wgsl/spmv-pull.wgsl.d.ts +15 -0
- package/dist/src/wgsl/spmv-pull.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/spmv-pull.wgsl.js +47 -0
- package/dist/src/wgsl/spmv-pull.wgsl.js.map +1 -0
- package/dist/src/wgsl/wcc-compress.wgsl.d.ts +9 -0
- package/dist/src/wgsl/wcc-compress.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/wcc-compress.wgsl.js +26 -0
- package/dist/src/wgsl/wcc-compress.wgsl.js.map +1 -0
- package/dist/src/wgsl/wcc-link-edges.wgsl.d.ts +13 -0
- package/dist/src/wgsl/wcc-link-edges.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/wcc-link-edges.wgsl.js +46 -0
- package/dist/src/wgsl/wcc-link-edges.wgsl.js.map +1 -0
- package/dist/src/wgsl/wcc-link-sample.wgsl.d.ts +11 -0
- package/dist/src/wgsl/wcc-link-sample.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/wcc-link-sample.wgsl.js +45 -0
- package/dist/src/wgsl/wcc-link-sample.wgsl.js.map +1 -0
- package/dist/src/wgsl/wcc-sample.wgsl.d.ts +10 -0
- package/dist/src/wgsl/wcc-sample.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/wcc-sample.wgsl.js +18 -0
- package/dist/src/wgsl/wcc-sample.wgsl.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/webgpu-graph-algorithms.js +1550 -29
- package/dist/webgpu-graph-algorithms.js.map +1 -1
- package/package.json +5 -4
- package/src/accelerator.ts +104 -8
- package/src/algorithms/components.ts +348 -0
- package/src/algorithms/pagerank.ts +343 -0
- package/src/algorithms/power-iteration.ts +278 -0
- package/src/algorithms/scope.ts +52 -0
- package/src/algorithms/spectral.ts +300 -0
- package/src/index.ts +20 -1
- package/src/kernel/dispatch.ts +12 -5
- package/src/kernels.ts +206 -5
- package/src/layouts/seed.ts +3 -1
- package/src/memory/residency.ts +200 -11
- package/src/primitives/core-shape.ts +103 -0
- package/src/primitives/segmented-reduce.ts +4 -36
- package/src/primitives/spmv.ts +155 -0
- package/src/types/accelerator.ts +40 -32
- package/src/types/algorithms.ts +83 -0
- package/src/types/layout.ts +2 -2
- package/src/types/options.ts +22 -53
- package/src/wgsl/pr-finalize.wgsl.ts +36 -0
- package/src/wgsl/pr-scale.wgsl.ts +48 -0
- package/src/wgsl/spmv-pull.wgsl.ts +47 -0
- package/src/wgsl/wcc-compress.wgsl.ts +26 -0
- package/src/wgsl/wcc-link-edges.wgsl.ts +46 -0
- package/src/wgsl/wcc-link-sample.wgsl.ts +45 -0
- package/src/wgsl/wcc-sample.wgsl.ts +18 -0
package/src/memory/residency.ts
CHANGED
|
@@ -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
|
|
349
|
-
*
|
|
350
|
-
*
|
|
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 -
|
|
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
|
-
|
|
365
|
-
|
|
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:
|
|
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
|
|
434
|
+
throw new WebGpuGraphError("E_UNSUPPORTED", `the ${name} view is not uploaded before P11`, {
|
|
409
435
|
feature: `view:${name}`,
|
|
410
|
-
hint: "
|
|
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
|
+
}
|