oidn-web 0.3.4 → 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 (122) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +140 -4
  3. package/benchmarks/compare.mjs +651 -0
  4. package/benchmarks/leak.mjs +255 -0
  5. package/benchmarks/results/before-spatial.json +391 -0
  6. package/benchmarks/results/before-spatial.md +47 -0
  7. package/benchmarks/results/int8-scan.json +2007 -0
  8. package/benchmarks/results/int8-scan.md +160 -0
  9. package/benchmarks/results/int8-w8a8-scan.json +2007 -0
  10. package/benchmarks/results/int8-w8a8-scan.md +160 -0
  11. package/benchmarks/results/int8-weight-channel.json +1413 -0
  12. package/benchmarks/results/int8-weight-channel.md +118 -0
  13. package/benchmarks/results/int8-weight-only.json +1437 -0
  14. package/benchmarks/results/int8-weight-only.md +118 -0
  15. package/benchmarks/results/kernel-webnn-final.json +1115 -0
  16. package/benchmarks/results/kernel-webnn-final.md +104 -0
  17. package/benchmarks/results/latest-optimized.json +375 -0
  18. package/benchmarks/results/latest-optimized.md +47 -0
  19. package/benchmarks/results/latest.json +391 -0
  20. package/benchmarks/results/latest.md +47 -0
  21. package/benchmarks/results/profile-baseline.json +331 -0
  22. package/benchmarks/results/profile-baseline.md +12 -0
  23. package/benchmarks/results/profile-conv2x.json +331 -0
  24. package/benchmarks/results/profile-conv2x.md +12 -0
  25. package/benchmarks/results/profile-fast-init.json +385 -0
  26. package/benchmarks/results/profile-fast-init.md +47 -0
  27. package/benchmarks/results/profile-fp16-fma.json +369 -0
  28. package/benchmarks/results/profile-fp16-fma.md +47 -0
  29. package/benchmarks/results/profile-fp16-tiled-decoder.json +369 -0
  30. package/benchmarks/results/profile-fp16-tiled-decoder.md +47 -0
  31. package/benchmarks/results/profile-fp16-tiled-encoder.json +369 -0
  32. package/benchmarks/results/profile-fp16-tiled-encoder.md +47 -0
  33. package/benchmarks/results/profile-fp16-unfused-pool.json +385 -0
  34. package/benchmarks/results/profile-fp16-unfused-pool.md +47 -0
  35. package/benchmarks/results/profile-input-major.json +347 -0
  36. package/benchmarks/results/profile-input-major.md +12 -0
  37. package/benchmarks/results/profile-k16.json +347 -0
  38. package/benchmarks/results/profile-k16.md +12 -0
  39. package/benchmarks/results/profile-k4.json +347 -0
  40. package/benchmarks/results/profile-k4.md +12 -0
  41. package/benchmarks/results/profile-pool-reuse.json +331 -0
  42. package/benchmarks/results/profile-pool-reuse.md +12 -0
  43. package/benchmarks/results/profile-precompiled.json +385 -0
  44. package/benchmarks/results/profile-precompiled.md +47 -0
  45. package/benchmarks/results/profile-static-channels.json +385 -0
  46. package/benchmarks/results/profile-static-channels.md +47 -0
  47. package/benchmarks/results/profile-static-io.json +385 -0
  48. package/benchmarks/results/profile-static-io.md +47 -0
  49. package/benchmarks/results/profile-tiled-conv.json +331 -0
  50. package/benchmarks/results/profile-tiled-conv.md +12 -0
  51. package/benchmarks/results/profile-tiled-decoder.json +347 -0
  52. package/benchmarks/results/profile-tiled-decoder.md +12 -0
  53. package/benchmarks/results/profile-tiled-matmul.json +331 -0
  54. package/benchmarks/results/profile-tiled-matmul.md +12 -0
  55. package/benchmarks/results/profile-unfused-decoder.json +379 -0
  56. package/benchmarks/results/profile-unfused-decoder.md +12 -0
  57. package/benchmarks/results/profile-unfused-pool.json +347 -0
  58. package/benchmarks/results/profile-unfused-pool.md +12 -0
  59. package/benchmarks/results/spatial-auto.json +575 -0
  60. package/benchmarks/results/spatial-auto.md +61 -0
  61. package/benchmarks/results/subgroup-smoke.json +1094 -0
  62. package/benchmarks/results/subgroup-smoke.md +104 -0
  63. package/benchmarks/results/webnn-smoke.json +739 -0
  64. package/benchmarks/results/webnn-smoke.md +76 -0
  65. package/dist/oidn.js +4189 -22603
  66. package/dist/oidn.umd.cjs +784 -5807
  67. package/lib/UNet.d.ts +66 -15
  68. package/lib/UNet.js +162 -257
  69. package/lib/UNet.js.map +1 -1
  70. package/lib/WGPUComputePass.d.ts +1 -1
  71. package/lib/WGPUComputePass.js +6 -4
  72. package/lib/WGPUComputePass.js.map +1 -1
  73. package/lib/backend.d.ts +8 -4
  74. package/lib/backend.js +36 -44
  75. package/lib/backend.js.map +1 -1
  76. package/lib/graphOptimizer.d.ts +54 -0
  77. package/lib/graphOptimizer.js +216 -0
  78. package/lib/graphOptimizer.js.map +1 -0
  79. package/lib/main.d.ts +33 -10
  80. package/lib/main.js +5 -0
  81. package/lib/main.js.map +1 -1
  82. package/lib/modelSpec.d.ts +80 -0
  83. package/lib/modelSpec.js +270 -0
  84. package/lib/modelSpec.js.map +1 -0
  85. package/lib/nativeUNet.d.ts +67 -0
  86. package/lib/nativeUNet.js +1735 -0
  87. package/lib/nativeUNet.js.map +1 -0
  88. package/lib/process.js +38 -35
  89. package/lib/process.js.map +1 -1
  90. package/lib/resourceTracker.d.ts +26 -0
  91. package/lib/resourceTracker.js +65 -0
  92. package/lib/resourceTracker.js.map +1 -0
  93. package/lib/tileScheduler.d.ts +33 -0
  94. package/lib/tileScheduler.js +86 -0
  95. package/lib/tileScheduler.js.map +1 -0
  96. package/lib/webnnUNet.d.ts +52 -0
  97. package/lib/webnnUNet.js +535 -0
  98. package/lib/webnnUNet.js.map +1 -0
  99. package/package.json +9 -5
  100. package/scripts/inspect-model.mjs +64 -0
  101. package/src/UNet.ts +236 -339
  102. package/src/WGPUComputePass.ts +6 -4
  103. package/src/backend.ts +42 -55
  104. package/src/graphOptimizer.ts +301 -0
  105. package/src/main.ts +71 -11
  106. package/src/modelSpec.ts +414 -0
  107. package/src/nativeUNet.ts +2256 -0
  108. package/src/process.ts +38 -36
  109. package/src/resourceTracker.ts +94 -0
  110. package/src/tileScheduler.ts +138 -0
  111. package/src/webnnUNet.ts +812 -0
  112. package/tests/modelSpec.test.mjs +128 -0
  113. package/tests/resourceLifecycle.test.mjs +383 -0
  114. package/tests/tileScheduler.test.mjs +90 -0
  115. package/lib/helper.d.ts +0 -4
  116. package/lib/helper.js +0 -33
  117. package/lib/helper.js.map +0 -1
  118. package/lib/kernels.d.ts +0 -1
  119. package/lib/kernels.js +0 -26
  120. package/lib/kernels.js.map +0 -1
  121. package/src/helper.ts +0 -43
  122. package/src/kernels.ts +0 -31
