@graphty/webgpu-graph-algorithms 0.6.6 → 0.6.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graphty/webgpu-graph-algorithms",
3
- "version": "0.6.6",
3
+ "version": "0.6.8",
4
4
  "description": "WebGPU-accelerated graph algorithms and layouts over the @graphty/graph-format snapshot, for Node (Dawn) and browsers",
5
5
  "author": "Adam Powers <apowers@ato.ms>",
6
6
  "type": "module",
@@ -61,7 +61,7 @@
61
61
  "homepage": "https://github.com/graphty-org/graphty-monorepo/tree/master/webgpu-graph-algorithms#readme",
62
62
  "dependencies": {
63
63
  "@webgpu/types": "^0.1.72",
64
- "@graphty/graph-format": "^1.0.5"
64
+ "@graphty/graph-format": "^1.0.6"
65
65
  },
66
66
  "peerDependencies": {
67
67
  "@graphty/algorithms": "^1.0.0 || ^2.0.0",
@@ -93,8 +93,8 @@
93
93
  "vite": "^7.0.5",
94
94
  "vitest": "^3.2.4",
95
95
  "webgpu": "0.4.0",
96
- "@graphty/algorithms": "^2.0.4",
97
- "@graphty/layout": "^1.10.0"
96
+ "@graphty/layout": "^1.10.1",
97
+ "@graphty/algorithms": "^2.0.5"
98
98
  },
99
99
  "scripts": {
100
100
  "build": "tsc -p tsconfig.build.json",
@@ -115,6 +115,7 @@
115
115
  "coverage": "vitest run --project=node --project=node-device-errors --coverage",
116
116
  "coverage:preview": "npx serve coverage -p ${PORT:?start it through servherd, which sets PORT}",
117
117
  "bench": "tsx benchmarks/run.ts",
118
+ "bench:ab": "node scripts/bench-ab.js",
118
119
  "bench:compare": "node scripts/bench-compare.js",
119
120
  "bench:append": "node scripts/bench-append-session.js",
120
121
  "benchmark": "tsx benchmarks/run.ts",
package/src/node/index.ts CHANGED
@@ -32,11 +32,15 @@ export interface NodeGpuOptions extends Omit<GpuContextOptions, "gpu" | "adapter
32
32
  readonly loadModule?: (() => Promise<unknown>) | undefined;
33
33
  }
34
34
 
35
- /** The Dawn GPU handle (spec 2.3): dispose() drops the reference so the process can exit. */
35
+ /**
36
+ * The Dawn GPU handle (spec 2.3). The Dawn instance behind `gpu` is shared by every handle created with the
37
+ * same flags and lives until the process exits (see `createNodeGpu`); it holds no event-loop handle, so it
38
+ * never keeps the process alive.
39
+ */
36
40
  export interface NodeGpuHandle {
37
41
  /** The GPU of `dawn.create(flags)`; reading it after dispose() throws E_DISPOSED. */
38
42
  readonly gpu: GPU;
39
- /** Drops the GPU reference; idempotent. */
43
+ /** Drops this handle's GPU reference (the shared instance stays alive); idempotent. */
40
44
  dispose(): void;
41
45
  }
42
46
 
@@ -49,6 +53,18 @@ interface DawnModule {
49
53
  globals?: unknown;
50
54
  }
51
55
 
