oidn-webgpu 0.0.0-stage → 0.6.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/CHANGELOG.md +182 -0
- package/LICENSE +21 -0
- package/README.md +89 -2
- package/dist/oidn.js +6140 -0
- package/dist/oidn.umd.cjs +1513 -0
- package/lib/UNet.d.ts +161 -0
- package/lib/UNet.js +536 -0
- package/lib/UNet.js.map +1 -0
- package/lib/WGPUComputePass.d.ts +62 -0
- package/lib/WGPUComputePass.js +278 -0
- package/lib/WGPUComputePass.js.map +1 -0
- package/lib/WGPUFullQuadPass.d.ts +50 -0
- package/lib/WGPUFullQuadPass.js +261 -0
- package/lib/WGPUFullQuadPass.js.map +1 -0
- package/lib/backend.d.ts +1 -0
- package/lib/backend.js +39 -0
- package/lib/backend.js.map +1 -0
- package/lib/finalRgbShader.d.ts +13 -0
- package/lib/finalRgbShader.js +160 -0
- package/lib/finalRgbShader.js.map +1 -0
- package/lib/gpuTypes.d.ts +158 -0
- package/lib/gpuTypes.js +27 -0
- package/lib/gpuTypes.js.map +1 -0
- package/lib/graphOptimizer.d.ts +54 -0
- package/lib/graphOptimizer.js +216 -0
- package/lib/graphOptimizer.js.map +1 -0
- package/lib/hdr.d.ts +31 -0
- package/lib/hdr.js +340 -0
- package/lib/hdr.js.map +1 -0
- package/lib/hdrTransfer.d.ts +22 -0
- package/lib/hdrTransfer.js +128 -0
- package/lib/hdrTransfer.js.map +1 -0
- package/lib/main.d.ts +57 -0
- package/lib/main.js +24 -0
- package/lib/main.js.map +1 -0
- package/lib/modelSpec.d.ts +80 -0
- package/lib/modelSpec.js +270 -0
- package/lib/modelSpec.js.map +1 -0
- package/lib/native/denoise.d.ts +25 -0
- package/lib/native/denoise.js +223 -0
- package/lib/native/denoise.js.map +1 -0
- package/lib/native/program.d.ts +53 -0
- package/lib/native/program.js +310 -0
- package/lib/native/program.js.map +1 -0
- package/lib/native/recordingDevice.d.ts +125 -0
- package/lib/native/recordingDevice.js +247 -0
- package/lib/native/recordingDevice.js.map +1 -0
- package/lib/nativeUNet.d.ts +25 -0
- package/lib/nativeUNet.js +184 -0
- package/lib/nativeUNet.js.map +1 -0
- package/lib/nativeUNetCore.d.ts +202 -0
- package/lib/nativeUNetCore.js +2945 -0
- package/lib/nativeUNetCore.js.map +1 -0
- package/lib/process.d.ts +37 -0
- package/lib/process.js +328 -0
- package/lib/process.js.map +1 -0
- package/lib/resourceTracker.d.ts +26 -0
- package/lib/resourceTracker.js +65 -0
- package/lib/resourceTracker.js.map +1 -0
- package/lib/sanitize.d.ts +15 -0
- package/lib/sanitize.js +28 -0
- package/lib/sanitize.js.map +1 -0
- package/lib/tileScheduler.d.ts +61 -0
- package/lib/tileScheduler.js +199 -0
- package/lib/tileScheduler.js.map +1 -0
- package/lib/tza.d.ts +15 -0
- package/lib/tza.js +114 -0
- package/lib/tza.js.map +1 -0
- package/lib/webnnUNet.d.ts +52 -0
- package/lib/webnnUNet.js +535 -0
- package/lib/webnnUNet.js.map +1 -0
- package/package.json +43 -4
- package/src/UNet.ts +861 -0
- package/src/WGPUComputePass.ts +382 -0
- package/src/WGPUFullQuadPass.ts +348 -0
- package/src/backend.ts +42 -0
- package/src/finalRgbShader.ts +186 -0
- package/src/gpuTypes.ts +179 -0
- package/src/graphOptimizer.ts +301 -0
- package/src/hdr.ts +398 -0
- package/src/hdrTransfer.ts +169 -0
- package/src/main.ts +126 -0
- package/src/modelSpec.ts +414 -0
- package/src/native/core-profile.json +40 -0
- package/src/native/denoise.ts +268 -0
- package/src/native/program.ts +326 -0
- package/src/native/recordingDevice.ts +341 -0
- package/src/nativeUNet.ts +269 -0
- package/src/nativeUNetCore.ts +3735 -0
- package/src/process.ts +368 -0
- package/src/resourceTracker.ts +94 -0
- package/src/sanitize.ts +29 -0
- package/src/tileScheduler.ts +330 -0
- package/src/tza.ts +136 -0
- package/src/webnnUNet.ts +812 -0
package/src/backend.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export async function initWebGPUBackend() {
|
|
2
|
+
if (!navigator.gpu) throw new Error('WebGPU is not available');
|
|
3
|
+
const gpuDescriptor: GPURequestAdapterOptions = {
|
|
4
|
+
powerPreference: 'high-performance'
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
const adapter = await navigator.gpu.requestAdapter(gpuDescriptor);
|
|
8
|
+
if (!adapter) throw new Error('No WebGPU adapter is available');
|
|
9
|
+
const deviceDescriptor: GPUDeviceDescriptor = {};
|
|
10
|
+
|
|
11
|
+
const requiredFeatures: GPUFeatureName[] = [];
|
|
12
|
+
if (adapter.features.has('timestamp-query')) {
|
|
13
|
+
requiredFeatures.push('timestamp-query');
|
|
14
|
+
}
|
|
15
|
+
if (adapter.features.has('bgra8unorm-storage')) {
|
|
16
|
+
requiredFeatures.push('bgra8unorm-storage');
|
|
17
|
+
}
|
|
18
|
+
if (adapter.features.has('shader-f16')) {
|
|
19
|
+
requiredFeatures.push('shader-f16');
|
|
20
|
+
}
|
|
21
|
+
// Experimental: only exposed by Chrome with unsafe WebGPU features enabled.
|
|
22
|
+
for (const feature of ['subgroups', 'chromium-experimental-subgroup-matrix']) {
|
|
23
|
+
if (adapter.features.has(feature)) {
|
|
24
|
+
requiredFeatures.push(feature as GPUFeatureName);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
deviceDescriptor.requiredFeatures = requiredFeatures;
|
|
28
|
+
|
|
29
|
+
const adapterLimits = adapter.limits;
|
|
30
|
+
deviceDescriptor.requiredLimits = {
|
|
31
|
+
maxComputeWorkgroupStorageSize:
|
|
32
|
+
adapterLimits.maxComputeWorkgroupStorageSize,
|
|
33
|
+
maxComputeWorkgroupsPerDimension:
|
|
34
|
+
adapterLimits.maxComputeWorkgroupsPerDimension,
|
|
35
|
+
maxStorageBufferBindingSize: adapterLimits.maxStorageBufferBindingSize,
|
|
36
|
+
maxBufferSize: adapterLimits.maxBufferSize,
|
|
37
|
+
maxComputeWorkgroupSizeX: adapterLimits.maxComputeWorkgroupSizeX,
|
|
38
|
+
maxComputeInvocationsPerWorkgroup:
|
|
39
|
+
adapterLimits.maxComputeInvocationsPerWorkgroup
|
|
40
|
+
};
|
|
41
|
+
return adapter.requestDevice(deviceDescriptor);
|
|
42
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
export type FinalRgbPrecision = 'fp16' | 'fp32';
|
|
2
|
+
export type FinalRgbActivation = 'relu' | 'identity';
|
|
3
|
+
|
|
4
|
+
const WORKGROUP_SIZE = 8;
|
|
5
|
+
const PATCH_SIZE = WORKGROUP_SIZE + 2;
|
|
6
|
+
const KERNEL_ELEMENTS = 3 * 3;
|
|
7
|
+
|
|
8
|
+
/** Shared storage required by the 8x8 final-RGB workgroup. */
|
|
9
|
+
export function sharedMemoryBytes(
|
|
10
|
+
precision: FinalRgbPrecision,
|
|
11
|
+
inputBlocks: number,
|
|
12
|
+
cacheWeights = false
|
|
13
|
+
) {
|
|
14
|
+
const bytesPerScalar = precision === 'fp16' ? 2 : 4;
|
|
15
|
+
const inputVec4Count = PATCH_SIZE * PATCH_SIZE * inputBlocks;
|
|
16
|
+
const weightVec4Count = cacheWeights
|
|
17
|
+
? KERNEL_ELEMENTS * inputBlocks * 4
|
|
18
|
+
: 0;
|
|
19
|
+
const vec4Count = inputVec4Count + weightVec4Count;
|
|
20
|
+
return vec4Count * 4 * bytesPerScalar;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function storageVecType(precision: FinalRgbPrecision) {
|
|
24
|
+
return precision === 'fp16' ? 'vec4<f16>' : 'vec4<f32>';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function shaderPreamble(precision: FinalRgbPrecision) {
|
|
28
|
+
return precision === 'fp16' ? 'enable f16;\n' : '';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function accumulationCode(
|
|
32
|
+
precision: FinalRgbPrecision,
|
|
33
|
+
inputExpression: string,
|
|
34
|
+
weightExpression: string
|
|
35
|
+
) {
|
|
36
|
+
const weight = (lane: number) =>
|
|
37
|
+
`${weightExpression}[weightBase + ${lane}u]`;
|
|
38
|
+
if (precision === 'fp16') {
|
|
39
|
+
return /* wgsl */ `
|
|
40
|
+
let inputValue = vec4<f16>(${inputExpression});
|
|
41
|
+
var partial = vec4<f16>(0.0h);
|
|
42
|
+
partial = fma(${weight(0)}, vec4<f16>(inputValue.x), partial);
|
|
43
|
+
partial = fma(${weight(1)}, vec4<f16>(inputValue.y), partial);
|
|
44
|
+
partial = fma(${weight(2)}, vec4<f16>(inputValue.z), partial);
|
|
45
|
+
partial = fma(${weight(3)}, vec4<f16>(inputValue.w), partial);
|
|
46
|
+
acc += vec4<f32>(partial);
|
|
47
|
+
`;
|
|
48
|
+
}
|
|
49
|
+
return /* wgsl */ `
|
|
50
|
+
let inputValue = vec4<f32>(${inputExpression});
|
|
51
|
+
acc = fma(vec4<f32>(${weight(0)}), vec4<f32>(inputValue.x), acc);
|
|
52
|
+
acc = fma(vec4<f32>(${weight(1)}), vec4<f32>(inputValue.y), acc);
|
|
53
|
+
acc = fma(vec4<f32>(${weight(2)}), vec4<f32>(inputValue.z), acc);
|
|
54
|
+
acc = fma(vec4<f32>(${weight(3)}), vec4<f32>(inputValue.w), acc);
|
|
55
|
+
`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Builds the final three-channel same-padded convolution shader.
|
|
60
|
+
*
|
|
61
|
+
* The bind-group layout and Params block intentionally match createConvShader:
|
|
62
|
+
* input, weights, bias, output, then uniform params. Weights retain the
|
|
63
|
+
* output-major packed ABI, with four vec4 values per (kernel position, input
|
|
64
|
+
* block), one for each input lane.
|
|
65
|
+
*/
|
|
66
|
+
export function createFinalRgbShader(
|
|
67
|
+
precision: FinalRgbPrecision,
|
|
68
|
+
activation: FinalRgbActivation,
|
|
69
|
+
inputBlocks: number,
|
|
70
|
+
cacheWeights = false
|
|
71
|
+
) {
|
|
72
|
+
if (!Number.isInteger(inputBlocks) || inputBlocks < 1) {
|
|
73
|
+
throw new Error(`Final RGB shader requires positive input blocks, got ${inputBlocks}`);
|
|
74
|
+
}
|
|
75
|
+
const inputType = storageVecType(precision);
|
|
76
|
+
const outputType = 'vec4<f32>';
|
|
77
|
+
const inputTileValues = PATCH_SIZE * PATCH_SIZE * inputBlocks;
|
|
78
|
+
const weightTileValues = KERNEL_ELEMENTS * inputBlocks * 4;
|
|
79
|
+
const stored = activation === 'relu'
|
|
80
|
+
? 'max(acc, vec4<f32>(0.0))'
|
|
81
|
+
: 'acc';
|
|
82
|
+
const accumulation = accumulationCode(
|
|
83
|
+
precision,
|
|
84
|
+
'inputTile[patchBase + inputBlock]',
|
|
85
|
+
cacheWeights ? 'weightTile' : 'weights'
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return /* wgsl */ `${shaderPreamble(precision)}
|
|
89
|
+
struct Params {
|
|
90
|
+
inputWidth: u32,
|
|
91
|
+
inputHeight: u32,
|
|
92
|
+
outputWidth: u32,
|
|
93
|
+
outputHeight: u32,
|
|
94
|
+
inputBlocks: u32,
|
|
95
|
+
outputBlocks: u32,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@group(0) @binding(0) var<storage, read> inputData: array<${inputType}>;
|
|
99
|
+
@group(0) @binding(1) var<storage, read> weights: array<${inputType}>;
|
|
100
|
+
@group(0) @binding(2) var<storage, read> bias: array<vec4<f32>>;
|
|
101
|
+
@group(0) @binding(3) var<storage, read_write> outputData: array<${outputType}>;
|
|
102
|
+
@group(0) @binding(4) var<uniform> params: Params;
|
|
103
|
+
|
|
104
|
+
var<workgroup> inputTile: array<${inputType}, ${inputTileValues}>;
|
|
105
|
+
${cacheWeights
|
|
106
|
+
? `var<workgroup> weightTile: array<${inputType}, ${weightTileValues}>;`
|
|
107
|
+
: ''}
|
|
108
|
+
|
|
109
|
+
@compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE}, 1)
|
|
110
|
+
fn main(
|
|
111
|
+
@builtin(local_invocation_id) localId: vec3<u32>,
|
|
112
|
+
@builtin(global_invocation_id) gid: vec3<u32>,
|
|
113
|
+
@builtin(workgroup_id) workgroupId: vec3<u32>
|
|
114
|
+
) {
|
|
115
|
+
let localLinear = localId.y * ${WORKGROUP_SIZE}u + localId.x;
|
|
116
|
+
for (
|
|
117
|
+
var loadIndex = localLinear;
|
|
118
|
+
loadIndex < ${inputTileValues}u;
|
|
119
|
+
loadIndex += ${WORKGROUP_SIZE * WORKGROUP_SIZE}u
|
|
120
|
+
) {
|
|
121
|
+
let tilePixel = loadIndex / ${inputBlocks}u;
|
|
122
|
+
let inputBlock = loadIndex % ${inputBlocks}u;
|
|
123
|
+
let tileX = tilePixel % ${PATCH_SIZE}u;
|
|
124
|
+
let tileY = tilePixel / ${PATCH_SIZE}u;
|
|
125
|
+
let inputX = i32(workgroupId.x * ${WORKGROUP_SIZE}u + tileX) - 1;
|
|
126
|
+
let inputY = i32(workgroupId.y * ${WORKGROUP_SIZE}u + tileY) - 1;
|
|
127
|
+
var value = ${inputType}(0.0);
|
|
128
|
+
if (
|
|
129
|
+
inputX >= 0 && inputX < i32(params.inputWidth) &&
|
|
130
|
+
inputY >= 0 && inputY < i32(params.inputHeight)
|
|
131
|
+
) {
|
|
132
|
+
let inputIndex =
|
|
133
|
+
(u32(inputY) * params.inputWidth + u32(inputX)) *
|
|
134
|
+
${inputBlocks}u + inputBlock;
|
|
135
|
+
value = inputData[inputIndex];
|
|
136
|
+
}
|
|
137
|
+
inputTile[loadIndex] = value;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
${cacheWeights ? ` for (
|
|
141
|
+
var loadIndex = localLinear;
|
|
142
|
+
loadIndex < ${weightTileValues}u;
|
|
143
|
+
loadIndex += ${WORKGROUP_SIZE * WORKGROUP_SIZE}u
|
|
144
|
+
) {
|
|
145
|
+
weightTile[loadIndex] = weights[loadIndex];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
` : ''} // Out-of-range invocations must reach this barrier before returning.
|
|
149
|
+
workgroupBarrier();
|
|
150
|
+
|
|
151
|
+
let outputInBounds =
|
|
152
|
+
gid.x < params.outputWidth &&
|
|
153
|
+
gid.y < params.outputHeight &&
|
|
154
|
+
gid.z < params.outputBlocks;
|
|
155
|
+
if (!outputInBounds) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
var acc = bias[gid.z];
|
|
160
|
+
for (var ky = 0u; ky < 3u; ky++) {
|
|
161
|
+
let inputY = i32(gid.y) + i32(ky) - 1;
|
|
162
|
+
if (inputY < 0 || inputY >= i32(params.inputHeight)) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
for (var kx = 0u; kx < 3u; kx++) {
|
|
166
|
+
let inputX = i32(gid.x) + i32(kx) - 1;
|
|
167
|
+
if (inputX < 0 || inputX >= i32(params.inputWidth)) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
let patchBase =
|
|
171
|
+
((localId.y + ky) * ${PATCH_SIZE}u + localId.x + kx) *
|
|
172
|
+
${inputBlocks}u;
|
|
173
|
+
for (var inputBlock = 0u; inputBlock < ${inputBlocks}u; inputBlock++) {
|
|
174
|
+
let weightBase =
|
|
175
|
+
((ky * 3u + kx) * ${inputBlocks}u + inputBlock) * 4u;
|
|
176
|
+
${accumulation}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let outputIndex =
|
|
182
|
+
(gid.y * params.outputWidth + gid.x) * ${1}u + gid.z;
|
|
183
|
+
outputData[outputIndex] = ${stored};
|
|
184
|
+
}
|
|
185
|
+
`;
|
|
186
|
+
}
|
package/src/gpuTypes.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The WebGPU subset used by the native WGSL runtime, as structural types and
|
|
3
|
+
* spec-defined flag values. A browser GPUDevice implements these operations,
|
|
4
|
+
* but its branded TypeScript types need the conversion in nativeUNet.ts;
|
|
5
|
+
* they also let an embedder supply its own device (for example one that
|
|
6
|
+
* records the work for replay in a native wgpu host) without lib.dom or
|
|
7
|
+
* WebGPU globals.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** GPUBufferUsage flag values from the WebGPU specification. */
|
|
11
|
+
export const BufferUsage = {
|
|
12
|
+
MAP_READ: 0x0001,
|
|
13
|
+
MAP_WRITE: 0x0002,
|
|
14
|
+
COPY_SRC: 0x0004,
|
|
15
|
+
COPY_DST: 0x0008,
|
|
16
|
+
INDEX: 0x0010,
|
|
17
|
+
VERTEX: 0x0020,
|
|
18
|
+
UNIFORM: 0x0040,
|
|
19
|
+
STORAGE: 0x0080,
|
|
20
|
+
INDIRECT: 0x0100,
|
|
21
|
+
QUERY_RESOLVE: 0x0200
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
24
|
+
/** GPUMapMode flag values from the WebGPU specification. */
|
|
25
|
+
export const MapMode = {
|
|
26
|
+
READ: 0x0001,
|
|
27
|
+
WRITE: 0x0002
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
// The synchronous core types exclude promises so scriptc's library mode can
|
|
31
|
+
// compile the core; browser-only code uses the asynchronous extensions below.
|
|
32
|
+
|
|
33
|
+
export interface OIDNGPUBuffer {
|
|
34
|
+
readonly size: number;
|
|
35
|
+
readonly mapState: string;
|
|
36
|
+
getMappedRange(offset?: number, size?: number): ArrayBuffer;
|
|
37
|
+
unmap(): void;
|
|
38
|
+
destroy(): void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface OIDNGPUMappableBuffer extends OIDNGPUBuffer {
|
|
42
|
+
mapAsync(mode: number, offset?: number, size?: number): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface OIDNGPUQuerySet {
|
|
46
|
+
destroy(): void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface OIDNGPUShaderModule {
|
|
50
|
+
readonly label: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface OIDNGPUBindGroupLayout {
|
|
54
|
+
readonly label: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface OIDNGPUBindGroup {
|
|
58
|
+
readonly label: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface OIDNGPUComputePipeline {
|
|
62
|
+
getBindGroupLayout(index: number): OIDNGPUBindGroupLayout;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface OIDNGPUCommandBuffer {
|
|
66
|
+
readonly label: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface OIDNGPUBindGroupEntry {
|
|
70
|
+
binding: number;
|
|
71
|
+
resource: { buffer: OIDNGPUBuffer; offset?: number; size?: number };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface OIDNGPUComputePassTimestampWrites {
|
|
75
|
+
querySet: OIDNGPUQuerySet;
|
|
76
|
+
beginningOfPassWriteIndex?: number;
|
|
77
|
+
endOfPassWriteIndex?: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface OIDNGPUComputePassEncoder {
|
|
81
|
+
setPipeline(pipeline: OIDNGPUComputePipeline): void;
|
|
82
|
+
setBindGroup(index: number, bindGroup: OIDNGPUBindGroup): void;
|
|
83
|
+
dispatchWorkgroups(x: number, y?: number, z?: number): void;
|
|
84
|
+
end(): void;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface OIDNGPUCommandEncoder {
|
|
88
|
+
beginComputePass(descriptor?: {
|
|
89
|
+
label?: string;
|
|
90
|
+
timestampWrites?: OIDNGPUComputePassTimestampWrites;
|
|
91
|
+
}): OIDNGPUComputePassEncoder;
|
|
92
|
+
copyBufferToBuffer(
|
|
93
|
+
source: OIDNGPUBuffer,
|
|
94
|
+
sourceOffset: number,
|
|
95
|
+
destination: OIDNGPUBuffer,
|
|
96
|
+
destinationOffset: number,
|
|
97
|
+
size: number
|
|
98
|
+
): void;
|
|
99
|
+
resolveQuerySet(
|
|
100
|
+
querySet: OIDNGPUQuerySet,
|
|
101
|
+
firstQuery: number,
|
|
102
|
+
queryCount: number,
|
|
103
|
+
destination: OIDNGPUBuffer,
|
|
104
|
+
destinationOffset: number
|
|
105
|
+
): void;
|
|
106
|
+
finish(): OIDNGPUCommandBuffer;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface OIDNGPUQueue {
|
|
110
|
+
submit(commandBuffers: OIDNGPUCommandBuffer[]): void;
|
|
111
|
+
writeBuffer(
|
|
112
|
+
buffer: OIDNGPUBuffer,
|
|
113
|
+
bufferOffset: number,
|
|
114
|
+
data: Uint8Array
|
|
115
|
+
): void;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface OIDNGPUAsyncQueue extends OIDNGPUQueue {
|
|
119
|
+
onSubmittedWorkDone(): Promise<void>;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface OIDNGPULimits {
|
|
123
|
+
readonly maxComputeWorkgroupStorageSize: number;
|
|
124
|
+
readonly maxComputeInvocationsPerWorkgroup: number;
|
|
125
|
+
readonly maxComputeWorkgroupSizeX: number;
|
|
126
|
+
readonly maxComputeWorkgroupSizeY: number;
|
|
127
|
+
readonly maxComputeWorkgroupsPerDimension: number;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface OIDNGPUFeatures {
|
|
131
|
+
has(name: string): boolean;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Subset of GPUAdapterInfo used to select subgroup-matrix configurations. */
|
|
135
|
+
export interface OIDNGPUAdapterInfo {
|
|
136
|
+
readonly subgroupMinSize?: number;
|
|
137
|
+
readonly subgroupMaxSize?: number;
|
|
138
|
+
readonly subgroupMatrixConfigs?: readonly {
|
|
139
|
+
componentType: string;
|
|
140
|
+
resultComponentType: string;
|
|
141
|
+
M: number;
|
|
142
|
+
N: number;
|
|
143
|
+
K: number;
|
|
144
|
+
}[];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface OIDNGPUDevice {
|
|
148
|
+
readonly features: OIDNGPUFeatures;
|
|
149
|
+
readonly limits: OIDNGPULimits;
|
|
150
|
+
readonly queue: OIDNGPUQueue;
|
|
151
|
+
createBuffer(descriptor: {
|
|
152
|
+
label?: string;
|
|
153
|
+
size: number;
|
|
154
|
+
usage: number;
|
|
155
|
+
mappedAtCreation?: boolean;
|
|
156
|
+
}): OIDNGPUBuffer;
|
|
157
|
+
createShaderModule(descriptor: { label?: string; code: string }): OIDNGPUShaderModule;
|
|
158
|
+
createComputePipeline(descriptor: {
|
|
159
|
+
label?: string;
|
|
160
|
+
layout: 'auto';
|
|
161
|
+
compute: { module: OIDNGPUShaderModule; entryPoint: string };
|
|
162
|
+
}): OIDNGPUComputePipeline;
|
|
163
|
+
createBindGroup(descriptor: {
|
|
164
|
+
label?: string;
|
|
165
|
+
layout: OIDNGPUBindGroupLayout;
|
|
166
|
+
entries: OIDNGPUBindGroupEntry[];
|
|
167
|
+
}): OIDNGPUBindGroup;
|
|
168
|
+
createCommandEncoder(descriptor?: { label?: string }): OIDNGPUCommandEncoder;
|
|
169
|
+
createQuerySet(descriptor: { label?: string; type: 'timestamp'; count: number }): OIDNGPUQuerySet;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface OIDNGPUAsyncDevice extends OIDNGPUDevice {
|
|
173
|
+
readonly queue: OIDNGPUAsyncQueue;
|
|
174
|
+
createComputePipelineAsync(descriptor: {
|
|
175
|
+
label?: string;
|
|
176
|
+
layout: 'auto';
|
|
177
|
+
compute: { module: OIDNGPUShaderModule; entryPoint: string };
|
|
178
|
+
}): Promise<OIDNGPUComputePipeline>;
|
|
179
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Conv2DNodeSpec,
|
|
3
|
+
MaxPool2DNodeSpec,
|
|
4
|
+
ModelNodeSpec,
|
|
5
|
+
UNetModelSpec,
|
|
6
|
+
Upsample2DNodeSpec,
|
|
7
|
+
UNetModelGraph
|
|
8
|
+
} from './modelSpec';
|
|
9
|
+
|
|
10
|
+
export interface FusedConvPoolNodeSpec {
|
|
11
|
+
op: 'fusedConvReluMaxPool2d';
|
|
12
|
+
id: string;
|
|
13
|
+
input: string;
|
|
14
|
+
conv: Conv2DNodeSpec;
|
|
15
|
+
pool: MaxPool2DNodeSpec;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface FusedUpsampleConcatConvNodeSpec {
|
|
19
|
+
op: 'fusedUpsampleConcatConv2d';
|
|
20
|
+
id: string;
|
|
21
|
+
/** Inputs stay in concat order because that order selects weight channels. */
|
|
22
|
+
inputs: readonly {
|
|
23
|
+
value: string;
|
|
24
|
+
upsample?: Upsample2DNodeSpec;
|
|
25
|
+
}[];
|
|
26
|
+
conv: Conv2DNodeSpec;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ExecutableModelNode =
|
|
30
|
+
| ModelNodeSpec
|
|
31
|
+
| FusedConvPoolNodeSpec
|
|
32
|
+
| FusedUpsampleConcatConvNodeSpec;
|
|
33
|
+
|
|
34
|
+
export interface GraphOptimizationOptions {
|
|
35
|
+
fuseConvPool?: boolean;
|
|
36
|
+
fuseUpsampleConcatConv?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface OptimizedModelGraph {
|
|
40
|
+
spec: UNetModelSpec;
|
|
41
|
+
nodes: readonly ExecutableModelNode[];
|
|
42
|
+
fusions: {
|
|
43
|
+
convPool: number;
|
|
44
|
+
upsampleConcatConv: number;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function nodeInputs(node: ModelNodeSpec): readonly string[] {
|
|
49
|
+
return node.op === 'concat' ? node.inputs : [node.input];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildConsumers(spec: UNetModelSpec) {
|
|
53
|
+
const consumers = new Map<string, ModelNodeSpec[]>();
|
|
54
|
+
for (const node of spec.nodes) {
|
|
55
|
+
for (const input of nodeInputs(node)) {
|
|
56
|
+
const list = consumers.get(input) ?? [];
|
|
57
|
+
list.push(node);
|
|
58
|
+
consumers.set(input, list);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return consumers;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Callers narrow on `op`; node ids are unique, so they compare consumers by id.
|
|
65
|
+
function onlyConsumer(
|
|
66
|
+
consumers: ReadonlyMap<string, ModelNodeSpec[]>,
|
|
67
|
+
value: string,
|
|
68
|
+
op: ModelNodeSpec['op']
|
|
69
|
+
): ModelNodeSpec | undefined {
|
|
70
|
+
const list = consumers.get(value);
|
|
71
|
+
if (list === undefined || list.length !== 1 || list[0].op !== op) return undefined;
|
|
72
|
+
return list[0];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Applies topology-only fusions. It never depends on a particular OIDN model
|
|
77
|
+
* name, so new descriptors automatically benefit from known graph patterns.
|
|
78
|
+
*/
|
|
79
|
+
export function optimizeModelGraph(
|
|
80
|
+
validated: UNetModelGraph,
|
|
81
|
+
options: GraphOptimizationOptions = {}
|
|
82
|
+
): OptimizedModelGraph {
|
|
83
|
+
const spec = validated.spec;
|
|
84
|
+
const consumers = buildConsumers(spec);
|
|
85
|
+
const nodesById = new Map(spec.nodes.map((node) => [node.id, node]));
|
|
86
|
+
const eliminated = new Set<string>();
|
|
87
|
+
const fusedAt = new Map<string, ExecutableModelNode>();
|
|
88
|
+
let convPool = 0;
|
|
89
|
+
let upsampleConcatConv = 0;
|
|
90
|
+
|
|
91
|
+
if (options.fuseConvPool !== false) {
|
|
92
|
+
for (const node of spec.nodes) {
|
|
93
|
+
if (node.op !== 'conv2d' || node.activation !== 'relu') continue;
|
|
94
|
+
const pool = onlyConsumer(consumers, node.id, 'maxPool2d');
|
|
95
|
+
if (!pool || pool.op !== 'maxPool2d' || pool.size !== 2 || pool.stride !== 2) continue;
|
|
96
|
+
|
|
97
|
+
eliminated.add(node.id);
|
|
98
|
+
fusedAt.set(pool.id, {
|
|
99
|
+
op: 'fusedConvReluMaxPool2d',
|
|
100
|
+
id: pool.id,
|
|
101
|
+
input: node.input,
|
|
102
|
+
conv: node,
|
|
103
|
+
pool
|
|
104
|
+
});
|
|
105
|
+
convPool++;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (options.fuseUpsampleConcatConv !== false) {
|
|
110
|
+
for (const node of spec.nodes) {
|
|
111
|
+
if (node.op !== 'conv2d') continue;
|
|
112
|
+
const concat = nodesById.get(node.input);
|
|
113
|
+
if (concat?.op !== 'concat' || concat.inputs.length !== 2) continue;
|
|
114
|
+
if (onlyConsumer(consumers, concat.id, 'conv2d')?.id !== node.id) continue;
|
|
115
|
+
// The native blocked layout can remove concat only when the source
|
|
116
|
+
// boundary is also a vec4 boundary. Other graphs keep the generic ops.
|
|
117
|
+
const firstInputChannels = validated.channelsByValue.get(concat.inputs[0]);
|
|
118
|
+
if (firstInputChannels === undefined || firstInputChannels % 4 !== 0) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const inputs = concat.inputs.map((value) => {
|
|
123
|
+
const candidate = nodesById.get(value);
|
|
124
|
+
if (
|
|
125
|
+
candidate?.op === 'upsample2d' &&
|
|
126
|
+
candidate.scale === 2 &&
|
|
127
|
+
candidate.mode === 'nearest' &&
|
|
128
|
+
onlyConsumer(consumers, candidate.id, 'concat')?.id === concat.id
|
|
129
|
+
) {
|
|
130
|
+
return { value: candidate.input, upsample: candidate };
|
|
131
|
+
}
|
|
132
|
+
return { value };
|
|
133
|
+
});
|
|
134
|
+
const upsampleCount = inputs.filter((input) => input.upsample).length;
|
|
135
|
+
if (upsampleCount !== 1) continue;
|
|
136
|
+
|
|
137
|
+
eliminated.add(concat.id);
|
|
138
|
+
for (const value of concat.inputs) {
|
|
139
|
+
const candidate = nodesById.get(value);
|
|
140
|
+
if (candidate?.op === 'upsample2d') eliminated.add(candidate.id);
|
|
141
|
+
}
|
|
142
|
+
fusedAt.set(node.id, {
|
|
143
|
+
op: 'fusedUpsampleConcatConv2d',
|
|
144
|
+
id: node.id,
|
|
145
|
+
inputs,
|
|
146
|
+
conv: node
|
|
147
|
+
});
|
|
148
|
+
upsampleConcatConv++;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const nodes: ExecutableModelNode[] = [];
|
|
153
|
+
for (const node of spec.nodes) {
|
|
154
|
+
const fused = fusedAt.get(node.id);
|
|
155
|
+
if (fused) {
|
|
156
|
+
nodes.push(fused);
|
|
157
|
+
} else if (!eliminated.has(node.id)) {
|
|
158
|
+
nodes.push(node);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
spec,
|
|
164
|
+
nodes,
|
|
165
|
+
fusions: { convPool, upsampleConcatConv }
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface ModelValueShape {
|
|
170
|
+
width: number;
|
|
171
|
+
height: number;
|
|
172
|
+
channels: number;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface PlannedModelNode {
|
|
176
|
+
node: ExecutableModelNode;
|
|
177
|
+
outputShape: ModelValueShape;
|
|
178
|
+
/** Last planned node that reads the output; output itself uses nodes.length. */
|
|
179
|
+
lastUse: number;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface ModelExecutionPlan extends OptimizedModelGraph {
|
|
183
|
+
inputShape: ModelValueShape;
|
|
184
|
+
valueShapes: ReadonlyMap<string, ModelValueShape>;
|
|
185
|
+
plannedNodes: readonly PlannedModelNode[];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function executableInputs(node: ExecutableModelNode): readonly string[] {
|
|
189
|
+
if (node.op === 'concat') return node.inputs;
|
|
190
|
+
if (node.op === 'fusedUpsampleConcatConv2d') {
|
|
191
|
+
return node.inputs.map((input) => input.value);
|
|
192
|
+
}
|
|
193
|
+
return [node.input];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function sameSpatialShape(
|
|
197
|
+
left: ModelValueShape,
|
|
198
|
+
right: ModelValueShape
|
|
199
|
+
): boolean {
|
|
200
|
+
return left.width === right.width && left.height === right.height;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Resolve all runtime shapes and value lifetimes before allocating GPU data. */
|
|
204
|
+
export function planModelExecution(
|
|
205
|
+
validated: UNetModelGraph,
|
|
206
|
+
width: number,
|
|
207
|
+
height: number,
|
|
208
|
+
options?: GraphOptimizationOptions
|
|
209
|
+
): ModelExecutionPlan {
|
|
210
|
+
if (!Number.isInteger(width) || width <= 0 || !Number.isInteger(height) || height <= 0) {
|
|
211
|
+
throw new Error(`Invalid model input size ${width}x${height}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const graph = optimizeModelGraph(validated, options);
|
|
215
|
+
const inputShape = { width, height, channels: validated.inputChannels };
|
|
216
|
+
const valueShapes = new Map<string, ModelValueShape>([
|
|
217
|
+
[validated.spec.input, inputShape]
|
|
218
|
+
]);
|
|
219
|
+
const outputShapes: ModelValueShape[] = [];
|
|
220
|
+
|
|
221
|
+
const shapeOf = (value: string, nodeId: string) => {
|
|
222
|
+
const shape = valueShapes.get(value);
|
|
223
|
+
if (!shape) throw new Error(`Planned node ${nodeId} reads missing value ${value}`);
|
|
224
|
+
return shape;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
for (const node of graph.nodes) {
|
|
228
|
+
let outputShape: ModelValueShape;
|
|
229
|
+
if (node.op === 'conv2d') {
|
|
230
|
+
const input = shapeOf(node.input, node.id);
|
|
231
|
+
outputShape = {
|
|
232
|
+
width: input.width,
|
|
233
|
+
height: input.height,
|
|
234
|
+
channels: validated.convChannels.get(node.id)!.outputChannels
|
|
235
|
+
};
|
|
236
|
+
} else if (node.op === 'maxPool2d') {
|
|
237
|
+
const input = shapeOf(node.input, node.id);
|
|
238
|
+
outputShape = {
|
|
239
|
+
width: Math.ceil(input.width / 2),
|
|
240
|
+
height: Math.ceil(input.height / 2),
|
|
241
|
+
channels: input.channels
|
|
242
|
+
};
|
|
243
|
+
} else if (node.op === 'upsample2d') {
|
|
244
|
+
const input = shapeOf(node.input, node.id);
|
|
245
|
+
outputShape = {
|
|
246
|
+
width: input.width * 2,
|
|
247
|
+
height: input.height * 2,
|
|
248
|
+
channels: input.channels
|
|
249
|
+
};
|
|
250
|
+
} else if (node.op === 'concat') {
|
|
251
|
+
const inputs = node.inputs.map((value) => shapeOf(value, node.id));
|
|
252
|
+
if (inputs.some((shape) => !sameSpatialShape(shape, inputs[0]))) {
|
|
253
|
+
throw new Error(`Concat ${node.id} has mismatched spatial shapes`);
|
|
254
|
+
}
|
|
255
|
+
outputShape = {
|
|
256
|
+
width: inputs[0].width,
|
|
257
|
+
height: inputs[0].height,
|
|
258
|
+
channels: inputs.reduce((sum, shape) => sum + shape.channels, 0)
|
|
259
|
+
};
|
|
260
|
+
} else if (node.op === 'fusedConvReluMaxPool2d') {
|
|
261
|
+
const input = shapeOf(node.input, node.id);
|
|
262
|
+
outputShape = {
|
|
263
|
+
width: Math.ceil(input.width / 2),
|
|
264
|
+
height: Math.ceil(input.height / 2),
|
|
265
|
+
channels: validated.convChannels.get(node.conv.id)!.outputChannels
|
|
266
|
+
};
|
|
267
|
+
} else {
|
|
268
|
+
const inputs = node.inputs.map((input) => {
|
|
269
|
+
const source = shapeOf(input.value, node.id);
|
|
270
|
+
return input.upsample
|
|
271
|
+
? { ...source, width: source.width * 2, height: source.height * 2 }
|
|
272
|
+
: source;
|
|
273
|
+
});
|
|
274
|
+
if (inputs.some((shape) => !sameSpatialShape(shape, inputs[0]))) {
|
|
275
|
+
throw new Error(`Fused decoder ${node.id} has mismatched spatial shapes`);
|
|
276
|
+
}
|
|
277
|
+
outputShape = {
|
|
278
|
+
width: inputs[0].width,
|
|
279
|
+
height: inputs[0].height,
|
|
280
|
+
channels: validated.convChannels.get(node.conv.id)!.outputChannels
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
valueShapes.set(node.id, outputShape);
|
|
285
|
+
outputShapes.push(outputShape);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const lastUses = new Map<string, number>();
|
|
289
|
+
graph.nodes.forEach((node, index) => {
|
|
290
|
+
for (const input of executableInputs(node)) lastUses.set(input, index);
|
|
291
|
+
});
|
|
292
|
+
lastUses.set(validated.spec.output, graph.nodes.length);
|
|
293
|
+
|
|
294
|
+
const plannedNodes = graph.nodes.map((node, index) => ({
|
|
295
|
+
node,
|
|
296
|
+
outputShape: outputShapes[index],
|
|
297
|
+
lastUse: lastUses.get(node.id) ?? index
|
|
298
|
+
}));
|
|
299
|
+
|
|
300
|
+
return { ...graph, inputShape, valueShapes, plannedNodes };
|
|
301
|
+
}
|