package/src/process.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { buffer, data } from '@tensorflow/tfjs';
2
1
  import { WGPUComputePass } from './WGPUComputePass';
3
2
 
4
3
  const a = 1.41283765e3;
@@ -157,18 +156,18 @@ export class GPUDataProcess {
157
156
  },
158
157
  {
159
158
  label: 'inputSize',
160
- type: 'vec2<f32>',
161
- data: new Float32Array(2)
159
+ type: 'vec2i',
160
+ data: new Int32Array(2)
162
161
  },
163
162
  {
164
163
  label: 'outputSize',
165
- type: 'vec2<f32>',
166
- data: new Float32Array(2)
164
+ type: 'vec2i',
165
+ data: new Int32Array(2)
167
166
  },
168
167
  {
169
168
  label: 'inputOffset',
170
- type: 'vec2<f32>',
171
- data: new Float32Array(2)
169
+ type: 'vec2i',
170
+ data: new Int32Array(2)
172
171
  }
173
172
  ];
174
173
  this._inputPassAux = new WGPUComputePass('inputPassAux', this._device, {
@@ -196,28 +195,28 @@ export class GPUDataProcess {
196
195
  },
197
196
  {
198
197
  label: 'inputSize',
199
- type: 'vec2<f32>',
200
- data: new Float32Array(2)
198
+ type: 'vec2i',
199
+ data: new Int32Array(2)
201
200
  },
202
201
  {
203
202
  label: 'outputSize',
204
- type: 'vec2<f32>',
205
- data: new Float32Array(2)
203
+ type: 'vec2i',
204
+ data: new Int32Array(2)
206
205
  },
207
206
  {
208
207
  label: 'imageSize',
209
- type: 'vec2<f32>',
210
- data: new Float32Array(2)
208
+ type: 'vec2i',
209
+ data: new Int32Array(2)
211
210
  },
212
211
  {
213
212
  label: 'inputOffset',
214
- type: 'vec2<f32>',
215
- data: new Float32Array(2)
213
+ type: 'vec2i',
214
+ data: new Int32Array(2)
216
215
  },
217
216
  {
218
217
  label: 'outputOffset',
219
- type: 'vec2<f32>',
220
- data: new Float32Array(2)
218
+ type: 'vec2i',
219
+ data: new Int32Array(2)
221
220
  }
222
221
  ],
223
222
  csDefine: '',
@@ -230,8 +229,8 @@ export class GPUDataProcess {
230
229
  uniforms: [
231
230
  {
232
231
  label: 'size',
233
- type: 'vec2<f32>',
234
- data: new Float32Array(2)
232
+ type: 'vec2i',
233
+ data: new Int32Array(2)
235
234
  }
236
235
  ],
237
236
  csMain: /*wgsl*/ `
@@ -279,12 +278,12 @@ fn PUForward(y: f32) -> f32 {
279
278
  : `in_${inputName}[inIdx]`;
280
279
  }
281
280
  const commonCSMain = /* wgsl */ `
282
- let x = f32(globalId.x);
283
- let y = f32(globalId.y);
284
- let inIdx = i32((y + inputOffset.y) * inputSize.x + (x + inputOffset.x));
281
+ let x = i32(globalId.x);
282
+ let y = i32(globalId.y);
283
+ let inIdx = (y + inputOffset.y) * inputSize.x + (x + inputOffset.x);
285
284
  let col = ${readInputCode('color')};
286
285
 
287
- let outIdx = i32(y * outputSize.x + x);
286
+ let outIdx = y * outputSize.x + x;
288
287
 
289
288
  if (${denoiseAlpha}) {
290
289
  // Denoise the inversed alpha. Or the anti aliased edge will be too dark after denoised
@@ -328,13 +327,13 @@ fn PUInverse(y: f32) -> f32 {
328
327
  }
329
328
  `,
330
329
  csMain: /* wgsl */ `
331
- let x = f32(globalId.x);
332
- let y = f32(globalId.y);
330
+ let x = i32(globalId.x);
331
+ let y = i32(globalId.y);
333
332
  if (x >= outputSize.x || y >= outputSize.y) {
334
333
  return;
335
334
  }
336
- let inIdx = i32((y + inputOffset.y) * inputSize.x + x + inputOffset.x);
337
- let outIdx = i32((y + outputOffset.y) * imageSize.x + x + outputOffset.x);
335
+ let inIdx = (y + inputOffset.y) * inputSize.x + x + inputOffset.x;
336
+ let outIdx = (y + outputOffset.y) * imageSize.x + x + outputOffset.x;
338
337
  let col = in_color[inIdx];
339
338
  let raw = ${
340
339
  isInputTexture
@@ -360,18 +359,18 @@ else {
360
359
  }
361
360
 
362
361
  setImageSize(w: number, h: number) {
363
- this._inputPassAux.setUniform('inputSize', new Float32Array([w, h]));
364
- this._inputPassColor.setUniform('inputSize', new Float32Array([w, h]));
365
- this._outputPass.setUniform('imageSize', new Float32Array([w, h]));
362
+ this._inputPassAux.setUniform('inputSize', new Int32Array([w, h]));
363
+ this._inputPassColor.setUniform('inputSize', new Int32Array([w, h]));
364
+ this._outputPass.setUniform('imageSize', new Int32Array([w, h]));
366
365
  this._outputPass.setSize(w, h);
367
366
  this._copyPass.setSize(w, h);
368
- this._copyPass.setUniform('size', new Float32Array([w, h]));
367
+ this._copyPass.setUniform('size', new Int32Array([w, h]));
369
368
  }
370
369
 
371
370
  setInputTile(tile: Tile) {
372
- const size = new Float32Array([tile.width, tile.height]);
371
+ const size = new Int32Array([tile.width, tile.height]);
373
372
  [this._inputPassAux, this._inputPassColor].forEach((inputPass) => {
374
- inputPass.setUniform('inputOffset', new Float32Array([tile.x, tile.y]));
373
+ inputPass.setUniform('inputOffset', new Int32Array([tile.x, tile.y]));
375
374
  inputPass.setUniform('outputSize', size);
376
375
  inputPass.setSize(size[0], size[1]);
377
376
  });
@@ -381,14 +380,14 @@ else {
381
380
 
382
381
  setOutputTile(dstTile: Tile, srcTile: Tile) {
383
382
  const outputPass = this._outputPass;
384
- const size = new Float32Array([dstTile.width, dstTile.height]);
383
+ const size = new Int32Array([dstTile.width, dstTile.height]);
385
384
  const dx = dstTile.x - srcTile.x;
386
385
  const dy = dstTile.y - srcTile.y;
387
386
  outputPass.setUniform('outputSize', size);
388
- outputPass.setUniform('inputOffset', new Float32Array([dx, dy]));
387
+ outputPass.setUniform('inputOffset', new Int32Array([dx, dy]));
389
388
  outputPass.setUniform(
390
389
  'outputOffset',
391
- new Float32Array([dstTile.x, dstTile.y])
390
+ new Int32Array([dstTile.x, dstTile.y])
392
391
  );
393
392
  outputPass.setExecuteSize(size[0], size[1]);
394
393
  }
@@ -490,5 +489,8 @@ else {
490
489
  dispose() {
491
490
  this._outputPass.dispose();
492
491
  this._inputPassAux.dispose();
492
+ this._inputPassColor.dispose();
493
+ // copyPass borrows outputPass's target and must not destroy it again.
494
+ this._copyPass.dispose(false);
493
495
  }
494
496
  }
@@ -0,0 +1,94 @@
1
+ export type OIDNResourceKind =
2
+ | 'gpu-buffer'
3
+ | 'gpu-query-set'
4
+ | 'ml-context'
5
+ | 'ml-graph'
6
+ | 'ml-tensor';
7
+
8
+ export interface OIDNResourceStats {
9
+ created: number;
10
+ destroyed: number;
11
+ live: number;
12
+ peakLive: number;
13
+ }
14
+
15
+ export interface OIDNResourceSnapshot {
16
+ live: number;
17
+ created: number;
18
+ destroyed: number;
19
+ peakLive: number;
20
+ pending: number;
21
+ byKind: Partial<Record<OIDNResourceKind, OIDNResourceStats>>;
22
+ }
23
+
24
+ interface MutableResourceStats extends OIDNResourceStats {
25
+ resources: Set<object>;
26
+ }
27
+
28
+ /**
29
+ * Counts explicitly owned browser resources. Keeping the ownership registry in
30
+ * production makes cache eviction and failure cleanup testable without relying
31
+ * on JS heap measurements, which do not include WebGPU allocations.
32
+ */
33
+ export class OIDNResourceTracker {
34
+ private _stats = new Map<OIDNResourceKind, MutableResourceStats>();
35
+
36
+ track<T extends object>(kind: OIDNResourceKind, resource: T): T {
37
+ let stats = this._stats.get(kind);
38
+ if (!stats) {
39
+ stats = {
40
+ created: 0,
41
+ destroyed: 0,
42
+ live: 0,
43
+ peakLive: 0,
44
+ resources: new Set()
45
+ };
46
+ this._stats.set(kind, stats);
47
+ }
48
+ if (stats.resources.has(resource)) return resource;
49
+ stats.resources.add(resource);
50
+ stats.created++;
51
+ stats.live++;
52
+ stats.peakLive = Math.max(stats.peakLive, stats.live);
53
+ return resource;
54
+ }
55
+
56
+ release(
57
+ kind: OIDNResourceKind,
58
+ resource: object | undefined,
59
+ destroy: () => void
60
+ ) {
61
+ if (!resource) return false;
62
+ const stats = this._stats.get(kind);
63
+ if (!stats?.resources.delete(resource)) return false;
64
+ try {
65
+ destroy();
66
+ } finally {
67
+ stats.destroyed++;
68
+ stats.live--;
69
+ }
70
+ return true;
71
+ }
72
+
73
+ snapshot(pending = 0): OIDNResourceSnapshot {
74
+ let live = 0;
75
+ let created = 0;
76
+ let destroyed = 0;
77
+ let peakLive = 0;
78
+ const byKind: OIDNResourceSnapshot['byKind'] = {};
79
+ for (const [kind, stats] of this._stats) {
80
+ const entry = {
81
+ created: stats.created,
82
+ destroyed: stats.destroyed,
83
+ live: stats.live,
84
+ peakLive: stats.peakLive
85
+ };
86
+ byKind[kind] = entry;
87
+ live += entry.live;
88
+ created += entry.created;
89
+ destroyed += entry.destroyed;
90
+ peakLive += entry.peakLive;
91
+ }
92
+ return { live, created, destroyed, peakLive, pending, byKind };
93
+ }
94
+ }
@@ -0,0 +1,138 @@
1
+ export interface DynamicTileOptions {
2
+ /** Smallest output tile edge. Defaults to 256. */
3
+ minTileSize?: number;
4
+ /** Initial output tile edge. Defaults to 384. */
5
+ initialTileSize?: number;
6
+ /** Desired upper bound for one tile's GPU latency. Defaults to 16 ms. */
7
+ targetTileTimeMs?: number;
8
+ /** Amount added or removed after each completed execution. Defaults to 128. */
9
+ adjustmentStep?: number;
10
+ }
11
+
12
+ export type DynamicTileSetting = boolean | DynamicTileOptions;
13
+
14
+ const defaultMinTileSize = 256;
15
+ const defaultInitialTileSize = 384;
16
+ const defaultTargetTileTimeMs = 16;
17
+ const defaultAdjustmentStep = 128;
18
+ export const OIDN_TILE_ALIGNMENT = 16;
19
+
20
+ function alignUp(value: number, alignment: number) {
21
+ return Math.ceil(value / alignment) * alignment;
22
+ }
23
+
24
+ function alignDown(value: number, alignment: number) {
25
+ return Math.floor(value / alignment) * alignment;
26
+ }
27
+
28
+ function clamp(value: number, min: number, max: number) {
29
+ return Math.min(Math.max(value, min), max);
30
+ }
31
+
32
+ function median(values: number[]) {
33
+ const sorted = [...values].sort((a, b) => a - b);
34
+ const middle = Math.floor(sorted.length / 2);
35
+ return sorted.length % 2
36
+ ? sorted[middle]
37
+ : (sorted[middle - 1] + sorted[middle]) / 2;
38
+ }
39
+
40
+ /** Fits one output dimension without exceeding the configured tile cap. */
41
+ export function fitTileDimension(imageSize: number, maxTileSize: number) {
42
+ return imageSize <= maxTileSize
43
+ ? Math.min(alignUp(imageSize, OIDN_TILE_ALIGNMENT), maxTileSize)
44
+ : maxTileSize;
45
+ }
46
+
47
+ /**
48
+ * Chooses an output tile size from completed GPU timings. A size change is
49
+ * applied to the next execution so one in-flight tiled pass keeps a stable
50
+ * overlap, allocation plan, and pipeline set.
51
+ */
52
+ export class DynamicTileController {
53
+ readonly enabled: boolean;
54
+ readonly maxTileSize: number;
55
+ readonly minTileSize: number;
56
+ readonly targetTileTimeMs: number;
57
+
58
+ private _tileSize: number;
59
+ private _adjustmentStep: number;
60
+
61
+ constructor(maxTileSize: number, setting: DynamicTileSetting = true) {
62
+ const options = typeof setting === 'object' ? setting : {};
63
+ this.enabled = setting !== false;
64
+ this.maxTileSize = Math.max(
65
+ OIDN_TILE_ALIGNMENT,
66
+ alignDown(maxTileSize, OIDN_TILE_ALIGNMENT)
67
+ );
68
+ this.minTileSize = clamp(
69
+ alignUp(options.minTileSize ?? defaultMinTileSize, OIDN_TILE_ALIGNMENT),
70
+ OIDN_TILE_ALIGNMENT,
71
+ this.maxTileSize
72
+ );
73
+ this.targetTileTimeMs = Math.max(
74
+ 1,
75
+ options.targetTileTimeMs ?? defaultTargetTileTimeMs
76
+ );
77
+ this._adjustmentStep = Math.max(
78
+ OIDN_TILE_ALIGNMENT,
79
+ alignUp(
80
+ options.adjustmentStep ?? defaultAdjustmentStep,
81
+ OIDN_TILE_ALIGNMENT
82
+ )
83
+ );
84
+ this._tileSize = this.enabled
85
+ ? clamp(
86
+ alignUp(
87
+ options.initialTileSize ?? defaultInitialTileSize,
88
+ OIDN_TILE_ALIGNMENT
89
+ ),
90
+ this.minTileSize,
91
+ this.maxTileSize
92
+ )
93
+ : this.maxTileSize;
94
+ }
95
+
96
+ get tileSize() {
97
+ return this._tileSize;
98
+ }
99
+
100
+ /** Returns true when the next execution should use a different tile size. */
101
+ observe(tileTimesMs: number[]) {
102
+ if (!this.enabled || tileTimesMs.length === 0) return false;
103
+
104
+ const validTimes = tileTimesMs.filter(
105
+ (duration) => Number.isFinite(duration) && duration >= 0
106
+ );
107
+ if (validTimes.length === 0) return false;
108
+
109
+ const observedTime = median(validTimes);
110
+ let nextTileSize = this._tileSize;
111
+ if (observedTime > this.targetTileTimeMs * 1.25) {
112
+ nextTileSize -= this._adjustmentStep;
113
+ } else if (observedTime < this.targetTileTimeMs * 0.65) {
114
+ nextTileSize += this._adjustmentStep;
115
+ }
116
+
117
+ nextTileSize = clamp(
118
+ alignUp(nextTileSize, OIDN_TILE_ALIGNMENT),
119
+ this.minTileSize,
120
+ this.maxTileSize
121
+ );
122
+ if (nextTileSize === this._tileSize) return false;
123
+ this._tileSize = nextTileSize;
124
+ return true;
125
+ }
126
+ }
127
+
128
+ /** Waits for all work submitted before this call and tolerates device loss. */
129
+ export async function waitForSubmittedGPUWork(
130
+ queue: Pick<GPUQueue, 'onSubmittedWorkDone'>
131
+ ) {
132
+ try {
133
+ await queue.onSubmittedWorkDone();
134
+ } catch {
135
+ // tileExecute has no error callback. Preserve its completion/cancellation
136
+ // behavior and let the owning GPUDevice report device loss separately.
137
+ }
138
+ }