56
+ /**
57
+ * Every GPU object `dawn.create()` returned, per module and flag list, kept for the life of the process.
58
+ * webgpu@0.4.0's adapters, devices and queues run their promises through an AsyncRunner that polls the Dawn
59
+ * instance by RAW pointer, and only the GPU object owns that instance: once the GPU object is collected, the
60
+ * next promise on any adapter or device it produced (a requestDevice on a probed adapter, a queue call or a
61
+ * late map / lost callback of a destroyed device) polls freed memory -- SIGSEGV in
62
+ * dawn::native::InstanceBase::ProcessEvents, on Metal and lavapipe alike (issue #30). dawn-node signals no
63
+ * point at which the instance has drained, so no GPU object is ever released; sharing one per flag list
64
+ * bounds what that keeps to one instance per configuration.
65
+ */
66
+ const instances = new Map<DawnModule, Map<string, GPU>>();
67
+
52
68
  /**
53
69
  * Whether a loaded module is usable as Dawn.
54
70
  * @param loaded - the module namespace
@@ -125,9 +141,11 @@ export function dawnFlags(options: NodeGpuOptions | undefined): string[] {
125
141
  }
126
142
 
127
143
  /**
128
- * import("webgpu"), install dawn.globals unless installGlobals === false, dawn.create(flags) (spec 2.3).
144
+ * import("webgpu"), install dawn.globals unless installGlobals === false, dawn.create(flags) (spec 2.3). The GPU
145
+ * object is created once per flag list and reused by every later call with the same flags; it is never released,
146
+ * because Dawn keeps polling its instance for the adapters and devices it produced (issue #30).
129
147
  * @param options - adapter / backend / dawnFeatures / software / installGlobals (and the test seam)
130
- * @returns the handle; `dispose()` drops the GPU reference so the process can exit
148
+ * @returns the handle; `dispose()` drops the handle's reference, never the shared instance
131
149
  * @throws WebGpuGraphError E_NO_WEBGPU { reason, hint } when the module does not load (missing, or its glibc is too old), has no create(), or create(flags) throws
132
150
  */
133
151
  export async function createNodeGpu(options?: NodeGpuOptions): Promise<NodeGpuHandle> {
@@ -150,12 +168,22 @@ export async function createNodeGpu(options?: NodeGpuOptions): Promise<NodeGpuHa
150
168
  if (options?.installGlobals !== false && typeof loaded.globals === "object" && loaded.globals !== null) {
151
169
  Object.assign(globalThis, loaded.globals);
152
170
  }
153
- let gpu: GPU;
154
- try {
155
- gpu = loaded.create(dawnFlags(options));
156
- } catch (err) {
157
- const reason = `dawn.create() threw: ${messageOf(err)}`;
158
- throw new WebGpuGraphError("E_NO_WEBGPU", `${reason}; ${INSTALL_HINT}`, { reason, hint: INSTALL_HINT });
171
+ const flags = dawnFlags(options);
172
+ const key = flags.join("\n");
173
+ let byFlags = instances.get(loaded);
174
+ if (byFlags === undefined) {
175
+ byFlags = new Map();
176
+ instances.set(loaded, byFlags);
177
+ }
178
+ let gpu = byFlags.get(key);
179
+ if (gpu === undefined) {
180
+ try {
181
+ gpu = loaded.create(flags);
182
+ } catch (err) {
183
+ const reason = `dawn.create() threw: ${messageOf(err)}`;
184
+ throw new WebGpuGraphError("E_NO_WEBGPU", `${reason}; ${INSTALL_HINT}`, { reason, hint: INSTALL_HINT });
185
+ }
186
+ byFlags.set(key, gpu);
159
187
  }
160
188
  return new DawnHandle(gpu);
161
189
  }
@@ -181,10 +209,9 @@ function contextOptionsOf(options: NodeGpuOptions): Omit<GpuContextOptions, "gpu
181
209
 
182
210
  /**
183
211
  * createNodeGpu + GpuContext.create({ gpu, runtime: "node", ...options }); ctx.dispose() also disposes the
184
- * handle, and a create() failure disposes it before rethrowing. The handle is dropped once `ctx.lost` has
185
- * settled, never before: under webgpu@0.4.0 a GPU object collected while the device it created is still
186
- * tearing down (its lost / work-done callbacks in flight) crashes or deadlocks the process (PLAN DECISION,
187
- * P1-T1; measured with tmp/p1t1/gc-race2.mjs), and device.destroy() reports the loss right away.
212
+ * handle, and a create() failure disposes it before rethrowing. Disposing is safe at any moment: the Dawn
213
+ * instance itself stays alive for the process (createNodeGpu), so a callback of the destroyed device that
214
+ * arrives late, or a call on `ctx.device` after dispose(), never reaches a freed instance.
188
215
  * @param options - the Node options
189
216
  * @returns the context
190
217
  */
@@ -197,11 +224,8 @@ export async function createNodeGpuContext(options?: NodeGpuOptions): Promise<Gp
197
224
  handle.dispose();
198
225
  throw err;
199
226
  }
200
- const { lost } = ctx;
201
227
  ctx.attachDisposer(() => {
202
- void lost.then(() => {
203
- handle.dispose();
204
- });
228
+ handle.dispose();
205
229
  });
206
230
  return ctx;
207
231
  }