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
|
@@ -0,0 +1,3735 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BufferUsage,
|
|
3
|
+
MapMode,
|
|
4
|
+
type OIDNGPUAdapterInfo,
|
|
5
|
+
type OIDNGPUBindGroup,
|
|
6
|
+
type OIDNGPUBindGroupEntry,
|
|
7
|
+
type OIDNGPUBuffer,
|
|
8
|
+
type OIDNGPUComputePipeline,
|
|
9
|
+
type OIDNGPUDevice,
|
|
10
|
+
type OIDNGPUQuerySet
|
|
11
|
+
} from './gpuTypes.js';
|
|
12
|
+
import { createFinalRgbShader, sharedMemoryBytes as finalRgbSharedMemoryBytes } from './finalRgbShader.js';
|
|
13
|
+
import {
|
|
14
|
+
optimizeModelGraph,
|
|
15
|
+
planModelExecution,
|
|
16
|
+
type ExecutableModelNode,
|
|
17
|
+
type FusedUpsampleConcatConvNodeSpec,
|
|
18
|
+
type ModelExecutionPlan,
|
|
19
|
+
type ModelValueShape
|
|
20
|
+
} from './graphOptimizer.js';
|
|
21
|
+
import type {
|
|
22
|
+
Conv2DNodeSpec,
|
|
23
|
+
UNetModelGraph,
|
|
24
|
+
ValidatedConvTensor,
|
|
25
|
+
ValidatedUNetModel
|
|
26
|
+
} from './modelSpec';
|
|
27
|
+
import { HostTensor } from './tza.js';
|
|
28
|
+
import {
|
|
29
|
+
OIDNResourceTracker,
|
|
30
|
+
type OIDNResourceSnapshot
|
|
31
|
+
} from './resourceTracker.js';
|
|
32
|
+
|
|
33
|
+
export type NativeUNetPrecision = 'fp32' | 'fp16';
|
|
34
|
+
export type NativeUNetPrecisionSetting = NativeUNetPrecision | 'auto';
|
|
35
|
+
export type NativeUNetKernel =
|
|
36
|
+
| 'direct'
|
|
37
|
+
| 'implicit-gemm'
|
|
38
|
+
| 'spatial'
|
|
39
|
+
| 'subgroup'
|
|
40
|
+
| 'subgroup-matrix';
|
|
41
|
+
export type NativeUNetKernelSetting = NativeUNetKernel | 'auto';
|
|
42
|
+
export type NativeUNetGemmWorkgroup = readonly [4 | 8 | 16, 4 | 8];
|
|
43
|
+
|
|
44
|
+
export interface NativeUNetGemmOptions {
|
|
45
|
+
/** Internal per-layer tile selection; explicit workgroup sizes remain fixed by default. */
|
|
46
|
+
tilePolicy?: 'fixed' | 'output-aligned';
|
|
47
|
+
/** Internal decoder source-branch scheduling experiment. */
|
|
48
|
+
decoderLoad?: 'per-load' | 'source-first';
|
|
49
|
+
/** Sub-pixel GEMM decoder experiment; defaults to FP16 with K-major weights. */
|
|
50
|
+
decoderSubPixel?: boolean;
|
|
51
|
+
/** Expand register-tile rows for native shader robustness; off by default in browsers. */
|
|
52
|
+
unrollRows?: boolean;
|
|
53
|
+
/** Internal pooling dispatch experiment for the GEMM execution path. */
|
|
54
|
+
poolLayout?: 'spatial' | 'channels';
|
|
55
|
+
/** Internal shared-memory padding experiment; does not change storage ABI. */
|
|
56
|
+
sharedLayout?: 'linear' | 'padded' | 'padded-input' | 'padded-weights';
|
|
57
|
+
/** Internal loop scheduling experiment; each output keeps the same K order. */
|
|
58
|
+
accumulationOrder?: 'k-major' | 'row-major';
|
|
59
|
+
/** Internal final-convolution experiments; retains the original FP16 partial order. */
|
|
60
|
+
finalLayer?: 'direct' | 'shared-input' | 'shared-input-weights' | 'shared-auto';
|
|
61
|
+
/** Internal load-layout experiments; packed loads preserve the FP16 storage bits. */
|
|
62
|
+
loadMode?: 'native' | 'packed-weights' | 'packed-all';
|
|
63
|
+
/** Incremental addressing (default), or the original analytic path for A/B comparisons. */
|
|
64
|
+
addressMode?: 'analytic' | 'incremental' | 'base-offset';
|
|
65
|
+
/** K-major (default) coalesces neighboring output-block loads; output-major preserves the old ABI. */
|
|
66
|
+
weightLayout?: 'output-major' | 'k-major';
|
|
67
|
+
/** Register tile height (default 8). The K reduction grouping stays fixed. */
|
|
68
|
+
rowsPerThread?: 2 | 4 | 8;
|
|
69
|
+
/** Workgroup width/height (default [8, 8]); width also selects output channel blocks. */
|
|
70
|
+
workgroupSize?: NativeUNetGemmWorkgroup;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Square multiply-add shape selected from `chromium-experimental-subgroup-matrix`. */
|
|
74
|
+
export interface NativeSubgroupMatrixConfig {
|
|
75
|
+
componentType: 'f16' | 'f32';
|
|
76
|
+
resultType: 'f16' | 'f32';
|
|
77
|
+
/** M = N = K. */
|
|
78
|
+
size: 8 | 16;
|
|
79
|
+
subgroupSize: number;
|
|
80
|
+
/**
|
|
81
|
+
* WGSL spelling: Chrome's `chromium_experimental_subgroup_matrix`, or
|
|
82
|
+
* wgpu/Naga's `wgpu_cooperative_matrix` for native hosts.
|
|
83
|
+
*/
|
|
84
|
+
dialect: 'tint' | 'naga';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Experimental subgroup-matrix tile tuning; not covered by semver. */
|
|
88
|
+
export interface NativeSubgroupMatrixOptions {
|
|
89
|
+
/** Output channel fragments per workgroup (default 2). */
|
|
90
|
+
outputFragments?: 1 | 2 | 4;
|
|
91
|
+
/** Preferred accumulation type when the adapter offers several. */
|
|
92
|
+
accumulation?: 'f32' | 'f16';
|
|
93
|
+
/** Sub-pixel decomposition of the upsampling decoder layers (default true). */
|
|
94
|
+
subPixel?: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Double-buffered staging with register prefetch when the workgroup memory
|
|
97
|
+
* limit allows it (default false: 6-10% slower on an Apple M4 Max).
|
|
98
|
+
*/
|
|
99
|
+
doubleBuffer?: boolean;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface NativeUNetOptions {
|
|
103
|
+
precision?: NativeUNetPrecisionSetting;
|
|
104
|
+
/** Experimental `subgroup-matrix` kernel tuning. */
|
|
105
|
+
subgroupMatrix?: NativeSubgroupMatrixOptions;
|
|
106
|
+
/**
|
|
107
|
+
* Convolution kernel selection. `auto` uses implicit GEMM for FP16/FP32
|
|
108
|
+
* convolutions and the direct kernel for the final output layer.
|
|
109
|
+
*/
|
|
110
|
+
kernel?: NativeUNetKernelSetting;
|
|
111
|
+
/** Optional implicit-GEMM tuning; other convolution kernels ignore it. */
|
|
112
|
+
gemm?: NativeUNetGemmOptions;
|
|
113
|
+
/** Maximum number of shape-dependent activation plans retained. */
|
|
114
|
+
shapeCacheSize?: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface NativeUNetLayerTiming {
|
|
118
|
+
id: string;
|
|
119
|
+
durationMs: number;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface NativeUNetExecutionProfile {
|
|
123
|
+
totalMs: number;
|
|
124
|
+
layers: NativeUNetLayerTiming[];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
interface PackedConvBuffers {
|
|
128
|
+
weights: OIDNGPUBuffer;
|
|
129
|
+
bias: OIDNGPUBuffer;
|
|
130
|
+
weightLayout: NonNullable<NativeUNetGemmOptions['weightLayout']>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface NativePipelineSpec {
|
|
134
|
+
key: string;
|
|
135
|
+
code: string;
|
|
136
|
+
kernel?: NativeUNetKernel;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
interface ActivationSlot {
|
|
140
|
+
buffer: OIDNGPUBuffer;
|
|
141
|
+
capacity: number;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface CachedExecution {
|
|
145
|
+
plan: ModelExecutionPlan;
|
|
146
|
+
valueBuffers: Map<string, OIDNGPUBuffer>;
|
|
147
|
+
slots: ActivationSlot[];
|
|
148
|
+
nodeBindings: OIDNGPUBindGroup[];
|
|
149
|
+
nodePipelines: OIDNGPUComputePipeline[];
|
|
150
|
+
nodeKernels: NativeUNetKernel[];
|
|
151
|
+
/** Subgroup-matrix output fragments per node (0 for other kernels). */
|
|
152
|
+
nodeFragments: number[];
|
|
153
|
+
inputPipeline: OIDNGPUComputePipeline;
|
|
154
|
+
inputUniform: OIDNGPUBuffer;
|
|
155
|
+
ownedBuffers: OIDNGPUBuffer[];
|
|
156
|
+
cpuInputBuffers?: OIDNGPUBuffer[];
|
|
157
|
+
cpuReadbackBuffer?: OIDNGPUBuffer;
|
|
158
|
+
lastUsed: number;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const pipelineCachesByDevice = new WeakMap<OIDNGPUDevice, Map<string, OIDNGPUComputePipeline>>();
|
|
162
|
+
|
|
163
|
+
function sharedPipelineCache(device: OIDNGPUDevice) {
|
|
164
|
+
let cache = pipelineCachesByDevice.get(device);
|
|
165
|
+
if (!cache) {
|
|
166
|
+
cache = new Map();
|
|
167
|
+
pipelineCachesByDevice.set(device, cache);
|
|
168
|
+
}
|
|
169
|
+
return cache;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const WORKGROUP_SIZE = 8;
|
|
173
|
+
const TILED_CONV_K_BLOCKS = 8;
|
|
174
|
+
const SPATIAL_CONV_WORKGROUP = 8;
|
|
175
|
+
const SPATIAL_CONV_PATCH = SPATIAL_CONV_WORKGROUP + 2;
|
|
176
|
+
|
|
177
|
+
function gemmTile(options: Readonly<Required<NativeUNetGemmOptions>>) {
|
|
178
|
+
const [workgroupX, workgroupY] = options.workgroupSize;
|
|
179
|
+
return {
|
|
180
|
+
workgroupX,
|
|
181
|
+
workgroupY,
|
|
182
|
+
rowsPerThread: options.rowsPerThread,
|
|
183
|
+
tileM: workgroupY * options.rowsPerThread,
|
|
184
|
+
tileNBlocks: workgroupX,
|
|
185
|
+
// Keep the half partial accumulation grouping independent of tile tuning.
|
|
186
|
+
tileKBlocks: TILED_CONV_K_BLOCKS
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Holes change only workgroup addresses. Cooperative loads still visit the
|
|
191
|
+
// original dense logical tiles and never read the unused padding elements.
|
|
192
|
+
function gemmSharedSizes(options: Readonly<Required<NativeUNetGemmOptions>>) {
|
|
193
|
+
const tile = gemmTile(options);
|
|
194
|
+
const paddedInput = options.sharedLayout === 'padded' || options.sharedLayout === 'padded-input';
|
|
195
|
+
const paddedWeights = options.sharedLayout === 'padded' || options.sharedLayout === 'padded-weights';
|
|
196
|
+
return {
|
|
197
|
+
input: tile.tileM * tile.tileKBlocks + (paddedInput ? tile.workgroupY : 0),
|
|
198
|
+
weights: tile.tileKBlocks * tile.tileNBlocks * (paddedWeights ? 5 : 4)
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function gemmSharedInputIndex(index: string, options: Readonly<Required<NativeUNetGemmOptions>>) {
|
|
203
|
+
return options.sharedLayout === 'padded' || options.sharedLayout === 'padded-input'
|
|
204
|
+
? `(${index}) + (${index}) / ${options.rowsPerThread * TILED_CONV_K_BLOCKS}u`
|
|
205
|
+
: index;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function gemmSharedWeightIndex(index: string, options: Readonly<Required<NativeUNetGemmOptions>>) {
|
|
209
|
+
return options.sharedLayout === 'padded' || options.sharedLayout === 'padded-weights'
|
|
210
|
+
? `(${index}) + (${index}) / 4u`
|
|
211
|
+
: index;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function roundUp(value: number, alignment: number) {
|
|
215
|
+
return Math.ceil(value / alignment) * alignment;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function blocksForChannels(channels: number) {
|
|
219
|
+
return Math.ceil(channels / 4);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function activationByteSize(
|
|
223
|
+
shape: ModelValueShape,
|
|
224
|
+
bytesPerScalar: number
|
|
225
|
+
) {
|
|
226
|
+
return (
|
|
227
|
+
shape.width *
|
|
228
|
+
shape.height *
|
|
229
|
+
blocksForChannels(shape.channels) *
|
|
230
|
+
4 *
|
|
231
|
+
bytesPerScalar
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let float16ToFloat32Lookup: Float32Array | undefined;
|
|
236
|
+
|
|
237
|
+
function halfBitsToNumber(bits: number) {
|
|
238
|
+
const sign = bits & 0x8000 ? -1 : 1;
|
|
239
|
+
const exponent = (bits >>> 10) & 0x1f;
|
|
240
|
+
const fraction = bits & 0x3ff;
|
|
241
|
+
if (exponent === 0) {
|
|
242
|
+
return sign * fraction * 2 ** -24;
|
|
243
|
+
}
|
|
244
|
+
if (exponent === 0x1f) {
|
|
245
|
+
return fraction === 0 ? sign * Infinity : NaN;
|
|
246
|
+
}
|
|
247
|
+
return sign * (1 + fraction / 1024) * 2 ** (exponent - 15);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const halfConversionView = new DataView(new ArrayBuffer(4));
|
|
251
|
+
|
|
252
|
+
/** IEEE binary16 bits of a number, rounding to nearest even like Float16Array. */
|
|
253
|
+
function numberToHalfBits(value: number) {
|
|
254
|
+
halfConversionView.setFloat32(0, value);
|
|
255
|
+
const bits = halfConversionView.getUint32(0);
|
|
256
|
+
const sign = (bits >>> 16) & 0x8000;
|
|
257
|
+
const exponent = (bits >>> 23) & 0xff;
|
|
258
|
+
const mantissa = bits & 0x7fffff;
|
|
259
|
+
if (exponent === 0xff) {
|
|
260
|
+
return sign | 0x7c00 | (mantissa ? 0x200 : 0);
|
|
261
|
+
}
|
|
262
|
+
// Rounds through float32 first; this matches direct rounding except for rare
|
|
263
|
+
// double-rounding ties. Official OIDN weights are already binary16.
|
|
264
|
+
const halfExponent = exponent - 127 + 15;
|
|
265
|
+
if (halfExponent >= 0x1f) return sign | 0x7c00;
|
|
266
|
+
if (halfExponent <= 0) {
|
|
267
|
+
if (halfExponent < -10) return sign;
|
|
268
|
+
const full = mantissa | 0x800000;
|
|
269
|
+
const shift = 14 - halfExponent;
|
|
270
|
+
const half = full >>> shift;
|
|
271
|
+
const remainder = full & ((1 << shift) - 1);
|
|
272
|
+
const midpoint = 1 << (shift - 1);
|
|
273
|
+
const rounded = remainder > midpoint || (remainder === midpoint && (half & 1)) ? half + 1 : half;
|
|
274
|
+
return sign | rounded;
|
|
275
|
+
}
|
|
276
|
+
const half = (halfExponent << 10) | (mantissa >>> 13);
|
|
277
|
+
const remainder = mantissa & 0x1fff;
|
|
278
|
+
// A carry out of the mantissa correctly increments the exponent.
|
|
279
|
+
return sign | (remainder > 0x1000 || (remainder === 0x1000 && (half & 1)) ? half + 1 : half);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function halfLookup() {
|
|
283
|
+
if (!float16ToFloat32Lookup) {
|
|
284
|
+
float16ToFloat32Lookup = new Float32Array(1 << 16);
|
|
285
|
+
for (let bits = 0; bits < float16ToFloat32Lookup.length; bits++) {
|
|
286
|
+
float16ToFloat32Lookup[bits] = halfBitsToNumber(bits);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return float16ToFloat32Lookup;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function tensorFloat32Values(tensor: HostTensor): Float32Array {
|
|
293
|
+
if (tensor.desc.dataType === 'Float32') {
|
|
294
|
+
return new Float32Array(
|
|
295
|
+
tensor.data.buffer,
|
|
296
|
+
tensor.data.byteOffset,
|
|
297
|
+
tensor.data.byteLength / 4
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
const bits = new Uint16Array(
|
|
301
|
+
tensor.data.buffer,
|
|
302
|
+
tensor.data.byteOffset,
|
|
303
|
+
tensor.data.byteLength / 2
|
|
304
|
+
);
|
|
305
|
+
const values = new Float32Array(bits.length);
|
|
306
|
+
if (bits.length < 4096) {
|
|
307
|
+
for (let index = 0; index < bits.length; index++) {
|
|
308
|
+
values[index] = halfBitsToNumber(bits[index]);
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
const lookup = halfLookup();
|
|
312
|
+
for (let index = 0; index < bits.length; index++) {
|
|
313
|
+
values[index] = lookup[bits[index]];
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return values;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function createMappedBuffer(
|
|
320
|
+
device: OIDNGPUDevice,
|
|
321
|
+
label: string,
|
|
322
|
+
data: Uint8Array,
|
|
323
|
+
usage: number
|
|
324
|
+
) {
|
|
325
|
+
const size = roundUp(data.byteLength, 4);
|
|
326
|
+
const buffer = device.createBuffer({
|
|
327
|
+
label,
|
|
328
|
+
size,
|
|
329
|
+
usage,
|
|
330
|
+
mappedAtCreation: true
|
|
331
|
+
});
|
|
332
|
+
new Uint8Array(buffer.getMappedRange()).set(data);
|
|
333
|
+
buffer.unmap();
|
|
334
|
+
return buffer;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function createUniformBuffer(
|
|
338
|
+
device: OIDNGPUDevice,
|
|
339
|
+
label: string,
|
|
340
|
+
values: readonly number[]
|
|
341
|
+
) {
|
|
342
|
+
const data = new Uint32Array(roundUp(values.length, 4));
|
|
343
|
+
data.set(values);
|
|
344
|
+
return createMappedBuffer(
|
|
345
|
+
device,
|
|
346
|
+
label,
|
|
347
|
+
new Uint8Array(data.buffer, data.byteOffset, data.byteLength),
|
|
348
|
+
BufferUsage.UNIFORM
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function packConvTensors(
|
|
353
|
+
device: OIDNGPUDevice,
|
|
354
|
+
id: string,
|
|
355
|
+
tensors: ValidatedConvTensor,
|
|
356
|
+
precision: NativeUNetPrecision,
|
|
357
|
+
weightLayout: NonNullable<NativeUNetGemmOptions['weightLayout']>
|
|
358
|
+
): PackedConvBuffers {
|
|
359
|
+
const inputBlocks = blocksForChannels(tensors.inputChannels);
|
|
360
|
+
const outputBlocks = blocksForChannels(tensors.outputChannels);
|
|
361
|
+
const packedWeightCount =
|
|
362
|
+
outputBlocks *
|
|
363
|
+
tensors.kernelHeight *
|
|
364
|
+
tensors.kernelWidth *
|
|
365
|
+
inputBlocks *
|
|
366
|
+
4 *
|
|
367
|
+
4;
|
|
368
|
+
const canCopyHalfBits =
|
|
369
|
+
precision === 'fp16' && tensors.weight.desc.dataType === 'Float16';
|
|
370
|
+
const halfWeights = precision === 'fp16'
|
|
371
|
+
? new Uint16Array(packedWeightCount)
|
|
372
|
+
: undefined;
|
|
373
|
+
const floatWeights = halfWeights
|
|
374
|
+
? undefined
|
|
375
|
+
: new Float32Array(packedWeightCount);
|
|
376
|
+
const sourceHalfBits = canCopyHalfBits
|
|
377
|
+
? new Uint16Array(
|
|
378
|
+
tensors.weight.data.buffer,
|
|
379
|
+
tensors.weight.data.byteOffset,
|
|
380
|
+
tensors.weight.data.byteLength / 2
|
|
381
|
+
)
|
|
382
|
+
: undefined;
|
|
383
|
+
const sourceValues = sourceHalfBits
|
|
384
|
+
? undefined
|
|
385
|
+
: tensorFloat32Values(tensors.weight);
|
|
386
|
+
|
|
387
|
+
for (let outputBlock = 0; outputBlock < outputBlocks; outputBlock++) {
|
|
388
|
+
for (let y = 0; y < tensors.kernelHeight; y++) {
|
|
389
|
+
for (let x = 0; x < tensors.kernelWidth; x++) {
|
|
390
|
+
for (let inputBlock = 0; inputBlock < inputBlocks; inputBlock++) {
|
|
391
|
+
for (let outputLane = 0; outputLane < 4; outputLane++) {
|
|
392
|
+
const outputChannel = outputBlock * 4 + outputLane;
|
|
393
|
+
for (let inputLane = 0; inputLane < 4; inputLane++) {
|
|
394
|
+
const inputChannel = inputBlock * 4 + inputLane;
|
|
395
|
+
const packedBlock = weightLayout === 'k-major'
|
|
396
|
+
? ((((y * tensors.kernelWidth + x) * inputBlocks + inputBlock) *
|
|
397
|
+
outputBlocks + outputBlock) * 16)
|
|
398
|
+
: ((((outputBlock * tensors.kernelHeight + y) *
|
|
399
|
+
tensors.kernelWidth +
|
|
400
|
+
x) *
|
|
401
|
+
inputBlocks +
|
|
402
|
+
inputBlock) *
|
|
403
|
+
16);
|
|
404
|
+
// One vec4 contains the four output lanes for an input lane.
|
|
405
|
+
// Both layouts preserve the vec4's output lanes and the
|
|
406
|
+
// convolution's reduction order; only the vec4 address changes.
|
|
407
|
+
const packedIndex =
|
|
408
|
+
packedBlock + inputLane * 4 + outputLane;
|
|
409
|
+
if (
|
|
410
|
+
outputChannel < tensors.outputChannels &&
|
|
411
|
+
inputChannel < tensors.inputChannels
|
|
412
|
+
) {
|
|
413
|
+
const sourceIndex =
|
|
414
|
+
((outputChannel * tensors.inputChannels + inputChannel) *
|
|
415
|
+
tensors.kernelHeight +
|
|
416
|
+
y) *
|
|
417
|
+
tensors.kernelWidth +
|
|
418
|
+
x;
|
|
419
|
+
if (halfWeights) {
|
|
420
|
+
halfWeights[packedIndex] = sourceHalfBits
|
|
421
|
+
? sourceHalfBits[sourceIndex]
|
|
422
|
+
: numberToHalfBits(sourceValues![sourceIndex]);
|
|
423
|
+
} else {
|
|
424
|
+
floatWeights![packedIndex] = sourceValues![sourceIndex];
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Bias stays f32 even for half activations because convolution accumulates
|
|
435
|
+
// in f32. Padded lanes are zero and never escape the final three channels.
|
|
436
|
+
const packedBias = new Float32Array(outputBlocks * 4);
|
|
437
|
+
packedBias.set(tensorFloat32Values(tensors.bias));
|
|
438
|
+
|
|
439
|
+
const weights = createMappedBuffer(
|
|
440
|
+
device,
|
|
441
|
+
`oidn/${id}/weights/${precision}/${weightLayout}`,
|
|
442
|
+
halfWeights
|
|
443
|
+
? new Uint8Array(halfWeights.buffer)
|
|
444
|
+
: new Uint8Array(floatWeights!.buffer),
|
|
445
|
+
BufferUsage.STORAGE
|
|
446
|
+
);
|
|
447
|
+
try {
|
|
448
|
+
return {
|
|
449
|
+
weights,
|
|
450
|
+
weightLayout,
|
|
451
|
+
bias: createMappedBuffer(
|
|
452
|
+
device,
|
|
453
|
+
`oidn/${id}/bias`,
|
|
454
|
+
new Uint8Array(packedBias.buffer),
|
|
455
|
+
BufferUsage.STORAGE
|
|
456
|
+
)
|
|
457
|
+
};
|
|
458
|
+
} catch (error) {
|
|
459
|
+
weights.destroy();
|
|
460
|
+
throw error;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function storageVecType(precision: NativeUNetPrecision) {
|
|
465
|
+
return precision === 'fp16' ? 'vec4<f16>' : 'vec4<f32>';
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function shaderPreamble(precision: NativeUNetPrecision) {
|
|
469
|
+
return precision === 'fp16' ? 'enable f16;\n' : '';
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function storeExpression(
|
|
473
|
+
expression: string,
|
|
474
|
+
outputPrecision: NativeUNetPrecision
|
|
475
|
+
) {
|
|
476
|
+
return outputPrecision === 'fp16'
|
|
477
|
+
? `vec4<f16>(${expression})`
|
|
478
|
+
: expression;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function activationExpression(
|
|
482
|
+
expression: string,
|
|
483
|
+
activation: Conv2DNodeSpec['activation']
|
|
484
|
+
) {
|
|
485
|
+
return activation === 'relu'
|
|
486
|
+
? `max(${expression}, vec4<f32>(0.0))`
|
|
487
|
+
: expression;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function accumulationCode(
|
|
491
|
+
inputExpression: string,
|
|
492
|
+
weightBase: string,
|
|
493
|
+
precision: NativeUNetPrecision
|
|
494
|
+
) {
|
|
495
|
+
if (precision === 'fp32') {
|
|
496
|
+
return /* wgsl */ `
|
|
497
|
+
let inputValue = vec4<f32>(${inputExpression});
|
|
498
|
+
let weightBase = ${weightBase};
|
|
499
|
+
acc = fma(vec4<f32>(weights[weightBase]), vec4<f32>(inputValue.x), acc);
|
|
500
|
+
acc = fma(vec4<f32>(weights[weightBase + 1u]), vec4<f32>(inputValue.y), acc);
|
|
501
|
+
acc = fma(vec4<f32>(weights[weightBase + 2u]), vec4<f32>(inputValue.z), acc);
|
|
502
|
+
acc = fma(vec4<f32>(weights[weightBase + 3u]), vec4<f32>(inputValue.w), acc);
|
|
503
|
+
`;
|
|
504
|
+
}
|
|
505
|
+
return /* wgsl */ `
|
|
506
|
+
let inputValue = vec4<f16>(${inputExpression});
|
|
507
|
+
let weightBase = ${weightBase};
|
|
508
|
+
var partial = vec4<f16>(0.0h);
|
|
509
|
+
partial = fma(weights[weightBase], vec4<f16>(inputValue.x), partial);
|
|
510
|
+
partial = fma(weights[weightBase + 1u], vec4<f16>(inputValue.y), partial);
|
|
511
|
+
partial = fma(weights[weightBase + 2u], vec4<f16>(inputValue.z), partial);
|
|
512
|
+
partial = fma(weights[weightBase + 3u], vec4<f16>(inputValue.w), partial);
|
|
513
|
+
acc += vec4<f32>(partial);
|
|
514
|
+
`;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function subgroupAccumulationCode(
|
|
518
|
+
inputExpression: string,
|
|
519
|
+
weightBase: string,
|
|
520
|
+
precision: NativeUNetPrecision
|
|
521
|
+
) {
|
|
522
|
+
const inputType = precision === 'fp16' ? 'vec4<f16>' : 'vec4<f32>';
|
|
523
|
+
const accumulator = precision === 'fp16'
|
|
524
|
+
? `var partial = vec4<f16>(0.0h);
|
|
525
|
+
partial = fma(subgroupBroadcast(weights[weightBase], 0u), ${inputType}(inputValue.x), partial);
|
|
526
|
+
partial = fma(subgroupBroadcast(weights[weightBase + 1u], 0u), ${inputType}(inputValue.y), partial);
|
|
527
|
+
partial = fma(subgroupBroadcast(weights[weightBase + 2u], 0u), ${inputType}(inputValue.z), partial);
|
|
528
|
+
partial = fma(subgroupBroadcast(weights[weightBase + 3u], 0u), ${inputType}(inputValue.w), partial);
|
|
529
|
+
acc += vec4<f32>(partial);`
|
|
530
|
+
: `acc = fma(subgroupBroadcast(weights[weightBase], 0u), vec4<f32>(inputValue.x), acc);
|
|
531
|
+
acc = fma(subgroupBroadcast(weights[weightBase + 1u], 0u), vec4<f32>(inputValue.y), acc);
|
|
532
|
+
acc = fma(subgroupBroadcast(weights[weightBase + 2u], 0u), vec4<f32>(inputValue.z), acc);
|
|
533
|
+
acc = fma(subgroupBroadcast(weights[weightBase + 3u], 0u), vec4<f32>(inputValue.w), acc);`;
|
|
534
|
+
return /* wgsl */ `
|
|
535
|
+
let inputValue = ${inputType}(${inputExpression});
|
|
536
|
+
let weightBase = ${weightBase};
|
|
537
|
+
${accumulator}
|
|
538
|
+
`;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function gemmPackedLoad(
|
|
542
|
+
precision: NativeUNetPrecision,
|
|
543
|
+
gemm: Readonly<Required<NativeUNetGemmOptions>>,
|
|
544
|
+
weights: boolean
|
|
545
|
+
) {
|
|
546
|
+
return precision === 'fp16' &&
|
|
547
|
+
(gemm.loadMode === 'packed-all' ||
|
|
548
|
+
(weights && gemm.loadMode === 'packed-weights'));
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// vec2<u32> reinterprets exactly the same eight bytes as vec4<f16>.
|
|
552
|
+
function gemmLoadExpression(expression: string, packed: boolean) {
|
|
553
|
+
return packed ? `bitcast<vec4<f16>>(${expression})` : expression;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function tiledAccumulationCode(precision: NativeUNetPrecision, gemm: Readonly<Required<NativeUNetGemmOptions>>) {
|
|
557
|
+
const { rowsPerThread, tileNBlocks, tileKBlocks } = gemmTile(gemm);
|
|
558
|
+
if (!gemm.unrollRows || gemm.accumulationOrder === 'row-major') {
|
|
559
|
+
return rolledAccumulationCode(precision, gemm);
|
|
560
|
+
}
|
|
561
|
+
const weightBase = `(tileK * ${tileNBlocks}u + localId.x) * ${gemm.sharedLayout === 'padded' || gemm.sharedLayout === 'padded-weights' ? 5 : 4}u`;
|
|
562
|
+
// Constant register-tile indices avoid robust dynamic private-array
|
|
563
|
+
// accesses in Naga. Leave the K loop rolled to limit shader/register size;
|
|
564
|
+
// every row keeps the same four FMAs and the same FP16 fold boundaries.
|
|
565
|
+
const rows = Array.from({ length: rowsPerThread }, (_, row) => row);
|
|
566
|
+
const type = storageVecType(precision);
|
|
567
|
+
return /* wgsl */ `
|
|
568
|
+
${precision === 'fp16' ? rows.map((row) => `var partial${row} = vec4<f16>(0.0h);`).join('\n ') : ''}
|
|
569
|
+
for (var tileK = 0u; tileK < ${tileKBlocks}u; tileK++) {
|
|
570
|
+
let weightBase = ${weightBase};
|
|
571
|
+
let w0 = weightTile[weightBase];
|
|
572
|
+
let w1 = weightTile[weightBase + 1u];
|
|
573
|
+
let w2 = weightTile[weightBase + 2u];
|
|
574
|
+
let w3 = weightTile[weightBase + 3u];
|
|
575
|
+
${rows.map((row) => {
|
|
576
|
+
const index = gemmSharedInputIndex(`(localId.y * ${rowsPerThread}u + ${row}u) * ${tileKBlocks}u + tileK`, gemm);
|
|
577
|
+
const target = precision === 'fp16' ? `partial${row}` : `acc[${row}]`;
|
|
578
|
+
return `let v${row} = inputTile[${index}];\n ` +
|
|
579
|
+
['x', 'y', 'z', 'w'].map((lane, k) => `${target} = fma(w${k}, ${type}(v${row}.${lane}), ${target});`).join('\n ');
|
|
580
|
+
}).join('\n ')}
|
|
581
|
+
}
|
|
582
|
+
${precision === 'fp16' ? rows.map((row) => `acc[${row}] += vec4<f32>(partial${row});`).join('\n ') : ''}
|
|
583
|
+
`;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function rolledAccumulationCode(precision: NativeUNetPrecision, gemm: Readonly<Required<NativeUNetGemmOptions>>) {
|
|
587
|
+
const { rowsPerThread, tileNBlocks, tileKBlocks } = gemmTile(gemm);
|
|
588
|
+
const inputIndex = gemmSharedInputIndex(`tileSpatial * ${tileKBlocks}u + tileK`, gemm);
|
|
589
|
+
const weightBase = `(tileK * ${tileNBlocks}u + localId.x) * ${gemm.sharedLayout === 'padded' || gemm.sharedLayout === 'padded-weights' ? 5 : 4}u`;
|
|
590
|
+
if (gemm.accumulationOrder === 'row-major') {
|
|
591
|
+
const valueType = storageVecType(precision);
|
|
592
|
+
const target = precision === 'fp16' ? 'partial' : 'acc[row]';
|
|
593
|
+
return /* wgsl */ `
|
|
594
|
+
for (var row = 0u; row < ${rowsPerThread}u; row++) {
|
|
595
|
+
${precision === 'fp16' ? 'var partial = vec4<f16>(0.0h);' : ''}
|
|
596
|
+
let tileSpatial = localId.y * ${rowsPerThread}u + row;
|
|
597
|
+
for (var tileK = 0u; tileK < ${tileKBlocks}u; tileK++) {
|
|
598
|
+
let weightBase = ${weightBase};
|
|
599
|
+
let inputValue = inputTile[${inputIndex}];
|
|
600
|
+
${target} = fma(weightTile[weightBase], ${valueType}(inputValue.x), ${target});
|
|
601
|
+
${target} = fma(weightTile[weightBase + 1u], ${valueType}(inputValue.y), ${target});
|
|
602
|
+
${target} = fma(weightTile[weightBase + 2u], ${valueType}(inputValue.z), ${target});
|
|
603
|
+
${target} = fma(weightTile[weightBase + 3u], ${valueType}(inputValue.w), ${target});
|
|
604
|
+
}
|
|
605
|
+
${precision === 'fp16' ? 'acc[row] += vec4<f32>(partial);' : ''}
|
|
606
|
+
}
|
|
607
|
+
`;
|
|
608
|
+
}
|
|
609
|
+
if (precision === 'fp16') {
|
|
610
|
+
return /* wgsl */ `
|
|
611
|
+
var partial: array<vec4<f16>, ${rowsPerThread}>;
|
|
612
|
+
for (var row = 0u; row < ${rowsPerThread}u; row++) {
|
|
613
|
+
partial[row] = vec4<f16>(0.0h);
|
|
614
|
+
}
|
|
615
|
+
for (var tileK = 0u; tileK < ${tileKBlocks}u; tileK++) {
|
|
616
|
+
let weightBase =
|
|
617
|
+
${weightBase};
|
|
618
|
+
for (var row = 0u; row < ${rowsPerThread}u; row++) {
|
|
619
|
+
let tileSpatial =
|
|
620
|
+
localId.y * ${rowsPerThread}u + row;
|
|
621
|
+
let inputValue =
|
|
622
|
+
inputTile[${inputIndex}];
|
|
623
|
+
partial[row] = fma(
|
|
624
|
+
weightTile[weightBase],
|
|
625
|
+
vec4<f16>(inputValue.x),
|
|
626
|
+
partial[row]
|
|
627
|
+
);
|
|
628
|
+
partial[row] = fma(
|
|
629
|
+
weightTile[weightBase + 1u],
|
|
630
|
+
vec4<f16>(inputValue.y),
|
|
631
|
+
partial[row]
|
|
632
|
+
);
|
|
633
|
+
partial[row] = fma(
|
|
634
|
+
weightTile[weightBase + 2u],
|
|
635
|
+
vec4<f16>(inputValue.z),
|
|
636
|
+
partial[row]
|
|
637
|
+
);
|
|
638
|
+
partial[row] = fma(
|
|
639
|
+
weightTile[weightBase + 3u],
|
|
640
|
+
vec4<f16>(inputValue.w),
|
|
641
|
+
partial[row]
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
for (var row = 0u; row < ${rowsPerThread}u; row++) {
|
|
646
|
+
acc[row] += vec4<f32>(partial[row]);
|
|
647
|
+
}
|
|
648
|
+
`;
|
|
649
|
+
}
|
|
650
|
+
return /* wgsl */ `
|
|
651
|
+
for (var tileK = 0u; tileK < ${tileKBlocks}u; tileK++) {
|
|
652
|
+
let weightBase =
|
|
653
|
+
${weightBase};
|
|
654
|
+
for (var row = 0u; row < ${rowsPerThread}u; row++) {
|
|
655
|
+
let tileSpatial =
|
|
656
|
+
localId.y * ${rowsPerThread}u + row;
|
|
657
|
+
let inputValue =
|
|
658
|
+
inputTile[${inputIndex}];
|
|
659
|
+
acc[row] = fma(
|
|
660
|
+
weightTile[weightBase],
|
|
661
|
+
vec4<f32>(inputValue.x),
|
|
662
|
+
acc[row]
|
|
663
|
+
);
|
|
664
|
+
acc[row] = fma(
|
|
665
|
+
weightTile[weightBase + 1u],
|
|
666
|
+
vec4<f32>(inputValue.y),
|
|
667
|
+
acc[row]
|
|
668
|
+
);
|
|
669
|
+
acc[row] = fma(
|
|
670
|
+
weightTile[weightBase + 2u],
|
|
671
|
+
vec4<f32>(inputValue.z),
|
|
672
|
+
acc[row]
|
|
673
|
+
);
|
|
674
|
+
acc[row] = fma(
|
|
675
|
+
weightTile[weightBase + 3u],
|
|
676
|
+
vec4<f32>(inputValue.w),
|
|
677
|
+
acc[row]
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
`;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function createConvShader(
|
|
685
|
+
precision: NativeUNetPrecision,
|
|
686
|
+
outputPrecision: NativeUNetPrecision,
|
|
687
|
+
activation: Conv2DNodeSpec['activation'],
|
|
688
|
+
inputBlocks: number,
|
|
689
|
+
outputBlocks: number
|
|
690
|
+
) {
|
|
691
|
+
const inputType = storageVecType(precision);
|
|
692
|
+
const weightType = storageVecType(precision);
|
|
693
|
+
const outputType = storageVecType(outputPrecision);
|
|
694
|
+
const stored = storeExpression(
|
|
695
|
+
activationExpression('acc', activation),
|
|
696
|
+
outputPrecision
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
return /* wgsl */ `${shaderPreamble(precision)}
|
|
700
|
+
struct Params {
|
|
701
|
+
inputWidth: u32,
|
|
702
|
+
inputHeight: u32,
|
|
703
|
+
outputWidth: u32,
|
|
704
|
+
outputHeight: u32,
|
|
705
|
+
inputBlocks: u32,
|
|
706
|
+
outputBlocks: u32,
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
@group(0) @binding(0) var<storage, read> inputData: array<${inputType}>;
|
|
710
|
+
@group(0) @binding(1) var<storage, read> weights: array<${weightType}>;
|
|
711
|
+
@group(0) @binding(2) var<storage, read> bias: array<vec4<f32>>;
|
|
712
|
+
@group(0) @binding(3) var<storage, read_write> outputData: array<${outputType}>;
|
|
713
|
+
@group(0) @binding(4) var<uniform> params: Params;
|
|
714
|
+
|
|
715
|
+
@compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE}, 1)
|
|
716
|
+
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
717
|
+
if (gid.x >= params.outputWidth || gid.y >= params.outputHeight || gid.z >= ${outputBlocks}u) {
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
var acc = bias[gid.z];
|
|
721
|
+
for (var ky = 0u; ky < 3u; ky++) {
|
|
722
|
+
let inputY = i32(gid.y) + i32(ky) - 1;
|
|
723
|
+
if (inputY < 0 || inputY >= i32(params.inputHeight)) { continue; }
|
|
724
|
+
for (var kx = 0u; kx < 3u; kx++) {
|
|
725
|
+
let inputX = i32(gid.x) + i32(kx) - 1;
|
|
726
|
+
if (inputX < 0 || inputX >= i32(params.inputWidth)) { continue; }
|
|
727
|
+
let pixelBase = (u32(inputY) * params.inputWidth + u32(inputX)) * ${inputBlocks}u;
|
|
728
|
+
for (var inputBlock = 0u; inputBlock < ${inputBlocks}u; inputBlock++) {
|
|
729
|
+
${accumulationCode(
|
|
730
|
+
'inputData[pixelBase + inputBlock]',
|
|
731
|
+
`((((gid.z * 3u + ky) * 3u + kx) * ${inputBlocks}u + inputBlock) * 4u)`,
|
|
732
|
+
precision
|
|
733
|
+
)}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
let outputIndex = (gid.y * params.outputWidth + gid.x) * ${outputBlocks}u + gid.z;
|
|
738
|
+
outputData[outputIndex] = ${stored};
|
|
739
|
+
}
|
|
740
|
+
`;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/** Direct convolution with subgroup-wide weight broadcast. */
|
|
744
|
+
function createSubgroupConvShader(
|
|
745
|
+
precision: NativeUNetPrecision,
|
|
746
|
+
outputPrecision: NativeUNetPrecision,
|
|
747
|
+
activation: Conv2DNodeSpec['activation'],
|
|
748
|
+
inputBlocks: number,
|
|
749
|
+
outputBlocks: number
|
|
750
|
+
) {
|
|
751
|
+
const inputType = storageVecType(precision);
|
|
752
|
+
const weightType = storageVecType(precision);
|
|
753
|
+
const outputType = storageVecType(outputPrecision);
|
|
754
|
+
const stored = storeExpression(
|
|
755
|
+
activationExpression('acc', activation),
|
|
756
|
+
outputPrecision
|
|
757
|
+
);
|
|
758
|
+
return /* wgsl */ `${shaderPreamble(precision)}
|
|
759
|
+
enable subgroups;
|
|
760
|
+
struct Params {
|
|
761
|
+
inputWidth: u32,
|
|
762
|
+
inputHeight: u32,
|
|
763
|
+
outputWidth: u32,
|
|
764
|
+
outputHeight: u32,
|
|
765
|
+
inputBlocks: u32,
|
|
766
|
+
outputBlocks: u32,
|
|
767
|
+
}
|
|
768
|
+
@group(0) @binding(0) var<storage, read> inputData: array<${inputType}>;
|
|
769
|
+
@group(0) @binding(1) var<storage, read> weights: array<${weightType}>;
|
|
770
|
+
@group(0) @binding(2) var<storage, read> bias: array<vec4<f32>>;
|
|
771
|
+
@group(0) @binding(3) var<storage, read_write> outputData: array<${outputType}>;
|
|
772
|
+
@group(0) @binding(4) var<uniform> params: Params;
|
|
773
|
+
|
|
774
|
+
@compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE}, 1)
|
|
775
|
+
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
776
|
+
let outputInBounds =
|
|
777
|
+
gid.x < params.outputWidth && gid.y < params.outputHeight;
|
|
778
|
+
var acc = bias[gid.z];
|
|
779
|
+
for (var ky = 0u; ky < 3u; ky++) {
|
|
780
|
+
let inputY = i32(gid.y) + i32(ky) - 1;
|
|
781
|
+
let clampedY = u32(clamp(inputY, 0, i32(params.inputHeight) - 1));
|
|
782
|
+
for (var kx = 0u; kx < 3u; kx++) {
|
|
783
|
+
let inputX = i32(gid.x) + i32(kx) - 1;
|
|
784
|
+
let clampedX = u32(clamp(inputX, 0, i32(params.inputWidth) - 1));
|
|
785
|
+
let inputInBounds =
|
|
786
|
+
outputInBounds && inputX >= 0 && inputY >= 0 &&
|
|
787
|
+
inputX < i32(params.inputWidth) && inputY < i32(params.inputHeight);
|
|
788
|
+
let pixelBase =
|
|
789
|
+
(clampedY * params.inputWidth + clampedX) * ${inputBlocks}u;
|
|
790
|
+
for (var inputBlock = 0u; inputBlock < ${inputBlocks}u; inputBlock++) {
|
|
791
|
+
${subgroupAccumulationCode(
|
|
792
|
+
`select(${inputType}(0.0), inputData[pixelBase + inputBlock], inputInBounds)`,
|
|
793
|
+
`((((gid.z * 3u + ky) * 3u + kx) * ${inputBlocks}u + inputBlock) * 4u)`,
|
|
794
|
+
precision
|
|
795
|
+
)}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
if (outputInBounds) {
|
|
800
|
+
let outputIndex =
|
|
801
|
+
(gid.y * params.outputWidth + gid.x) * ${outputBlocks}u + gid.z;
|
|
802
|
+
outputData[outputIndex] = ${stored};
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
`;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* A 2D convolution tile which loads the complete 3x3 halo into workgroup
|
|
810
|
+
* memory once. Kernel choice only depends on the operation shape, precision,
|
|
811
|
+
* and device limits; it is deliberately independent of OIDN model names.
|
|
812
|
+
*/
|
|
813
|
+
function createSpatialConvShader(
|
|
814
|
+
precision: NativeUNetPrecision,
|
|
815
|
+
outputPrecision: NativeUNetPrecision,
|
|
816
|
+
activation: Conv2DNodeSpec['activation'],
|
|
817
|
+
inputBlocks: number,
|
|
818
|
+
outputBlocks: number
|
|
819
|
+
) {
|
|
820
|
+
const inputType = storageVecType(precision);
|
|
821
|
+
const outputType = storageVecType(outputPrecision);
|
|
822
|
+
const stored = storeExpression(
|
|
823
|
+
activationExpression('acc', activation),
|
|
824
|
+
outputPrecision
|
|
825
|
+
);
|
|
826
|
+
const patchValues =
|
|
827
|
+
SPATIAL_CONV_PATCH * SPATIAL_CONV_PATCH * inputBlocks;
|
|
828
|
+
const workgroupThreads =
|
|
829
|
+
SPATIAL_CONV_WORKGROUP * SPATIAL_CONV_WORKGROUP;
|
|
830
|
+
|
|
831
|
+
return /* wgsl */ `${shaderPreamble(precision)}
|
|
832
|
+
struct Params {
|
|
833
|
+
inputWidth: u32,
|
|
834
|
+
inputHeight: u32,
|
|
835
|
+
outputWidth: u32,
|
|
836
|
+
outputHeight: u32,
|
|
837
|
+
inputBlocks: u32,
|
|
838
|
+
outputBlocks: u32,
|
|
839
|
+
}
|
|
840
|
+
@group(0) @binding(0) var<storage, read> inputData: array<${inputType}>;
|
|
841
|
+
@group(0) @binding(1) var<storage, read> weights: array<${inputType}>;
|
|
842
|
+
@group(0) @binding(2) var<storage, read> bias: array<vec4<f32>>;
|
|
843
|
+
@group(0) @binding(3) var<storage, read_write> outputData: array<${outputType}>;
|
|
844
|
+
@group(0) @binding(4) var<uniform> params: Params;
|
|
845
|
+
|
|
846
|
+
var<workgroup> inputPatch: array<${inputType}, ${patchValues}>;
|
|
847
|
+
|
|
848
|
+
@compute @workgroup_size(${SPATIAL_CONV_WORKGROUP}, ${SPATIAL_CONV_WORKGROUP}, 1)
|
|
849
|
+
fn main(
|
|
850
|
+
@builtin(local_invocation_id) localId: vec3<u32>,
|
|
851
|
+
@builtin(workgroup_id) workgroupId: vec3<u32>
|
|
852
|
+
) {
|
|
853
|
+
let localLinear =
|
|
854
|
+
localId.y * ${SPATIAL_CONV_WORKGROUP}u + localId.x;
|
|
855
|
+
for (
|
|
856
|
+
var loadIndex = localLinear;
|
|
857
|
+
loadIndex < ${patchValues}u;
|
|
858
|
+
loadIndex += ${workgroupThreads}u
|
|
859
|
+
) {
|
|
860
|
+
let patchPixel = loadIndex / ${inputBlocks}u;
|
|
861
|
+
let inputBlock = loadIndex % ${inputBlocks}u;
|
|
862
|
+
let patchX = patchPixel % ${SPATIAL_CONV_PATCH}u;
|
|
863
|
+
let patchY = patchPixel / ${SPATIAL_CONV_PATCH}u;
|
|
864
|
+
let inputX =
|
|
865
|
+
i32(workgroupId.x * ${SPATIAL_CONV_WORKGROUP}u + patchX) - 1;
|
|
866
|
+
let inputY =
|
|
867
|
+
i32(workgroupId.y * ${SPATIAL_CONV_WORKGROUP}u + patchY) - 1;
|
|
868
|
+
var value = ${inputType}(0.0);
|
|
869
|
+
if (
|
|
870
|
+
inputX >= 0 && inputX < i32(params.inputWidth) &&
|
|
871
|
+
inputY >= 0 && inputY < i32(params.inputHeight)
|
|
872
|
+
) {
|
|
873
|
+
let inputIndex =
|
|
874
|
+
(u32(inputY) * params.inputWidth + u32(inputX)) *
|
|
875
|
+
${inputBlocks}u + inputBlock;
|
|
876
|
+
value = inputData[inputIndex];
|
|
877
|
+
}
|
|
878
|
+
inputPatch[loadIndex] = value;
|
|
879
|
+
}
|
|
880
|
+
workgroupBarrier();
|
|
881
|
+
|
|
882
|
+
let outputX =
|
|
883
|
+
workgroupId.x * ${SPATIAL_CONV_WORKGROUP}u + localId.x;
|
|
884
|
+
let outputY =
|
|
885
|
+
workgroupId.y * ${SPATIAL_CONV_WORKGROUP}u + localId.y;
|
|
886
|
+
let outputBlock = workgroupId.z;
|
|
887
|
+
if (
|
|
888
|
+
outputX >= params.outputWidth || outputY >= params.outputHeight ||
|
|
889
|
+
outputBlock >= ${outputBlocks}u
|
|
890
|
+
) {
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
var acc = bias[outputBlock];
|
|
895
|
+
for (var ky = 0u; ky < 3u; ky++) {
|
|
896
|
+
for (var kx = 0u; kx < 3u; kx++) {
|
|
897
|
+
let patchBase =
|
|
898
|
+
((localId.y + ky) * ${SPATIAL_CONV_PATCH}u + localId.x + kx) *
|
|
899
|
+
${inputBlocks}u;
|
|
900
|
+
for (var inputBlock = 0u; inputBlock < ${inputBlocks}u; inputBlock++) {
|
|
901
|
+
${accumulationCode(
|
|
902
|
+
'inputPatch[patchBase + inputBlock]',
|
|
903
|
+
`((((outputBlock * 3u + ky) * 3u + kx) * ${inputBlocks}u + inputBlock) * 4u)`,
|
|
904
|
+
precision
|
|
905
|
+
)}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
let outputIndex =
|
|
910
|
+
(outputY * params.outputWidth + outputX) * ${outputBlocks}u + outputBlock;
|
|
911
|
+
outputData[outputIndex] = ${stored};
|
|
912
|
+
}
|
|
913
|
+
`;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/** Precompute spatial predicates once, then advance the original flattened K order. */
|
|
917
|
+
function tiledAddressCacheCode(inputBlocks: number, decoder: boolean, gemm: Readonly<Required<NativeUNetGemmOptions>>, sources?: { blocks: readonly number[]; upsampled: 0 | 1 }) {
|
|
918
|
+
const { workgroupX, workgroupY, tileM, tileKBlocks } = gemmTile(gemm);
|
|
919
|
+
const width = decoder ? 'params.outputWidth' : 'params.inputWidth';
|
|
920
|
+
const height = decoder ? 'params.outputHeight' : 'params.inputHeight';
|
|
921
|
+
const baseOffset = gemm.addressMode === 'base-offset';
|
|
922
|
+
// Decoder parity travels in the high mask bits, avoiding extra cached arrays.
|
|
923
|
+
const baseAssignments = sources
|
|
924
|
+
? sources.blocks.map((blocks, source) =>
|
|
925
|
+
`cachedBase${source}[load] = (${source === sources.upsampled ? 'y / 2u' : 'y'} * params.source${source}Width + ${source === sources.upsampled ? 'x / 2u' : 'x'}) * ${blocks}u;`
|
|
926
|
+
).join('\n ')
|
|
927
|
+
: `cachedBase0[load] = (y * params.inputWidth + x) * ${inputBlocks}u;`;
|
|
928
|
+
const loads = tileM * tileKBlocks /
|
|
929
|
+
(workgroupX * workgroupY);
|
|
930
|
+
return /* wgsl */ `
|
|
931
|
+
${baseOffset ? `var cachedBase0: array<u32, ${loads}>;
|
|
932
|
+
${sources ? `var cachedBase1: array<u32, ${loads}>;` : ''}` : `var cachedX: array<u32, ${loads}>;
|
|
933
|
+
var cachedY: array<u32, ${loads}>;`}
|
|
934
|
+
var cachedMask: array<u32, ${loads}>;
|
|
935
|
+
for (var load = 0u; load < ${loads}u; load++) {
|
|
936
|
+
let loadIndex = localLinear + load * ${workgroupX * workgroupY}u;
|
|
937
|
+
let spatial = workgroupId.x * ${tileM}u + loadIndex / ${tileKBlocks}u;
|
|
938
|
+
let x = spatial % params.outputWidth;
|
|
939
|
+
let y = spatial / params.outputWidth;
|
|
940
|
+
${baseOffset ? baseAssignments : `cachedX[load] = x;
|
|
941
|
+
cachedY[load] = y;`}
|
|
942
|
+
let columns =
|
|
943
|
+
select(0u, 0x049u, x > 0u && x - 1u < ${width}) |
|
|
944
|
+
select(0u, 0x092u, x < ${width}) |
|
|
945
|
+
select(0u, 0x124u, x + 1u < ${width});
|
|
946
|
+
let rows =
|
|
947
|
+
select(0u, 0x007u, y > 0u && y - 1u < ${height}) |
|
|
948
|
+
select(0u, 0x038u, y < ${height}) |
|
|
949
|
+
select(0u, 0x1c0u, y + 1u < ${height});
|
|
950
|
+
cachedMask[load] = select(0u, columns & rows, spatial < spatialCount)${baseOffset && sources ? ' | ((x & 1u) << 9u) | ((y & 1u) << 10u)' : ''};
|
|
951
|
+
}
|
|
952
|
+
var channelBlock = (localLinear % ${tileKBlocks}u) % ${inputBlocks}u;
|
|
953
|
+
var filterPosition = (localLinear % ${tileKBlocks}u) / ${inputBlocks}u;
|
|
954
|
+
`;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function tiledAddressAdvanceCode(inputBlocks: number, gemm: Readonly<Required<NativeUNetGemmOptions>>) {
|
|
958
|
+
const { tileKBlocks } = gemmTile(gemm);
|
|
959
|
+
// The remainder is less than inputBlocks, so at most one carry is needed.
|
|
960
|
+
return /* wgsl */ `
|
|
961
|
+
channelBlock += ${tileKBlocks % inputBlocks}u;
|
|
962
|
+
let carry = channelBlock >= ${inputBlocks}u;
|
|
963
|
+
channelBlock -= select(0u, ${inputBlocks}u, carry);
|
|
964
|
+
filterPosition += ${Math.floor(tileKBlocks / inputBlocks)}u + select(0u, 1u, carry);
|
|
965
|
+
`;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function tiledIncrementalInputCode(valueType: string, readValue: string, gemm: Readonly<Required<NativeUNetGemmOptions>>) {
|
|
969
|
+
const { workgroupX, workgroupY, tileM, tileKBlocks } = gemmTile(gemm);
|
|
970
|
+
const loads = tileM * tileKBlocks /
|
|
971
|
+
(workgroupX * workgroupY);
|
|
972
|
+
return /* wgsl */ `
|
|
973
|
+
let inputBlock = channelBlock;
|
|
974
|
+
let kernelY = filterPosition / 3u;
|
|
975
|
+
let kernelX = filterPosition % 3u;
|
|
976
|
+
for (var load = 0u; load < ${loads}u; load++) {
|
|
977
|
+
let loadIndex = localLinear + load * ${workgroupX * workgroupY}u;
|
|
978
|
+
var value = ${valueType}(0.0);
|
|
979
|
+
if (filterPosition < 9u && (cachedMask[load] & (1u << filterPosition)) != 0u) {
|
|
980
|
+
${gemm.addressMode === 'base-offset' ? '' : `let inputX = i32(cachedX[load]) + i32(kernelX) - 1;
|
|
981
|
+
let inputY = i32(cachedY[load]) + i32(kernelY) - 1;`}
|
|
982
|
+
${readValue}
|
|
983
|
+
}
|
|
984
|
+
inputTile[${gemmSharedInputIndex('loadIndex', gemm)}] = value;
|
|
985
|
+
}
|
|
986
|
+
`;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/**
|
|
990
|
+
* Implicit-GEMM convolution for FP16/FP32. This follows the packed WebGPU
|
|
991
|
+
* shape: configurable spatial rows and vec4 output blocks per workgroup.
|
|
992
|
+
* The eight-vec4 K tile stays fixed to preserve FP16 rounding across variants.
|
|
993
|
+
*/
|
|
994
|
+
function createTiledConvShader(
|
|
995
|
+
precision: NativeUNetPrecision,
|
|
996
|
+
outputPrecision: NativeUNetPrecision,
|
|
997
|
+
activation: Conv2DNodeSpec['activation'],
|
|
998
|
+
inputBlocks: number,
|
|
999
|
+
outputBlocks: number,
|
|
1000
|
+
gemm: Readonly<Required<NativeUNetGemmOptions>>
|
|
1001
|
+
) {
|
|
1002
|
+
const { workgroupX, workgroupY, rowsPerThread, tileM, tileNBlocks, tileKBlocks } = gemmTile(gemm);
|
|
1003
|
+
const { addressMode, weightLayout } = gemm;
|
|
1004
|
+
const inputType = storageVecType(precision);
|
|
1005
|
+
const outputType = storageVecType(outputPrecision);
|
|
1006
|
+
const packedInput = gemmPackedLoad(precision, gemm, false);
|
|
1007
|
+
const packedWeights = gemmPackedLoad(precision, gemm, true);
|
|
1008
|
+
const inputStorageType = packedInput ? 'vec2<u32>' : inputType;
|
|
1009
|
+
const weightStorageType = packedWeights ? 'vec2<u32>' : inputType;
|
|
1010
|
+
const stored = storeExpression(
|
|
1011
|
+
activationExpression('acc', activation),
|
|
1012
|
+
outputPrecision
|
|
1013
|
+
);
|
|
1014
|
+
const workgroupThreads =
|
|
1015
|
+
workgroupX * workgroupY;
|
|
1016
|
+
const inputTileValues = tileM * tileKBlocks;
|
|
1017
|
+
const weightTileValues =
|
|
1018
|
+
tileKBlocks * tileNBlocks * 4;
|
|
1019
|
+
return /* wgsl */ `${shaderPreamble(precision)}
|
|
1020
|
+
struct Params {
|
|
1021
|
+
inputWidth: u32,
|
|
1022
|
+
inputHeight: u32,
|
|
1023
|
+
outputWidth: u32,
|
|
1024
|
+
outputHeight: u32,
|
|
1025
|
+
inputBlocks: u32,
|
|
1026
|
+
outputBlocks: u32,
|
|
1027
|
+
}
|
|
1028
|
+
@group(0) @binding(0) var<storage, read> inputData: array<${inputStorageType}>;
|
|
1029
|
+
@group(0) @binding(1) var<storage, read> weights: array<${weightStorageType}>;
|
|
1030
|
+
@group(0) @binding(2) var<storage, read> bias: array<vec4<f32>>;
|
|
1031
|
+
@group(0) @binding(3) var<storage, read_write> outputData: array<${outputType}>;
|
|
1032
|
+
@group(0) @binding(4) var<uniform> params: Params;
|
|
1033
|
+
|
|
1034
|
+
var<workgroup> inputTile: array<${inputType}, ${gemmSharedSizes(gemm).input}>;
|
|
1035
|
+
var<workgroup> weightTile: array<${inputType}, ${gemmSharedSizes(gemm).weights}>;
|
|
1036
|
+
|
|
1037
|
+
@compute @workgroup_size(${workgroupX}, ${workgroupY}, 1)
|
|
1038
|
+
fn main(
|
|
1039
|
+
@builtin(local_invocation_id) localId: vec3<u32>,
|
|
1040
|
+
@builtin(workgroup_id) workgroupId: vec3<u32>
|
|
1041
|
+
) {
|
|
1042
|
+
let spatialBase =
|
|
1043
|
+
workgroupId.x * ${tileM}u +
|
|
1044
|
+
localId.y * ${rowsPerThread}u;
|
|
1045
|
+
let outputBlock =
|
|
1046
|
+
workgroupId.y * ${tileNBlocks}u + localId.x;
|
|
1047
|
+
let spatialCount = params.outputWidth * params.outputHeight;
|
|
1048
|
+
var acc: array<vec4<f32>, ${rowsPerThread}>;
|
|
1049
|
+
if (outputBlock < ${outputBlocks}u) {
|
|
1050
|
+
for (var row = 0u; row < ${rowsPerThread}u; row++) {
|
|
1051
|
+
acc[row] = bias[outputBlock];
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
let localLinear =
|
|
1056
|
+
localId.y * ${workgroupX}u + localId.x;
|
|
1057
|
+
${addressMode !== 'analytic' ? tiledAddressCacheCode(inputBlocks, false, gemm) : ''}
|
|
1058
|
+
let totalK = ${inputBlocks * 9}u;
|
|
1059
|
+
for (var kBase = 0u; kBase < totalK; kBase += ${tileKBlocks}u) {
|
|
1060
|
+
${addressMode !== 'analytic' ? tiledIncrementalInputCode(inputType, `
|
|
1061
|
+
${addressMode === 'base-offset' ? `let offset = (i32(kernelY) - 1) * i32(params.inputWidth) + i32(kernelX) - 1;
|
|
1062
|
+
let inputIndex = cachedBase0[load] + u32(offset * ${inputBlocks}) + inputBlock;` : `let inputIndex = (u32(inputY) * params.inputWidth + u32(inputX)) *
|
|
1063
|
+
${inputBlocks}u + inputBlock;`}
|
|
1064
|
+
value = ${gemmLoadExpression('inputData[inputIndex]', packedInput)};
|
|
1065
|
+
`, gemm) : /* wgsl */ `
|
|
1066
|
+
for (
|
|
1067
|
+
var loadIndex = localLinear;
|
|
1068
|
+
loadIndex < ${inputTileValues}u;
|
|
1069
|
+
loadIndex += ${workgroupThreads}u
|
|
1070
|
+
) {
|
|
1071
|
+
let tileSpatial = loadIndex / ${tileKBlocks}u;
|
|
1072
|
+
let tileK = loadIndex % ${tileKBlocks}u;
|
|
1073
|
+
let inputSpatialIndex =
|
|
1074
|
+
workgroupId.x * ${tileM}u + tileSpatial;
|
|
1075
|
+
let kIndex = kBase + tileK;
|
|
1076
|
+
var value = ${inputType}(0.0);
|
|
1077
|
+
if (inputSpatialIndex < spatialCount && kIndex < totalK) {
|
|
1078
|
+
let outputY = inputSpatialIndex / params.outputWidth;
|
|
1079
|
+
let outputX = inputSpatialIndex % params.outputWidth;
|
|
1080
|
+
let inputBlock = kIndex % ${inputBlocks}u;
|
|
1081
|
+
let kernelIndex = kIndex / ${inputBlocks}u;
|
|
1082
|
+
let kernelY = kernelIndex / 3u;
|
|
1083
|
+
let kernelX = kernelIndex % 3u;
|
|
1084
|
+
let inputY = i32(outputY) + i32(kernelY) - 1;
|
|
1085
|
+
let inputX = i32(outputX) + i32(kernelX) - 1;
|
|
1086
|
+
if (
|
|
1087
|
+
inputY >= 0 && inputY < i32(params.inputHeight) &&
|
|
1088
|
+
inputX >= 0 && inputX < i32(params.inputWidth)
|
|
1089
|
+
) {
|
|
1090
|
+
let inputIndex =
|
|
1091
|
+
(u32(inputY) * params.inputWidth + u32(inputX)) *
|
|
1092
|
+
${inputBlocks}u + inputBlock;
|
|
1093
|
+
value = ${gemmLoadExpression('inputData[inputIndex]', packedInput)};
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
inputTile[${gemmSharedInputIndex('loadIndex', gemm)}] = value;
|
|
1097
|
+
}
|
|
1098
|
+
`}
|
|
1099
|
+
|
|
1100
|
+
for (
|
|
1101
|
+
var loadIndex = localLinear;
|
|
1102
|
+
loadIndex < ${weightTileValues}u;
|
|
1103
|
+
loadIndex += ${workgroupThreads}u
|
|
1104
|
+
) {
|
|
1105
|
+
let tileK = loadIndex / ${tileNBlocks * 4}u;
|
|
1106
|
+
let outputRemainder = loadIndex % ${tileNBlocks * 4}u;
|
|
1107
|
+
let tileOutputBlock = outputRemainder / 4u;
|
|
1108
|
+
let outputLane = outputRemainder % 4u;
|
|
1109
|
+
let loadedOutputBlock =
|
|
1110
|
+
workgroupId.y * ${tileNBlocks}u + tileOutputBlock;
|
|
1111
|
+
let kIndex = kBase + tileK;
|
|
1112
|
+
var value = ${inputType}(0.0);
|
|
1113
|
+
if (loadedOutputBlock < ${outputBlocks}u && kIndex < totalK) {
|
|
1114
|
+
${weightLayout === 'k-major' ? /* wgsl */ `
|
|
1115
|
+
let weightIndex = (kIndex * ${outputBlocks}u + loadedOutputBlock) * 4u + outputLane;
|
|
1116
|
+
` : addressMode !== 'analytic' ? /* wgsl */ `
|
|
1117
|
+
let weightIndex = (loadedOutputBlock * ${inputBlocks * 9}u + kIndex) * 4u + outputLane;
|
|
1118
|
+
` : /* wgsl */ `
|
|
1119
|
+
let inputBlock = kIndex % ${inputBlocks}u;
|
|
1120
|
+
let kernelIndex = kIndex / ${inputBlocks}u;
|
|
1121
|
+
let kernelY = kernelIndex / 3u;
|
|
1122
|
+
let kernelX = kernelIndex % 3u;
|
|
1123
|
+
let weightIndex =
|
|
1124
|
+
((((loadedOutputBlock * 3u + kernelY) * 3u + kernelX) *
|
|
1125
|
+
${inputBlocks}u + inputBlock) * 4u + outputLane);
|
|
1126
|
+
`}
|
|
1127
|
+
value = ${gemmLoadExpression('weights[weightIndex]', packedWeights)};
|
|
1128
|
+
}
|
|
1129
|
+
weightTile[${gemmSharedWeightIndex('loadIndex', gemm)}] = value;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
workgroupBarrier();
|
|
1133
|
+
${tiledAccumulationCode(precision, gemm)}
|
|
1134
|
+
workgroupBarrier();
|
|
1135
|
+
${addressMode !== 'analytic' ? tiledAddressAdvanceCode(inputBlocks, gemm) : ''}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
if (outputBlock < ${outputBlocks}u) {
|
|
1139
|
+
for (var row = 0u; row < ${rowsPerThread}u; row++) {
|
|
1140
|
+
let spatialIndex = spatialBase + row;
|
|
1141
|
+
if (spatialIndex < spatialCount) {
|
|
1142
|
+
let outputIndex = spatialIndex * ${outputBlocks}u + outputBlock;
|
|
1143
|
+
outputData[outputIndex] = ${stored.replaceAll('acc', 'acc[row]')};
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
`;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function createFusedConvPoolShader(
|
|
1152
|
+
precision: NativeUNetPrecision,
|
|
1153
|
+
activation: Conv2DNodeSpec['activation'],
|
|
1154
|
+
inputBlocks: number,
|
|
1155
|
+
outputBlocks: number
|
|
1156
|
+
) {
|
|
1157
|
+
const valueType = storageVecType(precision);
|
|
1158
|
+
const activated = activationExpression('acc', activation);
|
|
1159
|
+
return /* wgsl */ `${shaderPreamble(precision)}
|
|
1160
|
+
struct Params {
|
|
1161
|
+
inputWidth: u32,
|
|
1162
|
+
inputHeight: u32,
|
|
1163
|
+
outputWidth: u32,
|
|
1164
|
+
outputHeight: u32,
|
|
1165
|
+
inputBlocks: u32,
|
|
1166
|
+
outputBlocks: u32,
|
|
1167
|
+
}
|
|
1168
|
+
@group(0) @binding(0) var<storage, read> inputData: array<${valueType}>;
|
|
1169
|
+
@group(0) @binding(1) var<storage, read> weights: array<${valueType}>;
|
|
1170
|
+
@group(0) @binding(2) var<storage, read> bias: array<vec4<f32>>;
|
|
1171
|
+
@group(0) @binding(3) var<storage, read_write> outputData: array<${valueType}>;
|
|
1172
|
+
@group(0) @binding(4) var<uniform> params: Params;
|
|
1173
|
+
|
|
1174
|
+
@compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE}, 1)
|
|
1175
|
+
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
1176
|
+
if (gid.x >= params.outputWidth || gid.y >= params.outputHeight || gid.z >= ${outputBlocks}u) {
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
var pooled = vec4<f32>(-3.402823466e+38);
|
|
1180
|
+
for (var py = 0u; py < 2u; py++) {
|
|
1181
|
+
let centerY = gid.y * 2u + py;
|
|
1182
|
+
if (centerY >= params.inputHeight) { continue; }
|
|
1183
|
+
for (var px = 0u; px < 2u; px++) {
|
|
1184
|
+
let centerX = gid.x * 2u + px;
|
|
1185
|
+
if (centerX >= params.inputWidth) { continue; }
|
|
1186
|
+
var acc = bias[gid.z];
|
|
1187
|
+
for (var ky = 0u; ky < 3u; ky++) {
|
|
1188
|
+
let inputY = i32(centerY) + i32(ky) - 1;
|
|
1189
|
+
if (inputY < 0 || inputY >= i32(params.inputHeight)) { continue; }
|
|
1190
|
+
for (var kx = 0u; kx < 3u; kx++) {
|
|
1191
|
+
let inputX = i32(centerX) + i32(kx) - 1;
|
|
1192
|
+
if (inputX < 0 || inputX >= i32(params.inputWidth)) { continue; }
|
|
1193
|
+
let pixelBase = (u32(inputY) * params.inputWidth + u32(inputX)) * ${inputBlocks}u;
|
|
1194
|
+
for (var inputBlock = 0u; inputBlock < ${inputBlocks}u; inputBlock++) {
|
|
1195
|
+
${accumulationCode(
|
|
1196
|
+
'inputData[pixelBase + inputBlock]',
|
|
1197
|
+
`((((gid.z * 3u + ky) * 3u + kx) * ${inputBlocks}u + inputBlock) * 4u)`,
|
|
1198
|
+
precision
|
|
1199
|
+
)}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
pooled = max(pooled, ${activated});
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
let outputIndex = (gid.y * params.outputWidth + gid.x) * ${outputBlocks}u + gid.z;
|
|
1207
|
+
outputData[outputIndex] = ${storeExpression('pooled', precision)};
|
|
1208
|
+
}
|
|
1209
|
+
`;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
function createMaxPoolShader(
|
|
1213
|
+
precision: NativeUNetPrecision,
|
|
1214
|
+
outputBlocks: number,
|
|
1215
|
+
coalesced: boolean
|
|
1216
|
+
) {
|
|
1217
|
+
const valueType = storageVecType(precision);
|
|
1218
|
+
return /* wgsl */ `${shaderPreamble(precision)}
|
|
1219
|
+
struct Params {
|
|
1220
|
+
inputWidth: u32,
|
|
1221
|
+
inputHeight: u32,
|
|
1222
|
+
outputWidth: u32,
|
|
1223
|
+
outputHeight: u32,
|
|
1224
|
+
outputBlocks: u32,
|
|
1225
|
+
}
|
|
1226
|
+
@group(0) @binding(0) var<storage, read> inputData: array<${valueType}>;
|
|
1227
|
+
@group(0) @binding(1) var<storage, read_write> outputData: array<${valueType}>;
|
|
1228
|
+
@group(0) @binding(2) var<uniform> params: Params;
|
|
1229
|
+
|
|
1230
|
+
@compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE}, 1)
|
|
1231
|
+
fn main(
|
|
1232
|
+
@builtin(global_invocation_id) gid: vec3<u32>${coalesced ? ',\n @builtin(num_workgroups) groupCount: vec3<u32>' : ''}
|
|
1233
|
+
) {
|
|
1234
|
+
${coalesced ? `let linearX = gid.x + gid.z * groupCount.x * ${WORKGROUP_SIZE}u;
|
|
1235
|
+
let outputBlock = linearX % ${outputBlocks}u;
|
|
1236
|
+
let outputX = linearX / ${outputBlocks}u;` : `let outputBlock = gid.z;
|
|
1237
|
+
let outputX = gid.x;`}
|
|
1238
|
+
if (
|
|
1239
|
+
outputX >= params.outputWidth ||
|
|
1240
|
+
gid.y >= params.outputHeight ||
|
|
1241
|
+
outputBlock >= ${outputBlocks}u
|
|
1242
|
+
) {
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
var pooled = vec4<f32>(-3.402823466e+38);
|
|
1246
|
+
for (var py = 0u; py < 2u; py++) {
|
|
1247
|
+
let inputY = gid.y * 2u + py;
|
|
1248
|
+
if (inputY >= params.inputHeight) { continue; }
|
|
1249
|
+
for (var px = 0u; px < 2u; px++) {
|
|
1250
|
+
let inputX = outputX * 2u + px;
|
|
1251
|
+
if (inputX >= params.inputWidth) { continue; }
|
|
1252
|
+
let inputIndex =
|
|
1253
|
+
(inputY * params.inputWidth + inputX) * ${outputBlocks}u + outputBlock;
|
|
1254
|
+
pooled = max(pooled, vec4<f32>(inputData[inputIndex]));
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
let outputIndex =
|
|
1258
|
+
(gid.y * params.outputWidth + outputX) * ${outputBlocks}u + outputBlock;
|
|
1259
|
+
outputData[outputIndex] = ${storeExpression('pooled', precision)};
|
|
1260
|
+
}
|
|
1261
|
+
`;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function createTiledDecoderShader(
|
|
1265
|
+
precision: NativeUNetPrecision,
|
|
1266
|
+
outputPrecision: NativeUNetPrecision,
|
|
1267
|
+
activation: Conv2DNodeSpec['activation'],
|
|
1268
|
+
sourceBlocks: readonly number[],
|
|
1269
|
+
upsampledSource: 0 | 1,
|
|
1270
|
+
outputBlocks: number,
|
|
1271
|
+
gemm: Readonly<Required<NativeUNetGemmOptions>>
|
|
1272
|
+
) {
|
|
1273
|
+
const { workgroupX, workgroupY, rowsPerThread, tileM, tileNBlocks, tileKBlocks } = gemmTile(gemm);
|
|
1274
|
+
const { addressMode, weightLayout } = gemm;
|
|
1275
|
+
const valueType = storageVecType(precision);
|
|
1276
|
+
const outputType = storageVecType(outputPrecision);
|
|
1277
|
+
const packedInput = gemmPackedLoad(precision, gemm, false);
|
|
1278
|
+
const packedWeights = gemmPackedLoad(precision, gemm, true);
|
|
1279
|
+
const inputStorageType = packedInput ? 'vec2<u32>' : valueType;
|
|
1280
|
+
const weightStorageType = packedWeights ? 'vec2<u32>' : valueType;
|
|
1281
|
+
const inputBlocks = sourceBlocks[0] + sourceBlocks[1];
|
|
1282
|
+
const stored = storeExpression(
|
|
1283
|
+
activationExpression('acc[row]', activation),
|
|
1284
|
+
outputPrecision
|
|
1285
|
+
);
|
|
1286
|
+
const workgroupThreads =
|
|
1287
|
+
workgroupX * workgroupY;
|
|
1288
|
+
const inputTileValues = tileM * tileKBlocks;
|
|
1289
|
+
const weightTileValues =
|
|
1290
|
+
tileKBlocks * tileNBlocks * 4;
|
|
1291
|
+
const sourceRead = (source: 0 | 1, blockExpression: string) => {
|
|
1292
|
+
const sourceX =
|
|
1293
|
+
source === upsampledSource
|
|
1294
|
+
? 'u32(inputX) / 2u'
|
|
1295
|
+
: 'u32(inputX)';
|
|
1296
|
+
const sourceY =
|
|
1297
|
+
source === upsampledSource
|
|
1298
|
+
? 'u32(inputY) / 2u'
|
|
1299
|
+
: 'u32(inputY)';
|
|
1300
|
+
return /* wgsl */ `
|
|
1301
|
+
{
|
|
1302
|
+
let sourceBlock = ${blockExpression};
|
|
1303
|
+
${addressMode === 'base-offset' ? `let dx = ${source === upsampledSource ? '(i32((cachedMask[load] >> 9u) & 1u) + i32(kernelX) - 1) >> 1u' : 'i32(kernelX) - 1'};
|
|
1304
|
+
let dy = ${source === upsampledSource ? '(i32((cachedMask[load] >> 10u) & 1u) + i32(kernelY) - 1) >> 1u' : 'i32(kernelY) - 1'};
|
|
1305
|
+
let offset = (dy * i32(params.source${source}Width) + dx) * ${sourceBlocks[source]};
|
|
1306
|
+
let sourceIndex = cachedBase${source}[load] + u32(offset) + sourceBlock;` : `let sourceX = ${sourceX};
|
|
1307
|
+
let sourceY = ${sourceY};
|
|
1308
|
+
let sourceIndex =
|
|
1309
|
+
(sourceY * params.source${source}Width + sourceX) *
|
|
1310
|
+
${sourceBlocks[source]}u + sourceBlock;` }
|
|
1311
|
+
value = ${gemmLoadExpression(`input${source}[sourceIndex]`, packedInput)};
|
|
1312
|
+
}`;
|
|
1313
|
+
};
|
|
1314
|
+
return /* wgsl */ `${shaderPreamble(precision)}
|
|
1315
|
+
struct Params {
|
|
1316
|
+
outputWidth: u32,
|
|
1317
|
+
outputHeight: u32,
|
|
1318
|
+
outputBlocks: u32,
|
|
1319
|
+
inputBlocks: u32,
|
|
1320
|
+
source0Width: u32,
|
|
1321
|
+
source0Height: u32,
|
|
1322
|
+
source1Width: u32,
|
|
1323
|
+
source1Height: u32,
|
|
1324
|
+
}
|
|
1325
|
+
@group(0) @binding(0) var<storage, read> input0: array<${inputStorageType}>;
|
|
1326
|
+
@group(0) @binding(1) var<storage, read> input1: array<${inputStorageType}>;
|
|
1327
|
+
@group(0) @binding(2) var<storage, read> weights: array<${weightStorageType}>;
|
|
1328
|
+
@group(0) @binding(3) var<storage, read> bias: array<vec4<f32>>;
|
|
1329
|
+
@group(0) @binding(4) var<storage, read_write> outputData: array<${outputType}>;
|
|
1330
|
+
@group(0) @binding(5) var<uniform> params: Params;
|
|
1331
|
+
|
|
1332
|
+
var<workgroup> inputTile: array<${valueType}, ${gemmSharedSizes(gemm).input}>;
|
|
1333
|
+
var<workgroup> weightTile: array<${valueType}, ${gemmSharedSizes(gemm).weights}>;
|
|
1334
|
+
|
|
1335
|
+
@compute @workgroup_size(${workgroupX}, ${workgroupY}, 1)
|
|
1336
|
+
fn main(
|
|
1337
|
+
@builtin(local_invocation_id) localId: vec3<u32>,
|
|
1338
|
+
@builtin(workgroup_id) workgroupId: vec3<u32>
|
|
1339
|
+
) {
|
|
1340
|
+
let spatialBase =
|
|
1341
|
+
workgroupId.x * ${tileM}u +
|
|
1342
|
+
localId.y * ${rowsPerThread}u;
|
|
1343
|
+
let outputBlock =
|
|
1344
|
+
workgroupId.y * ${tileNBlocks}u + localId.x;
|
|
1345
|
+
let spatialCount = params.outputWidth * params.outputHeight;
|
|
1346
|
+
var acc: array<vec4<f32>, ${rowsPerThread}>;
|
|
1347
|
+
if (outputBlock < ${outputBlocks}u) {
|
|
1348
|
+
for (var row = 0u; row < ${rowsPerThread}u; row++) {
|
|
1349
|
+
acc[row] = bias[outputBlock];
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
let localLinear =
|
|
1354
|
+
localId.y * ${workgroupX}u + localId.x;
|
|
1355
|
+
${addressMode !== 'analytic' ? tiledAddressCacheCode(inputBlocks, true, gemm, { blocks: sourceBlocks, upsampled: upsampledSource }) : ''}
|
|
1356
|
+
let totalK = ${inputBlocks * 9}u;
|
|
1357
|
+
for (var kBase = 0u; kBase < totalK; kBase += ${tileKBlocks}u) {
|
|
1358
|
+
${addressMode !== 'analytic' ? (gemm.decoderLoad === 'source-first' ? `
|
|
1359
|
+
if (channelBlock < ${sourceBlocks[0]}u) {
|
|
1360
|
+
${tiledIncrementalInputCode(valueType, sourceRead(0, 'inputBlock'), gemm)}
|
|
1361
|
+
} else {
|
|
1362
|
+
${tiledIncrementalInputCode(valueType, sourceRead(1, `inputBlock - ${sourceBlocks[0]}u`), gemm)}
|
|
1363
|
+
}
|
|
1364
|
+
` : tiledIncrementalInputCode(valueType, `
|
|
1365
|
+
if (inputBlock < ${sourceBlocks[0]}u) {
|
|
1366
|
+
${sourceRead(0, 'inputBlock')}
|
|
1367
|
+
} else {
|
|
1368
|
+
${sourceRead(1, `inputBlock - ${sourceBlocks[0]}u`)}
|
|
1369
|
+
}
|
|
1370
|
+
`, gemm)) : /* wgsl */ `
|
|
1371
|
+
for (
|
|
1372
|
+
var loadIndex = localLinear;
|
|
1373
|
+
loadIndex < ${inputTileValues}u;
|
|
1374
|
+
loadIndex += ${workgroupThreads}u
|
|
1375
|
+
) {
|
|
1376
|
+
let tileSpatial = loadIndex / ${tileKBlocks}u;
|
|
1377
|
+
let tileK = loadIndex % ${tileKBlocks}u;
|
|
1378
|
+
let outputSpatialIndex =
|
|
1379
|
+
workgroupId.x * ${tileM}u + tileSpatial;
|
|
1380
|
+
let kIndex = kBase + tileK;
|
|
1381
|
+
var value = ${valueType}(0.0);
|
|
1382
|
+
if (outputSpatialIndex < spatialCount && kIndex < totalK) {
|
|
1383
|
+
let outputY = outputSpatialIndex / params.outputWidth;
|
|
1384
|
+
let outputX = outputSpatialIndex % params.outputWidth;
|
|
1385
|
+
let inputBlock = kIndex % ${inputBlocks}u;
|
|
1386
|
+
let kernelIndex = kIndex / ${inputBlocks}u;
|
|
1387
|
+
let kernelY = kernelIndex / 3u;
|
|
1388
|
+
let kernelX = kernelIndex % 3u;
|
|
1389
|
+
let inputY = i32(outputY) + i32(kernelY) - 1;
|
|
1390
|
+
let inputX = i32(outputX) + i32(kernelX) - 1;
|
|
1391
|
+
if (
|
|
1392
|
+
inputY >= 0 && inputY < i32(params.outputHeight) &&
|
|
1393
|
+
inputX >= 0 && inputX < i32(params.outputWidth)
|
|
1394
|
+
) {
|
|
1395
|
+
if (inputBlock < ${sourceBlocks[0]}u) {
|
|
1396
|
+
${sourceRead(0, 'inputBlock')}
|
|
1397
|
+
} else {
|
|
1398
|
+
${sourceRead(1, `inputBlock - ${sourceBlocks[0]}u`)}
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
inputTile[${gemmSharedInputIndex('loadIndex', gemm)}] = value;
|
|
1403
|
+
}
|
|
1404
|
+
`}
|
|
1405
|
+
|
|
1406
|
+
for (
|
|
1407
|
+
var loadIndex = localLinear;
|
|
1408
|
+
loadIndex < ${weightTileValues}u;
|
|
1409
|
+
loadIndex += ${workgroupThreads}u
|
|
1410
|
+
) {
|
|
1411
|
+
let tileK = loadIndex / ${tileNBlocks * 4}u;
|
|
1412
|
+
let outputRemainder = loadIndex % ${tileNBlocks * 4}u;
|
|
1413
|
+
let tileOutputBlock = outputRemainder / 4u;
|
|
1414
|
+
let outputLane = outputRemainder % 4u;
|
|
1415
|
+
let loadedOutputBlock =
|
|
1416
|
+
workgroupId.y * ${tileNBlocks}u + tileOutputBlock;
|
|
1417
|
+
let kIndex = kBase + tileK;
|
|
1418
|
+
var value = ${valueType}(0.0);
|
|
1419
|
+
if (loadedOutputBlock < ${outputBlocks}u && kIndex < totalK) {
|
|
1420
|
+
${weightLayout === 'k-major' ? /* wgsl */ `
|
|
1421
|
+
let weightIndex = (kIndex * ${outputBlocks}u + loadedOutputBlock) * 4u + outputLane;
|
|
1422
|
+
` : addressMode !== 'analytic' ? /* wgsl */ `
|
|
1423
|
+
let weightIndex = (loadedOutputBlock * ${inputBlocks * 9}u + kIndex) * 4u + outputLane;
|
|
1424
|
+
` : /* wgsl */ `
|
|
1425
|
+
let inputBlock = kIndex % ${inputBlocks}u;
|
|
1426
|
+
let kernelIndex = kIndex / ${inputBlocks}u;
|
|
1427
|
+
let kernelY = kernelIndex / 3u;
|
|
1428
|
+
let kernelX = kernelIndex % 3u;
|
|
1429
|
+
let weightIndex =
|
|
1430
|
+
((((loadedOutputBlock * 3u + kernelY) * 3u + kernelX) *
|
|
1431
|
+
${inputBlocks}u + inputBlock) * 4u + outputLane);
|
|
1432
|
+
`}
|
|
1433
|
+
value = ${gemmLoadExpression('weights[weightIndex]', packedWeights)};
|
|
1434
|
+
}
|
|
1435
|
+
weightTile[${gemmSharedWeightIndex('loadIndex', gemm)}] = value;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
workgroupBarrier();
|
|
1439
|
+
${tiledAccumulationCode(precision, gemm)}
|
|
1440
|
+
workgroupBarrier();
|
|
1441
|
+
${addressMode !== 'analytic' ? tiledAddressAdvanceCode(inputBlocks, gemm) : ''}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
if (outputBlock < ${outputBlocks}u) {
|
|
1445
|
+
for (var row = 0u; row < ${rowsPerThread}u; row++) {
|
|
1446
|
+
let spatialIndex = spatialBase + row;
|
|
1447
|
+
if (spatialIndex < spatialCount) {
|
|
1448
|
+
let outputIndex = spatialIndex * ${outputBlocks}u + outputBlock;
|
|
1449
|
+
outputData[outputIndex] = ${stored};
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
`;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* One workgroup owns one output phase, so its GEMM rows share the same four
|
|
1459
|
+
* combined upsampling taps. Skip inputs retain their original nine taps.
|
|
1460
|
+
* The shared tiles and short FP16 reduction groups match the regular GEMM;
|
|
1461
|
+
* only the decoder's mathematical decomposition and reduction order change.
|
|
1462
|
+
*/
|
|
1463
|
+
function createTiledSubpixelDecoderShader(
|
|
1464
|
+
precision: NativeUNetPrecision,
|
|
1465
|
+
outputPrecision: NativeUNetPrecision,
|
|
1466
|
+
activation: Conv2DNodeSpec['activation'],
|
|
1467
|
+
sourceBlocks: readonly number[],
|
|
1468
|
+
upsampledSource: 0 | 1,
|
|
1469
|
+
outputBlocks: number,
|
|
1470
|
+
gemm: Readonly<Required<NativeUNetGemmOptions>>
|
|
1471
|
+
) {
|
|
1472
|
+
const tile = gemmTile(gemm);
|
|
1473
|
+
const { workgroupX, workgroupY, rowsPerThread, tileM, tileNBlocks, tileKBlocks } = tile;
|
|
1474
|
+
const threads = workgroupX * workgroupY;
|
|
1475
|
+
const loads = tileM * tileKBlocks / threads;
|
|
1476
|
+
const valueType = storageVecType(precision);
|
|
1477
|
+
const packedInput = gemmPackedLoad(precision, gemm, false);
|
|
1478
|
+
const packedWeights = gemmPackedLoad(precision, gemm, true);
|
|
1479
|
+
const upBlocks = sourceBlocks[upsampledSource];
|
|
1480
|
+
const skipSource = (1 - upsampledSource) as 0 | 1;
|
|
1481
|
+
const skipBlocks = sourceBlocks[skipSource];
|
|
1482
|
+
const reduction = (source: 0 | 1, upsampled: boolean) => {
|
|
1483
|
+
const blocks = sourceBlocks[source];
|
|
1484
|
+
const taps = upsampled ? 4 : 9;
|
|
1485
|
+
const weightOffset = upsampled
|
|
1486
|
+
? `phase * ${4 * upBlocks * outputBlocks * 4}u`
|
|
1487
|
+
: `${SUBPIXEL_MATRICES * upBlocks * outputBlocks * 4}u`;
|
|
1488
|
+
const offset = upsampled
|
|
1489
|
+
? `(i32(tap / 2u) + i32(phaseY) - 1) * i32(params.source${source}Width) + i32(tap % 2u) + i32(phaseX) - 1`
|
|
1490
|
+
: `(i32(tap / 3u) - 1) * i32(params.source${source}Width) + i32(tap % 3u) - 1`;
|
|
1491
|
+
return /* wgsl */ `
|
|
1492
|
+
for (var kBase = 0u; kBase < ${taps * blocks}u; kBase += ${tileKBlocks}u) {
|
|
1493
|
+
let inputK = kBase + localLinear % ${tileKBlocks}u;
|
|
1494
|
+
let tap = inputK / ${blocks}u;
|
|
1495
|
+
let channel = inputK % ${blocks}u;
|
|
1496
|
+
for (var load = 0u; load < ${loads}u; load++) {
|
|
1497
|
+
let i = localLinear + load * ${threads}u;
|
|
1498
|
+
var value = ${valueType}(0.0);
|
|
1499
|
+
if (tap < ${taps}u && (mask${source}[load] & (1u << tap)) != 0u) {
|
|
1500
|
+
let offset = ${offset};
|
|
1501
|
+
let index = u32(base${source}[load] + offset) * ${blocks}u + channel;
|
|
1502
|
+
value = ${gemmLoadExpression(`input${source}[index]`, packedInput)};
|
|
1503
|
+
}
|
|
1504
|
+
inputTile[${gemmSharedInputIndex('i', gemm)}] = value;
|
|
1505
|
+
}
|
|
1506
|
+
for (var i = localLinear; i < ${tileKBlocks * tileNBlocks * 4}u; i += ${threads}u) {
|
|
1507
|
+
let k = kBase + i / ${tileNBlocks * 4}u;
|
|
1508
|
+
let outputBlock = workgroupId.y * ${tileNBlocks}u + (i % ${tileNBlocks * 4}u) / 4u;
|
|
1509
|
+
var value = ${valueType}(0.0);
|
|
1510
|
+
if (k < ${taps * blocks}u && outputBlock < ${outputBlocks}u) {
|
|
1511
|
+
let index = ${weightOffset} + (k * ${outputBlocks}u + outputBlock) * 4u + i % 4u;
|
|
1512
|
+
value = ${gemmLoadExpression('weights[index]', packedWeights)};
|
|
1513
|
+
}
|
|
1514
|
+
weightTile[${gemmSharedWeightIndex('i', gemm)}] = value;
|
|
1515
|
+
}
|
|
1516
|
+
workgroupBarrier();
|
|
1517
|
+
${tiledAccumulationCode(precision, gemm)}
|
|
1518
|
+
workgroupBarrier();
|
|
1519
|
+
}`;
|
|
1520
|
+
};
|
|
1521
|
+
const cache = (source: 0 | 1, upsampled: boolean) => {
|
|
1522
|
+
const taps = upsampled ? 4 : 9;
|
|
1523
|
+
const side = upsampled ? 2 : 3;
|
|
1524
|
+
const x = upsampled ? 'x' : 'x * 2u + phaseX';
|
|
1525
|
+
const y = upsampled ? 'y' : 'y * 2u + phaseY';
|
|
1526
|
+
return /* wgsl */ `
|
|
1527
|
+
let x${source} = i32(${x});
|
|
1528
|
+
let y${source} = i32(${y});
|
|
1529
|
+
base${source}[load] = y${source} * i32(params.source${source}Width) + x${source};
|
|
1530
|
+
var bits${source} = 0u;
|
|
1531
|
+
for (var tap = 0u; tap < ${taps}u; tap++) {
|
|
1532
|
+
let sx = x${source} + i32(tap % ${side}u) ${upsampled ? '+ i32(phaseX)' : ''} - 1;
|
|
1533
|
+
let sy = y${source} + i32(tap / ${side}u) ${upsampled ? '+ i32(phaseY)' : ''} - 1;
|
|
1534
|
+
if (valid && sx >= 0 && sy >= 0 && sx < i32(params.source${source}Width) && sy < i32(params.source${source}Height)) {
|
|
1535
|
+
bits${source} |= 1u << tap;
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
mask${source}[load] = bits${source};`;
|
|
1539
|
+
};
|
|
1540
|
+
return /* wgsl */ `${shaderPreamble(precision)}
|
|
1541
|
+
struct Params {
|
|
1542
|
+
outputWidth: u32, outputHeight: u32, outputBlocks: u32, inputBlocks: u32,
|
|
1543
|
+
source0Width: u32, source0Height: u32, source1Width: u32, source1Height: u32,
|
|
1544
|
+
}
|
|
1545
|
+
@group(0) @binding(0) var<storage, read> input0: array<${packedInput ? 'vec2<u32>' : valueType}>;
|
|
1546
|
+
@group(0) @binding(1) var<storage, read> input1: array<${packedInput ? 'vec2<u32>' : valueType}>;
|
|
1547
|
+
@group(0) @binding(2) var<storage, read> weights: array<${packedWeights ? 'vec2<u32>' : valueType}>;
|
|
1548
|
+
@group(0) @binding(3) var<storage, read> bias: array<vec4<f32>>;
|
|
1549
|
+
@group(0) @binding(4) var<storage, read_write> outputData: array<${storageVecType(outputPrecision)}>;
|
|
1550
|
+
@group(0) @binding(5) var<uniform> params: Params;
|
|
1551
|
+
var<workgroup> inputTile: array<${valueType}, ${gemmSharedSizes(gemm).input}>;
|
|
1552
|
+
var<workgroup> weightTile: array<${valueType}, ${gemmSharedSizes(gemm).weights}>;
|
|
1553
|
+
|
|
1554
|
+
@compute @workgroup_size(${workgroupX}, ${workgroupY}, 1)
|
|
1555
|
+
fn main(@builtin(local_invocation_id) localId: vec3<u32>, @builtin(workgroup_id) workgroupId: vec3<u32>) {
|
|
1556
|
+
let phase = workgroupId.z;
|
|
1557
|
+
let phaseX = phase & 1u;
|
|
1558
|
+
let phaseY = phase >> 1u;
|
|
1559
|
+
let phaseWidth = (params.outputWidth + 1u) / 2u;
|
|
1560
|
+
let localLinear = localId.y * ${workgroupX}u + localId.x;
|
|
1561
|
+
let outputBlock = workgroupId.y * ${tileNBlocks}u + localId.x;
|
|
1562
|
+
var acc: array<vec4<f32>, ${rowsPerThread}>;
|
|
1563
|
+
if (outputBlock < ${outputBlocks}u) {
|
|
1564
|
+
for (var row = 0u; row < ${rowsPerThread}u; row++) { acc[row] = bias[outputBlock]; }
|
|
1565
|
+
}
|
|
1566
|
+
var base0: array<i32, ${loads}>;
|
|
1567
|
+
var base1: array<i32, ${loads}>;
|
|
1568
|
+
var mask0: array<u32, ${loads}>;
|
|
1569
|
+
var mask1: array<u32, ${loads}>;
|
|
1570
|
+
for (var load = 0u; load < ${loads}u; load++) {
|
|
1571
|
+
let spatial = workgroupId.x * ${tileM}u + (localLinear + load * ${threads}u) / ${tileKBlocks}u;
|
|
1572
|
+
let x = spatial % phaseWidth;
|
|
1573
|
+
let y = spatial / phaseWidth;
|
|
1574
|
+
let valid = x * 2u + phaseX < params.outputWidth && y * 2u + phaseY < params.outputHeight;
|
|
1575
|
+
${cache(upsampledSource, true)}
|
|
1576
|
+
${cache(skipSource, false)}
|
|
1577
|
+
}
|
|
1578
|
+
${reduction(upsampledSource, true)}
|
|
1579
|
+
${reduction(skipSource, false)}
|
|
1580
|
+
if (outputBlock < ${outputBlocks}u) {
|
|
1581
|
+
for (var row = 0u; row < ${rowsPerThread}u; row++) {
|
|
1582
|
+
let spatial = workgroupId.x * ${tileM}u + localId.y * ${rowsPerThread}u + row;
|
|
1583
|
+
let x = (spatial % phaseWidth) * 2u + phaseX;
|
|
1584
|
+
let y = (spatial / phaseWidth) * 2u + phaseY;
|
|
1585
|
+
if (x < params.outputWidth && y < params.outputHeight) {
|
|
1586
|
+
outputData[(y * params.outputWidth + x) * ${outputBlocks}u + outputBlock] = ${storeExpression(activationExpression('acc[row]', activation), outputPrecision)};
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
`;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
function createFusedDecoderShader(
|
|
1595
|
+
precision: NativeUNetPrecision,
|
|
1596
|
+
outputPrecision: NativeUNetPrecision,
|
|
1597
|
+
activation: Conv2DNodeSpec['activation'],
|
|
1598
|
+
sourceBlocks: readonly number[],
|
|
1599
|
+
upsampledSource: 0 | 1,
|
|
1600
|
+
outputBlocks: number
|
|
1601
|
+
) {
|
|
1602
|
+
const valueType = storageVecType(precision);
|
|
1603
|
+
const outputType = storageVecType(outputPrecision);
|
|
1604
|
+
const inputBlocks = sourceBlocks[0] + sourceBlocks[1];
|
|
1605
|
+
const sourceCode = (source: 0 | 1, blockOffset: number) => {
|
|
1606
|
+
const isUpsampled = source === upsampledSource;
|
|
1607
|
+
return /* wgsl */ `
|
|
1608
|
+
{
|
|
1609
|
+
let sourceX = ${isUpsampled ? 'u32(inputX) / 2u' : 'u32(inputX)'};
|
|
1610
|
+
let sourceY = ${isUpsampled ? 'u32(inputY) / 2u' : 'u32(inputY)'};
|
|
1611
|
+
let sourcePixelBase = (sourceY * params.source${source}Width + sourceX) * ${sourceBlocks[source]}u;
|
|
1612
|
+
for (var sourceBlock = 0u; sourceBlock < ${sourceBlocks[source]}u; sourceBlock++) {
|
|
1613
|
+
let inputBlock = ${blockOffset}u + sourceBlock;
|
|
1614
|
+
${accumulationCode(
|
|
1615
|
+
`input${source}[sourcePixelBase + sourceBlock]`,
|
|
1616
|
+
`((((gid.z * 3u + ky) * 3u + kx) * ${inputBlocks}u + inputBlock) * 4u)`,
|
|
1617
|
+
precision
|
|
1618
|
+
)}
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
`;
|
|
1622
|
+
};
|
|
1623
|
+
const stored = storeExpression(
|
|
1624
|
+
activationExpression('acc', activation),
|
|
1625
|
+
outputPrecision
|
|
1626
|
+
);
|
|
1627
|
+
return /* wgsl */ `${shaderPreamble(precision)}
|
|
1628
|
+
struct Params {
|
|
1629
|
+
outputWidth: u32,
|
|
1630
|
+
outputHeight: u32,
|
|
1631
|
+
outputBlocks: u32,
|
|
1632
|
+
inputBlocks: u32,
|
|
1633
|
+
source0Width: u32,
|
|
1634
|
+
source0Height: u32,
|
|
1635
|
+
source1Width: u32,
|
|
1636
|
+
source1Height: u32,
|
|
1637
|
+
}
|
|
1638
|
+
@group(0) @binding(0) var<storage, read> input0: array<${valueType}>;
|
|
1639
|
+
@group(0) @binding(1) var<storage, read> input1: array<${valueType}>;
|
|
1640
|
+
@group(0) @binding(2) var<storage, read> weights: array<${valueType}>;
|
|
1641
|
+
@group(0) @binding(3) var<storage, read> bias: array<vec4<f32>>;
|
|
1642
|
+
@group(0) @binding(4) var<storage, read_write> outputData: array<${outputType}>;
|
|
1643
|
+
@group(0) @binding(5) var<uniform> params: Params;
|
|
1644
|
+
|
|
1645
|
+
@compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE}, 1)
|
|
1646
|
+
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
1647
|
+
if (gid.x >= params.outputWidth || gid.y >= params.outputHeight || gid.z >= ${outputBlocks}u) {
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
var acc = bias[gid.z];
|
|
1651
|
+
for (var ky = 0u; ky < 3u; ky++) {
|
|
1652
|
+
let inputY = i32(gid.y) + i32(ky) - 1;
|
|
1653
|
+
if (inputY < 0 || inputY >= i32(params.outputHeight)) { continue; }
|
|
1654
|
+
for (var kx = 0u; kx < 3u; kx++) {
|
|
1655
|
+
let inputX = i32(gid.x) + i32(kx) - 1;
|
|
1656
|
+
if (inputX < 0 || inputX >= i32(params.outputWidth)) { continue; }
|
|
1657
|
+
${sourceCode(0, 0)}
|
|
1658
|
+
${sourceCode(1, sourceBlocks[0])}
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
let outputIndex = (gid.y * params.outputWidth + gid.x) * ${outputBlocks}u + gid.z;
|
|
1662
|
+
outputData[outputIndex] = ${stored};
|
|
1663
|
+
}
|
|
1664
|
+
`;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
function createSpatialDecoderShader(
|
|
1668
|
+
precision: NativeUNetPrecision,
|
|
1669
|
+
outputPrecision: NativeUNetPrecision,
|
|
1670
|
+
activation: Conv2DNodeSpec['activation'],
|
|
1671
|
+
sourceBlocks: readonly number[],
|
|
1672
|
+
upsampledSource: 0 | 1,
|
|
1673
|
+
outputBlocks: number
|
|
1674
|
+
) {
|
|
1675
|
+
const valueType = storageVecType(precision);
|
|
1676
|
+
const outputType = storageVecType(outputPrecision);
|
|
1677
|
+
const inputBlocks = sourceBlocks[0] + sourceBlocks[1];
|
|
1678
|
+
const patchValues =
|
|
1679
|
+
SPATIAL_CONV_PATCH * SPATIAL_CONV_PATCH * inputBlocks;
|
|
1680
|
+
const workgroupThreads =
|
|
1681
|
+
SPATIAL_CONV_WORKGROUP * SPATIAL_CONV_WORKGROUP;
|
|
1682
|
+
const stored = storeExpression(
|
|
1683
|
+
activationExpression('acc', activation),
|
|
1684
|
+
outputPrecision
|
|
1685
|
+
);
|
|
1686
|
+
const sourceRead = (source: 0 | 1, blockExpression: string) => {
|
|
1687
|
+
const sourceX =
|
|
1688
|
+
source === upsampledSource ? 'u32(inputX) / 2u' : 'u32(inputX)';
|
|
1689
|
+
const sourceY =
|
|
1690
|
+
source === upsampledSource ? 'u32(inputY) / 2u' : 'u32(inputY)';
|
|
1691
|
+
return /* wgsl */ `
|
|
1692
|
+
let sourceBlock = ${blockExpression};
|
|
1693
|
+
let sourceIndex =
|
|
1694
|
+
(${sourceY} * params.source${source}Width + ${sourceX}) *
|
|
1695
|
+
${sourceBlocks[source]}u + sourceBlock;
|
|
1696
|
+
value = input${source}[sourceIndex];`;
|
|
1697
|
+
};
|
|
1698
|
+
|
|
1699
|
+
return /* wgsl */ `${shaderPreamble(precision)}
|
|
1700
|
+
struct Params {
|
|
1701
|
+
outputWidth: u32,
|
|
1702
|
+
outputHeight: u32,
|
|
1703
|
+
outputBlocks: u32,
|
|
1704
|
+
inputBlocks: u32,
|
|
1705
|
+
source0Width: u32,
|
|
1706
|
+
source0Height: u32,
|
|
1707
|
+
source1Width: u32,
|
|
1708
|
+
source1Height: u32,
|
|
1709
|
+
}
|
|
1710
|
+
@group(0) @binding(0) var<storage, read> input0: array<${valueType}>;
|
|
1711
|
+
@group(0) @binding(1) var<storage, read> input1: array<${valueType}>;
|
|
1712
|
+
@group(0) @binding(2) var<storage, read> weights: array<${valueType}>;
|
|
1713
|
+
@group(0) @binding(3) var<storage, read> bias: array<vec4<f32>>;
|
|
1714
|
+
@group(0) @binding(4) var<storage, read_write> outputData: array<${outputType}>;
|
|
1715
|
+
@group(0) @binding(5) var<uniform> params: Params;
|
|
1716
|
+
|
|
1717
|
+
var<workgroup> inputPatch: array<${valueType}, ${patchValues}>;
|
|
1718
|
+
|
|
1719
|
+
@compute @workgroup_size(${SPATIAL_CONV_WORKGROUP}, ${SPATIAL_CONV_WORKGROUP}, 1)
|
|
1720
|
+
fn main(
|
|
1721
|
+
@builtin(local_invocation_id) localId: vec3<u32>,
|
|
1722
|
+
@builtin(workgroup_id) workgroupId: vec3<u32>
|
|
1723
|
+
) {
|
|
1724
|
+
let localLinear =
|
|
1725
|
+
localId.y * ${SPATIAL_CONV_WORKGROUP}u + localId.x;
|
|
1726
|
+
for (
|
|
1727
|
+
var loadIndex = localLinear;
|
|
1728
|
+
loadIndex < ${patchValues}u;
|
|
1729
|
+
loadIndex += ${workgroupThreads}u
|
|
1730
|
+
) {
|
|
1731
|
+
let patchPixel = loadIndex / ${inputBlocks}u;
|
|
1732
|
+
let inputBlock = loadIndex % ${inputBlocks}u;
|
|
1733
|
+
let patchX = patchPixel % ${SPATIAL_CONV_PATCH}u;
|
|
1734
|
+
let patchY = patchPixel / ${SPATIAL_CONV_PATCH}u;
|
|
1735
|
+
let inputX =
|
|
1736
|
+
i32(workgroupId.x * ${SPATIAL_CONV_WORKGROUP}u + patchX) - 1;
|
|
1737
|
+
let inputY =
|
|
1738
|
+
i32(workgroupId.y * ${SPATIAL_CONV_WORKGROUP}u + patchY) - 1;
|
|
1739
|
+
var value = ${valueType}(0.0);
|
|
1740
|
+
if (
|
|
1741
|
+
inputX >= 0 && inputX < i32(params.outputWidth) &&
|
|
1742
|
+
inputY >= 0 && inputY < i32(params.outputHeight)
|
|
1743
|
+
) {
|
|
1744
|
+
if (inputBlock < ${sourceBlocks[0]}u) {
|
|
1745
|
+
${sourceRead(0, 'inputBlock')}
|
|
1746
|
+
} else {
|
|
1747
|
+
${sourceRead(1, `inputBlock - ${sourceBlocks[0]}u`)}
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
inputPatch[loadIndex] = value;
|
|
1751
|
+
}
|
|
1752
|
+
workgroupBarrier();
|
|
1753
|
+
|
|
1754
|
+
let outputX =
|
|
1755
|
+
workgroupId.x * ${SPATIAL_CONV_WORKGROUP}u + localId.x;
|
|
1756
|
+
let outputY =
|
|
1757
|
+
workgroupId.y * ${SPATIAL_CONV_WORKGROUP}u + localId.y;
|
|
1758
|
+
let outputBlock = workgroupId.z;
|
|
1759
|
+
if (
|
|
1760
|
+
outputX >= params.outputWidth || outputY >= params.outputHeight ||
|
|
1761
|
+
outputBlock >= ${outputBlocks}u
|
|
1762
|
+
) {
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
var acc = bias[outputBlock];
|
|
1767
|
+
for (var ky = 0u; ky < 3u; ky++) {
|
|
1768
|
+
for (var kx = 0u; kx < 3u; kx++) {
|
|
1769
|
+
let patchBase =
|
|
1770
|
+
((localId.y + ky) * ${SPATIAL_CONV_PATCH}u + localId.x + kx) *
|
|
1771
|
+
${inputBlocks}u;
|
|
1772
|
+
for (var inputBlock = 0u; inputBlock < ${inputBlocks}u; inputBlock++) {
|
|
1773
|
+
${accumulationCode(
|
|
1774
|
+
'inputPatch[patchBase + inputBlock]',
|
|
1775
|
+
`((((outputBlock * 3u + ky) * 3u + kx) * ${inputBlocks}u + inputBlock) * 4u)`,
|
|
1776
|
+
precision
|
|
1777
|
+
)}
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
let outputIndex =
|
|
1782
|
+
(outputY * params.outputWidth + outputX) * ${outputBlocks}u + outputBlock;
|
|
1783
|
+
outputData[outputIndex] = ${stored};
|
|
1784
|
+
}
|
|
1785
|
+
`;
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
export function createInputPackShader(
|
|
1789
|
+
precision: NativeUNetPrecision,
|
|
1790
|
+
sourceCount: number
|
|
1791
|
+
) {
|
|
1792
|
+
const outputType = storageVecType(precision);
|
|
1793
|
+
const inputBindings = Array.from(
|
|
1794
|
+
{ length: sourceCount },
|
|
1795
|
+
(_, index) =>
|
|
1796
|
+
`@group(0) @binding(${index}) var<storage, read> input${index}: array<vec4<f32>>;`
|
|
1797
|
+
).join('\n');
|
|
1798
|
+
const readBranches = Array.from({ length: sourceCount }, (_, index) => {
|
|
1799
|
+
const firstChannel = index * 3;
|
|
1800
|
+
return `if (channel < ${firstChannel + 3}u) { return input${index}[pixel][channel - ${firstChannel}u]; }`;
|
|
1801
|
+
}).join('\n ');
|
|
1802
|
+
const outputBinding = sourceCount;
|
|
1803
|
+
const paramsBinding = sourceCount + 1;
|
|
1804
|
+
return /* wgsl */ `${shaderPreamble(precision)}
|
|
1805
|
+
struct Params {
|
|
1806
|
+
width: u32,
|
|
1807
|
+
height: u32,
|
|
1808
|
+
outputBlocks: u32,
|
|
1809
|
+
inputChannels: u32,
|
|
1810
|
+
}
|
|
1811
|
+
${inputBindings}
|
|
1812
|
+
@group(0) @binding(${outputBinding}) var<storage, read_write> outputData: array<${outputType}>;
|
|
1813
|
+
@group(0) @binding(${paramsBinding}) var<uniform> params: Params;
|
|
1814
|
+
|
|
1815
|
+
fn readChannel(pixel: u32, channel: u32) -> f32 {
|
|
1816
|
+
${readBranches}
|
|
1817
|
+
return 0.0;
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
@compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE}, 1)
|
|
1821
|
+
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
1822
|
+
if (gid.x >= params.width || gid.y >= params.height || gid.z >= params.outputBlocks) {
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
let pixel = gid.y * params.width + gid.x;
|
|
1826
|
+
let firstChannel = gid.z * 4u;
|
|
1827
|
+
let value = vec4<f32>(
|
|
1828
|
+
readChannel(pixel, firstChannel),
|
|
1829
|
+
readChannel(pixel, firstChannel + 1u),
|
|
1830
|
+
readChannel(pixel, firstChannel + 2u),
|
|
1831
|
+
readChannel(pixel, firstChannel + 3u)
|
|
1832
|
+
);
|
|
1833
|
+
outputData[pixel * params.outputBlocks + gid.z] = ${storeExpression('value', precision)};
|
|
1834
|
+
}
|
|
1835
|
+
`;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
const SUBGROUP_MATRIX_FEATURE = 'chromium-experimental-subgroup-matrix';
|
|
1839
|
+
/** wgpu's feature name; native hosts report it through their device. */
|
|
1840
|
+
const COOPERATIVE_MATRIX_FEATURE = 'wgpu-cooperative-matrix';
|
|
1841
|
+
const SUBGROUP_MATRIX_TILE_WIDTH = 32;
|
|
1842
|
+
// Four subgroups own two output rows each, matching the 2x2 pooling windows.
|
|
1843
|
+
const SUBGROUP_MATRIX_TILE_HEIGHT = 8;
|
|
1844
|
+
const SUBGROUP_MATRIX_SUBGROUPS = 4;
|
|
1845
|
+
// Below this many workgroups a layer uses one output fragment per workgroup.
|
|
1846
|
+
const SUBGROUP_MATRIX_MIN_WORKGROUPS = 1024;
|
|
1847
|
+
|
|
1848
|
+
interface AdapterSubgroupMatrixInfo {
|
|
1849
|
+
subgroupMinSize?: number;
|
|
1850
|
+
subgroupMaxSize?: number;
|
|
1851
|
+
subgroupMatrixConfigs?: readonly {
|
|
1852
|
+
componentType: string;
|
|
1853
|
+
resultComponentType: string;
|
|
1854
|
+
M: number;
|
|
1855
|
+
N: number;
|
|
1856
|
+
K: number;
|
|
1857
|
+
}[];
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
/**
|
|
1861
|
+
* Picks a square configuration, preferring f32 accumulation: mixed f16
|
|
1862
|
+
* inputs first, then f32 inputs (whose f16-exact values give the same
|
|
1863
|
+
* products), and f16 accumulation last.
|
|
1864
|
+
*/
|
|
1865
|
+
export function selectSubgroupMatrixConfig(
|
|
1866
|
+
device: OIDNGPUDevice,
|
|
1867
|
+
accumulation: 'f32' | 'f16' = 'f32'
|
|
1868
|
+
): NativeSubgroupMatrixConfig | undefined {
|
|
1869
|
+
const dialect = device.features.has(SUBGROUP_MATRIX_FEATURE)
|
|
1870
|
+
? 'tint'
|
|
1871
|
+
: device.features.has(COOPERATIVE_MATRIX_FEATURE)
|
|
1872
|
+
? 'naga'
|
|
1873
|
+
: undefined;
|
|
1874
|
+
if (!dialect || !device.features.has('subgroups')) return undefined;
|
|
1875
|
+
const info = (device as OIDNGPUDevice & { adapterInfo?: OIDNGPUAdapterInfo }).adapterInfo;
|
|
1876
|
+
const subgroupSize = info?.subgroupMaxSize ?? 0;
|
|
1877
|
+
if (!info || !subgroupSize || info.subgroupMinSize !== subgroupSize) return undefined;
|
|
1878
|
+
const preferences: readonly [string, string][] = accumulation === 'f16'
|
|
1879
|
+
? [['f16', 'f16'], ['f16', 'f32'], ['f32', 'f32']]
|
|
1880
|
+
: [['f16', 'f32'], ['f32', 'f32'], ['f16', 'f16']];
|
|
1881
|
+
const shader16 = device.features.has('shader-f16');
|
|
1882
|
+
for (const [componentType, resultType] of preferences) {
|
|
1883
|
+
if (!shader16 && (componentType === 'f16' || resultType === 'f16')) continue;
|
|
1884
|
+
for (const size of [8, 16] as const) {
|
|
1885
|
+
const match = info.subgroupMatrixConfigs?.find((config) =>
|
|
1886
|
+
config.componentType === componentType &&
|
|
1887
|
+
config.resultComponentType === resultType &&
|
|
1888
|
+
config.M === size && config.N === size && config.K === size);
|
|
1889
|
+
if (match) {
|
|
1890
|
+
return {
|
|
1891
|
+
componentType: componentType as 'f16' | 'f32',
|
|
1892
|
+
resultType: resultType as 'f16' | 'f32',
|
|
1893
|
+
size,
|
|
1894
|
+
subgroupSize,
|
|
1895
|
+
dialect
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
return undefined;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
function subgroupMatrixFragments(
|
|
1904
|
+
config: NativeSubgroupMatrixConfig,
|
|
1905
|
+
options: NativeSubgroupMatrixOptions
|
|
1906
|
+
) {
|
|
1907
|
+
return options.outputFragments ?? 2;
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
function subgroupMatrixSharedBytes(
|
|
1911
|
+
config: NativeSubgroupMatrixConfig,
|
|
1912
|
+
outputFragments: number,
|
|
1913
|
+
subPixel: boolean,
|
|
1914
|
+
doubleBuffer: boolean
|
|
1915
|
+
) {
|
|
1916
|
+
const component = config.componentType === 'f16' ? 2 : 4;
|
|
1917
|
+
const result = config.resultType === 'f16' ? 2 : 4;
|
|
1918
|
+
const size = config.size;
|
|
1919
|
+
const halo = (SUBGROUP_MATRIX_TILE_HEIGHT + 2) * (SUBGROUP_MATRIX_TILE_WIDTH + 2) * size;
|
|
1920
|
+
const matrices = subPixel ? 16 : 9;
|
|
1921
|
+
return component * (doubleBuffer ? 2 : 1) * (halo + matrices * size * size * outputFragments) +
|
|
1922
|
+
result * SUBGROUP_MATRIX_SUBGROUPS * SUBGROUP_MATRIX_TILE_WIDTH * size;
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
/** WGSL spelling of the subgroup-matrix operations for one configuration. */
|
|
1926
|
+
function subgroupMatrixDialect(config: NativeSubgroupMatrixConfig) {
|
|
1927
|
+
const size = config.size;
|
|
1928
|
+
const naga = config.dialect === 'naga';
|
|
1929
|
+
const matrixType = (scalar: string, tint: string, role: string) => naga
|
|
1930
|
+
? `coop_mat${size}x${size}<${scalar}, ${role}>`
|
|
1931
|
+
: `subgroup_matrix_${tint}<${scalar}, ${size}, ${size}>`;
|
|
1932
|
+
const result = matrixType(config.resultType, 'result', 'C');
|
|
1933
|
+
return {
|
|
1934
|
+
left: matrixType(config.componentType, 'left', 'A'),
|
|
1935
|
+
right: matrixType(config.componentType, 'right', 'B'),
|
|
1936
|
+
result,
|
|
1937
|
+
load: (type: string, array: string, offset: string, stride: string) => naga
|
|
1938
|
+
? `coopLoadT<${type}>(&${array}[${offset}], ${stride})`
|
|
1939
|
+
: `subgroupMatrixLoad<${type}, row_major>(&${array}, ${offset}, ${stride})`,
|
|
1940
|
+
store: (array: string, offset: string, value: string, stride: string) => naga
|
|
1941
|
+
? `coopStoreT(${value}, &${array}[${offset}], ${stride});`
|
|
1942
|
+
: `subgroupMatrixStore<row_major>(&${array}, ${offset}, ${value}, ${stride});`,
|
|
1943
|
+
multiplyAdd: naga ? 'coopMultiplyAdd' : 'subgroupMatrixMultiplyAccumulate',
|
|
1944
|
+
declare: (name: string) => naga ? `var ${name}: ${result};` : `var ${name} = ${result}();`,
|
|
1945
|
+
// Naga provides subgroups without (and rejects) `enable subgroups`.
|
|
1946
|
+
extensions: naga
|
|
1947
|
+
? 'enable wgpu_cooperative_matrix;'
|
|
1948
|
+
: `enable subgroups;
|
|
1949
|
+
enable chromium_experimental_subgroup_matrix;
|
|
1950
|
+
diagnostic(off, chromium_experimental.subgroup_matrix_uniformity);`
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
/** One workgroup-memory array filled per input-channel chunk. */
|
|
1955
|
+
interface SubgroupMatrixStage {
|
|
1956
|
+
array: string;
|
|
1957
|
+
/** vec4 values staged per chunk. */
|
|
1958
|
+
vectors: number;
|
|
1959
|
+
/** vec4 capacity of one buffer half (default `vectors`). */
|
|
1960
|
+
stride?: number;
|
|
1961
|
+
/** WGSL that may assign `value` from `i` (vec4 index) and `stageChunk`. */
|
|
1962
|
+
read: string;
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
/**
|
|
1966
|
+
* Loop over input-channel chunks that stages `stages` and runs `body` on
|
|
1967
|
+
* them. The body addresses each array at `<array>Base`. Double buffering
|
|
1968
|
+
* prefetches the next chunk into registers while the body runs, then stores
|
|
1969
|
+
* it into the other half of each array: one barrier per chunk instead of two.
|
|
1970
|
+
*/
|
|
1971
|
+
function subgroupMatrixChunkLoop(
|
|
1972
|
+
chunks: number,
|
|
1973
|
+
stages: readonly SubgroupMatrixStage[],
|
|
1974
|
+
body: string,
|
|
1975
|
+
threads: number,
|
|
1976
|
+
valueType: string,
|
|
1977
|
+
component: string,
|
|
1978
|
+
doubleBuffer: boolean
|
|
1979
|
+
) {
|
|
1980
|
+
const storeValue = (stage: SubgroupMatrixStage, value: string, base: string) => `{
|
|
1981
|
+
let staged = vec4<${component}>(${value});
|
|
1982
|
+
let base = ${base} + i * 4u;
|
|
1983
|
+
${stage.array}[base] = staged.x;
|
|
1984
|
+
${stage.array}[base + 1u] = staged.y;
|
|
1985
|
+
${stage.array}[base + 2u] = staged.z;
|
|
1986
|
+
${stage.array}[base + 3u] = staged.w;
|
|
1987
|
+
}`;
|
|
1988
|
+
// Keep staging loops' trip counts uniform before the matrix operations.
|
|
1989
|
+
// A localIndex-dependent trip count produces nondeterministic results on
|
|
1990
|
+
// NVIDIA Vulkan with runtime checks disabled, despite the following barrier.
|
|
1991
|
+
const direct = (base: (stage: SubgroupMatrixStage) => string) => stages.map((stage) => `
|
|
1992
|
+
for (var batch = 0u; batch < ${Math.ceil(stage.vectors / threads)}u; batch++) {
|
|
1993
|
+
let i = localIndex + batch * ${threads}u;
|
|
1994
|
+
if (i < ${stage.vectors}u) {
|
|
1995
|
+
var value = ${valueType}(0.0);
|
|
1996
|
+
${stage.read}
|
|
1997
|
+
${storeValue(stage, 'value', base(stage))}
|
|
1998
|
+
}
|
|
1999
|
+
}`).join('');
|
|
2000
|
+
const bases = (buffer: string) => stages
|
|
2001
|
+
.map((stage) => `let ${stage.array}Base = ${buffer} * ${(stage.stride ?? stage.vectors) * 4}u;`)
|
|
2002
|
+
.join('\n ');
|
|
2003
|
+
if (!doubleBuffer || chunks < 2) {
|
|
2004
|
+
return /* wgsl */ `
|
|
2005
|
+
for (var chunk = 0u; chunk < ${chunks}u; chunk++) {
|
|
2006
|
+
let stageChunk = chunk;
|
|
2007
|
+
${bases('0u')}
|
|
2008
|
+
${direct((stage) => `${stage.array}Base`)}
|
|
2009
|
+
workgroupBarrier();
|
|
2010
|
+
${body}
|
|
2011
|
+
workgroupBarrier();
|
|
2012
|
+
}`;
|
|
2013
|
+
}
|
|
2014
|
+
const registers = stages.map((stage) => Math.ceil(stage.vectors / threads));
|
|
2015
|
+
const prefetch = stages.map((stage, index) =>
|
|
2016
|
+
Array.from({ length: registers[index] }, (_, slot) => `
|
|
2017
|
+
{
|
|
2018
|
+
let i = localIndex + ${slot * threads}u;
|
|
2019
|
+
var value = ${valueType}(0.0);
|
|
2020
|
+
if (i < ${stage.vectors}u) {
|
|
2021
|
+
${stage.read}
|
|
2022
|
+
}
|
|
2023
|
+
prefetch${index}_${slot} = value;
|
|
2024
|
+
}`).join('')).join('');
|
|
2025
|
+
const declarations = stages.map((stage, index) =>
|
|
2026
|
+
Array.from({ length: registers[index] }, (_, slot) =>
|
|
2027
|
+
`var prefetch${index}_${slot} = ${valueType}(0.0);`).join('\n ')).join('\n ');
|
|
2028
|
+
const storePrefetched = stages.map((stage, index) =>
|
|
2029
|
+
Array.from({ length: registers[index] }, (_, slot) => `
|
|
2030
|
+
{
|
|
2031
|
+
let i = localIndex + ${slot * threads}u;
|
|
2032
|
+
if (i < ${stage.vectors}u) ${storeValue(stage, `prefetch${index}_${slot}`, `next * ${(stage.stride ?? stage.vectors) * 4}u`)}
|
|
2033
|
+
}`).join('')).join('');
|
|
2034
|
+
return /* wgsl */ `
|
|
2035
|
+
{
|
|
2036
|
+
let stageChunk = 0u;
|
|
2037
|
+
${direct(() => '0u')}
|
|
2038
|
+
}
|
|
2039
|
+
workgroupBarrier();
|
|
2040
|
+
for (var chunk = 0u; chunk < ${chunks}u; chunk++) {
|
|
2041
|
+
let buffer = chunk & 1u;
|
|
2042
|
+
let next = 1u - buffer;
|
|
2043
|
+
${bases('buffer')}
|
|
2044
|
+
${declarations}
|
|
2045
|
+
if (chunk + 1u < ${chunks}u) {
|
|
2046
|
+
let stageChunk = chunk + 1u;
|
|
2047
|
+
${prefetch}
|
|
2048
|
+
}
|
|
2049
|
+
${body}
|
|
2050
|
+
if (chunk + 1u < ${chunks}u) {
|
|
2051
|
+
${storePrefetched}
|
|
2052
|
+
}
|
|
2053
|
+
workgroupBarrier();
|
|
2054
|
+
}`;
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
/** Fixed native tap offsets preserve the chunk/tap reduction order. */
|
|
2058
|
+
function subgroupMatrixTapLoop(config: NativeSubgroupMatrixConfig, body: string) {
|
|
2059
|
+
// Explicit taps reduce native matrix latency on M4 Max. Keep the browser
|
|
2060
|
+
// shader's loop form and both paths' original accumulation order.
|
|
2061
|
+
return config.dialect === 'naga'
|
|
2062
|
+
? Array.from({ length: 9 }, (_, tap) => `{ let tap = ${tap}u; ${body} }`).join('\n')
|
|
2063
|
+
: `for (var tap = 0u; tap < 9u; tap++) { ${body} }`;
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
/**
|
|
2067
|
+
* 3x3 convolution as an implicit GEMM on subgroup matrices. Each workgroup
|
|
2068
|
+
* computes 8 rows x 32 pixels x (fragments x S) output channels. For every
|
|
2069
|
+
* chunk of S input channels it stages a zero-padded halo tile and the chunk's
|
|
2070
|
+
* weights in workgroup memory, so each A fragment is a contiguous run of S
|
|
2071
|
+
* pixels for one tap. A decoder reads its concatenated sources (one of them
|
|
2072
|
+
* nearest-upsampled) while staging, like the other fused decoder kernels.
|
|
2073
|
+
*/
|
|
2074
|
+
function createSubgroupMatrixConvShader(
|
|
2075
|
+
precision: NativeUNetPrecision,
|
|
2076
|
+
outputPrecision: NativeUNetPrecision,
|
|
2077
|
+
activation: Conv2DNodeSpec['activation'],
|
|
2078
|
+
inputBlocks: number,
|
|
2079
|
+
outputBlocks: number,
|
|
2080
|
+
config: NativeSubgroupMatrixConfig,
|
|
2081
|
+
outputFragments: number,
|
|
2082
|
+
doubleBuffer: boolean,
|
|
2083
|
+
decoder?: { sourceBlocks: readonly number[]; upsampledSource: 0 | 1 },
|
|
2084
|
+
pool = false
|
|
2085
|
+
) {
|
|
2086
|
+
const size = config.size;
|
|
2087
|
+
const component = config.componentType;
|
|
2088
|
+
const result = config.resultType;
|
|
2089
|
+
const tileWidth = SUBGROUP_MATRIX_TILE_WIDTH;
|
|
2090
|
+
const haloWidth = tileWidth + 2;
|
|
2091
|
+
const haloRows = SUBGROUP_MATRIX_TILE_HEIGHT + 2;
|
|
2092
|
+
const buffers = doubleBuffer ? 2 : 1;
|
|
2093
|
+
const chunkBlocks = size / 4;
|
|
2094
|
+
const chunks = Math.ceil(inputBlocks / chunkBlocks);
|
|
2095
|
+
const outputChannels = outputFragments * size;
|
|
2096
|
+
const pixelFragments = tileWidth / size;
|
|
2097
|
+
const threads = SUBGROUP_MATRIX_SUBGROUPS * config.subgroupSize;
|
|
2098
|
+
const valueType = storageVecType(precision);
|
|
2099
|
+
const outputType = storageVecType(outputPrecision);
|
|
2100
|
+
const { left, right, load, store, multiplyAdd, declare, extensions } = subgroupMatrixDialect(config);
|
|
2101
|
+
const accumulators: string[] = [];
|
|
2102
|
+
for (let row = 0; row < 2; row++) {
|
|
2103
|
+
for (let pixel = 0; pixel < pixelFragments; pixel++) {
|
|
2104
|
+
for (let fragment = 0; fragment < outputFragments; fragment++) {
|
|
2105
|
+
accumulators.push(`acc${row}_${pixel}_${fragment}`);
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
const declarations = accumulators.map(declare).join('\n ');
|
|
2110
|
+
const rightLoads = Array.from({ length: outputFragments }, (_, fragment) =>
|
|
2111
|
+
`let b${fragment} = ${load(right, 'weightTile', `weightTileBase + tap * ${size * outputChannels}u + ${fragment * size}u`, `${outputChannels}u`)};`
|
|
2112
|
+
).join('\n ');
|
|
2113
|
+
const multiplies: string[] = [];
|
|
2114
|
+
for (let row = 0; row < 2; row++) {
|
|
2115
|
+
for (let pixel = 0; pixel < pixelFragments; pixel++) {
|
|
2116
|
+
multiplies.push(
|
|
2117
|
+
`{
|
|
2118
|
+
let a = ${load(left, 'inputTile', `inputTileBase + ((rowBase + ${row}u + ky) * ${haloWidth}u + ${pixel * size}u + kx) * ${size}u`, `${size}u`)};
|
|
2119
|
+
${Array.from({ length: outputFragments }, (_, fragment) =>
|
|
2120
|
+
`acc${row}_${pixel}_${fragment} = ${multiplyAdd}(a, b${fragment}, acc${row}_${pixel}_${fragment});`
|
|
2121
|
+
).join('\n ')}
|
|
2122
|
+
}`
|
|
2123
|
+
);
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
const tapBody = `let ky = tap / 3u;
|
|
2127
|
+
let kx = tap % 3u;
|
|
2128
|
+
${rightLoads}
|
|
2129
|
+
${multiplies.join('\n ')}`;
|
|
2130
|
+
const taps = subgroupMatrixTapLoop(config, tapBody);
|
|
2131
|
+
const epilogues: string[] = [];
|
|
2132
|
+
if (pool) {
|
|
2133
|
+
// Bias, ReLU and FP16 conversion are monotone, so pooling before them
|
|
2134
|
+
// preserves the separate conv -> FP16 -> max-pool result for finite values.
|
|
2135
|
+
for (let fragment = 0; fragment < outputFragments; fragment++) {
|
|
2136
|
+
epilogues.push(`{
|
|
2137
|
+
var pooled = vec4<f32>(-3.402823e38);
|
|
2138
|
+
let pixel = lane / ${chunkBlocks}u;
|
|
2139
|
+
let outputBlock = blockBase + ${fragment * chunkBlocks}u + lane % ${chunkBlocks}u;
|
|
2140
|
+
${Array.from({ length: 2 }, (_, row) => `
|
|
2141
|
+
${Array.from({ length: pixelFragments }, (_, pixel) =>
|
|
2142
|
+
store('resultTile', `resultBase + ${pixel * size * size}u`, `acc${row}_${pixel}_${fragment}`, `${size}u`)
|
|
2143
|
+
).join('\n ')}
|
|
2144
|
+
workgroupBarrier();
|
|
2145
|
+
{
|
|
2146
|
+
let base = resultBase + pixel * ${size * 2}u + (lane % ${chunkBlocks}u) * 4u;
|
|
2147
|
+
let first = vec4<f32>(resultTile[base], resultTile[base + 1u], resultTile[base + 2u], resultTile[base + 3u]);
|
|
2148
|
+
let second = vec4<f32>(resultTile[base + ${size}u], resultTile[base + ${size + 1}u], resultTile[base + ${size + 2}u], resultTile[base + ${size + 3}u]);
|
|
2149
|
+
if (tileY + rowBase + ${row}u < params.inputHeight) {
|
|
2150
|
+
pooled = max(pooled, first);
|
|
2151
|
+
if (tileX + pixel * 2u + 1u < params.inputWidth) { pooled = max(pooled, second); }
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
workgroupBarrier();`).join('\n ')}
|
|
2155
|
+
let outputX = tileX / 2u + pixel;
|
|
2156
|
+
let outputY = (tileY + rowBase) / 2u;
|
|
2157
|
+
if (outputX < params.outputWidth && outputY < params.outputHeight && outputBlock < ${outputBlocks}u) {
|
|
2158
|
+
let acc = pooled + bias[outputBlock];
|
|
2159
|
+
outputData[(outputY * params.outputWidth + outputX) * ${outputBlocks}u + outputBlock] = ${storeExpression(activationExpression('acc', activation), outputPrecision)};
|
|
2160
|
+
}
|
|
2161
|
+
}`);
|
|
2162
|
+
}
|
|
2163
|
+
} else {
|
|
2164
|
+
for (let row = 0; row < 2; row++) {
|
|
2165
|
+
for (let fragment = 0; fragment < outputFragments; fragment++) {
|
|
2166
|
+
epilogues.push(`{
|
|
2167
|
+
${Array.from({ length: pixelFragments }, (_, pixel) =>
|
|
2168
|
+
store('resultTile', `resultBase + ${pixel * size * size}u`, `acc${row}_${pixel}_${fragment}`, `${size}u`)
|
|
2169
|
+
).join('\n ')}
|
|
2170
|
+
workgroupBarrier();
|
|
2171
|
+
let outputY = tileY + rowBase + ${row}u;
|
|
2172
|
+
for (var e = lane; e < ${tileWidth * chunkBlocks}u; e += ${config.subgroupSize}u) {
|
|
2173
|
+
let pixel = e / ${chunkBlocks}u;
|
|
2174
|
+
let outputX = tileX + pixel;
|
|
2175
|
+
let outputBlock = blockBase + ${fragment * chunkBlocks}u + e % ${chunkBlocks}u;
|
|
2176
|
+
if (outputX < params.outputWidth && outputY < params.outputHeight && outputBlock < ${outputBlocks}u) {
|
|
2177
|
+
let base = resultBase + pixel * ${size}u + (e % ${chunkBlocks}u) * 4u;
|
|
2178
|
+
let acc = vec4<f32>(f32(resultTile[base]), f32(resultTile[base + 1u]),
|
|
2179
|
+
f32(resultTile[base + 2u]), f32(resultTile[base + 3u])) + bias[outputBlock];
|
|
2180
|
+
outputData[(outputY * params.outputWidth + outputX) * ${outputBlocks}u + outputBlock] = ${storeExpression(activationExpression('acc', activation), outputPrecision)};
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
workgroupBarrier();
|
|
2184
|
+
}`);
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
const sourceRead = decoder
|
|
2189
|
+
? (() => {
|
|
2190
|
+
const [blocks0, blocks1] = decoder.sourceBlocks;
|
|
2191
|
+
const coordinates = (source: 0 | 1) => source === decoder.upsampledSource
|
|
2192
|
+
? `min(u32(inputY) / 2u, params.source${source}Height - 1u) * params.source${source}Width + min(u32(inputX) / 2u, params.source${source}Width - 1u)`
|
|
2193
|
+
: `u32(inputY) * params.source${source}Width + u32(inputX)`;
|
|
2194
|
+
return /* wgsl */ `if (block < ${blocks0}u) {
|
|
2195
|
+
value = input0[(${coordinates(0)}) * ${blocks0}u + block];
|
|
2196
|
+
} else {
|
|
2197
|
+
value = input1[(${coordinates(1)}) * ${blocks1}u + block - ${blocks0}u];
|
|
2198
|
+
}`;
|
|
2199
|
+
})()
|
|
2200
|
+
: `value = inputData[(u32(inputY) * params.inputWidth + u32(inputX)) * ${inputBlocks}u + block];`;
|
|
2201
|
+
const params = decoder
|
|
2202
|
+
? `struct Params {
|
|
2203
|
+
outputWidth: u32,
|
|
2204
|
+
outputHeight: u32,
|
|
2205
|
+
outputBlocks: u32,
|
|
2206
|
+
inputBlocks: u32,
|
|
2207
|
+
source0Width: u32,
|
|
2208
|
+
source0Height: u32,
|
|
2209
|
+
source1Width: u32,
|
|
2210
|
+
source1Height: u32,
|
|
2211
|
+
}
|
|
2212
|
+
@group(0) @binding(0) var<storage, read> input0: array<${valueType}>;
|
|
2213
|
+
@group(0) @binding(1) var<storage, read> input1: array<${valueType}>;
|
|
2214
|
+
@group(0) @binding(2) var<storage, read> weights: array<${valueType}>;
|
|
2215
|
+
@group(0) @binding(3) var<storage, read> bias: array<vec4<f32>>;
|
|
2216
|
+
@group(0) @binding(4) var<storage, read_write> outputData: array<${outputType}>;
|
|
2217
|
+
@group(0) @binding(5) var<uniform> params: Params;`
|
|
2218
|
+
: `struct Params {
|
|
2219
|
+
inputWidth: u32,
|
|
2220
|
+
inputHeight: u32,
|
|
2221
|
+
outputWidth: u32,
|
|
2222
|
+
outputHeight: u32,
|
|
2223
|
+
inputBlocks: u32,
|
|
2224
|
+
outputBlocks: u32,
|
|
2225
|
+
}
|
|
2226
|
+
@group(0) @binding(0) var<storage, read> inputData: array<${valueType}>;
|
|
2227
|
+
@group(0) @binding(1) var<storage, read> weights: array<${valueType}>;
|
|
2228
|
+
@group(0) @binding(2) var<storage, read> bias: array<vec4<f32>>;
|
|
2229
|
+
@group(0) @binding(3) var<storage, read_write> outputData: array<${outputType}>;
|
|
2230
|
+
@group(0) @binding(4) var<uniform> params: Params;`;
|
|
2231
|
+
const imageWidth = decoder ? 'params.outputWidth' : 'params.inputWidth';
|
|
2232
|
+
const imageHeight = decoder ? 'params.outputHeight' : 'params.inputHeight';
|
|
2233
|
+
const needsF16 = precision === 'fp16' || outputPrecision === 'fp16' ||
|
|
2234
|
+
component === 'f16' || result === 'f16';
|
|
2235
|
+
return /* wgsl */ `${needsF16 ? 'enable f16;\n' : ''}${extensions}
|
|
2236
|
+
${params}
|
|
2237
|
+
|
|
2238
|
+
var<workgroup> inputTile: array<${component}, ${buffers * haloRows * haloWidth * size}>;
|
|
2239
|
+
var<workgroup> weightTile: array<${component}, ${buffers * 9 * size * outputChannels}>;
|
|
2240
|
+
var<workgroup> resultTile: array<${result}, ${SUBGROUP_MATRIX_SUBGROUPS * tileWidth * size}>;
|
|
2241
|
+
|
|
2242
|
+
@compute @workgroup_size(${threads})
|
|
2243
|
+
fn main(
|
|
2244
|
+
@builtin(local_invocation_index) localIndex: u32,
|
|
2245
|
+
@builtin(workgroup_id) workgroupId: vec3<u32>,
|
|
2246
|
+
@builtin(subgroup_id) subgroup: u32,
|
|
2247
|
+
@builtin(subgroup_invocation_id) lane: u32
|
|
2248
|
+
) {
|
|
2249
|
+
let tileX = workgroupId.x * ${tileWidth}u;
|
|
2250
|
+
let tileY = workgroupId.y * ${SUBGROUP_MATRIX_TILE_HEIGHT}u;
|
|
2251
|
+
let blockBase = workgroupId.z * ${outputChannels / 4}u;
|
|
2252
|
+
let rowBase = subgroup * 2u;
|
|
2253
|
+
let resultBase = subgroup * ${tileWidth * size}u;
|
|
2254
|
+
${declarations}
|
|
2255
|
+
|
|
2256
|
+
${subgroupMatrixChunkLoop(chunks, [
|
|
2257
|
+
{
|
|
2258
|
+
array: 'inputTile',
|
|
2259
|
+
vectors: haloRows * haloWidth * chunkBlocks,
|
|
2260
|
+
read: /* wgsl */ `let pixel = i / ${chunkBlocks}u;
|
|
2261
|
+
let block = stageChunk * ${chunkBlocks}u + i % ${chunkBlocks}u;
|
|
2262
|
+
let inputX = i32(tileX + pixel % ${haloWidth}u) - 1;
|
|
2263
|
+
let inputY = i32(tileY + pixel / ${haloWidth}u) - 1;
|
|
2264
|
+
if (block < ${inputBlocks}u && inputX >= 0 && inputY >= 0 &&
|
|
2265
|
+
inputX < i32(${imageWidth}) && inputY < i32(${imageHeight})) {
|
|
2266
|
+
${sourceRead}
|
|
2267
|
+
}`
|
|
2268
|
+
},
|
|
2269
|
+
{
|
|
2270
|
+
array: 'weightTile',
|
|
2271
|
+
vectors: 9 * size * outputChannels / 4,
|
|
2272
|
+
read: /* wgsl */ `let quad = i % ${outputChannels / 4}u;
|
|
2273
|
+
let row = i / ${outputChannels / 4}u;
|
|
2274
|
+
let tap = row / ${size}u;
|
|
2275
|
+
let inputChannel = stageChunk * ${size}u + row % ${size}u;
|
|
2276
|
+
let outputBlock = blockBase + quad;
|
|
2277
|
+
if (inputChannel < ${inputBlocks * 4}u && outputBlock < ${outputBlocks}u) {
|
|
2278
|
+
value = weights[((tap * ${inputBlocks}u + inputChannel / 4u) * ${outputBlocks}u + outputBlock) * 4u + inputChannel % 4u];
|
|
2279
|
+
}`
|
|
2280
|
+
}
|
|
2281
|
+
], taps, threads, valueType, component, doubleBuffer)}
|
|
2282
|
+
|
|
2283
|
+
${epilogues.join('\n ')}
|
|
2284
|
+
}
|
|
2285
|
+
`;
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
// Sub-pixel decomposition of a 3x3 convolution of a 2x nearest-upsampled
|
|
2289
|
+
// source: along each axis, output phase p reads two source pixels at offsets
|
|
2290
|
+
// SUBPIXEL_OFFSETS[p] with the 3x3 weight rows SUBPIXEL_TAPS[p][d] summed.
|
|
2291
|
+
const SUBPIXEL_OFFSETS: readonly (readonly number[])[] = [[-1, 0], [0, 1]];
|
|
2292
|
+
const SUBPIXEL_TAPS: readonly (readonly (readonly number[])[])[] = [[[0], [1, 2]], [[0, 1], [2]]];
|
|
2293
|
+
|
|
2294
|
+
/** Upsampled-source weight matrices per chunk: 4 phases x 2x2 taps. */
|
|
2295
|
+
const SUBPIXEL_MATRICES = 16;
|
|
2296
|
+
|
|
2297
|
+
/**
|
|
2298
|
+
* Packs a fused upsample/concat decoder for `createSubgroupMatrixSubpixelShader`:
|
|
2299
|
+
* first the upsampled source's combined weights, vec4 index
|
|
2300
|
+
* (((phase * 4 + tap) * upBlocks + block) * outputBlocks + outputBlock) * 4 + lane
|
|
2301
|
+
* with phase = py * 2 + px and tap = dy * 2 + dx, then the other source's
|
|
2302
|
+
* 3x3 weights in the k-major layout. Combined weights are summed in f32 and
|
|
2303
|
+
* rounded once to the storage precision.
|
|
2304
|
+
*/
|
|
2305
|
+
function packSubpixelDecoderTensors(
|
|
2306
|
+
device: OIDNGPUDevice,
|
|
2307
|
+
id: string,
|
|
2308
|
+
tensors: ValidatedConvTensor,
|
|
2309
|
+
precision: NativeUNetPrecision,
|
|
2310
|
+
sourceBlocks: readonly number[],
|
|
2311
|
+
upsampledSource: 0 | 1
|
|
2312
|
+
): PackedConvBuffers {
|
|
2313
|
+
const outputBlocks = blocksForChannels(tensors.outputChannels);
|
|
2314
|
+
const upBlocks = sourceBlocks[upsampledSource];
|
|
2315
|
+
const skipBlocks = sourceBlocks[1 - upsampledSource];
|
|
2316
|
+
const upBase = upsampledSource === 0 ? 0 : sourceBlocks[0] * 4;
|
|
2317
|
+
const skipBase = upsampledSource === 0 ? sourceBlocks[0] * 4 : 0;
|
|
2318
|
+
const upValues = SUBPIXEL_MATRICES * upBlocks * outputBlocks * 16;
|
|
2319
|
+
const values = new Float32Array(upValues + 9 * skipBlocks * outputBlocks * 16);
|
|
2320
|
+
const source = tensorFloat32Values(tensors.weight);
|
|
2321
|
+
const weight = (outputChannel: number, inputChannel: number, y: number, x: number) =>
|
|
2322
|
+
outputChannel < tensors.outputChannels && inputChannel < tensors.inputChannels
|
|
2323
|
+
? source[((outputChannel * tensors.inputChannels + inputChannel) * 3 + y) * 3 + x]
|
|
2324
|
+
: 0;
|
|
2325
|
+
for (let outputChannel = 0; outputChannel < outputBlocks * 4; outputChannel++) {
|
|
2326
|
+
const outputBlock = outputChannel >> 2;
|
|
2327
|
+
const outputLane = outputChannel & 3;
|
|
2328
|
+
for (let channel = 0; channel < upBlocks * 4; channel++) {
|
|
2329
|
+
for (let phase = 0; phase < 4; phase++) {
|
|
2330
|
+
for (let tap = 0; tap < 4; tap++) {
|
|
2331
|
+
let sum = 0;
|
|
2332
|
+
for (const y of SUBPIXEL_TAPS[phase >> 1][tap >> 1]) {
|
|
2333
|
+
for (const x of SUBPIXEL_TAPS[phase & 1][tap & 1]) {
|
|
2334
|
+
sum += weight(outputChannel, upBase + channel, y, x);
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
const vector = ((phase * 4 + tap) * upBlocks + (channel >> 2)) * outputBlocks + outputBlock;
|
|
2338
|
+
values[vector * 16 + (channel & 3) * 4 + outputLane] = sum;
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
for (let channel = 0; channel < skipBlocks * 4; channel++) {
|
|
2343
|
+
for (let tap = 0; tap < 9; tap++) {
|
|
2344
|
+
const vector = (tap * skipBlocks + (channel >> 2)) * outputBlocks + outputBlock;
|
|
2345
|
+
values[upValues + vector * 16 + (channel & 3) * 4 + outputLane] =
|
|
2346
|
+
weight(outputChannel, skipBase + channel, (tap / 3) | 0, tap % 3);
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
let bytes: Uint8Array;
|
|
2351
|
+
if (precision === 'fp16') {
|
|
2352
|
+
const half = new Uint16Array(values.length);
|
|
2353
|
+
for (let index = 0; index < values.length; index++) half[index] = numberToHalfBits(values[index]);
|
|
2354
|
+
bytes = new Uint8Array(half.buffer);
|
|
2355
|
+
} else {
|
|
2356
|
+
bytes = new Uint8Array(values.buffer);
|
|
2357
|
+
}
|
|
2358
|
+
const packedBias = new Float32Array(outputBlocks * 4);
|
|
2359
|
+
packedBias.set(tensorFloat32Values(tensors.bias));
|
|
2360
|
+
const weights = createMappedBuffer(device, `oidn/${id}/weights/${precision}/subpixel`, bytes, BufferUsage.STORAGE);
|
|
2361
|
+
try {
|
|
2362
|
+
return {
|
|
2363
|
+
weights,
|
|
2364
|
+
weightLayout: 'k-major',
|
|
2365
|
+
bias: createMappedBuffer(device, `oidn/${id}/bias`, new Uint8Array(packedBias.buffer), BufferUsage.STORAGE)
|
|
2366
|
+
};
|
|
2367
|
+
} catch (error) {
|
|
2368
|
+
weights.destroy();
|
|
2369
|
+
throw error;
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
/**
|
|
2374
|
+
* Fused upsample/concat decoder convolution with the sub-pixel decomposition
|
|
2375
|
+
* (weights from `packSubpixelDecoderTensors`). Subgroup s computes output
|
|
2376
|
+
* phase (py, px) = (s >> 1, s & 1): rows py, py + 2, ... of the 8-row tile and
|
|
2377
|
+
* every other pixel from px, so each fragment holds S same-phase pixels and
|
|
2378
|
+
* each combined weight tile feeds all of the subgroup's fragments. The
|
|
2379
|
+
* upsampled source is staged at half resolution, where a fragment is S
|
|
2380
|
+
* contiguous source pixels; the other source keeps its 3x3 taps and loads
|
|
2381
|
+
* its fragments from the full-resolution halo with a two-pixel stride.
|
|
2382
|
+
*/
|
|
2383
|
+
function createSubgroupMatrixSubpixelShader(
|
|
2384
|
+
precision: NativeUNetPrecision,
|
|
2385
|
+
outputPrecision: NativeUNetPrecision,
|
|
2386
|
+
activation: Conv2DNodeSpec['activation'],
|
|
2387
|
+
outputBlocks: number,
|
|
2388
|
+
config: NativeSubgroupMatrixConfig,
|
|
2389
|
+
outputFragments: number,
|
|
2390
|
+
sourceBlocks: readonly number[],
|
|
2391
|
+
upsampledSource: 0 | 1,
|
|
2392
|
+
doubleBuffer: boolean
|
|
2393
|
+
) {
|
|
2394
|
+
const size = config.size;
|
|
2395
|
+
const buffers = doubleBuffer ? 2 : 1;
|
|
2396
|
+
const component = config.componentType;
|
|
2397
|
+
const result = config.resultType;
|
|
2398
|
+
const tileWidth = SUBGROUP_MATRIX_TILE_WIDTH;
|
|
2399
|
+
const tileHeight = SUBGROUP_MATRIX_TILE_HEIGHT;
|
|
2400
|
+
const haloWidth = tileWidth + 2;
|
|
2401
|
+
const haloRows = tileHeight + 2;
|
|
2402
|
+
const upWidth = tileWidth / 2 + 2;
|
|
2403
|
+
const upRows = tileHeight / 2 + 2;
|
|
2404
|
+
const chunkBlocks = size / 4;
|
|
2405
|
+
const upBlocks = sourceBlocks[upsampledSource];
|
|
2406
|
+
const skipBlocks = sourceBlocks[1 - upsampledSource];
|
|
2407
|
+
const upChunks = Math.ceil(upBlocks / chunkBlocks);
|
|
2408
|
+
const skipChunks = Math.ceil(skipBlocks / chunkBlocks);
|
|
2409
|
+
const outputChannels = outputFragments * size;
|
|
2410
|
+
const rows = tileHeight / 2;
|
|
2411
|
+
const segments = tileWidth / 2 / size;
|
|
2412
|
+
const threads = SUBGROUP_MATRIX_SUBGROUPS * config.subgroupSize;
|
|
2413
|
+
const valueType = storageVecType(precision);
|
|
2414
|
+
const outputType = storageVecType(outputPrecision);
|
|
2415
|
+
const { left, right, load, store, multiplyAdd, declare, extensions } = subgroupMatrixDialect(config);
|
|
2416
|
+
const accumulator = (row: number, segment: number, fragment: number) =>
|
|
2417
|
+
`acc${row}_${segment}_${fragment}`;
|
|
2418
|
+
const declarations: string[] = [];
|
|
2419
|
+
for (let row = 0; row < rows; row++) {
|
|
2420
|
+
for (let segment = 0; segment < segments; segment++) {
|
|
2421
|
+
for (let fragment = 0; fragment < outputFragments; fragment++) {
|
|
2422
|
+
declarations.push(declare(accumulator(row, segment, fragment)));
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
const multiplyRows = (aOffset: (row: number, segment: number) => string, stride: string) => {
|
|
2427
|
+
const lines: string[] = [];
|
|
2428
|
+
for (let row = 0; row < rows; row++) {
|
|
2429
|
+
for (let segment = 0; segment < segments; segment++) {
|
|
2430
|
+
lines.push(`{
|
|
2431
|
+
let a = ${load(left, 'inputTile', `inputTileBase + ${aOffset(row, segment)}`, stride)};
|
|
2432
|
+
${Array.from({ length: outputFragments }, (_, fragment) =>
|
|
2433
|
+
`${accumulator(row, segment, fragment)} = ${multiplyAdd}(a, b${fragment}, ${accumulator(row, segment, fragment)});`
|
|
2434
|
+
).join('\n ')}
|
|
2435
|
+
}`);
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
return lines.join('\n ');
|
|
2439
|
+
};
|
|
2440
|
+
const rightLoads = (matrix: string) => Array.from({ length: outputFragments }, (_, fragment) =>
|
|
2441
|
+
`let b${fragment} = ${load(right, 'weightTile', `weightTileBase + ${matrix} * ${size * outputChannels}u + ${fragment * size}u`, `${outputChannels}u`)};`
|
|
2442
|
+
).join('\n ');
|
|
2443
|
+
const sourceIndex = (source: 0 | 1, x: string, y: string) =>
|
|
2444
|
+
`(${y} * params.source${source}Width + ${x}) * ${sourceBlocks[source]}u`;
|
|
2445
|
+
// Each half of a double-buffered array must fit the larger of the two loops.
|
|
2446
|
+
const inputVectors = Math.max(haloRows * haloWidth, upRows * upWidth) * chunkBlocks;
|
|
2447
|
+
const weightVectors = SUBPIXEL_MATRICES * size * outputChannels / 4;
|
|
2448
|
+
const weightStage = (matrices: number, blocks: number, base: number): SubgroupMatrixStage => ({
|
|
2449
|
+
array: 'weightTile',
|
|
2450
|
+
vectors: matrices * size * outputChannels / 4,
|
|
2451
|
+
stride: weightVectors,
|
|
2452
|
+
read: /* wgsl */ `let quad = i % ${outputChannels / 4}u;
|
|
2453
|
+
let row = i / ${outputChannels / 4}u;
|
|
2454
|
+
let matrix = row / ${size}u;
|
|
2455
|
+
let inputChannel = stageChunk * ${size}u + row % ${size}u;
|
|
2456
|
+
let outputBlock = blockBase + quad;
|
|
2457
|
+
if (inputChannel < ${blocks * 4}u && outputBlock < ${outputBlocks}u) {
|
|
2458
|
+
value = weights[${base}u + ((matrix * ${blocks}u + inputChannel / 4u) * ${outputBlocks}u + outputBlock) * 4u + inputChannel % 4u];
|
|
2459
|
+
}`
|
|
2460
|
+
});
|
|
2461
|
+
const inputStage = (rowsStaged: number, width: number, read: string): SubgroupMatrixStage => ({
|
|
2462
|
+
array: 'inputTile',
|
|
2463
|
+
vectors: rowsStaged * width * chunkBlocks,
|
|
2464
|
+
stride: inputVectors,
|
|
2465
|
+
read: /* wgsl */ `let pixel = i / ${chunkBlocks}u;
|
|
2466
|
+
let block = stageChunk * ${chunkBlocks}u + i % ${chunkBlocks}u;
|
|
2467
|
+
${read}`
|
|
2468
|
+
});
|
|
2469
|
+
const upRead = /* wgsl */ `let inputX = i32(tileX / 2u + pixel % ${upWidth}u) - 1;
|
|
2470
|
+
let inputY = i32(tileY / 2u + pixel / ${upWidth}u) - 1;
|
|
2471
|
+
if (block < ${upBlocks}u && inputX >= 0 && inputY >= 0 &&
|
|
2472
|
+
inputX < i32(params.source${upsampledSource}Width) && inputY < i32(params.source${upsampledSource}Height)) {
|
|
2473
|
+
value = input${upsampledSource}[${sourceIndex(upsampledSource, 'u32(inputX)', 'u32(inputY)')} + block];
|
|
2474
|
+
}`;
|
|
2475
|
+
const skipSource = (1 - upsampledSource) as 0 | 1;
|
|
2476
|
+
const skipRead = /* wgsl */ `let inputX = i32(tileX + pixel % ${haloWidth}u) - 1;
|
|
2477
|
+
let inputY = i32(tileY + pixel / ${haloWidth}u) - 1;
|
|
2478
|
+
if (block < ${skipBlocks}u && inputX >= 0 && inputY >= 0 &&
|
|
2479
|
+
inputX < i32(params.outputWidth) && inputY < i32(params.outputHeight)) {
|
|
2480
|
+
value = input${skipSource}[${sourceIndex(skipSource, 'u32(inputX)', 'u32(inputY)')} + block];
|
|
2481
|
+
}`;
|
|
2482
|
+
const upMultiplies: string[] = [];
|
|
2483
|
+
for (let tapY = 0; tapY < 2; tapY++) {
|
|
2484
|
+
for (let tapX = 0; tapX < 2; tapX++) {
|
|
2485
|
+
// Source offsets of this tap for the subgroup's phase, plus the halo.
|
|
2486
|
+
upMultiplies.push(`{
|
|
2487
|
+
let matrix = phase * 4u + ${tapY * 2 + tapX}u;
|
|
2488
|
+
let offsetY = select(${SUBPIXEL_OFFSETS[0][tapY] + 1}u, ${SUBPIXEL_OFFSETS[1][tapY] + 1}u, phaseY == 1u);
|
|
2489
|
+
let offsetX = select(${SUBPIXEL_OFFSETS[0][tapX] + 1}u, ${SUBPIXEL_OFFSETS[1][tapX] + 1}u, phaseX == 1u);
|
|
2490
|
+
${rightLoads('matrix')}
|
|
2491
|
+
${multiplyRows((row, segment) => `((${row}u + offsetY) * ${upWidth}u + ${segment * size}u + offsetX) * ${size}u`, `${size}u`)}
|
|
2492
|
+
}`);
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
const skipTapBody = `let ky = tap / 3u;
|
|
2496
|
+
let kx = tap % 3u;
|
|
2497
|
+
${rightLoads('tap')}
|
|
2498
|
+
${multiplyRows((row, segment) => `((phaseY + ${row * 2}u + ky) * ${haloWidth}u + phaseX + kx + ${segment * size * 2}u) * ${size}u`, `${size * 2}u`)}`;
|
|
2499
|
+
const skipTaps = subgroupMatrixTapLoop(config, skipTapBody);
|
|
2500
|
+
const epilogues: string[] = [];
|
|
2501
|
+
for (let row = 0; row < rows; row++) {
|
|
2502
|
+
for (let fragment = 0; fragment < outputFragments; fragment++) {
|
|
2503
|
+
epilogues.push(`{
|
|
2504
|
+
${Array.from({ length: segments }, (_, segment) =>
|
|
2505
|
+
store('resultTile', `resultBase + ${segment * size * size}u`, accumulator(row, segment, fragment), `${size}u`)
|
|
2506
|
+
).join('\n ')}
|
|
2507
|
+
workgroupBarrier();
|
|
2508
|
+
let outputY = tileY + phaseY + ${row * 2}u;
|
|
2509
|
+
for (var e = lane; e < ${(tileWidth / 2) * chunkBlocks}u; e += ${config.subgroupSize}u) {
|
|
2510
|
+
let pixel = e / ${chunkBlocks}u;
|
|
2511
|
+
let outputX = tileX + phaseX + pixel * 2u;
|
|
2512
|
+
let outputBlock = blockBase + ${fragment * chunkBlocks}u + e % ${chunkBlocks}u;
|
|
2513
|
+
if (outputX < params.outputWidth && outputY < params.outputHeight && outputBlock < ${outputBlocks}u) {
|
|
2514
|
+
let base = resultBase + pixel * ${size}u + (e % ${chunkBlocks}u) * 4u;
|
|
2515
|
+
let acc = vec4<f32>(f32(resultTile[base]), f32(resultTile[base + 1u]),
|
|
2516
|
+
f32(resultTile[base + 2u]), f32(resultTile[base + 3u])) + bias[outputBlock];
|
|
2517
|
+
outputData[(outputY * params.outputWidth + outputX) * ${outputBlocks}u + outputBlock] = ${storeExpression(activationExpression('acc', activation), outputPrecision)};
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
workgroupBarrier();
|
|
2521
|
+
}`);
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
const needsF16 = precision === 'fp16' || outputPrecision === 'fp16' ||
|
|
2525
|
+
component === 'f16' || result === 'f16';
|
|
2526
|
+
return /* wgsl */ `${needsF16 ? 'enable f16;\n' : ''}${extensions}
|
|
2527
|
+
struct Params {
|
|
2528
|
+
outputWidth: u32,
|
|
2529
|
+
outputHeight: u32,
|
|
2530
|
+
outputBlocks: u32,
|
|
2531
|
+
inputBlocks: u32,
|
|
2532
|
+
source0Width: u32,
|
|
2533
|
+
source0Height: u32,
|
|
2534
|
+
source1Width: u32,
|
|
2535
|
+
source1Height: u32,
|
|
2536
|
+
}
|
|
2537
|
+
@group(0) @binding(0) var<storage, read> input0: array<${valueType}>;
|
|
2538
|
+
@group(0) @binding(1) var<storage, read> input1: array<${valueType}>;
|
|
2539
|
+
@group(0) @binding(2) var<storage, read> weights: array<${valueType}>;
|
|
2540
|
+
@group(0) @binding(3) var<storage, read> bias: array<vec4<f32>>;
|
|
2541
|
+
@group(0) @binding(4) var<storage, read_write> outputData: array<${outputType}>;
|
|
2542
|
+
@group(0) @binding(5) var<uniform> params: Params;
|
|
2543
|
+
|
|
2544
|
+
var<workgroup> inputTile: array<${component}, ${buffers * inputVectors * 4}>;
|
|
2545
|
+
var<workgroup> weightTile: array<${component}, ${buffers * weightVectors * 4}>;
|
|
2546
|
+
var<workgroup> resultTile: array<${result}, ${SUBGROUP_MATRIX_SUBGROUPS * (tileWidth / 2) * size}>;
|
|
2547
|
+
|
|
2548
|
+
@compute @workgroup_size(${threads})
|
|
2549
|
+
fn main(
|
|
2550
|
+
@builtin(local_invocation_index) localIndex: u32,
|
|
2551
|
+
@builtin(workgroup_id) workgroupId: vec3<u32>,
|
|
2552
|
+
@builtin(subgroup_id) subgroup: u32,
|
|
2553
|
+
@builtin(subgroup_invocation_id) lane: u32
|
|
2554
|
+
) {
|
|
2555
|
+
let tileX = workgroupId.x * ${tileWidth}u;
|
|
2556
|
+
let tileY = workgroupId.y * ${tileHeight}u;
|
|
2557
|
+
let blockBase = workgroupId.z * ${outputChannels / 4}u;
|
|
2558
|
+
let phase = subgroup;
|
|
2559
|
+
let phaseY = subgroup >> 1u;
|
|
2560
|
+
let phaseX = subgroup & 1u;
|
|
2561
|
+
let resultBase = subgroup * ${(tileWidth / 2) * size}u;
|
|
2562
|
+
${declarations.join('\n ')}
|
|
2563
|
+
|
|
2564
|
+
${subgroupMatrixChunkLoop(upChunks, [inputStage(upRows, upWidth, upRead), weightStage(SUBPIXEL_MATRICES, upBlocks, 0)],
|
|
2565
|
+
upMultiplies.join('\n '), threads, valueType, component, doubleBuffer)}
|
|
2566
|
+
|
|
2567
|
+
${subgroupMatrixChunkLoop(skipChunks, [
|
|
2568
|
+
inputStage(haloRows, haloWidth, skipRead),
|
|
2569
|
+
weightStage(9, skipBlocks, SUBPIXEL_MATRICES * upBlocks * outputBlocks * 4)
|
|
2570
|
+
], skipTaps, threads, valueType, component, doubleBuffer)}
|
|
2571
|
+
|
|
2572
|
+
${epilogues.join('\n ')}
|
|
2573
|
+
}
|
|
2574
|
+
`;
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
function executableInputs(node: ExecutableModelNode): readonly string[] {
|
|
2578
|
+
if (node.op === 'concat') return node.inputs;
|
|
2579
|
+
if (node.op === 'fusedUpsampleConcatConv2d') {
|
|
2580
|
+
return node.inputs.map((input) => input.value);
|
|
2581
|
+
}
|
|
2582
|
+
return [node.input];
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
export function resolveNativeUNetPrecision(
|
|
2586
|
+
device: OIDNGPUDevice,
|
|
2587
|
+
requested: NativeUNetPrecisionSetting = 'auto'
|
|
2588
|
+
): NativeUNetPrecision {
|
|
2589
|
+
const hasShaderF16 = device.features.has('shader-f16');
|
|
2590
|
+
if (requested === 'fp16' && !hasShaderF16) {
|
|
2591
|
+
throw new Error(
|
|
2592
|
+
'OIDN FP16 was requested but the OIDNGPUDevice does not have shader-f16 enabled'
|
|
2593
|
+
);
|
|
2594
|
+
}
|
|
2595
|
+
if (requested === 'auto') return hasShaderF16 ? 'fp16' : 'fp32';
|
|
2596
|
+
return requested;
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
/**
|
|
2600
|
+
* Native, model-driven OIDN U-Net executor without asynchronous operations, so
|
|
2601
|
+
* that scriptc can compile it for native hosts. `NativeUNetExecutor` adds the
|
|
2602
|
+
* browser's asynchronous preparation, CPU readback and profiling.
|
|
2603
|
+
*/
|
|
2604
|
+
export class NativeUNetCore {
|
|
2605
|
+
readonly usedKernels = new Set<NativeUNetKernel>();
|
|
2606
|
+
|
|
2607
|
+
/** Immutable packed weights and biases, for native resource sharing. */
|
|
2608
|
+
get weightBuffers(): OIDNGPUBuffer[] {
|
|
2609
|
+
return [...this._packedConvs.values()].flatMap(packed => [packed.weights, packed.bias]);
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
readonly precision: NativeUNetPrecision;
|
|
2613
|
+
readonly kernelSetting: NativeUNetKernelSetting;
|
|
2614
|
+
readonly gemm: Readonly<Required<NativeUNetGemmOptions>>;
|
|
2615
|
+
readonly maxSpatialInputBlocks: number;
|
|
2616
|
+
readonly subgroupsAvailable: boolean;
|
|
2617
|
+
/** Selected configuration when the `subgroup-matrix` kernel can run. */
|
|
2618
|
+
readonly subgroupMatrix?: NativeSubgroupMatrixConfig;
|
|
2619
|
+
readonly subgroupMatrixOutputFragments: number;
|
|
2620
|
+
readonly subgroupMatrixSubPixel: boolean;
|
|
2621
|
+
readonly subgroupMatrixDoubleBuffer: boolean;
|
|
2622
|
+
private _subgroupMatrixFixedFragments: boolean;
|
|
2623
|
+
|
|
2624
|
+
protected _model: UNetModelGraph;
|
|
2625
|
+
private _gemmByOutputBlocks = new Map<number, Readonly<Required<NativeUNetGemmOptions>>>();
|
|
2626
|
+
private _packedConvs = new Map<string, PackedConvBuffers>();
|
|
2627
|
+
protected _pipelineCache: Map<string, OIDNGPUComputePipeline>;
|
|
2628
|
+
private _executionCache = new Map<string, CachedExecution>();
|
|
2629
|
+
protected _retiredExecutions = new Set<CachedExecution>();
|
|
2630
|
+
private _clock = 0;
|
|
2631
|
+
private _shapeCacheSize: number;
|
|
2632
|
+
private _profileNextExecution = false;
|
|
2633
|
+
protected _profileOperations = 0;
|
|
2634
|
+
protected _resources = new OIDNResourceTracker();
|
|
2635
|
+
protected _disposed = false;
|
|
2636
|
+
|
|
2637
|
+
constructor(
|
|
2638
|
+
protected _device: OIDNGPUDevice,
|
|
2639
|
+
model: ValidatedUNetModel,
|
|
2640
|
+
options: NativeUNetOptions = {}
|
|
2641
|
+
) {
|
|
2642
|
+
this._pipelineCache = sharedPipelineCache(_device);
|
|
2643
|
+
this.precision = resolveNativeUNetPrecision(
|
|
2644
|
+
_device,
|
|
2645
|
+
options.precision ?? 'auto'
|
|
2646
|
+
);
|
|
2647
|
+
this.kernelSetting = options.kernel ?? 'auto';
|
|
2648
|
+
const workgroupSize = options.gemm?.workgroupSize ?? [8, 8];
|
|
2649
|
+
this.gemm = Object.freeze({
|
|
2650
|
+
tilePolicy: options.gemm?.tilePolicy ?? (options.gemm?.workgroupSize ? 'fixed' : 'output-aligned'),
|
|
2651
|
+
decoderLoad: options.gemm?.decoderLoad ?? 'per-load',
|
|
2652
|
+
decoderSubPixel: options.gemm?.decoderSubPixel ?? (this.precision === 'fp16'),
|
|
2653
|
+
unrollRows: options.gemm?.unrollRows ?? false,
|
|
2654
|
+
poolLayout: options.gemm?.poolLayout ?? 'channels',
|
|
2655
|
+
sharedLayout: options.gemm?.sharedLayout ?? (this.precision === 'fp16' ? 'padded-input' : 'padded'),
|
|
2656
|
+
accumulationOrder: options.gemm?.accumulationOrder ?? 'k-major',
|
|
2657
|
+
finalLayer: options.gemm?.finalLayer ?? 'shared-auto',
|
|
2658
|
+
loadMode: options.gemm?.loadMode ?? 'native',
|
|
2659
|
+
addressMode: options.gemm?.addressMode ?? 'incremental',
|
|
2660
|
+
weightLayout: options.gemm?.weightLayout ?? 'k-major',
|
|
2661
|
+
rowsPerThread: options.gemm?.rowsPerThread ?? 8,
|
|
2662
|
+
workgroupSize: Object.freeze([workgroupSize[0], workgroupSize[1]]) as NativeUNetGemmWorkgroup
|
|
2663
|
+
});
|
|
2664
|
+
if (!['fixed', 'output-aligned'].includes(this.gemm.tilePolicy)) {
|
|
2665
|
+
throw new Error(`Unsupported GEMM tile policy: ${this.gemm.tilePolicy}`);
|
|
2666
|
+
}
|
|
2667
|
+
if (!['per-load', 'source-first'].includes(this.gemm.decoderLoad)) {
|
|
2668
|
+
throw new Error(`Unsupported GEMM decoder load: ${this.gemm.decoderLoad}`);
|
|
2669
|
+
}
|
|
2670
|
+
if (!['spatial', 'channels'].includes(this.gemm.poolLayout)) {
|
|
2671
|
+
throw new Error(`Unsupported GEMM pool layout: ${this.gemm.poolLayout}`);
|
|
2672
|
+
}
|
|
2673
|
+
if (!['linear', 'padded', 'padded-input', 'padded-weights'].includes(this.gemm.sharedLayout)) {
|
|
2674
|
+
throw new Error(`Unsupported GEMM shared layout: ${this.gemm.sharedLayout}`);
|
|
2675
|
+
}
|
|
2676
|
+
if (!['k-major', 'row-major'].includes(this.gemm.accumulationOrder)) {
|
|
2677
|
+
throw new Error(`Unsupported GEMM accumulation order: ${this.gemm.accumulationOrder}`);
|
|
2678
|
+
}
|
|
2679
|
+
if (!['direct', 'shared-input', 'shared-input-weights', 'shared-auto'].includes(this.gemm.finalLayer)) {
|
|
2680
|
+
throw new Error(`Unsupported GEMM final layer: ${this.gemm.finalLayer}`);
|
|
2681
|
+
}
|
|
2682
|
+
if (!['native', 'packed-weights', 'packed-all'].includes(this.gemm.loadMode)) {
|
|
2683
|
+
throw new Error(`Unsupported GEMM load mode: ${this.gemm.loadMode}`);
|
|
2684
|
+
}
|
|
2685
|
+
if (!['analytic', 'incremental', 'base-offset'].includes(this.gemm.addressMode)) {
|
|
2686
|
+
throw new Error(`Unsupported GEMM address mode: ${this.gemm.addressMode}`);
|
|
2687
|
+
}
|
|
2688
|
+
if (!['output-major', 'k-major'].includes(this.gemm.weightLayout)) {
|
|
2689
|
+
throw new Error(`Unsupported GEMM weight layout: ${this.gemm.weightLayout}`);
|
|
2690
|
+
}
|
|
2691
|
+
const tile = gemmTile(this.gemm);
|
|
2692
|
+
const tileStorageBytes =
|
|
2693
|
+
(gemmSharedSizes(this.gemm).input + gemmSharedSizes(this.gemm).weights) *
|
|
2694
|
+
4 * (this.precision === 'fp16' ? 2 : 4);
|
|
2695
|
+
if (
|
|
2696
|
+
![2, 4, 8].includes(tile.rowsPerThread) ||
|
|
2697
|
+
![4, 8, 16].includes(tile.workgroupX) ||
|
|
2698
|
+
![4, 8].includes(tile.workgroupY) ||
|
|
2699
|
+
workgroupSize.length !== 2 ||
|
|
2700
|
+
tile.workgroupX > _device.limits.maxComputeWorkgroupSizeX ||
|
|
2701
|
+
tile.workgroupY > _device.limits.maxComputeWorkgroupSizeY ||
|
|
2702
|
+
tile.workgroupX * tile.workgroupY > _device.limits.maxComputeInvocationsPerWorkgroup ||
|
|
2703
|
+
tileStorageBytes > _device.limits.maxComputeWorkgroupStorageSize
|
|
2704
|
+
) {
|
|
2705
|
+
throw new Error('Unsupported GEMM tile configuration for this OIDNGPUDevice');
|
|
2706
|
+
}
|
|
2707
|
+
this.subgroupsAvailable = _device.features.has(
|
|
2708
|
+
'subgroups'
|
|
2709
|
+
);
|
|
2710
|
+
const matrixOptions = options.subgroupMatrix ?? {};
|
|
2711
|
+
const matrixConfig = this.kernelSetting === 'subgroup-matrix'
|
|
2712
|
+
? selectSubgroupMatrixConfig(_device, matrixOptions.accumulation)
|
|
2713
|
+
: undefined;
|
|
2714
|
+
this.subgroupMatrixOutputFragments = matrixConfig
|
|
2715
|
+
? subgroupMatrixFragments(matrixConfig, matrixOptions)
|
|
2716
|
+
: 0;
|
|
2717
|
+
this.subgroupMatrixSubPixel = matrixOptions.subPixel ?? true;
|
|
2718
|
+
this._subgroupMatrixFixedFragments = matrixOptions.outputFragments !== undefined;
|
|
2719
|
+
const fitsSharedMemory = (doubleBuffer: boolean) => matrixConfig !== undefined &&
|
|
2720
|
+
subgroupMatrixSharedBytes(
|
|
2721
|
+
matrixConfig, this.subgroupMatrixOutputFragments, this.subgroupMatrixSubPixel, doubleBuffer
|
|
2722
|
+
) <= _device.limits.maxComputeWorkgroupStorageSize;
|
|
2723
|
+
// A 16x16 matrix with two fragments can exceed a browser's 32 KiB limit.
|
|
2724
|
+
// Keep hardware matrices available with one fragment before using GEMM.
|
|
2725
|
+
if (matrixConfig && matrixOptions.outputFragments === undefined && !fitsSharedMemory(false)) {
|
|
2726
|
+
this.subgroupMatrixOutputFragments = 1;
|
|
2727
|
+
}
|
|
2728
|
+
this.subgroupMatrixDoubleBuffer = (matrixOptions.doubleBuffer ?? false) && fitsSharedMemory(true);
|
|
2729
|
+
if (
|
|
2730
|
+
matrixConfig &&
|
|
2731
|
+
SUBGROUP_MATRIX_SUBGROUPS * matrixConfig.subgroupSize <= _device.limits.maxComputeInvocationsPerWorkgroup &&
|
|
2732
|
+
SUBGROUP_MATRIX_SUBGROUPS * matrixConfig.subgroupSize <= _device.limits.maxComputeWorkgroupSizeX &&
|
|
2733
|
+
fitsSharedMemory(this.subgroupMatrixDoubleBuffer)
|
|
2734
|
+
) {
|
|
2735
|
+
this.subgroupMatrix = matrixConfig;
|
|
2736
|
+
}
|
|
2737
|
+
this.maxSpatialInputBlocks =
|
|
2738
|
+
this.precision === 'fp16' &&
|
|
2739
|
+
_device.limits.maxComputeInvocationsPerWorkgroup >=
|
|
2740
|
+
SPATIAL_CONV_WORKGROUP * SPATIAL_CONV_WORKGROUP &&
|
|
2741
|
+
_device.limits.maxComputeWorkgroupSizeX >= SPATIAL_CONV_WORKGROUP &&
|
|
2742
|
+
_device.limits.maxComputeWorkgroupSizeY >= SPATIAL_CONV_WORKGROUP
|
|
2743
|
+
? Math.floor(
|
|
2744
|
+
_device.limits.maxComputeWorkgroupStorageSize /
|
|
2745
|
+
(SPATIAL_CONV_PATCH * SPATIAL_CONV_PATCH * 4 * 2)
|
|
2746
|
+
)
|
|
2747
|
+
: 0;
|
|
2748
|
+
this._shapeCacheSize = Math.max(1, options.shapeCacheSize ?? 2);
|
|
2749
|
+
|
|
2750
|
+
if (
|
|
2751
|
+
model.inputChannels % 3 !== 0 ||
|
|
2752
|
+
model.inputChannels < 3 ||
|
|
2753
|
+
model.inputChannels > 9
|
|
2754
|
+
) {
|
|
2755
|
+
throw new Error(
|
|
2756
|
+
`Native OIDN expects 3, 6, or 9 input channels, got ${model.inputChannels}`
|
|
2757
|
+
);
|
|
2758
|
+
}
|
|
2759
|
+
this._model = {
|
|
2760
|
+
spec: model.spec,
|
|
2761
|
+
inputChannels: model.inputChannels,
|
|
2762
|
+
outputChannels: model.outputChannels,
|
|
2763
|
+
channelsByValue: new Map(model.channelsByValue),
|
|
2764
|
+
convChannels: new Map(model.convChannels)
|
|
2765
|
+
};
|
|
2766
|
+
const subPixelDecoders = new Map<string, { sourceBlocks: number[]; upsampledSource: 0 | 1 }>();
|
|
2767
|
+
for (const node of optimizeModelGraph(this._model, {
|
|
2768
|
+
fuseConvPool: this._matrixPool()
|
|
2769
|
+
}).nodes) {
|
|
2770
|
+
if (node.op === 'fusedUpsampleConcatConv2d') {
|
|
2771
|
+
const decoder = this._subPixelDecoder(node);
|
|
2772
|
+
if (decoder) subPixelDecoders.set(node.conv.id, decoder);
|
|
2773
|
+
}
|
|
2774
|
+
if (
|
|
2775
|
+
node.op !== 'conv2d' &&
|
|
2776
|
+
node.op !== 'maxPool2d' &&
|
|
2777
|
+
node.op !== 'fusedConvReluMaxPool2d' &&
|
|
2778
|
+
node.op !== 'fusedUpsampleConcatConv2d'
|
|
2779
|
+
) {
|
|
2780
|
+
throw new Error(
|
|
2781
|
+
`Native OIDN descriptor ${model.spec.id} leaves unsupported ` +
|
|
2782
|
+
`${node.op} node ${node.id} after graph optimization`
|
|
2783
|
+
);
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
try {
|
|
2787
|
+
for (const [id, tensors] of model.convTensors) {
|
|
2788
|
+
// Kernel selection is shape-independent. Pack only the active layout
|
|
2789
|
+
// for this executor, keeping direct/spatial/subgroup and final weights
|
|
2790
|
+
// in their original ABI without duplicating GPU allocations.
|
|
2791
|
+
const kernel = this._selectConvKernel(
|
|
2792
|
+
blocksForChannels(tensors.inputChannels), id === model.spec.output
|
|
2793
|
+
);
|
|
2794
|
+
const weightLayout = kernel === 'implicit-gemm'
|
|
2795
|
+
? this.gemm.weightLayout
|
|
2796
|
+
: kernel === 'subgroup-matrix'
|
|
2797
|
+
? 'k-major'
|
|
2798
|
+
: 'output-major';
|
|
2799
|
+
const decoder = subPixelDecoders.get(id);
|
|
2800
|
+
const packed = decoder
|
|
2801
|
+
? packSubpixelDecoderTensors(
|
|
2802
|
+
_device, id, tensors, this.precision, decoder.sourceBlocks, decoder.upsampledSource
|
|
2803
|
+
)
|
|
2804
|
+
: packConvTensors(
|
|
2805
|
+
_device, id, tensors, this.precision, weightLayout
|
|
2806
|
+
);
|
|
2807
|
+
this._resources.track('gpu-buffer', packed.weights);
|
|
2808
|
+
this._resources.track('gpu-buffer', packed.bias);
|
|
2809
|
+
this._packedConvs.set(id, packed);
|
|
2810
|
+
}
|
|
2811
|
+
} catch (error) {
|
|
2812
|
+
for (const packed of this._packedConvs.values()) {
|
|
2813
|
+
this._releaseBuffer(packed.weights);
|
|
2814
|
+
this._releaseBuffer(packed.bias);
|
|
2815
|
+
}
|
|
2816
|
+
this._packedConvs.clear();
|
|
2817
|
+
throw error;
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
private _pipeline(key: string, code: string) {
|
|
2822
|
+
let pipeline = this._pipelineCache.get(key);
|
|
2823
|
+
if (!pipeline) {
|
|
2824
|
+
pipeline = this._device.createComputePipeline({
|
|
2825
|
+
label: `oidn/${key}`,
|
|
2826
|
+
layout: 'auto',
|
|
2827
|
+
compute: {
|
|
2828
|
+
module: this._device.createShaderModule({
|
|
2829
|
+
label: `oidn/${key}`,
|
|
2830
|
+
code
|
|
2831
|
+
}),
|
|
2832
|
+
entryPoint: 'main'
|
|
2833
|
+
}
|
|
2834
|
+
});
|
|
2835
|
+
this._pipelineCache.set(key, pipeline);
|
|
2836
|
+
}
|
|
2837
|
+
return pipeline;
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
|
|
2841
|
+
|
|
2842
|
+
protected _nodePipelineSpec(
|
|
2843
|
+
node: ExecutableModelNode,
|
|
2844
|
+
isFinal: boolean,
|
|
2845
|
+
fragments = this.subgroupMatrixOutputFragments
|
|
2846
|
+
): NativePipelineSpec {
|
|
2847
|
+
if (node.op === 'conv2d') {
|
|
2848
|
+
const outputPrecision = isFinal ? 'fp32' : this.precision;
|
|
2849
|
+
const inputBlocks = blocksForChannels(
|
|
2850
|
+
this._model.convChannels.get(node.id)!.inputChannels
|
|
2851
|
+
);
|
|
2852
|
+
const outputBlocks = blocksForChannels(
|
|
2853
|
+
this._model.convChannels.get(node.id)!.outputChannels
|
|
2854
|
+
);
|
|
2855
|
+
const gemm = this._gemmForOutput(outputBlocks);
|
|
2856
|
+
const kernel = this._selectConvKernel(inputBlocks, isFinal);
|
|
2857
|
+
const cacheFinalWeights = gemm.finalLayer === 'shared-input-weights' ||
|
|
2858
|
+
(gemm.finalLayer === 'shared-auto' &&
|
|
2859
|
+
finalRgbSharedMemoryBytes(this.precision, inputBlocks, true) <=
|
|
2860
|
+
this._device.limits.maxComputeWorkgroupStorageSize);
|
|
2861
|
+
if (
|
|
2862
|
+
kernel !== 'subgroup-matrix' &&
|
|
2863
|
+
isFinal && this._model.convChannels.get(node.id)!.outputChannels === 3 &&
|
|
2864
|
+
(this.kernelSetting === 'auto' || this.kernelSetting === 'implicit-gemm' ||
|
|
2865
|
+
this.kernelSetting === 'subgroup-matrix') &&
|
|
2866
|
+
gemm.finalLayer !== 'direct' &&
|
|
2867
|
+
this._device.limits.maxComputeWorkgroupSizeX >= 8 &&
|
|
2868
|
+
this._device.limits.maxComputeWorkgroupSizeY >= 8 &&
|
|
2869
|
+
this._device.limits.maxComputeInvocationsPerWorkgroup >= 64 &&
|
|
2870
|
+
finalRgbSharedMemoryBytes(this.precision, inputBlocks, cacheFinalWeights) <=
|
|
2871
|
+
this._device.limits.maxComputeWorkgroupStorageSize
|
|
2872
|
+
) {
|
|
2873
|
+
return {
|
|
2874
|
+
key: `conv-final-rgb/${this.precision}/${node.activation}/in${inputBlocks}/weights-${cacheFinalWeights ? 'shared' : 'storage'}`,
|
|
2875
|
+
kernel: 'direct',
|
|
2876
|
+
code: createFinalRgbShader(this.precision, node.activation, inputBlocks, cacheFinalWeights)
|
|
2877
|
+
};
|
|
2878
|
+
}
|
|
2879
|
+
if (kernel === 'subgroup-matrix') {
|
|
2880
|
+
return this._subgroupMatrixSpec(
|
|
2881
|
+
`conv/${node.activation}/in${inputBlocks}/out${outputBlocks}`,
|
|
2882
|
+
outputPrecision,
|
|
2883
|
+
node.activation,
|
|
2884
|
+
inputBlocks,
|
|
2885
|
+
outputBlocks,
|
|
2886
|
+
fragments
|
|
2887
|
+
);
|
|
2888
|
+
}
|
|
2889
|
+
const key =
|
|
2890
|
+
`conv-${kernel}/${this.precision}/` +
|
|
2891
|
+
`${outputPrecision}/${node.activation}/` +
|
|
2892
|
+
`in${inputBlocks}/out${outputBlocks}` +
|
|
2893
|
+
(kernel === 'implicit-gemm'
|
|
2894
|
+
? `/address-${gemm.addressMode}/weights-${gemm.weightLayout}-v1` +
|
|
2895
|
+
`/tile-${gemm.workgroupSize.join('x')}-r${gemm.rowsPerThread}/loads-${gemm.loadMode}/shared-${gemm.sharedLayout}/acc-${gemm.accumulationOrder}/unroll-${Number(gemm.unrollRows)}`
|
|
2896
|
+
: '');
|
|
2897
|
+
return {
|
|
2898
|
+
key,
|
|
2899
|
+
kernel,
|
|
2900
|
+
code: kernel === 'implicit-gemm'
|
|
2901
|
+
? createTiledConvShader(
|
|
2902
|
+
this.precision,
|
|
2903
|
+
outputPrecision,
|
|
2904
|
+
node.activation,
|
|
2905
|
+
inputBlocks,
|
|
2906
|
+
outputBlocks,
|
|
2907
|
+
gemm
|
|
2908
|
+
)
|
|
2909
|
+
: kernel === 'spatial'
|
|
2910
|
+
? createSpatialConvShader(
|
|
2911
|
+
this.precision,
|
|
2912
|
+
outputPrecision,
|
|
2913
|
+
node.activation,
|
|
2914
|
+
inputBlocks,
|
|
2915
|
+
outputBlocks
|
|
2916
|
+
)
|
|
2917
|
+
: kernel === 'subgroup'
|
|
2918
|
+
? createSubgroupConvShader(
|
|
2919
|
+
this.precision,
|
|
2920
|
+
outputPrecision,
|
|
2921
|
+
node.activation,
|
|
2922
|
+
inputBlocks,
|
|
2923
|
+
outputBlocks
|
|
2924
|
+
)
|
|
2925
|
+
: createConvShader(
|
|
2926
|
+
this.precision,
|
|
2927
|
+
outputPrecision,
|
|
2928
|
+
node.activation,
|
|
2929
|
+
inputBlocks,
|
|
2930
|
+
outputBlocks
|
|
2931
|
+
)
|
|
2932
|
+
};
|
|
2933
|
+
}
|
|
2934
|
+
if (node.op === 'maxPool2d') {
|
|
2935
|
+
const outputBlocks = blocksForChannels(
|
|
2936
|
+
this._model.channelsByValue.get(node.id)!
|
|
2937
|
+
);
|
|
2938
|
+
const coalesced = this._coalescedPool();
|
|
2939
|
+
const key = `max-pool/${this.precision}/out${outputBlocks}/${coalesced ? 'channels' : 'spatial'}`;
|
|
2940
|
+
return {
|
|
2941
|
+
key,
|
|
2942
|
+
kernel: 'direct',
|
|
2943
|
+
code: createMaxPoolShader(this.precision, outputBlocks, coalesced)
|
|
2944
|
+
};
|
|
2945
|
+
}
|
|
2946
|
+
if (node.op === 'fusedConvReluMaxPool2d') {
|
|
2947
|
+
const inputBlocks = blocksForChannels(
|
|
2948
|
+
this._model.convChannels.get(node.conv.id)!.inputChannels
|
|
2949
|
+
);
|
|
2950
|
+
const outputBlocks = blocksForChannels(
|
|
2951
|
+
this._model.convChannels.get(node.conv.id)!.outputChannels
|
|
2952
|
+
);
|
|
2953
|
+
if (this._matrixPool()) {
|
|
2954
|
+
return this._subgroupMatrixSpec(
|
|
2955
|
+
`conv-pool/${node.conv.activation}/in${inputBlocks}/out${outputBlocks}`,
|
|
2956
|
+
this.precision, node.conv.activation, inputBlocks, outputBlocks, fragments, undefined, true
|
|
2957
|
+
);
|
|
2958
|
+
}
|
|
2959
|
+
const key =
|
|
2960
|
+
`conv-pool/${this.precision}/${node.conv.activation}/` +
|
|
2961
|
+
`in${inputBlocks}/out${outputBlocks}`;
|
|
2962
|
+
return {
|
|
2963
|
+
key,
|
|
2964
|
+
kernel: 'direct',
|
|
2965
|
+
code: createFusedConvPoolShader(
|
|
2966
|
+
this.precision,
|
|
2967
|
+
node.conv.activation,
|
|
2968
|
+
inputBlocks,
|
|
2969
|
+
outputBlocks
|
|
2970
|
+
)
|
|
2971
|
+
};
|
|
2972
|
+
}
|
|
2973
|
+
if (node.op === 'fusedUpsampleConcatConv2d') {
|
|
2974
|
+
if (node.inputs.length !== 2) {
|
|
2975
|
+
throw new Error(`Native fused decoder ${node.id} requires two inputs`);
|
|
2976
|
+
}
|
|
2977
|
+
const sourceBlocks = node.inputs.map((input) =>
|
|
2978
|
+
blocksForChannels(this._model.channelsByValue.get(input.value)!)
|
|
2979
|
+
);
|
|
2980
|
+
const upsampledSource = node.inputs.findIndex((input) => input.upsample);
|
|
2981
|
+
if (upsampledSource !== 0 && upsampledSource !== 1) {
|
|
2982
|
+
throw new Error(`Native fused decoder ${node.id} has no upsample input`);
|
|
2983
|
+
}
|
|
2984
|
+
const inputBlocks = sourceBlocks[0] + sourceBlocks[1];
|
|
2985
|
+
const outputPrecision = isFinal ? 'fp32' : this.precision;
|
|
2986
|
+
const selectedKernel = this._selectConvKernel(inputBlocks, isFinal);
|
|
2987
|
+
// The subgroup broadcast path currently targets the common standalone
|
|
2988
|
+
// convolution layout; fused decoder reads use the direct kernel.
|
|
2989
|
+
const kernel = selectedKernel === 'subgroup'
|
|
2990
|
+
? 'direct'
|
|
2991
|
+
: selectedKernel;
|
|
2992
|
+
const outputBlocks = blocksForChannels(
|
|
2993
|
+
this._model.convChannels.get(node.conv.id)!.outputChannels
|
|
2994
|
+
);
|
|
2995
|
+
if (kernel === 'subgroup-matrix' && this._subPixelDecoder(node)) {
|
|
2996
|
+
const config = this.subgroupMatrix!;
|
|
2997
|
+
return {
|
|
2998
|
+
key: `subgroup-matrix-subpixel/${this.precision}/${outputPrecision}/` +
|
|
2999
|
+
`${config.dialect}/${config.componentType}-${config.resultType}-${config.size}-sg${config.subgroupSize}/` +
|
|
3000
|
+
`n${fragments}${this.subgroupMatrixDoubleBuffer ? '-db' : ''}/${node.conv.activation}/${sourceBlocks.join('+')}/out${outputBlocks}/up${upsampledSource}`,
|
|
3001
|
+
kernel: 'subgroup-matrix',
|
|
3002
|
+
code: createSubgroupMatrixSubpixelShader(
|
|
3003
|
+
this.precision,
|
|
3004
|
+
outputPrecision,
|
|
3005
|
+
node.conv.activation,
|
|
3006
|
+
outputBlocks,
|
|
3007
|
+
config,
|
|
3008
|
+
fragments,
|
|
3009
|
+
sourceBlocks,
|
|
3010
|
+
upsampledSource,
|
|
3011
|
+
this.subgroupMatrixDoubleBuffer
|
|
3012
|
+
)
|
|
3013
|
+
};
|
|
3014
|
+
}
|
|
3015
|
+
if (kernel === 'subgroup-matrix') {
|
|
3016
|
+
return this._subgroupMatrixSpec(
|
|
3017
|
+
`decoder/${node.conv.activation}/${sourceBlocks.join('+')}/out${outputBlocks}/up${upsampledSource}`,
|
|
3018
|
+
outputPrecision,
|
|
3019
|
+
node.conv.activation,
|
|
3020
|
+
inputBlocks,
|
|
3021
|
+
outputBlocks,
|
|
3022
|
+
fragments,
|
|
3023
|
+
{ sourceBlocks, upsampledSource }
|
|
3024
|
+
);
|
|
3025
|
+
}
|
|
3026
|
+
const gemm = this._gemmForOutput(outputBlocks);
|
|
3027
|
+
const key =
|
|
3028
|
+
`decoder-${kernel}${kernel === 'implicit-gemm' && this._subPixelDecoder(node) ? '-subpixel' : ''}/${this.precision}/${outputPrecision}/` +
|
|
3029
|
+
`${node.conv.activation}/` +
|
|
3030
|
+
`${sourceBlocks.join('+')}/out${outputBlocks}/up${upsampledSource}` +
|
|
3031
|
+
(kernel === 'implicit-gemm'
|
|
3032
|
+
? `/address-${gemm.addressMode}/weights-${gemm.weightLayout}-v1` +
|
|
3033
|
+
`/tile-${gemm.workgroupSize.join('x')}-r${gemm.rowsPerThread}/loads-${gemm.loadMode}/shared-${gemm.sharedLayout}/acc-${gemm.accumulationOrder}/unroll-${Number(gemm.unrollRows)}/decoder-${gemm.decoderLoad}`
|
|
3034
|
+
: '');
|
|
3035
|
+
return {
|
|
3036
|
+
key,
|
|
3037
|
+
kernel,
|
|
3038
|
+
code: kernel === 'implicit-gemm'
|
|
3039
|
+
? (this._subPixelDecoder(node) ? createTiledSubpixelDecoderShader : createTiledDecoderShader)(
|
|
3040
|
+
this.precision,
|
|
3041
|
+
outputPrecision,
|
|
3042
|
+
node.conv.activation,
|
|
3043
|
+
sourceBlocks,
|
|
3044
|
+
upsampledSource,
|
|
3045
|
+
outputBlocks,
|
|
3046
|
+
gemm
|
|
3047
|
+
)
|
|
3048
|
+
: kernel === 'spatial'
|
|
3049
|
+
? createSpatialDecoderShader(
|
|
3050
|
+
this.precision,
|
|
3051
|
+
outputPrecision,
|
|
3052
|
+
node.conv.activation,
|
|
3053
|
+
sourceBlocks,
|
|
3054
|
+
upsampledSource,
|
|
3055
|
+
outputBlocks
|
|
3056
|
+
)
|
|
3057
|
+
: createFusedDecoderShader(
|
|
3058
|
+
this.precision,
|
|
3059
|
+
outputPrecision,
|
|
3060
|
+
node.conv.activation,
|
|
3061
|
+
sourceBlocks,
|
|
3062
|
+
upsampledSource,
|
|
3063
|
+
outputBlocks
|
|
3064
|
+
)
|
|
3065
|
+
};
|
|
3066
|
+
}
|
|
3067
|
+
throw new Error(
|
|
3068
|
+
`Native OIDN does not implement unfused ${node.op} node ${node.id}`
|
|
3069
|
+
);
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
/** The decoder's sources when it runs with the sub-pixel decomposition. */
|
|
3073
|
+
private _subPixelDecoder(node: FusedUpsampleConcatConvNodeSpec) {
|
|
3074
|
+
if (node.id === this._model.spec.output || node.inputs.length !== 2) {
|
|
3075
|
+
return undefined;
|
|
3076
|
+
}
|
|
3077
|
+
const kernel = this._selectConvKernel(
|
|
3078
|
+
blocksForChannels(this._model.convChannels.get(node.conv.id)!.inputChannels), false
|
|
3079
|
+
);
|
|
3080
|
+
if (!(kernel === 'subgroup-matrix' && this.subgroupMatrixSubPixel) &&
|
|
3081
|
+
!(kernel === 'implicit-gemm' && this.gemm.decoderSubPixel && this.gemm.weightLayout === 'k-major')) {
|
|
3082
|
+
return undefined;
|
|
3083
|
+
}
|
|
3084
|
+
const upsampledSource = node.inputs[0].upsample ? 0 : node.inputs[1].upsample ? 1 : -1;
|
|
3085
|
+
if (upsampledSource === -1) return undefined;
|
|
3086
|
+
return {
|
|
3087
|
+
sourceBlocks: node.inputs.map((input) =>
|
|
3088
|
+
blocksForChannels(this._model.channelsByValue.get(input.value)!)
|
|
3089
|
+
),
|
|
3090
|
+
upsampledSource: upsampledSource as 0 | 1
|
|
3091
|
+
};
|
|
3092
|
+
}
|
|
3093
|
+
|
|
3094
|
+
private _subgroupMatrixSpec(
|
|
3095
|
+
key: string,
|
|
3096
|
+
outputPrecision: NativeUNetPrecision,
|
|
3097
|
+
activation: Conv2DNodeSpec['activation'],
|
|
3098
|
+
inputBlocks: number,
|
|
3099
|
+
outputBlocks: number,
|
|
3100
|
+
fragments: number,
|
|
3101
|
+
decoder?: { sourceBlocks: readonly number[]; upsampledSource: 0 | 1 },
|
|
3102
|
+
pool = false
|
|
3103
|
+
): NativePipelineSpec {
|
|
3104
|
+
const config = this.subgroupMatrix!;
|
|
3105
|
+
return {
|
|
3106
|
+
key: `subgroup-matrix/${this.precision}/${outputPrecision}/` +
|
|
3107
|
+
`${config.dialect}/${config.componentType}-${config.resultType}-${config.size}-sg${config.subgroupSize}/` +
|
|
3108
|
+
`n${fragments}${this.subgroupMatrixDoubleBuffer ? '-db' : ''}/${key}`,
|
|
3109
|
+
kernel: 'subgroup-matrix',
|
|
3110
|
+
code: createSubgroupMatrixConvShader(
|
|
3111
|
+
this.precision,
|
|
3112
|
+
outputPrecision,
|
|
3113
|
+
activation,
|
|
3114
|
+
inputBlocks,
|
|
3115
|
+
outputBlocks,
|
|
3116
|
+
config,
|
|
3117
|
+
fragments,
|
|
3118
|
+
this.subgroupMatrixDoubleBuffer,
|
|
3119
|
+
decoder,
|
|
3120
|
+
pool
|
|
3121
|
+
)
|
|
3122
|
+
};
|
|
3123
|
+
}
|
|
3124
|
+
|
|
3125
|
+
/**
|
|
3126
|
+
* Output fragments per workgroup for a subgroup-matrix layer: the default
|
|
3127
|
+
* unless that leaves fewer than SUBGROUP_MATRIX_MIN_WORKGROUPS workgroups,
|
|
3128
|
+
* where one fragment keeps more of the GPU busy (low-resolution layers).
|
|
3129
|
+
* Never more fragments than the output channels fill (the final RGB layer).
|
|
3130
|
+
*/
|
|
3131
|
+
private _subgroupMatrixFragmentsFor(shape: ModelValueShape) {
|
|
3132
|
+
const fragments = Math.min(
|
|
3133
|
+
this.subgroupMatrixOutputFragments,
|
|
3134
|
+
Math.ceil(blocksForChannels(shape.channels) * 4 / this.subgroupMatrix!.size)
|
|
3135
|
+
);
|
|
3136
|
+
if (this._subgroupMatrixFixedFragments || fragments === 1) return fragments;
|
|
3137
|
+
const workgroups = Math.ceil(shape.width / SUBGROUP_MATRIX_TILE_WIDTH) *
|
|
3138
|
+
Math.ceil(shape.height / SUBGROUP_MATRIX_TILE_HEIGHT) *
|
|
3139
|
+
Math.ceil(blocksForChannels(shape.channels) * 4 / (fragments * this.subgroupMatrix!.size));
|
|
3140
|
+
return workgroups < SUBGROUP_MATRIX_MIN_WORKGROUPS ? 1 : fragments;
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
// Wider output tiles reuse each input load across twice as many channels.
|
|
3144
|
+
// Require full output blocks and keep the validated base tile as fallback.
|
|
3145
|
+
private _gemmForOutput(outputBlocks: number): Readonly<Required<NativeUNetGemmOptions>> {
|
|
3146
|
+
if (this.gemm.tilePolicy !== 'output-aligned' ||
|
|
3147
|
+
this.gemm.workgroupSize[0] !== 8 || outputBlocks <= 0 || outputBlocks % 16 !== 0) {
|
|
3148
|
+
return this.gemm;
|
|
3149
|
+
}
|
|
3150
|
+
const cached = this._gemmByOutputBlocks.get(outputBlocks);
|
|
3151
|
+
if (cached) return cached;
|
|
3152
|
+
const candidate = Object.freeze({
|
|
3153
|
+
...this.gemm,
|
|
3154
|
+
workgroupSize: Object.freeze([16, this.gemm.workgroupSize[1]]) as NativeUNetGemmWorkgroup
|
|
3155
|
+
});
|
|
3156
|
+
const tile = gemmTile(candidate);
|
|
3157
|
+
const sizes = gemmSharedSizes(candidate);
|
|
3158
|
+
const limits = this._device.limits;
|
|
3159
|
+
const fits = tile.workgroupX <= limits.maxComputeWorkgroupSizeX &&
|
|
3160
|
+
tile.workgroupY <= limits.maxComputeWorkgroupSizeY &&
|
|
3161
|
+
tile.workgroupX * tile.workgroupY <= limits.maxComputeInvocationsPerWorkgroup &&
|
|
3162
|
+
(sizes.input + sizes.weights) * 4 * (this.precision === 'fp16' ? 2 : 4) <= limits.maxComputeWorkgroupStorageSize;
|
|
3163
|
+
const selected = fits ? candidate : this.gemm;
|
|
3164
|
+
this._gemmByOutputBlocks.set(outputBlocks, selected);
|
|
3165
|
+
return selected;
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3168
|
+
// Keep fusion on the Large native 8x8 mixed-precision configuration measured
|
|
3169
|
+
// on Metal. Other matrix shapes and browser kernels retain separate pooling.
|
|
3170
|
+
private _matrixPool() {
|
|
3171
|
+
const config = this.subgroupMatrix;
|
|
3172
|
+
return this._model.spec.family === 'oidn-unet-large' && this.precision === 'fp16' &&
|
|
3173
|
+
config?.dialect === 'naga' && config.size === 8 && config.subgroupSize === 32 &&
|
|
3174
|
+
config.componentType === 'f16' && config.resultType === 'f32';
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
private _coalescedPool() {
|
|
3178
|
+
return this.gemm.poolLayout === 'channels' &&
|
|
3179
|
+
(this.kernelSetting === 'auto' || this.kernelSetting === 'implicit-gemm' ||
|
|
3180
|
+
this.kernelSetting === 'subgroup-matrix');
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
private _selectConvKernel(
|
|
3184
|
+
inputBlocks: number,
|
|
3185
|
+
isFinal: boolean
|
|
3186
|
+
): NativeUNetKernel {
|
|
3187
|
+
const spatialFits =
|
|
3188
|
+
this.precision === 'fp16' &&
|
|
3189
|
+
inputBlocks <= this.maxSpatialInputBlocks;
|
|
3190
|
+
if (this.kernelSetting === 'direct') return 'direct';
|
|
3191
|
+
if (this.kernelSetting === 'spatial') {
|
|
3192
|
+
return spatialFits ? 'spatial' : 'direct';
|
|
3193
|
+
}
|
|
3194
|
+
if (this.kernelSetting === 'implicit-gemm') {
|
|
3195
|
+
return isFinal ? 'direct' : 'implicit-gemm';
|
|
3196
|
+
}
|
|
3197
|
+
if (this.kernelSetting === 'subgroup') {
|
|
3198
|
+
return this.subgroupsAvailable ? 'subgroup' : 'direct';
|
|
3199
|
+
}
|
|
3200
|
+
if (this.kernelSetting === 'subgroup-matrix') {
|
|
3201
|
+
// The 64-thread final RGB kernel is far below matrix throughput on
|
|
3202
|
+
// native M4 Max; Chrome keeps it until measured there.
|
|
3203
|
+
if (isFinal) return this.subgroupMatrix?.dialect === 'naga' ? 'subgroup-matrix' : 'direct';
|
|
3204
|
+
return this.subgroupMatrix ? 'subgroup-matrix' : 'implicit-gemm';
|
|
3205
|
+
}
|
|
3206
|
+
if (!isFinal) return 'implicit-gemm';
|
|
3207
|
+
return 'direct';
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3210
|
+
private _nodePipeline(node: ExecutableModelNode, isFinal: boolean) {
|
|
3211
|
+
const { key, code } = this._nodePipelineSpec(node, isFinal);
|
|
3212
|
+
return this._pipeline(key, code);
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
private _createExecution(width: number, height: number): CachedExecution {
|
|
3216
|
+
const fuseConvPool = this._matrixPool();
|
|
3217
|
+
const plan = planModelExecution(this._model, width, height, { fuseConvPool });
|
|
3218
|
+
const valueBuffers = new Map<string, OIDNGPUBuffer>();
|
|
3219
|
+
const slots: ActivationSlot[] = [];
|
|
3220
|
+
const plannedSlots: { capacity: number; lastUse: number; values: string[] }[] = [];
|
|
3221
|
+
const lastUses = new Map<string, number>();
|
|
3222
|
+
plan.nodes.forEach((node, index) => {
|
|
3223
|
+
for (const input of executableInputs(node)) lastUses.set(input, index);
|
|
3224
|
+
});
|
|
3225
|
+
lastUses.set(plan.spec.output, plan.nodes.length);
|
|
3226
|
+
const createdBuffers: OIDNGPUBuffer[] = [];
|
|
3227
|
+
const own = (buffer: OIDNGPUBuffer) => {
|
|
3228
|
+
createdBuffers.push(buffer);
|
|
3229
|
+
return this._resources.track('gpu-buffer', buffer);
|
|
3230
|
+
};
|
|
3231
|
+
|
|
3232
|
+
try {
|
|
3233
|
+
const allocate = (
|
|
3234
|
+
value: string,
|
|
3235
|
+
shape: ModelValueShape,
|
|
3236
|
+
bytesPerScalar: number,
|
|
3237
|
+
index: number
|
|
3238
|
+
) => {
|
|
3239
|
+
const requiredSize = activationByteSize(shape, bytesPerScalar);
|
|
3240
|
+
const free = plannedSlots.filter((slot) => slot.lastUse < index);
|
|
3241
|
+
let slot = free
|
|
3242
|
+
.filter((candidate) => candidate.capacity >= requiredSize)
|
|
3243
|
+
.sort((a, b) => a.capacity - b.capacity)[0];
|
|
3244
|
+
// Plan lifetimes before creating GPU buffers. A retired small slot can
|
|
3245
|
+
// grow for the decoder after encoder pool fusion. Other paths keep
|
|
3246
|
+
// their previous slot selection.
|
|
3247
|
+
if (!slot && fuseConvPool) slot = free.sort((a, b) => b.capacity - a.capacity)[0];
|
|
3248
|
+
if (!slot) {
|
|
3249
|
+
slot = { capacity: 0, lastUse: -2, values: [] };
|
|
3250
|
+
plannedSlots.push(slot);
|
|
3251
|
+
}
|
|
3252
|
+
slot.capacity = Math.max(slot.capacity, requiredSize);
|
|
3253
|
+
slot.lastUse = lastUses.get(value) ?? -1;
|
|
3254
|
+
slot.values.push(value);
|
|
3255
|
+
};
|
|
3256
|
+
|
|
3257
|
+
allocate(
|
|
3258
|
+
plan.spec.input,
|
|
3259
|
+
plan.inputShape,
|
|
3260
|
+
this.precision === 'fp16' ? 2 : 4,
|
|
3261
|
+
-1
|
|
3262
|
+
);
|
|
3263
|
+
plan.plannedNodes.forEach(({ node, outputShape }, index) => {
|
|
3264
|
+
const isFinal = node.id === plan.spec.output;
|
|
3265
|
+
allocate(
|
|
3266
|
+
node.id,
|
|
3267
|
+
outputShape,
|
|
3268
|
+
isFinal || this.precision === 'fp32' ? 4 : 2,
|
|
3269
|
+
index
|
|
3270
|
+
);
|
|
3271
|
+
});
|
|
3272
|
+
plannedSlots.forEach((slot, index) => {
|
|
3273
|
+
const buffer = own(this._device.createBuffer({
|
|
3274
|
+
label: `oidn/activation/${width}x${height}/${index}`,
|
|
3275
|
+
size: roundUp(slot.capacity, 4),
|
|
3276
|
+
usage: BufferUsage.STORAGE | BufferUsage.COPY_SRC | BufferUsage.COPY_DST
|
|
3277
|
+
}));
|
|
3278
|
+
slots.push({ buffer, capacity: slot.capacity });
|
|
3279
|
+
for (const value of slot.values) valueBuffers.set(value, buffer);
|
|
3280
|
+
});
|
|
3281
|
+
|
|
3282
|
+
const inputSourceCount = this._model.inputChannels / 3;
|
|
3283
|
+
const inputKey = `pack/${this.precision}/${inputSourceCount}`;
|
|
3284
|
+
const inputPipeline = this._pipeline(
|
|
3285
|
+
inputKey,
|
|
3286
|
+
createInputPackShader(this.precision, inputSourceCount)
|
|
3287
|
+
);
|
|
3288
|
+
const nodePipelines: OIDNGPUComputePipeline[] = [];
|
|
3289
|
+
const nodeKernels: NativeUNetKernel[] = [];
|
|
3290
|
+
const nodeFragments: number[] = [];
|
|
3291
|
+
const nodeBindings: OIDNGPUBindGroup[] = [];
|
|
3292
|
+
const ownedBuffers: OIDNGPUBuffer[] = [];
|
|
3293
|
+
|
|
3294
|
+
plan.plannedNodes.forEach(({ node, outputShape }, index) => {
|
|
3295
|
+
const isFinal = node.id === plan.spec.output;
|
|
3296
|
+
const fragments = this.subgroupMatrix ? this._subgroupMatrixFragmentsFor(outputShape) : 0;
|
|
3297
|
+
const pipelineSpec = this._nodePipelineSpec(node, isFinal, fragments);
|
|
3298
|
+
if (pipelineSpec.kernel) this.usedKernels.add(pipelineSpec.kernel);
|
|
3299
|
+
const pipeline = this._pipeline(pipelineSpec.key, pipelineSpec.code);
|
|
3300
|
+
nodePipelines.push(pipeline);
|
|
3301
|
+
nodeKernels.push(pipelineSpec.kernel ?? 'direct');
|
|
3302
|
+
nodeFragments.push(pipelineSpec.kernel === 'subgroup-matrix' ? fragments : 0);
|
|
3303
|
+
const output = valueBuffers.get(node.id)!;
|
|
3304
|
+
let entries: OIDNGPUBindGroupEntry[];
|
|
3305
|
+
let uniformValues: number[];
|
|
3306
|
+
let convId: string;
|
|
3307
|
+
|
|
3308
|
+
if (node.op === 'conv2d') {
|
|
3309
|
+
const inputShape = plan.valueShapes.get(node.input)!;
|
|
3310
|
+
convId = node.id;
|
|
3311
|
+
uniformValues = [
|
|
3312
|
+
inputShape.width,
|
|
3313
|
+
inputShape.height,
|
|
3314
|
+
outputShape.width,
|
|
3315
|
+
outputShape.height,
|
|
3316
|
+
blocksForChannels(inputShape.channels),
|
|
3317
|
+
blocksForChannels(outputShape.channels)
|
|
3318
|
+
];
|
|
3319
|
+
const packed = this._packedConvs.get(convId)!;
|
|
3320
|
+
entries = [
|
|
3321
|
+
{ binding: 0, resource: { buffer: valueBuffers.get(node.input)! } },
|
|
3322
|
+
{ binding: 1, resource: { buffer: packed.weights } },
|
|
3323
|
+
{ binding: 2, resource: { buffer: packed.bias } },
|
|
3324
|
+
{ binding: 3, resource: { buffer: output } }
|
|
3325
|
+
];
|
|
3326
|
+
} else if (node.op === 'maxPool2d') {
|
|
3327
|
+
const inputShape = plan.valueShapes.get(node.input)!;
|
|
3328
|
+
uniformValues = [
|
|
3329
|
+
inputShape.width,
|
|
3330
|
+
inputShape.height,
|
|
3331
|
+
outputShape.width,
|
|
3332
|
+
outputShape.height,
|
|
3333
|
+
blocksForChannels(outputShape.channels)
|
|
3334
|
+
];
|
|
3335
|
+
entries = [
|
|
3336
|
+
{ binding: 0, resource: { buffer: valueBuffers.get(node.input)! } },
|
|
3337
|
+
{ binding: 1, resource: { buffer: output } }
|
|
3338
|
+
];
|
|
3339
|
+
} else if (node.op === 'fusedConvReluMaxPool2d') {
|
|
3340
|
+
const inputShape = plan.valueShapes.get(node.input)!;
|
|
3341
|
+
convId = node.conv.id;
|
|
3342
|
+
uniformValues = [
|
|
3343
|
+
inputShape.width,
|
|
3344
|
+
inputShape.height,
|
|
3345
|
+
outputShape.width,
|
|
3346
|
+
outputShape.height,
|
|
3347
|
+
blocksForChannels(inputShape.channels),
|
|
3348
|
+
blocksForChannels(outputShape.channels)
|
|
3349
|
+
];
|
|
3350
|
+
const packed = this._packedConvs.get(convId)!;
|
|
3351
|
+
entries = [
|
|
3352
|
+
{ binding: 0, resource: { buffer: valueBuffers.get(node.input)! } },
|
|
3353
|
+
{ binding: 1, resource: { buffer: packed.weights } },
|
|
3354
|
+
{ binding: 2, resource: { buffer: packed.bias } },
|
|
3355
|
+
{ binding: 3, resource: { buffer: output } }
|
|
3356
|
+
];
|
|
3357
|
+
} else if (node.op === 'fusedUpsampleConcatConv2d') {
|
|
3358
|
+
convId = node.conv.id;
|
|
3359
|
+
const firstShape = plan.valueShapes.get(node.inputs[0].value)!;
|
|
3360
|
+
const secondShape = plan.valueShapes.get(node.inputs[1].value)!;
|
|
3361
|
+
uniformValues = [
|
|
3362
|
+
outputShape.width,
|
|
3363
|
+
outputShape.height,
|
|
3364
|
+
blocksForChannels(outputShape.channels),
|
|
3365
|
+
blocksForChannels(this._model.convChannels.get(convId)!.inputChannels),
|
|
3366
|
+
firstShape.width,
|
|
3367
|
+
firstShape.height,
|
|
3368
|
+
secondShape.width,
|
|
3369
|
+
secondShape.height
|
|
3370
|
+
];
|
|
3371
|
+
const packed = this._packedConvs.get(convId)!;
|
|
3372
|
+
entries = [
|
|
3373
|
+
{
|
|
3374
|
+
binding: 0,
|
|
3375
|
+
resource: { buffer: valueBuffers.get(node.inputs[0].value)! }
|
|
3376
|
+
},
|
|
3377
|
+
{
|
|
3378
|
+
binding: 1,
|
|
3379
|
+
resource: { buffer: valueBuffers.get(node.inputs[1].value)! }
|
|
3380
|
+
},
|
|
3381
|
+
{ binding: 2, resource: { buffer: packed.weights } },
|
|
3382
|
+
{ binding: 3, resource: { buffer: packed.bias } },
|
|
3383
|
+
{ binding: 4, resource: { buffer: output } }
|
|
3384
|
+
];
|
|
3385
|
+
} else {
|
|
3386
|
+
throw new Error(`Unexpected native node ${node.op}`);
|
|
3387
|
+
}
|
|
3388
|
+
|
|
3389
|
+
const uniform = own(
|
|
3390
|
+
createUniformBuffer(
|
|
3391
|
+
this._device,
|
|
3392
|
+
`oidn/${node.id}/params/${width}x${height}`,
|
|
3393
|
+
uniformValues
|
|
3394
|
+
)
|
|
3395
|
+
);
|
|
3396
|
+
ownedBuffers.push(uniform);
|
|
3397
|
+
entries.push({ binding: entries.length, resource: { buffer: uniform } });
|
|
3398
|
+
nodeBindings.push(
|
|
3399
|
+
this._device.createBindGroup({
|
|
3400
|
+
label: `oidn/${node.id}/bindings`,
|
|
3401
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
3402
|
+
entries
|
|
3403
|
+
})
|
|
3404
|
+
);
|
|
3405
|
+
});
|
|
3406
|
+
|
|
3407
|
+
const inputUniform = own(
|
|
3408
|
+
createUniformBuffer(
|
|
3409
|
+
this._device,
|
|
3410
|
+
`oidn/input/params/${width}x${height}`,
|
|
3411
|
+
[
|
|
3412
|
+
width,
|
|
3413
|
+
height,
|
|
3414
|
+
blocksForChannels(this._model.inputChannels),
|
|
3415
|
+
this._model.inputChannels
|
|
3416
|
+
]
|
|
3417
|
+
)
|
|
3418
|
+
);
|
|
3419
|
+
ownedBuffers.push(inputUniform);
|
|
3420
|
+
|
|
3421
|
+
return {
|
|
3422
|
+
plan,
|
|
3423
|
+
valueBuffers,
|
|
3424
|
+
slots,
|
|
3425
|
+
nodeBindings,
|
|
3426
|
+
nodePipelines,
|
|
3427
|
+
nodeKernels,
|
|
3428
|
+
nodeFragments,
|
|
3429
|
+
inputPipeline,
|
|
3430
|
+
inputUniform,
|
|
3431
|
+
ownedBuffers,
|
|
3432
|
+
lastUsed: ++this._clock
|
|
3433
|
+
};
|
|
3434
|
+
} catch (error) {
|
|
3435
|
+
for (const buffer of createdBuffers) this._releaseBuffer(buffer);
|
|
3436
|
+
throw error;
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
|
|
3440
|
+
protected _execution(width: number, height: number) {
|
|
3441
|
+
if (this._disposed) throw new Error('Native OIDN executor is disposed');
|
|
3442
|
+
const key = `${width}x${height}`;
|
|
3443
|
+
let execution = this._executionCache.get(key);
|
|
3444
|
+
if (!execution) {
|
|
3445
|
+
execution = this._createExecution(width, height);
|
|
3446
|
+
this._executionCache.set(key, execution);
|
|
3447
|
+
if (this._executionCache.size > this._shapeCacheSize) {
|
|
3448
|
+
const oldest = [...this._executionCache.entries()]
|
|
3449
|
+
.filter(([candidateKey]) => candidateKey !== key)
|
|
3450
|
+
.sort((a, b) => a[1].lastUsed - b[1].lastUsed)[0];
|
|
3451
|
+
if (oldest) {
|
|
3452
|
+
this._executionCache.delete(oldest[0]);
|
|
3453
|
+
this._retireExecution(oldest[1]);
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
execution.lastUsed = ++this._clock;
|
|
3458
|
+
return execution;
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3461
|
+
/**
|
|
3462
|
+
* Releases an evicted plan. The synchronous core destroys it at once; the
|
|
3463
|
+
* browser executor waits for the queue first.
|
|
3464
|
+
*/
|
|
3465
|
+
protected _retireExecution(execution: CachedExecution) {
|
|
3466
|
+
this._destroyExecution(execution);
|
|
3467
|
+
}
|
|
3468
|
+
|
|
3469
|
+
/** Allocates shape-dependent execution resources before interactive use. */
|
|
3470
|
+
prewarm(shapes: readonly { width: number; height: number }[]) {
|
|
3471
|
+
for (const shape of shapes) {
|
|
3472
|
+
this._execution(shape.width, shape.height);
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3476
|
+
/** Captures per-pass GPU timestamps for the next execute call when supported. */
|
|
3477
|
+
profileNextExecution() {
|
|
3478
|
+
if (!this._device.features.has('timestamp-query')) return false;
|
|
3479
|
+
this._profileNextExecution = true;
|
|
3480
|
+
return true;
|
|
3481
|
+
}
|
|
3482
|
+
|
|
3483
|
+
/** Shape-owned packed input; external preprocessing must write all lanes. */
|
|
3484
|
+
packedInputBuffer(width: number, height: number) {
|
|
3485
|
+
const execution = this._execution(width, height);
|
|
3486
|
+
return execution.valueBuffers.get(execution.plan.spec.input)!;
|
|
3487
|
+
}
|
|
3488
|
+
|
|
3489
|
+
/** Executes after a preprocessing pass has filled packedInputBuffer. */
|
|
3490
|
+
executePacked(width: number, height: number) {
|
|
3491
|
+
return this._execute([], width, height, true);
|
|
3492
|
+
}
|
|
3493
|
+
|
|
3494
|
+
execute(inputBuffers: readonly OIDNGPUBuffer[], width: number, height: number) {
|
|
3495
|
+
return this._execute(inputBuffers, width, height, false);
|
|
3496
|
+
}
|
|
3497
|
+
|
|
3498
|
+
private _execute(inputBuffers: readonly OIDNGPUBuffer[], width: number, height: number, packedInput: boolean) {
|
|
3499
|
+
const sourceCount = this._model.inputChannels / 3;
|
|
3500
|
+
if (!packedInput && inputBuffers.length !== sourceCount) {
|
|
3501
|
+
throw new Error(
|
|
3502
|
+
`Native OIDN expected ${sourceCount} input buffers, got ${inputBuffers.length}`
|
|
3503
|
+
);
|
|
3504
|
+
}
|
|
3505
|
+
const execution = this._execution(width, height);
|
|
3506
|
+
const profileLabels = [
|
|
3507
|
+
...(packedInput ? [] : ['input-pack']),
|
|
3508
|
+
...execution.plan.nodes.map((node) => node.id)
|
|
3509
|
+
];
|
|
3510
|
+
const shouldProfile =
|
|
3511
|
+
this._profileNextExecution &&
|
|
3512
|
+
this._device.features.has('timestamp-query');
|
|
3513
|
+
this._profileNextExecution = false;
|
|
3514
|
+
const queryCount = profileLabels.length * 2;
|
|
3515
|
+
const querySet = shouldProfile
|
|
3516
|
+
? this._resources.track(
|
|
3517
|
+
'gpu-query-set',
|
|
3518
|
+
this._device.createQuerySet({ type: 'timestamp', count: queryCount })
|
|
3519
|
+
)
|
|
3520
|
+
: undefined;
|
|
3521
|
+
const queryBufferSize = queryCount * 8;
|
|
3522
|
+
let queryResolveBuffer: OIDNGPUBuffer | undefined;
|
|
3523
|
+
let queryReadbackBuffer: OIDNGPUBuffer | undefined;
|
|
3524
|
+
try {
|
|
3525
|
+
queryResolveBuffer = shouldProfile
|
|
3526
|
+
? this._resources.track(
|
|
3527
|
+
'gpu-buffer',
|
|
3528
|
+
this._device.createBuffer({
|
|
3529
|
+
label: `oidn/profile/resolve/${width}x${height}`,
|
|
3530
|
+
size: queryBufferSize,
|
|
3531
|
+
usage: BufferUsage.QUERY_RESOLVE | BufferUsage.COPY_SRC
|
|
3532
|
+
})
|
|
3533
|
+
)
|
|
3534
|
+
: undefined;
|
|
3535
|
+
queryReadbackBuffer = shouldProfile
|
|
3536
|
+
? this._resources.track(
|
|
3537
|
+
'gpu-buffer',
|
|
3538
|
+
this._device.createBuffer({
|
|
3539
|
+
label: `oidn/profile/readback/${width}x${height}`,
|
|
3540
|
+
size: queryBufferSize,
|
|
3541
|
+
usage: BufferUsage.COPY_DST | BufferUsage.MAP_READ
|
|
3542
|
+
})
|
|
3543
|
+
)
|
|
3544
|
+
: undefined;
|
|
3545
|
+
} catch (error) {
|
|
3546
|
+
this._releaseBuffer(queryReadbackBuffer);
|
|
3547
|
+
this._releaseBuffer(queryResolveBuffer);
|
|
3548
|
+
this._releaseQuerySet(querySet);
|
|
3549
|
+
throw error;
|
|
3550
|
+
}
|
|
3551
|
+
const passDescriptor = (label: string, index: number) => ({
|
|
3552
|
+
label,
|
|
3553
|
+
...(querySet
|
|
3554
|
+
? {
|
|
3555
|
+
timestampWrites: {
|
|
3556
|
+
querySet,
|
|
3557
|
+
beginningOfPassWriteIndex: index * 2,
|
|
3558
|
+
endOfPassWriteIndex: index * 2 + 1
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
: {})
|
|
3562
|
+
});
|
|
3563
|
+
try {
|
|
3564
|
+
const encoder = this._device.createCommandEncoder({
|
|
3565
|
+
label: `oidn/native/${width}x${height}`
|
|
3566
|
+
});
|
|
3567
|
+
|
|
3568
|
+
if (!packedInput) {
|
|
3569
|
+
const inputEntries: OIDNGPUBindGroupEntry[] = inputBuffers.map(
|
|
3570
|
+
(buffer, binding) => ({ binding, resource: { buffer } })
|
|
3571
|
+
);
|
|
3572
|
+
inputEntries.push({
|
|
3573
|
+
binding: sourceCount,
|
|
3574
|
+
resource: { buffer: execution.valueBuffers.get(execution.plan.spec.input)! }
|
|
3575
|
+
});
|
|
3576
|
+
inputEntries.push({
|
|
3577
|
+
binding: sourceCount + 1,
|
|
3578
|
+
resource: { buffer: execution.inputUniform }
|
|
3579
|
+
});
|
|
3580
|
+
const inputBindings = this._device.createBindGroup({
|
|
3581
|
+
label: 'oidn/input/bindings',
|
|
3582
|
+
layout: execution.inputPipeline.getBindGroupLayout(0),
|
|
3583
|
+
entries: inputEntries
|
|
3584
|
+
});
|
|
3585
|
+
const pass = encoder.beginComputePass(
|
|
3586
|
+
passDescriptor('oidn/input-pack', 0)
|
|
3587
|
+
);
|
|
3588
|
+
pass.setPipeline(execution.inputPipeline);
|
|
3589
|
+
pass.setBindGroup(0, inputBindings);
|
|
3590
|
+
pass.dispatchWorkgroups(
|
|
3591
|
+
Math.ceil(width / WORKGROUP_SIZE),
|
|
3592
|
+
Math.ceil(height / WORKGROUP_SIZE),
|
|
3593
|
+
blocksForChannels(this._model.inputChannels)
|
|
3594
|
+
);
|
|
3595
|
+
pass.end();
|
|
3596
|
+
}
|
|
3597
|
+
|
|
3598
|
+
execution.plan.plannedNodes.forEach(({ node, outputShape }, index) => {
|
|
3599
|
+
const pass = encoder.beginComputePass(
|
|
3600
|
+
passDescriptor(`oidn/${execution.plan.nodes[index].id}`, index + (packedInput ? 0 : 1))
|
|
3601
|
+
);
|
|
3602
|
+
pass.setPipeline(execution.nodePipelines[index]);
|
|
3603
|
+
pass.setBindGroup(0, execution.nodeBindings[index]);
|
|
3604
|
+
if (node.op === 'maxPool2d' && this._coalescedPool()) {
|
|
3605
|
+
const groupsX = Math.ceil(
|
|
3606
|
+
outputShape.width * blocksForChannels(outputShape.channels) / WORKGROUP_SIZE
|
|
3607
|
+
);
|
|
3608
|
+
const maxGroups = this._device.limits.maxComputeWorkgroupsPerDimension;
|
|
3609
|
+
// Wide channel rows continue in Z instead of exceeding the X limit.
|
|
3610
|
+
pass.dispatchWorkgroups(
|
|
3611
|
+
Math.min(groupsX, maxGroups),
|
|
3612
|
+
Math.ceil(outputShape.height / WORKGROUP_SIZE),
|
|
3613
|
+
Math.ceil(groupsX / maxGroups)
|
|
3614
|
+
);
|
|
3615
|
+
} else if (execution.nodeKernels[index] === 'subgroup-matrix') {
|
|
3616
|
+
pass.dispatchWorkgroups(
|
|
3617
|
+
Math.ceil(outputShape.width * (node.op === 'fusedConvReluMaxPool2d' ? 2 : 1) / SUBGROUP_MATRIX_TILE_WIDTH),
|
|
3618
|
+
Math.ceil(outputShape.height * (node.op === 'fusedConvReluMaxPool2d' ? 2 : 1) / SUBGROUP_MATRIX_TILE_HEIGHT),
|
|
3619
|
+
Math.ceil(
|
|
3620
|
+
blocksForChannels(outputShape.channels) /
|
|
3621
|
+
(execution.nodeFragments[index] * this.subgroupMatrix!.size / 4)
|
|
3622
|
+
)
|
|
3623
|
+
);
|
|
3624
|
+
} else if (execution.nodeKernels[index] === 'implicit-gemm') {
|
|
3625
|
+
const tile = gemmTile(this._gemmForOutput(blocksForChannels(outputShape.channels)));
|
|
3626
|
+
const subPixel = node.op === 'fusedUpsampleConcatConv2d' && this._subPixelDecoder(node) !== undefined;
|
|
3627
|
+
pass.dispatchWorkgroups(
|
|
3628
|
+
Math.ceil(
|
|
3629
|
+
(subPixel ? Math.ceil(outputShape.width / 2) * Math.ceil(outputShape.height / 2)
|
|
3630
|
+
: outputShape.width * outputShape.height) / tile.tileM
|
|
3631
|
+
),
|
|
3632
|
+
Math.ceil(
|
|
3633
|
+
blocksForChannels(outputShape.channels) / tile.tileNBlocks
|
|
3634
|
+
),
|
|
3635
|
+
subPixel ? 4 : 1
|
|
3636
|
+
);
|
|
3637
|
+
} else {
|
|
3638
|
+
pass.dispatchWorkgroups(
|
|
3639
|
+
Math.ceil(outputShape.width / WORKGROUP_SIZE),
|
|
3640
|
+
Math.ceil(outputShape.height / WORKGROUP_SIZE),
|
|
3641
|
+
blocksForChannels(outputShape.channels)
|
|
3642
|
+
);
|
|
3643
|
+
}
|
|
3644
|
+
pass.end();
|
|
3645
|
+
});
|
|
3646
|
+
|
|
3647
|
+
if (querySet) {
|
|
3648
|
+
encoder.resolveQuerySet(
|
|
3649
|
+
querySet,
|
|
3650
|
+
0,
|
|
3651
|
+
queryCount,
|
|
3652
|
+
queryResolveBuffer!,
|
|
3653
|
+
0
|
|
3654
|
+
);
|
|
3655
|
+
encoder.copyBufferToBuffer(
|
|
3656
|
+
queryResolveBuffer!,
|
|
3657
|
+
0,
|
|
3658
|
+
queryReadbackBuffer!,
|
|
3659
|
+
0,
|
|
3660
|
+
queryBufferSize
|
|
3661
|
+
);
|
|
3662
|
+
}
|
|
3663
|
+
|
|
3664
|
+
this._device.queue.submit([encoder.finish()]);
|
|
3665
|
+
} catch (error) {
|
|
3666
|
+
this._releaseBuffer(queryReadbackBuffer);
|
|
3667
|
+
this._releaseBuffer(queryResolveBuffer);
|
|
3668
|
+
this._releaseQuerySet(querySet);
|
|
3669
|
+
throw error;
|
|
3670
|
+
}
|
|
3671
|
+
if (querySet) {
|
|
3672
|
+
this._profileOperations++;
|
|
3673
|
+
this._readProfile(querySet, queryResolveBuffer!, queryReadbackBuffer!, profileLabels);
|
|
3674
|
+
}
|
|
3675
|
+
return execution.valueBuffers.get(execution.plan.spec.output)!;
|
|
3676
|
+
}
|
|
3677
|
+
|
|
3678
|
+
/**
|
|
3679
|
+
* Receives the timestamp queries of a profiled execution. The synchronous
|
|
3680
|
+
* core cannot read them back, so it releases them.
|
|
3681
|
+
*/
|
|
3682
|
+
protected _readProfile(
|
|
3683
|
+
querySet: OIDNGPUQuerySet,
|
|
3684
|
+
resolveBuffer: OIDNGPUBuffer,
|
|
3685
|
+
readbackBuffer: OIDNGPUBuffer,
|
|
3686
|
+
labels: string[]
|
|
3687
|
+
) {
|
|
3688
|
+
void labels;
|
|
3689
|
+
this._releaseQuerySet(querySet);
|
|
3690
|
+
this._releaseBuffer(resolveBuffer);
|
|
3691
|
+
this._releaseBuffer(readbackBuffer);
|
|
3692
|
+
this._profileOperations--;
|
|
3693
|
+
}
|
|
3694
|
+
|
|
3695
|
+
protected _releaseBuffer(buffer: OIDNGPUBuffer | undefined) {
|
|
3696
|
+
this._resources.release('gpu-buffer', buffer, () => buffer!.destroy());
|
|
3697
|
+
}
|
|
3698
|
+
|
|
3699
|
+
protected _releaseQuerySet(querySet: OIDNGPUQuerySet | undefined) {
|
|
3700
|
+
this._resources.release(
|
|
3701
|
+
'gpu-query-set',
|
|
3702
|
+
querySet,
|
|
3703
|
+
() => querySet!.destroy()
|
|
3704
|
+
);
|
|
3705
|
+
}
|
|
3706
|
+
|
|
3707
|
+
protected _destroyExecution(execution: CachedExecution) {
|
|
3708
|
+
execution.slots.forEach((slot) => this._releaseBuffer(slot.buffer));
|
|
3709
|
+
execution.ownedBuffers.forEach((buffer) => this._releaseBuffer(buffer));
|
|
3710
|
+
}
|
|
3711
|
+
|
|
3712
|
+
getResourceInfo(): OIDNResourceSnapshot {
|
|
3713
|
+
return this._resources.snapshot(
|
|
3714
|
+
this._retiredExecutions.size + this._profileOperations
|
|
3715
|
+
);
|
|
3716
|
+
}
|
|
3717
|
+
|
|
3718
|
+
dispose() {
|
|
3719
|
+
if (this._disposed) return;
|
|
3720
|
+
this._disposed = true;
|
|
3721
|
+
for (const packed of this._packedConvs.values()) {
|
|
3722
|
+
this._releaseBuffer(packed.weights);
|
|
3723
|
+
this._releaseBuffer(packed.bias);
|
|
3724
|
+
}
|
|
3725
|
+
this._packedConvs.clear();
|
|
3726
|
+
for (const execution of this._executionCache.values()) {
|
|
3727
|
+
this._destroyExecution(execution);
|
|
3728
|
+
}
|
|
3729
|
+
this._executionCache.clear();
|
|
3730
|
+
for (const execution of this._retiredExecutions) {
|
|
3731
|
+
this._destroyExecution(execution);
|
|
3732
|
+
}
|
|
3733
|
+
this._retiredExecutions.clear();
|
|
3734
|
+
}
|
|
3735
|
+
}
|