oidn-web 0.4.0 → 0.5.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 (106) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +83 -19
  3. package/dist/oidn.js +3239 -2642
  4. package/dist/oidn.umd.cjs +512 -296
  5. package/lib/UNet.d.ts +54 -20
  6. package/lib/UNet.js +194 -118
  7. package/lib/UNet.js.map +1 -1
  8. package/lib/backend.d.ts +1 -8
  9. package/lib/backend.js +1 -9
  10. package/lib/backend.js.map +1 -1
  11. package/lib/finalRgbShader.d.ts +13 -0
  12. package/lib/finalRgbShader.js +160 -0
  13. package/lib/finalRgbShader.js.map +1 -0
  14. package/lib/graphOptimizer.js +1 -2
  15. package/lib/graphOptimizer.js.map +1 -1
  16. package/lib/hdrTransfer.d.ts +14 -0
  17. package/lib/hdrTransfer.js +61 -0
  18. package/lib/hdrTransfer.js.map +1 -0
  19. package/lib/main.d.ts +16 -7
  20. package/lib/main.js +4 -5
  21. package/lib/main.js.map +1 -1
  22. package/lib/nativeUNet.d.ts +39 -3
  23. package/lib/nativeUNet.js +449 -120
  24. package/lib/nativeUNet.js.map +1 -1
  25. package/lib/process.d.ts +5 -11
  26. package/lib/process.js +35 -49
  27. package/lib/process.js.map +1 -1
  28. package/lib/tileScheduler.d.ts +32 -4
  29. package/lib/tileScheduler.js +133 -20
  30. package/lib/tileScheduler.js.map +1 -1
  31. package/package.json +9 -2
  32. package/src/UNet.ts +287 -158
  33. package/src/backend.ts +1 -14
  34. package/src/finalRgbShader.ts +186 -0
  35. package/src/graphOptimizer.ts +1 -2
  36. package/src/hdrTransfer.ts +88 -0
  37. package/src/main.ts +28 -13
  38. package/src/nativeUNet.ts +515 -116
  39. package/src/process.ts +43 -70
  40. package/src/tileScheduler.ts +216 -24
  41. package/benchmarks/compare.mjs +0 -651
  42. package/benchmarks/leak.mjs +0 -255
  43. package/benchmarks/results/before-spatial.json +0 -391
  44. package/benchmarks/results/before-spatial.md +0 -47
  45. package/benchmarks/results/int8-scan.json +0 -2007
  46. package/benchmarks/results/int8-scan.md +0 -160
  47. package/benchmarks/results/int8-w8a8-scan.json +0 -2007
  48. package/benchmarks/results/int8-w8a8-scan.md +0 -160
  49. package/benchmarks/results/int8-weight-channel.json +0 -1413
  50. package/benchmarks/results/int8-weight-channel.md +0 -118
  51. package/benchmarks/results/int8-weight-only.json +0 -1437
  52. package/benchmarks/results/int8-weight-only.md +0 -118
  53. package/benchmarks/results/kernel-webnn-final.json +0 -1115
  54. package/benchmarks/results/kernel-webnn-final.md +0 -104
  55. package/benchmarks/results/latest-optimized.json +0 -375
  56. package/benchmarks/results/latest-optimized.md +0 -47
  57. package/benchmarks/results/latest.json +0 -391
  58. package/benchmarks/results/latest.md +0 -47
  59. package/benchmarks/results/profile-baseline.json +0 -331
  60. package/benchmarks/results/profile-baseline.md +0 -12
  61. package/benchmarks/results/profile-conv2x.json +0 -331
  62. package/benchmarks/results/profile-conv2x.md +0 -12
  63. package/benchmarks/results/profile-fast-init.json +0 -385
  64. package/benchmarks/results/profile-fast-init.md +0 -47
  65. package/benchmarks/results/profile-fp16-fma.json +0 -369
  66. package/benchmarks/results/profile-fp16-fma.md +0 -47
  67. package/benchmarks/results/profile-fp16-tiled-decoder.json +0 -369
  68. package/benchmarks/results/profile-fp16-tiled-decoder.md +0 -47
  69. package/benchmarks/results/profile-fp16-tiled-encoder.json +0 -369
  70. package/benchmarks/results/profile-fp16-tiled-encoder.md +0 -47
  71. package/benchmarks/results/profile-fp16-unfused-pool.json +0 -385
  72. package/benchmarks/results/profile-fp16-unfused-pool.md +0 -47
  73. package/benchmarks/results/profile-input-major.json +0 -347
  74. package/benchmarks/results/profile-input-major.md +0 -12
  75. package/benchmarks/results/profile-k16.json +0 -347
  76. package/benchmarks/results/profile-k16.md +0 -12
  77. package/benchmarks/results/profile-k4.json +0 -347
  78. package/benchmarks/results/profile-k4.md +0 -12
  79. package/benchmarks/results/profile-pool-reuse.json +0 -331
  80. package/benchmarks/results/profile-pool-reuse.md +0 -12
  81. package/benchmarks/results/profile-precompiled.json +0 -385
  82. package/benchmarks/results/profile-precompiled.md +0 -47
  83. package/benchmarks/results/profile-static-channels.json +0 -385
  84. package/benchmarks/results/profile-static-channels.md +0 -47
  85. package/benchmarks/results/profile-static-io.json +0 -385
  86. package/benchmarks/results/profile-static-io.md +0 -47
  87. package/benchmarks/results/profile-tiled-conv.json +0 -331
  88. package/benchmarks/results/profile-tiled-conv.md +0 -12
  89. package/benchmarks/results/profile-tiled-decoder.json +0 -347
  90. package/benchmarks/results/profile-tiled-decoder.md +0 -12
  91. package/benchmarks/results/profile-tiled-matmul.json +0 -331
  92. package/benchmarks/results/profile-tiled-matmul.md +0 -12
  93. package/benchmarks/results/profile-unfused-decoder.json +0 -379
  94. package/benchmarks/results/profile-unfused-decoder.md +0 -12
  95. package/benchmarks/results/profile-unfused-pool.json +0 -347
  96. package/benchmarks/results/profile-unfused-pool.md +0 -12
  97. package/benchmarks/results/spatial-auto.json +0 -575
  98. package/benchmarks/results/spatial-auto.md +0 -61
  99. package/benchmarks/results/subgroup-smoke.json +0 -1094
  100. package/benchmarks/results/subgroup-smoke.md +0 -104
  101. package/benchmarks/results/webnn-smoke.json +0 -739
  102. package/benchmarks/results/webnn-smoke.md +0 -76
  103. package/scripts/inspect-model.mjs +0 -64
  104. package/tests/modelSpec.test.mjs +0 -128
  105. package/tests/resourceLifecycle.test.mjs +0 -383
  106. package/tests/tileScheduler.test.mjs +0 -90
@@ -1,76 +0,0 @@
1
- # oidn-web benchmark
2
-
3
- - Current: `e6b8577b1c69cc8ce979022717984aec5b9ea881`
4
- - TFJS baseline: `136c00c34aa5cf4561b4137034dbd643177f0a5f`
5
- - Input: 512x512, fixed tile 512, 1 runs after 0 warmup(s)
6
- - Adapter: apple
7
-
8
- | Runtime | Init ms | Median ms | P95 ms | Speedup | Precision |
9
- | --- | ---: | ---: | ---: | ---: | --- |
10
- | TFJS (136c00c) | 9.70 | 486.20 | 486.20 | 1.00x | fp32 |
11
- | WGSL FP32 | 38.50 | 43.10 | 43.10 | 11.28x | fp32 |
12
- | WGSL FP16 Direct | 28.50 | 34.10 | 34.10 | 14.26x | fp16 |
13
- | WGSL FP16 Implicit GEMM | 29.90 | 34.80 | 34.80 | 13.97x | fp16 |
14
- | WGSL FP16 Spatial (experimental) | 32.30 | 70.10 | 70.10 | 6.94x | fp16 |
15
- | WebNN FP16 (experimental) | skipped | - | - | - | Error: WebNN FP16 WebGPU interop failed: NotSupportedError: Failed to execute 'createExportableTensor' on 'MLContext': WebGPU Interop is not supported. |
16
-
17
- ## Output validation
18
-
19
- Compared against sampled TFJS FP32 output.
20
-
21
- | Runtime | Status | MAE | RMSE | Max error | Samples |
22
- | --- | --- | ---: | ---: | ---: | ---: |
23
- | WGSL FP32 | pass | 1.11e-6 | 1.81e-6 | 1.36e-5 | 6144 |
24
- | WGSL FP16 Direct | pass | 1.02e-3 | 1.61e-3 | 1.24e-2 | 6144 |
25
- | WGSL FP16 Implicit GEMM | pass | 1.56e-3 | 2.55e-3 | 4.48e-2 | 6144 |
26
- | WGSL FP16 Spatial (experimental) | pass | 1.02e-3 | 1.61e-3 | 1.24e-2 | 6144 |
27
-
28
- ## GPU hot layers
29
-
30
- ### WGSL FP32
31
-
32
- Profiled GPU total: 36.56 ms
33
-
34
- | Node | GPU ms |
35
- | --- | ---: |
36
- | dec_conv1a | 6.87 |
37
- | dec_conv2a | 4.79 |
38
- | dec_conv1b | 4.17 |
39
- | enc_conv1b | 4.16 |
40
- | dec_conv3a | 2.32 |
41
-
42
- ### WGSL FP16 Direct
43
-
44
- Profiled GPU total: 28.70 ms
45
-
46
- | Node | GPU ms |
47
- | --- | ---: |
48
- | dec_conv1a | 4.40 |
49
- | dec_conv1b | 3.43 |
50
- | enc_conv1b | 3.13 |
51
- | dec_conv2a | 2.99 |
52
- | dec_conv3a | 2.24 |
53
-
54
- ### WGSL FP16 Implicit GEMM
55
-
56
- Profiled GPU total: 30.75 ms
57
-
58
- | Node | GPU ms |
59
- | --- | ---: |
60
- | dec_conv1a | 6.28 |
61
- | dec_conv2a | 4.14 |
62
- | enc_conv1b | 3.09 |
63
- | dec_conv1b | 2.98 |
64
- | dec_conv3a | 1.90 |
65
-
66
- ### WGSL FP16 Spatial (experimental)
67
-
68
- Profiled GPU total: 64.85 ms
69
-
70
- | Node | GPU ms |
71
- | --- | ---: |
72
- | dec_conv1a | 23.57 |
73
- | dec_conv1b | 5.90 |
74
- | enc_conv1b | 5.86 |
75
- | dec_conv2b | 4.89 |
76
- | enc_conv2b | 4.34 |
@@ -1,64 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- import { readFile } from 'node:fs/promises';
3
- import { resolve } from 'node:path';
4
-
5
- import { parseTZA } from '../lib/tza.js';
6
- import {
7
- detectUNetModelSpec,
8
- validateUNetModel
9
- } from '../lib/modelSpec.js';
10
-
11
- const modelPath = process.argv[2];
12
- if (!modelPath) {
13
- console.error('Usage: npm run model:inspect -- path/to/model.tza');
14
- process.exit(2);
15
- }
16
-
17
- const absolutePath = resolve(modelPath);
18
- const file = await readFile(absolutePath);
19
- const arrayBuffer = file.buffer.slice(
20
- file.byteOffset,
21
- file.byteOffset + file.byteLength
22
- );
23
- const tensors = parseTZA(arrayBuffer);
24
- const tensorSignature = [...tensors]
25
- .map(([name, tensor]) => ({
26
- name,
27
- dims: tensor.desc.dims,
28
- layout: tensor.desc.layout,
29
- dataType: tensor.desc.dataType,
30
- bytes: tensor.data.byteLength
31
- }))
32
- .sort((left, right) => left.name.localeCompare(right.name));
33
-
34
- let model;
35
- let compatibilityError;
36
- try {
37
- const spec = detectUNetModelSpec(tensors);
38
- const validated = validateUNetModel(tensors, spec);
39
- model = {
40
- descriptor: spec.id,
41
- family: spec.family,
42
- inputChannels: validated.inputChannels,
43
- outputChannels: validated.outputChannels,
44
- dataType: validated.tensorDataType,
45
- receptiveField: spec.receptiveField
46
- };
47
- } catch (error) {
48
- compatibilityError = error instanceof Error ? error.message : String(error);
49
- process.exitCode = 1;
50
- }
51
-
52
- console.log(
53
- JSON.stringify(
54
- {
55
- path: absolutePath,
56
- sha256: createHash('sha256').update(file).digest('hex'),
57
- model,
58
- compatibilityError,
59
- tensors: tensorSignature
60
- },
61
- null,
62
- 2
63
- )
64
- );
@@ -1,128 +0,0 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
-
4
- import {
5
- detectUNetModelSpec,
6
- OIDN_UNET_LARGE_SPEC,
7
- OIDN_UNET_SMALL_SPEC,
8
- validateUNetModel
9
- } from '../lib/modelSpec.js';
10
- import { optimizeModelGraph, planModelExecution } from '../lib/graphOptimizer.js';
11
- import { HostTensor, TensorDesc } from '../lib/tza.js';
12
-
13
- function hostTensor(dims, layout = 'x', dataType = 'Float16') {
14
- const desc = new TensorDesc();
15
- desc.dims = [...dims];
16
- desc.paddedDims = [...dims];
17
- desc.layout = layout;
18
- desc.dataType = dataType;
19
- return new HostTensor(desc, new Uint8Array(desc.getByteSize()));
20
- }
21
-
22
- function makeModelTensors(spec, inputChannels = 3) {
23
- const tensors = new Map();
24
- const channels = new Map([[spec.input, inputChannels]]);
25
-
26
- for (const node of spec.nodes) {
27
- if (node.op === 'conv2d') {
28
- const inChannels = channels.get(node.input);
29
- assert.notEqual(inChannels, undefined);
30
- const outChannels = node.id === spec.output ? 3 : 4;
31
- tensors.set(
32
- node.weight,
33
- hostTensor([outChannels, inChannels, 3, 3], 'oihw')
34
- );
35
- tensors.set(node.bias, hostTensor([outChannels]));
36
- channels.set(node.id, outChannels);
37
- } else if (node.op === 'concat') {
38
- channels.set(
39
- node.id,
40
- node.inputs.reduce((sum, input) => sum + channels.get(input), 0)
41
- );
42
- } else {
43
- channels.set(node.id, channels.get(node.input));
44
- }
45
- }
46
- return tensors;
47
- }
48
-
49
- test('detects and validates built-in small and large descriptors', () => {
50
- for (const spec of [OIDN_UNET_SMALL_SPEC, OIDN_UNET_LARGE_SPEC]) {
51
- const tensors = makeModelTensors(spec, 9);
52
- assert.equal(detectUNetModelSpec(tensors), spec);
53
- const validated = validateUNetModel(tensors);
54
- assert.equal(validated.spec, spec);
55
- assert.equal(validated.inputChannels, 9);
56
- assert.equal(validated.outputChannels, 3);
57
- assert.equal(validated.tensorDataType, 'Float16');
58
- }
59
- });
60
-
61
- test('rejects an unknown topology instead of silently using a stale graph', () => {
62
- const tensors = makeModelTensors(OIDN_UNET_SMALL_SPEC);
63
- tensors.set('future_block.weight', hostTensor([4, 4, 3, 3], 'oihw'));
64
- assert.throws(
65
- () => detectUNetModelSpec(tensors),
66
- /Unsupported OIDN model topology/
67
- );
68
- });
69
-
70
- test('validates tensor byte length and graph channel flow', () => {
71
- const tensors = makeModelTensors(OIDN_UNET_SMALL_SPEC);
72
- tensors.get('enc_conv0.weight').data = new Uint8Array(2);
73
- assert.throws(
74
- () => validateUNetModel(tensors, OIDN_UNET_SMALL_SPEC),
75
- /bytes, expected/
76
- );
77
-
78
- const wrongChannels = makeModelTensors(OIDN_UNET_SMALL_SPEC);
79
- wrongChannels.set('enc_conv1.weight', hostTensor([4, 7, 3, 3], 'oihw'));
80
- assert.throws(
81
- () => validateUNetModel(wrongChannels, OIDN_UNET_SMALL_SPEC),
82
- /expects 7 input channels/
83
- );
84
- });
85
-
86
- test('fuses topology patterns without hard-coding a model family', () => {
87
- const validated = validateUNetModel(
88
- makeModelTensors(OIDN_UNET_SMALL_SPEC),
89
- OIDN_UNET_SMALL_SPEC
90
- );
91
- const graph = optimizeModelGraph(validated);
92
-
93
- assert.deepEqual(graph.fusions, {
94
- convPool: 4,
95
- upsampleConcatConv: 4
96
- });
97
- assert.equal(
98
- graph.nodes.filter((node) => node.op === 'fusedConvReluMaxPool2d').length,
99
- 4
100
- );
101
- assert.equal(
102
- graph.nodes.filter((node) => node.op === 'fusedUpsampleConcatConv2d')
103
- .length,
104
- 4
105
- );
106
- assert.equal(graph.nodes.length, 16);
107
- });
108
-
109
- test('plans fused graph shapes and lifetimes', () => {
110
- const validated = validateUNetModel(
111
- makeModelTensors(OIDN_UNET_LARGE_SPEC, 9),
112
- OIDN_UNET_LARGE_SPEC
113
- );
114
- const plan = planModelExecution(validated, 384, 256);
115
-
116
- assert.deepEqual(plan.inputShape, { width: 384, height: 256, channels: 9 });
117
- assert.deepEqual(plan.valueShapes.get('pool4'), {
118
- width: 24,
119
- height: 16,
120
- channels: 4
121
- });
122
- assert.deepEqual(plan.valueShapes.get(OIDN_UNET_LARGE_SPEC.output), {
123
- width: 384,
124
- height: 256,
125
- channels: 3
126
- });
127
- assert.equal(plan.plannedNodes.at(-1).lastUse, plan.nodes.length);
128
- });
@@ -1,383 +0,0 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
-
4
- import { validateUNetModel } from '../lib/modelSpec.js';
5
- import { OIDNResourceTracker } from '../lib/resourceTracker.js';
6
- import { HostTensor, TensorDesc } from '../lib/tza.js';
7
- import { NativeUNetExecutor } from '../lib/nativeUNet.js';
8
- import { WebNNUNetExecutor } from '../lib/webnnUNet.js';
9
-
10
- const TEST_SPEC = {
11
- schemaVersion: 1,
12
- id: 'resource-test-unet',
13
- family: 'resource-test',
14
- input: 'input',
15
- output: 'output',
16
- receptiveField: 3,
17
- nodes: [
18
- {
19
- op: 'conv2d',
20
- id: 'output',
21
- input: 'input',
22
- weight: 'output.weight',
23
- bias: 'output.bias',
24
- activation: 'identity',
25
- padding: 'same'
26
- }
27
- ]
28
- };
29
-
30
- function hostTensor(dims, layout = 'x') {
31
- const desc = new TensorDesc();
32
- desc.dims = [...dims];
33
- desc.paddedDims = [...dims];
34
- desc.layout = layout;
35
- desc.dataType = 'Float16';
36
- return new HostTensor(desc, new Uint8Array(desc.getByteSize()));
37
- }
38
-
39
- function testModel() {
40
- return validateUNetModel(
41
- new Map([
42
- ['output.weight', hostTensor([3, 3, 3, 3], 'oihw')],
43
- ['output.bias', hostTensor([3])]
44
- ]),
45
- TEST_SPEC
46
- );
47
- }
48
-
49
- class FakeResource {
50
- destroyCalls = 0;
51
-
52
- destroy() {
53
- this.destroyCalls++;
54
- }
55
- }
56
-
57
- class FakeBuffer extends FakeResource {
58
- mapState = 'unmapped';
59
-
60
- constructor(size = 16, mappedAtCreation = false) {
61
- super();
62
- this.data = new ArrayBuffer(size);
63
- this.mapState = mappedAtCreation ? 'mapped' : 'unmapped';
64
- }
65
-
66
- getMappedRange() {
67
- return this.data;
68
- }
69
-
70
- unmap() {
71
- this.mapState = 'unmapped';
72
- }
73
- }
74
-
75
- function fakeDevice() {
76
- const device = {
77
- features: new Set(['shader-f16']),
78
- limits: {
79
- maxComputeInvocationsPerWorkgroup: 256,
80
- maxComputeWorkgroupSizeX: 256,
81
- maxComputeWorkgroupSizeY: 256,
82
- maxComputeWorkgroupStorageSize: 32768
83
- },
84
- failBindGroup: false,
85
- failBufferAt: undefined,
86
- createBufferCalls: 0,
87
- buffers: [],
88
- queue: {
89
- submit() {},
90
- onSubmittedWorkDone: () => Promise.resolve()
91
- },
92
- createShaderModule: () => ({}),
93
- createComputePipeline: () => ({ getBindGroupLayout: () => ({}) }),
94
- createComputePipelineAsync: async () => ({
95
- getBindGroupLayout: () => ({})
96
- }),
97
- createBuffer(descriptor) {
98
- this.createBufferCalls++;
99
- if (this.createBufferCalls === this.failBufferAt) {
100
- throw new Error('injected buffer allocation failure');
101
- }
102
- const buffer = new FakeBuffer(
103
- descriptor.size,
104
- descriptor.mappedAtCreation
105
- );
106
- this.buffers.push(buffer);
107
- return buffer;
108
- },
109
- createBindGroup() {
110
- if (this.failBindGroup) throw new Error('injected bind group failure');
111
- return {};
112
- },
113
- createCommandEncoder: () => ({
114
- beginComputePass: () => ({
115
- setPipeline() {},
116
- setBindGroup() {},
117
- dispatchWorkgroups() {},
118
- end() {}
119
- }),
120
- finish: () => ({})
121
- })
122
- };
123
- return device;
124
- }
125
-
126
- function deferred() {
127
- let resolve;
128
- let reject;
129
- const promise = new Promise((resolvePromise, rejectPromise) => {
130
- resolve = resolvePromise;
131
- reject = rejectPromise;
132
- });
133
- return { promise, resolve, reject };
134
- }
135
-
136
- async function withFakeWebNN(run, { build } = {}) {
137
- const previous = {
138
- navigator: Object.getOwnPropertyDescriptor(globalThis, 'navigator'),
139
- builder: Object.getOwnPropertyDescriptor(globalThis, 'MLGraphBuilder'),
140
- usage: Object.getOwnPropertyDescriptor(globalThis, 'GPUBufferUsage')
141
- };
142
- const device = fakeDevice();
143
- const context = new FakeResource();
144
- Object.assign(context, {
145
- async createExportableTensor() {
146
- return new FakeResource();
147
- },
148
- async exportToGPU() {
149
- return device.createBuffer({ size: 16 });
150
- },
151
- dispatch() {},
152
- opSupportLimits() {
153
- const operand = { dataTypes: ['float16'] };
154
- return { conv2d: { input: operand, filter: operand, output: operand } };
155
- },
156
- async readTensor() {
157
- return new ArrayBuffer(0);
158
- },
159
- writeTensor() {}
160
- });
161
-
162
- class FakeBuilder {
163
- input() {
164
- return {};
165
- }
166
- constant() {
167
- return {};
168
- }
169
- conv2d() {
170
- return {};
171
- }
172
- relu(value) {
173
- return value;
174
- }
175
- maxPool2d() {
176
- return {};
177
- }
178
- resample2d() {
179
- return {};
180
- }
181
- concat() {
182
- return {};
183
- }
184
- async build(outputs) {
185
- return build ? build(outputs) : new FakeResource();
186
- }
187
- }
188
-
189
- Object.defineProperties(globalThis, {
190
- navigator: {
191
- configurable: true,
192
- value: { ml: { createContext: async () => context } }
193
- },
194
- MLGraphBuilder: { configurable: true, value: FakeBuilder },
195
- GPUBufferUsage: {
196
- configurable: true,
197
- value: {
198
- STORAGE: 1,
199
- COPY_SRC: 2,
200
- COPY_DST: 4,
201
- UNIFORM: 8,
202
- QUERY_RESOLVE: 16,
203
- MAP_READ: 32
204
- }
205
- }
206
- });
207
-
208
- try {
209
- await run({ device, context });
210
- } finally {
211
- for (const [key, descriptor] of Object.entries(previous)) {
212
- const name =
213
- key === 'builder'
214
- ? 'MLGraphBuilder'
215
- : key === 'usage'
216
- ? 'GPUBufferUsage'
217
- : key;
218
- if (descriptor) Object.defineProperty(globalThis, name, descriptor);
219
- else delete globalThis[name];
220
- }
221
- }
222
- }
223
-
224
- test('resource tracker releases each owned handle exactly once', () => {
225
- const tracker = new OIDNResourceTracker();
226
- const buffer = new FakeResource();
227
- tracker.track('gpu-buffer', buffer);
228
- tracker.track('gpu-buffer', buffer);
229
-
230
- assert.deepEqual(tracker.snapshot(), {
231
- live: 1,
232
- created: 1,
233
- destroyed: 0,
234
- peakLive: 1,
235
- pending: 0,
236
- byKind: {
237
- 'gpu-buffer': { created: 1, destroyed: 0, live: 1, peakLive: 1 }
238
- }
239
- });
240
- tracker.release('gpu-buffer', buffer, () => buffer.destroy());
241
- tracker.release('gpu-buffer', buffer, () => buffer.destroy());
242
- assert.equal(buffer.destroyCalls, 1);
243
- assert.equal(tracker.snapshot().live, 0);
244
- assert.equal(tracker.snapshot().created, tracker.snapshot().destroyed);
245
- });
246
-
247
- test('native WGSL shape eviction and dispose release every owned GPU buffer', async () => {
248
- await withFakeWebNN(async ({ device }) => {
249
- const executor = new NativeUNetExecutor(device, testModel(), {
250
- precision: 'fp16',
251
- shapeCacheSize: 2,
252
- kernel: 'direct'
253
- });
254
- const input = new FakeBuffer();
255
- executor.execute([input], 16, 16);
256
- executor.execute([input], 32, 16);
257
- executor.execute([input], 48, 16);
258
- await new Promise((resolve) => setImmediate(resolve));
259
-
260
- const cached = executor.getResourceInfo();
261
- assert.equal(cached.pending, 0);
262
- assert.equal(cached.byKind['gpu-buffer'].live, 10);
263
- assert.equal(cached.byKind['gpu-buffer'].destroyed, 4);
264
-
265
- executor.dispose();
266
- executor.dispose();
267
- const disposed = executor.getResourceInfo();
268
- assert.equal(disposed.pending, 0);
269
- assert.equal(disposed.live, 0);
270
- assert.equal(disposed.created, disposed.destroyed);
271
- assert.ok(device.buffers.every((buffer) => buffer.destroyCalls === 1));
272
- assert.equal(input.destroyCalls, 0);
273
- });
274
- });
275
-
276
- test('native WGSL rolls back a partially allocated shape on failure', async () => {
277
- await withFakeWebNN(async ({ device }) => {
278
- const executor = new NativeUNetExecutor(device, testModel(), {
279
- precision: 'fp16',
280
- kernel: 'direct'
281
- });
282
- const baseline = executor.getResourceInfo().live;
283
- device.failBufferAt = device.createBufferCalls + 3;
284
-
285
- assert.throws(
286
- () => executor.execute([new FakeBuffer()], 16, 16),
287
- /injected buffer allocation failure/
288
- );
289
- assert.equal(executor.getResourceInfo().live, baseline);
290
- executor.dispose();
291
- assert.equal(executor.getResourceInfo().live, 0);
292
- assert.ok(device.buffers.every((buffer) => buffer.destroyCalls === 1));
293
- });
294
- });
295
-
296
- test('WebNN shape cache stays bounded and dispose returns to zero live resources', async () => {
297
- await withFakeWebNN(async ({ device, context }) => {
298
- const executor = new WebNNUNetExecutor(device, testModel(), {
299
- precision: 'fp16',
300
- shapeCacheSize: 2
301
- });
302
- await executor.prepare();
303
- await executor.prewarm([
304
- { width: 16, height: 16 },
305
- { width: 32, height: 16 },
306
- { width: 48, height: 16 }
307
- ]);
308
- await Promise.resolve();
309
-
310
- const cached = executor.getResourceInfo();
311
- assert.equal(cached.pending, 0);
312
- assert.equal(cached.byKind['ml-context'].live, 1);
313
- assert.equal(cached.byKind['ml-graph'].live, 2);
314
- assert.equal(cached.byKind['ml-tensor'].live, 4);
315
- assert.equal(cached.byKind['gpu-buffer'].live, 6);
316
-
317
- executor.dispose();
318
- executor.dispose();
319
- const disposed = executor.getResourceInfo();
320
- assert.equal(disposed.pending, 0);
321
- assert.equal(disposed.live, 0);
322
- assert.equal(disposed.created, disposed.destroyed);
323
- assert.equal(context.destroyCalls, 1);
324
- assert.ok(device.buffers.every((buffer) => buffer.destroyCalls === 1));
325
- });
326
- });
327
-
328
- test('WebNN cleans a graph that finishes after dispose', async () => {
329
- const build = deferred();
330
- await withFakeWebNN(
331
- async ({ device }) => {
332
- const executor = new WebNNUNetExecutor(device, testModel(), {
333
- precision: 'fp16'
334
- });
335
- await executor.prepare();
336
- const prewarm = executor.prewarm([{ width: 16, height: 16 }]);
337
- await Promise.resolve();
338
- assert.equal(executor.getResourceInfo().pending, 1);
339
-
340
- executor.dispose();
341
- build.resolve(new FakeResource());
342
- await assert.rejects(prewarm, /disposed/);
343
-
344
- const resources = executor.getResourceInfo();
345
- assert.equal(resources.pending, 0);
346
- assert.equal(resources.live, 0);
347
- assert.equal(resources.created, resources.destroyed);
348
- },
349
- { build: () => build.promise }
350
- );
351
- });
352
-
353
- test('WebNN releases exported GPU buffers when command setup throws', async () => {
354
- await withFakeWebNN(async ({ device }) => {
355
- const executor = new WebNNUNetExecutor(device, testModel(), {
356
- precision: 'fp16'
357
- });
358
- await executor.prepare();
359
- await executor.prewarm([{ width: 16, height: 16 }]);
360
- const baseline = executor.getResourceInfo().live;
361
- device.failBindGroup = true;
362
-
363
- await assert.rejects(
364
- executor.execute([new FakeBuffer()], 16, 16),
365
- /injected bind group failure/
366
- );
367
- assert.equal(executor.getResourceInfo().live, baseline);
368
- executor.dispose();
369
- assert.equal(executor.getResourceInfo().live, 0);
370
- });
371
- });
372
-
373
- test('WebNN prepare failures destroy the partially initialized context', async () => {
374
- await withFakeWebNN(async ({ device, context }) => {
375
- context.opSupportLimits = () => ({});
376
- const executor = new WebNNUNetExecutor(device, testModel(), {
377
- precision: 'fp16'
378
- });
379
- await assert.rejects(executor.prepare(), /does not support FP16/);
380
- assert.equal(context.destroyCalls, 1);
381
- assert.equal(executor.getResourceInfo().live, 0);
382
- });
383
- });