Xenova HF Staff commited on
Commit
c22712e
·
verified ·
1 Parent(s): 8733597

sync 6fdf6301e2bb

Browse files
README.md CHANGED
@@ -1,3 +1,92 @@
1
  ---
 
2
  license: apache-2.0
 
 
 
 
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ library_name: kernels
3
  license: apache-2.0
4
+ tags:
5
+ - kernel
6
+ - webgpu
7
+ - wgsl
8
  ---
9
+ # com.microsoft.GroupNorm
10
+
11
+ `com.microsoft` · ONNX Runtime contrib operator · contrib since_version 1
12
+
13
+ ## Description
14
+
15
+ Group normalization over a 4-D image tensor with an optional fused SiLU: `Y = gamma * (X - mean) / sqrt(variance + epsilon) + beta`, with mean and variance taken per batch item over each group of `C / groups` channels and every spatial position. Both layouts are implemented: `channels_last = 1` reads `(N, H, W, C)` and `channels_last = 0` reads `(N, C, H, W)`. `X`/`Y` and `gamma`/`beta` carry independent element types, so float16 activations with float32 weights is a first-class case. Statistics and the affine are float32 whatever the tensor type; only the store narrows.
16
+
17
+ See the [ONNX Runtime `GroupNorm` contrib-operator spec](https://github.com/microsoft/onnxruntime/blob/main/docs/ContribOperators.md#com.microsoft.GroupNorm) for the reference semantics.
18
+
19
+ ## Inputs
20
+
21
+ | Name | Logical dtype | Rank | Shape | Description | Presence |
22
+ | --- | --- | --- | --- | --- | --- |
23
+ | `X` | `T` | `4` | — | Input image tensor, `(N, H, W, C)` when `channels_last` is 1 and `(N, C, H, W)` otherwise. | required |
24
+ | `gamma` | `M` | `1` | — | Per-channel affine scale of shape `(C)`. | required |
25
+ | `beta` | `M` | `1` | — | Per-channel affine offset of shape `(C)`. | required |
26
+
27
+ ## Outputs
28
+
29
+ | Name | Logical dtype | Rank | Shape | Description | Presence |
30
+ | --- | --- | --- | --- | --- | --- |
31
+ | `Y` | `T` | `4` | same as `X` | Normalized tensor with the same shape and element type as `X`. | required |
32
+
33
+ ## Attributes
34
+
35
+ Attributes and default values (overridable per request):
36
+
37
+ | Attribute | Default | Description |
38
+ | --- | --- | --- |
39
+ | `activation` | — | Activation applied after the affine transform: `0` for none, `1` for SiLU. The request must supply it; the operator declares no default. |
40
+ | `channels_last` | `1` | `1` when `X` and `Y` are `(N, H, W, C)`, `0` when they are `(N, C, H, W)`. Defaults to `1`. Any non-zero value selects the channels-last layout. |
41
+ | `epsilon` | `0.00001` | Value added to the group variance before the reciprocal square root. Defaults to `1e-5`. |
42
+ | `groups` | — | Number of channel groups the statistics are taken over. It must divide `C`. The request must supply it; the operator declares no default. |
43
+
44
+ ## Type constraints
45
+
46
+ | Variable | Allowed dtypes |
47
+ | --- | --- |
48
+ | `T` | `float32`, `float16` |
49
+ | `M` | `float32`, `float16` |
50
+
51
+ ## Implementation variants
52
+
53
+ One implementation is selected per call from the device capabilities, the request shapes and the dtypes; these notes say what each one covers.
54
+
55
+ - `split` — Three dispatches: partial group statistics, combine mean and reciprocal standard deviation, then normalize and apply affine weights. Channels-last statistics use four, two, or one aligned values per lane; group blocks and spatial rows follow device limits.
56
+ - `fused` — One workgroup per batch item and group, walking the group twice inside a single dispatch: once for the statistics and once to write the affine result. It avoids the split route's two extra launches on small tensors and is the route that needs no scratch buffer.
57
+
58
+ ## Files
59
+
60
+ - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, per-variant templates, provenance)
61
+ - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
62
+ - [`test.json`](build/webgpu/test.json) — correctness cases
63
+ - [`bench.json`](build/webgpu/bench.json) — benchmark cases
64
+ - [`group-norm-combine.wgsl.jinja`](build/webgpu/group-norm-combine.wgsl.jinja)
65
+ - [`group-norm-fused.wgsl.jinja`](build/webgpu/group-norm-fused.wgsl.jinja)
66
+ - [`group-norm-slab.wgsl.jinja`](build/webgpu/group-norm-slab.wgsl.jinja)
67
+
68
+ ## Use with `@huggingface/kernels`
69
+
70
+ ```sh
71
+ npm install --save-exact @huggingface/kernels@0.0.1-preview.3
72
+ ```
73
+
74
+ Required output shapes and logical data types are inferred from the supplied inputs and attributes; result tensors are allocated automatically.
75
+
76
+ The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
77
+ It follows the `v1` branch as fixes land. To pin exact artifact bytes, pass a 40-character commit `revision` instead of `version`.
78
+
79
+ Replace each `*Data` placeholder with a typed array containing the corresponding input data.
80
+
81
+ ```js
82
+ import { getKernel } from "@huggingface/kernels";
83
+
84
+ const kernel = await getKernel("webgpu-kernels/com.microsoft.GroupNorm", { version: 1 });
85
+ const { Y } = await kernel({
86
+ X: { data: XData, shape: [1, 2, 2, 16] },
87
+ gamma: { data: gammaData, shape: [16] },
88
+ beta: { data: betaData, shape: [16] },
89
+ }, {
90
+ attrs: { groups: 4, activation: 0 },
91
+ });
92
+ ```
build/webgpu/bench.json ADDED
@@ -0,0 +1,425 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "tunableSpace": { "WORKGROUP_SIZE": [128, 256] },
3
+ "cases": [
4
+ {
5
+ "name": "flux-vae-mid-1x64x64x512-g32-f16",
6
+ "preset": "smoke",
7
+ "provenance": { "note": "Flux.2 VAE decoder mid/up-block group norm, NHWC, SiLU" },
8
+ "vars": { "dtype": "float16", "weightDtype": "float32", "count": 2097152, "channels": 512 },
9
+ "attrs": { "groups": 32, "activation": 1, "channels_last": 1, "epsilon": 0.000001 },
10
+ "inputs": {
11
+ "X": { "dtype": "float16", "shape": [1, 64, 64, 512], "dist": "normal", "seed": 100, "scale": 0.5 },
12
+ "gamma": { "dtype": "float32", "shape": [512], "dist": "uniform", "seed": 101, "scale": 0.25, "offset": 1 },
13
+ "beta": { "dtype": "float32", "shape": [512], "dist": "normal", "seed": 102, "scale": 0.1 }
14
+ },
15
+ "outputs": { "Y": { "dtype": "float16", "shape": [1, 64, 64, 512], "dist": "empty" } },
16
+ "bench": {
17
+ "metrics": [
18
+ {
19
+ "type": "bandwidth",
20
+ "value": "args.count * 2 * dtypeBytes(args.dtype) + args.channels * 2 * dtypeBytes(args.weightDtype)"
21
+ }
22
+ ],
23
+ "primary": true
24
+ }
25
+ },
26
+ {
27
+ "name": "flux-vae-mid-1x64x64x512-g32-f32",
28
+ "preset": "smoke",
29
+ "provenance": { "note": "Flux.2 VAE decoder mid/up-block group norm at float32" },
30
+ "vars": { "dtype": "float32", "weightDtype": "float32", "count": 2097152, "channels": 512 },
31
+ "attrs": { "groups": 32, "activation": 1, "channels_last": 1, "epsilon": 0.000001 },
32
+ "inputs": {
33
+ "X": { "dtype": "float32", "shape": [1, 64, 64, 512], "dist": "normal", "seed": 110, "scale": 0.5 },
34
+ "gamma": { "dtype": "float32", "shape": [512], "dist": "uniform", "seed": 111, "scale": 0.25, "offset": 1 },
35
+ "beta": { "dtype": "float32", "shape": [512], "dist": "normal", "seed": 112, "scale": 0.1 }
36
+ },
37
+ "outputs": { "Y": { "dtype": "float32", "shape": [1, 64, 64, 512], "dist": "empty" } },
38
+ "bench": {
39
+ "metrics": [
40
+ {
41
+ "type": "bandwidth",
42
+ "value": "args.count * 2 * dtypeBytes(args.dtype) + args.channels * 2 * dtypeBytes(args.weightDtype)"
43
+ }
44
+ ]
45
+ }
46
+ },
47
+ {
48
+ "name": "sd15-unet-2x64x64x320-g32-f16",
49
+ "preset": "smoke",
50
+ "provenance": {
51
+ "note": "Stable-Diffusion-1.5 UNet resnet at the classifier-free-guidance batch; 10 channels per group"
52
+ },
53
+ "vars": { "dtype": "float16", "weightDtype": "float32", "count": 2621440, "channels": 320 },
54
+ "attrs": { "groups": 32, "activation": 1, "channels_last": 1 },
55
+ "inputs": {
56
+ "X": { "dtype": "float16", "shape": [2, 64, 64, 320], "dist": "normal", "seed": 120, "scale": 0.5 },
57
+ "gamma": { "dtype": "float32", "shape": [320], "dist": "uniform", "seed": 121, "scale": 0.25, "offset": 1 },
58
+ "beta": { "dtype": "float32", "shape": [320], "dist": "normal", "seed": 122, "scale": 0.1 }
59
+ },
60
+ "outputs": { "Y": { "dtype": "float16", "shape": [2, 64, 64, 320], "dist": "empty" } },
61
+ "bench": {
62
+ "metrics": [
63
+ {
64
+ "type": "bandwidth",
65
+ "value": "args.count * 2 * dtypeBytes(args.dtype) + args.channels * 2 * dtypeBytes(args.weightDtype)"
66
+ }
67
+ ]
68
+ }
69
+ },
70
+ {
71
+ "name": "sd15-unet-2x32x32x640-g32-f16",
72
+ "preset": "smoke",
73
+ "provenance": { "note": "Stable-Diffusion-1.5 UNet mid-resolution resnet; 20 channels per group" },
74
+ "vars": { "dtype": "float16", "weightDtype": "float32", "count": 1310720, "channels": 640 },
75
+ "attrs": { "groups": 32, "activation": 1, "channels_last": 1 },
76
+ "inputs": {
77
+ "X": { "dtype": "float16", "shape": [2, 32, 32, 640], "dist": "normal", "seed": 130, "scale": 0.5 },
78
+ "gamma": { "dtype": "float32", "shape": [640], "dist": "uniform", "seed": 131, "scale": 0.25, "offset": 1 },
79
+ "beta": { "dtype": "float32", "shape": [640], "dist": "normal", "seed": 132, "scale": 0.1 }
80
+ },
81
+ "outputs": { "Y": { "dtype": "float16", "shape": [2, 32, 32, 640], "dist": "empty" } },
82
+ "bench": {
83
+ "metrics": [
84
+ {
85
+ "type": "bandwidth",
86
+ "value": "args.count * 2 * dtypeBytes(args.dtype) + args.channels * 2 * dtypeBytes(args.weightDtype)"
87
+ }
88
+ ]
89
+ }
90
+ },
91
+ {
92
+ "name": "sd15-unet-2x8x8x1280-g32-f16",
93
+ "preset": "smoke",
94
+ "provenance": {
95
+ "note": "Stable-Diffusion-1.5 UNet lowest-resolution resnet; the few-element, low-parallelism corner"
96
+ },
97
+ "vars": { "dtype": "float16", "weightDtype": "float32", "count": 163840, "channels": 1280 },
98
+ "attrs": { "groups": 32, "activation": 1, "channels_last": 1 },
99
+ "inputs": {
100
+ "X": { "dtype": "float16", "shape": [2, 8, 8, 1280], "dist": "normal", "seed": 140, "scale": 0.5 },
101
+ "gamma": { "dtype": "float32", "shape": [1280], "dist": "uniform", "seed": 141, "scale": 0.25, "offset": 1 },
102
+ "beta": { "dtype": "float32", "shape": [1280], "dist": "normal", "seed": 142, "scale": 0.1 }
103
+ },
104
+ "outputs": { "Y": { "dtype": "float16", "shape": [2, 8, 8, 1280], "dist": "empty" } },
105
+ "bench": {
106
+ "metrics": [
107
+ {
108
+ "type": "bandwidth",
109
+ "value": "args.count * 2 * dtypeBytes(args.dtype) + args.channels * 2 * dtypeBytes(args.weightDtype)"
110
+ }
111
+ ]
112
+ }
113
+ },
114
+ {
115
+ "name": "odd-cpg-1x64x64x96-g32-f32",
116
+ "preset": "smoke",
117
+ "provenance": {
118
+ "note": "Three channels per group: the odd channels-per-group case the CUDA kernel refuses outright"
119
+ },
120
+ "vars": { "dtype": "float32", "weightDtype": "float32", "count": 393216, "channels": 96 },
121
+ "attrs": { "groups": 32, "activation": 1, "channels_last": 1 },
122
+ "inputs": {
123
+ "X": { "dtype": "float32", "shape": [1, 64, 64, 96], "dist": "normal", "seed": 150, "scale": 0.5 },
124
+ "gamma": { "dtype": "float32", "shape": [96], "dist": "uniform", "seed": 151, "scale": 0.25, "offset": 1 },
125
+ "beta": { "dtype": "float32", "shape": [96], "dist": "normal", "seed": 152, "scale": 0.1 }
126
+ },
127
+ "outputs": { "Y": { "dtype": "float32", "shape": [1, 64, 64, 96], "dist": "empty" } },
128
+ "bench": {
129
+ "metrics": [
130
+ {
131
+ "type": "bandwidth",
132
+ "value": "args.count * 2 * dtypeBytes(args.dtype) + args.channels * 2 * dtypeBytes(args.weightDtype)"
133
+ }
134
+ ]
135
+ }
136
+ },
137
+ {
138
+ "name": "nchw-4x64x128x128-g32-f32",
139
+ "preset": "smoke",
140
+ "provenance": {
141
+ "note": "Channels-first at the shape ai.onnx.GroupNormalization benches, for a direct comparison"
142
+ },
143
+ "vars": { "dtype": "float32", "weightDtype": "float32", "count": 4194304, "channels": 64 },
144
+ "attrs": { "groups": 32, "activation": 0, "channels_last": 0 },
145
+ "inputs": {
146
+ "X": { "dtype": "float32", "shape": [4, 64, 128, 128], "dist": "normal", "seed": 160, "scale": 0.5 },
147
+ "gamma": { "dtype": "float32", "shape": [64], "dist": "uniform", "seed": 161, "scale": 0.25, "offset": 1 },
148
+ "beta": { "dtype": "float32", "shape": [64], "dist": "normal", "seed": 162, "scale": 0.1 }
149
+ },
150
+ "outputs": { "Y": { "dtype": "float32", "shape": [4, 64, 128, 128], "dist": "empty" } },
151
+ "bench": {
152
+ "metrics": [
153
+ {
154
+ "type": "bandwidth",
155
+ "value": "args.count * 2 * dtypeBytes(args.dtype) + args.channels * 2 * dtypeBytes(args.weightDtype)"
156
+ }
157
+ ]
158
+ }
159
+ },
160
+ {
161
+ "name": "nchw-2x320x64x64-g32-f16",
162
+ "preset": "smoke",
163
+ "provenance": { "note": "Channels-first Stable-Diffusion-1.5 resnet width with the fused SiLU" },
164
+ "vars": { "dtype": "float16", "weightDtype": "float32", "count": 2621440, "channels": 320 },
165
+ "attrs": { "groups": 32, "activation": 1, "channels_last": 0 },
166
+ "inputs": {
167
+ "X": { "dtype": "float16", "shape": [2, 320, 64, 64], "dist": "normal", "seed": 170, "scale": 0.5 },
168
+ "gamma": { "dtype": "float32", "shape": [320], "dist": "uniform", "seed": 171, "scale": 0.25, "offset": 1 },
169
+ "beta": { "dtype": "float32", "shape": [320], "dist": "normal", "seed": 172, "scale": 0.1 }
170
+ },
171
+ "outputs": { "Y": { "dtype": "float16", "shape": [2, 320, 64, 64], "dist": "empty" } },
172
+ "bench": {
173
+ "metrics": [
174
+ {
175
+ "type": "bandwidth",
176
+ "value": "args.count * 2 * dtypeBytes(args.dtype) + args.channels * 2 * dtypeBytes(args.weightDtype)"
177
+ }
178
+ ]
179
+ }
180
+ },
181
+ {
182
+ "name": "micro-2x2x2x128-g32-f16",
183
+ "preset": "smoke",
184
+ "provenance": {
185
+ "note": "ORT: onnxruntime/test/contrib_ops/group_norm_op_test.cc GroupNormTest.GroupNorm_128 geometry"
186
+ },
187
+ "vars": { "dtype": "float16", "weightDtype": "float32", "count": 1024, "channels": 128 },
188
+ "attrs": { "groups": 32, "activation": 0, "channels_last": 1 },
189
+ "inputs": {
190
+ "X": { "dtype": "float16", "shape": [2, 2, 2, 128], "dist": "normal", "seed": 180, "scale": 0.5 },
191
+ "gamma": { "dtype": "float32", "shape": [128], "dist": "uniform", "seed": 181, "scale": 0.25, "offset": 1 },
192
+ "beta": { "dtype": "float32", "shape": [128], "dist": "normal", "seed": 182, "scale": 0.1 }
193
+ },
194
+ "outputs": { "Y": { "dtype": "float16", "shape": [2, 2, 2, 128], "dist": "empty" } },
195
+ "bench": {
196
+ "metrics": [
197
+ {
198
+ "type": "bandwidth",
199
+ "value": "args.count * 2 * dtypeBytes(args.dtype) + args.channels * 2 * dtypeBytes(args.weightDtype)"
200
+ }
201
+ ]
202
+ }
203
+ },
204
+ {
205
+ "name": "flux-vae-up-1x128x128x512-g32-f16",
206
+ "preset": "scale",
207
+ "provenance": { "note": "Flux.2 VAE decoder after the first 2x upsample" },
208
+ "vars": { "dtype": "float16", "weightDtype": "float32", "count": 8388608, "channels": 512 },
209
+ "attrs": { "groups": 32, "activation": 1, "channels_last": 1, "epsilon": 0.000001 },
210
+ "inputs": {
211
+ "X": { "dtype": "float16", "shape": [1, 128, 128, 512], "dist": "normal", "seed": 200, "scale": 0.5 },
212
+ "gamma": { "dtype": "float32", "shape": [512], "dist": "uniform", "seed": 201, "scale": 0.25, "offset": 1 },
213
+ "beta": { "dtype": "float32", "shape": [512], "dist": "normal", "seed": 202, "scale": 0.1 }
214
+ },
215
+ "outputs": { "Y": { "dtype": "float16", "shape": [1, 128, 128, 512], "dist": "empty" } },
216
+ "bench": {
217
+ "metrics": [
218
+ {
219
+ "type": "bandwidth",
220
+ "value": "args.count * 2 * dtypeBytes(args.dtype) + args.channels * 2 * dtypeBytes(args.weightDtype)"
221
+ }
222
+ ]
223
+ }
224
+ },
225
+ {
226
+ "name": "flux-vae-up-1x256x256x256-g32-f16",
227
+ "preset": "scale",
228
+ "provenance": { "note": "Flux.2 VAE decoder after the second 2x upsample" },
229
+ "vars": { "dtype": "float16", "weightDtype": "float32", "count": 16777216, "channels": 256 },
230
+ "attrs": { "groups": 32, "activation": 1, "channels_last": 1, "epsilon": 0.000001 },
231
+ "inputs": {
232
+ "X": { "dtype": "float16", "shape": [1, 256, 256, 256], "dist": "normal", "seed": 210, "scale": 0.5 },
233
+ "gamma": { "dtype": "float32", "shape": [256], "dist": "uniform", "seed": 211, "scale": 0.25, "offset": 1 },
234
+ "beta": { "dtype": "float32", "shape": [256], "dist": "normal", "seed": 212, "scale": 0.1 }
235
+ },
236
+ "outputs": { "Y": { "dtype": "float16", "shape": [1, 256, 256, 256], "dist": "empty" } },
237
+ "bench": {
238
+ "metrics": [
239
+ {
240
+ "type": "bandwidth",
241
+ "value": "args.count * 2 * dtypeBytes(args.dtype) + args.channels * 2 * dtypeBytes(args.weightDtype)"
242
+ }
243
+ ]
244
+ }
245
+ },
246
+ {
247
+ "name": "flux-vae-convout-1x512x512x128-g32-f16",
248
+ "preset": "model",
249
+ "provenance": { "note": "Flux.2 VAE conv_norm_out: the largest single group-norm dispatch in the decoder" },
250
+ "vars": { "dtype": "float16", "weightDtype": "float32", "count": 33554432, "channels": 128 },
251
+ "attrs": { "groups": 32, "activation": 1, "channels_last": 1, "epsilon": 0.000001 },
252
+ "inputs": {
253
+ "X": { "dtype": "float16", "shape": [1, 512, 512, 128], "dist": "normal", "seed": 220, "scale": 0.5 },
254
+ "gamma": { "dtype": "float32", "shape": [128], "dist": "uniform", "seed": 221, "scale": 0.25, "offset": 1 },
255
+ "beta": { "dtype": "float32", "shape": [128], "dist": "normal", "seed": 222, "scale": 0.1 }
256
+ },
257
+ "outputs": { "Y": { "dtype": "float16", "shape": [1, 512, 512, 128], "dist": "empty" } },
258
+ "bench": {
259
+ "metrics": [
260
+ {
261
+ "type": "bandwidth",
262
+ "value": "args.count * 2 * dtypeBytes(args.dtype) + args.channels * 2 * dtypeBytes(args.weightDtype)"
263
+ }
264
+ ]
265
+ }
266
+ },
267
+ {
268
+ "name": "nchw-1x512x64x64-g32-f16",
269
+ "preset": "smoke",
270
+ "provenance": {
271
+ "note": "Channels-first at the Flux.2 VAE mid-block width: 32 groups, the fewest reduction outputs this op sees"
272
+ },
273
+ "vars": { "dtype": "float16", "weightDtype": "float32", "count": 2097152, "channels": 512 },
274
+ "attrs": { "groups": 32, "activation": 1, "channels_last": 0 },
275
+ "inputs": {
276
+ "X": { "dtype": "float16", "shape": [1, 512, 64, 64], "dist": "normal", "seed": 300, "scale": 0.5 },
277
+ "gamma": { "dtype": "float32", "shape": [512], "dist": "uniform", "seed": 301, "scale": 0.25, "offset": 1 },
278
+ "beta": { "dtype": "float32", "shape": [512], "dist": "normal", "seed": 302, "scale": 0.1 }
279
+ },
280
+ "outputs": { "Y": { "dtype": "float16", "shape": [1, 512, 64, 64], "dist": "empty" } },
281
+ "bench": {
282
+ "metrics": [
283
+ {
284
+ "type": "bandwidth",
285
+ "value": "args.count * 2 * dtypeBytes(args.dtype) + args.channels * 2 * dtypeBytes(args.weightDtype)"
286
+ }
287
+ ]
288
+ }
289
+ },
290
+ {
291
+ "name": "nchw-8x64x64x64-g32-f32",
292
+ "preset": "smoke",
293
+ "provenance": { "note": "Channels-first with the batch axis carrying the parallelism: 256 reduction outputs" },
294
+ "vars": { "dtype": "float32", "weightDtype": "float32", "count": 2097152, "channels": 64 },
295
+ "attrs": { "groups": 32, "activation": 0, "channels_last": 0 },
296
+ "inputs": {
297
+ "X": { "dtype": "float32", "shape": [8, 64, 64, 64], "dist": "normal", "seed": 310, "scale": 0.5 },
298
+ "gamma": { "dtype": "float32", "shape": [64], "dist": "uniform", "seed": 311, "scale": 0.25, "offset": 1 },
299
+ "beta": { "dtype": "float32", "shape": [64], "dist": "normal", "seed": 312, "scale": 0.1 }
300
+ },
301
+ "outputs": { "Y": { "dtype": "float32", "shape": [8, 64, 64, 64], "dist": "empty" } },
302
+ "bench": {
303
+ "metrics": [
304
+ {
305
+ "type": "bandwidth",
306
+ "value": "args.count * 2 * dtypeBytes(args.dtype) + args.channels * 2 * dtypeBytes(args.weightDtype)"
307
+ }
308
+ ]
309
+ }
310
+ },
311
+ {
312
+ "name": "nchw-2x320x128x128-g32-f16",
313
+ "preset": "scale",
314
+ "provenance": { "note": "Channels-first Stable-Diffusion resnet at a working set past cache" },
315
+ "vars": { "dtype": "float16", "weightDtype": "float32", "count": 10485760, "channels": 320 },
316
+ "attrs": { "groups": 32, "activation": 1, "channels_last": 0 },
317
+ "inputs": {
318
+ "X": { "dtype": "float16", "shape": [2, 320, 128, 128], "dist": "normal", "seed": 320, "scale": 0.5 },
319
+ "gamma": { "dtype": "float32", "shape": [320], "dist": "uniform", "seed": 321, "scale": 0.25, "offset": 1 },
320
+ "beta": { "dtype": "float32", "shape": [320], "dist": "normal", "seed": 322, "scale": 0.1 }
321
+ },
322
+ "outputs": { "Y": { "dtype": "float16", "shape": [2, 320, 128, 128], "dist": "empty" } },
323
+ "bench": {
324
+ "metrics": [
325
+ {
326
+ "type": "bandwidth",
327
+ "value": "args.count * 2 * dtypeBytes(args.dtype) + args.channels * 2 * dtypeBytes(args.weightDtype)"
328
+ }
329
+ ]
330
+ }
331
+ },
332
+ {
333
+ "name": "nhwc-even-groups-2x64x64x320-g32-f32",
334
+ "preset": "smoke",
335
+ "provenance": {
336
+ "note": "Even channel-group widths spanning one, multiple, and capacity-limited channel rows per workgroup, including float32 activations. Same distributions as the authored large NHWC case."
337
+ },
338
+ "vars": { "dtype": "float32", "weightDtype": "float32", "count": 2621440, "channels": 320 },
339
+ "attrs": { "groups": 32, "activation": 1, "channels_last": 1 },
340
+ "inputs": {
341
+ "X": { "dtype": "float32", "shape": [2, 64, 64, 320], "dist": "normal", "seed": 120, "scale": 0.5 },
342
+ "gamma": { "dtype": "float32", "shape": [320], "dist": "uniform", "seed": 121, "scale": 0.25, "offset": 1 },
343
+ "beta": { "dtype": "float32", "shape": [320], "dist": "normal", "seed": 122, "scale": 0.1 }
344
+ },
345
+ "outputs": { "Y": { "dtype": "float32", "shape": [2, 64, 64, 320], "dist": "empty" } },
346
+ "bench": {
347
+ "metrics": [
348
+ {
349
+ "type": "bandwidth",
350
+ "value": "args.count * 2 * dtypeBytes(args.dtype) + args.channels * 2 * dtypeBytes(args.weightDtype)"
351
+ }
352
+ ]
353
+ }
354
+ },
355
+ {
356
+ "name": "nhwc-even-groups-2x64x64x64-g32-f16",
357
+ "preset": "smoke",
358
+ "provenance": {
359
+ "note": "Even channel-group widths spanning one, multiple, and capacity-limited channel rows per workgroup, including float32 activations. Same distributions as the authored large NHWC case."
360
+ },
361
+ "vars": { "dtype": "float16", "weightDtype": "float32", "count": 524288, "channels": 64 },
362
+ "attrs": { "groups": 32, "activation": 1, "channels_last": 1 },
363
+ "inputs": {
364
+ "X": { "dtype": "float16", "shape": [2, 64, 64, 64], "dist": "normal", "seed": 120, "scale": 0.5 },
365
+ "gamma": { "dtype": "float32", "shape": [64], "dist": "uniform", "seed": 121, "scale": 0.25, "offset": 1 },
366
+ "beta": { "dtype": "float32", "shape": [64], "dist": "normal", "seed": 122, "scale": 0.1 }
367
+ },
368
+ "outputs": { "Y": { "dtype": "float16", "shape": [2, 64, 64, 64], "dist": "empty" } },
369
+ "bench": {
370
+ "metrics": [
371
+ {
372
+ "type": "bandwidth",
373
+ "value": "args.count * 2 * dtypeBytes(args.dtype) + args.channels * 2 * dtypeBytes(args.weightDtype)"
374
+ }
375
+ ]
376
+ }
377
+ },
378
+ {
379
+ "name": "nhwc-even-groups-2x64x64x192-g32-f16",
380
+ "preset": "smoke",
381
+ "provenance": {
382
+ "note": "Even channel-group widths spanning one, multiple, and capacity-limited channel rows per workgroup, including float32 activations. Same distributions as the authored large NHWC case."
383
+ },
384
+ "vars": { "dtype": "float16", "weightDtype": "float32", "count": 1572864, "channels": 192 },
385
+ "attrs": { "groups": 32, "activation": 1, "channels_last": 1 },
386
+ "inputs": {
387
+ "X": { "dtype": "float16", "shape": [2, 64, 64, 192], "dist": "normal", "seed": 120, "scale": 0.5 },
388
+ "gamma": { "dtype": "float32", "shape": [192], "dist": "uniform", "seed": 121, "scale": 0.25, "offset": 1 },
389
+ "beta": { "dtype": "float32", "shape": [192], "dist": "normal", "seed": 122, "scale": 0.1 }
390
+ },
391
+ "outputs": { "Y": { "dtype": "float16", "shape": [2, 64, 64, 192], "dist": "empty" } },
392
+ "bench": {
393
+ "metrics": [
394
+ {
395
+ "type": "bandwidth",
396
+ "value": "args.count * 2 * dtypeBytes(args.dtype) + args.channels * 2 * dtypeBytes(args.weightDtype)"
397
+ }
398
+ ]
399
+ }
400
+ },
401
+ {
402
+ "name": "nhwc-even-groups-2x64x64x448-g32-f16",
403
+ "preset": "smoke",
404
+ "provenance": {
405
+ "note": "Even channel-group widths spanning one, multiple, and capacity-limited channel rows per workgroup, including float32 activations. Same distributions as the authored large NHWC case."
406
+ },
407
+ "vars": { "dtype": "float16", "weightDtype": "float32", "count": 3670016, "channels": 448 },
408
+ "attrs": { "groups": 32, "activation": 1, "channels_last": 1 },
409
+ "inputs": {
410
+ "X": { "dtype": "float16", "shape": [2, 64, 64, 448], "dist": "normal", "seed": 120, "scale": 0.5 },
411
+ "gamma": { "dtype": "float32", "shape": [448], "dist": "uniform", "seed": 121, "scale": 0.25, "offset": 1 },
412
+ "beta": { "dtype": "float32", "shape": [448], "dist": "normal", "seed": 122, "scale": 0.1 }
413
+ },
414
+ "outputs": { "Y": { "dtype": "float16", "shape": [2, 64, 64, 448], "dist": "empty" } },
415
+ "bench": {
416
+ "metrics": [
417
+ {
418
+ "type": "bandwidth",
419
+ "value": "args.count * 2 * dtypeBytes(args.dtype) + args.channels * 2 * dtypeBytes(args.weightDtype)"
420
+ }
421
+ ]
422
+ }
423
+ }
424
+ ]
425
+ }
build/webgpu/group-norm-combine.wgsl.jinja ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% set comp0 = ".x" if vecSpec > 1 else "" %}
2
+ {% macro flat_index_2d(workgroupSize, name="i", bound="params.count", guardInline=false) %}
3
+ {% set wgTerm = workgroupSize ~ "u" if workgroupSize is number else workgroupSize %}
4
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
5
+ // per-axis workgroup fold width.
6
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ wgTerm }};
7
+ if ({{ name }} >= {{ bound }}) {
8
+ return;
9
+ }{% endmacro %}
10
+ {{ env.wgsl.resourceDeclarations }}
11
+
12
+ // The partials hold sum(value - shift) and sum((value - shift)^2) over disjoint blocks
13
+ // of the group, all taken against the same `shift` — the group's first element — so
14
+ // they add directly. Recovering the mean adds the shift back; the variance is formed
15
+ // from the shifted second moment and clamped at zero, which is where a group whose
16
+ // values are all equal would otherwise land a few ulp below it.
17
+ const GROUPS: u32 = {{ groupsSpec }}u;
18
+ const CPG: u32 = {{ cpgSpec }}u;
19
+ const ROWS: u32 = {{ normRowsSpec }}u;
20
+ const PARTIALS_PER_ROW: u32 = {{ partialsPerRowSpec }}u;
21
+ const GROUP_SIZE: f32 = {{ groupSizeSpec }}.0;
22
+ const EPSILON: f32 = {{ epsilonSpec }};
23
+ const WG: u32 = {{ combineWgSpec }}u;
24
+ {% if phaseLayout %}
25
+ const HW: u32 = {{ dimHWSpec }}u;
26
+ const CHUNKS: u32 = {{ chunksSpec }}u;
27
+ const VEC: u32 = {{ vecSpec }}u;
28
+ {% else %}
29
+ const CHANNELS: u32 = {{ dimCSpec }}u;
30
+ const PLANE_V: u32 = {{ planeVecsSpec }}u;
31
+ {% endif %}
32
+
33
+ @compute @workgroup_size(WG)
34
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
35
+ {{ flat_index_2d("WG", "row", "ROWS") }}
36
+ let n = row / GROUPS;
37
+ let g = row % GROUPS;
38
+ {% if phaseLayout %}
39
+ let shift = f32(src[n * HW * CHUNKS + g * (CPG / VEC)]{{ comp0 }});
40
+ {% else %}
41
+ let shift = f32(src[(n * CHANNELS + g * CPG) * PLANE_V]{{ comp0 }});
42
+ {% endif %}
43
+ var total = vec2<f32>(0.0);
44
+ for (var k = 0u; k < PARTIALS_PER_ROW; k = k + 1u) {
45
+ total = total + partials[row * PARTIALS_PER_ROW + k];
46
+ }
47
+ let mean_d = total.x / GROUP_SIZE;
48
+ let variance = max(total.y / GROUP_SIZE - mean_d * mean_d, 0.0);
49
+ stats[row] = vec2<f32>(shift + mean_d, inverseSqrt(variance + EPSILON));
50
+ }
build/webgpu/group-norm-fused.wgsl.jinja ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% set comp0 = ".x" if useVec4 else "" %}
2
+ {% macro pre_norm(idx, sidx) %}
3
+ {% set x = vecF32 ~ "(X[" ~ idx ~ "])" %}
4
+ {% set skipTerm = "skip_v" if broadcastSkip else vecF32 ~ "(skip[" ~ sidx ~ "])" %}
5
+ {{ vecF32 ~ "(" ~ storeElement ~ "(" ~ x ~ " + " ~ skipTerm ~ " + bias_v))" if hasSkip else x }}{% endmacro %}
6
+ {% macro shift_at(idx, sidx, ch) %}
7
+ {% set x = "f32(X[" ~ idx ~ "]" ~ comp0 ~ ")" %}
8
+ {% set skipTerm = "f32(skip[" ~ sidx ~ "]" ~ ("" if broadcastSkip else comp0) ~ ")" %}
9
+ {% set biasTerm = " + f32(bias[" ~ ch ~ "])" if hasBias else "" %}
10
+ {{ "f32(" ~ tScalar ~ "(" ~ x ~ " + " ~ skipTerm ~ biasTerm ~ "))" if hasSkip else x }}{% endmacro %}
11
+ {{ env.wgsl.resourceDeclarations }}
12
+
13
+ // Group normalization in one dispatch. The workgroup accumulates the group's shifted
14
+ // moments, folds them through workgroup memory, then walks the same elements again to
15
+ // write `gamma * (value - mean) * inv_std + beta`. Every intermediate is f32 whatever
16
+ // the tensor type; only the store narrows. The reduction is a plain halving tree over
17
+ // a power-of-two-padded scratch array, so it is deterministic and needs no subgroup
18
+ // support.
19
+ const CPG: u32 = {{ cpgSpec }}u;
20
+ const GROUPS: u32 = {{ groupsSpec }}u;
21
+ const ROWS: u32 = {{ normRowsSpec }}u;
22
+ const GROUP_SIZE: f32 = {{ groupSizeSpec }}.0;
23
+ const EPSILON: f32 = {{ epsilonSpec }};
24
+ const WG: u32 = {{ wgSpec }}u;
25
+ const LANES: u32 = {{ ldsSpec }}u;
26
+ {% if phaseLayout %}
27
+ const HW: u32 = {{ dimHWSpec }}u;
28
+ const VEC: u32 = {{ vecSpec }}u;
29
+ const CHUNKS: u32 = {{ chunksSpec }}u;
30
+ const SEGMENT: u32 = {{ fusedChunksSpec }}u;
31
+ const SUB_ROWS: u32 = {{ fusedRowsSpec }}u;
32
+ {% else %}
33
+ const CHANNELS: u32 = {{ dimCSpec }}u;
34
+ const PLANE_V: u32 = {{ planeVecsSpec }}u;
35
+ const SPAN_V: u32 = {{ spanVecsSpec }}u;
36
+ {% endif %}
37
+
38
+ // LANES is WG rounded up to a power of two; the lanes past WG are zeroed so the
39
+ // halving tree below stays uniform.
40
+ var<workgroup> lane_sums: array<vec2<f32>, LANES>;
41
+ {% if activationSpec == 1 %}
42
+
43
+ fn silu_e(v: {{ vecF32 }}) -> {{ vecF32 }} {
44
+ return v / ({{ vecF32 }}(1.0) + exp(-v));
45
+ }
46
+ {% endif %}
47
+
48
+ @compute @workgroup_size(WG)
49
+ fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
50
+ // 2D-folded row index: wg.y carries the high bits past the dispatch's per-axis
51
+ // workgroup fold width.
52
+ let row = wg.x + wg.y * {{ DISPATCH_FOLD_WIDTH }}u;
53
+ if (row >= ROWS) {
54
+ return;
55
+ }
56
+ let n = row / GROUPS;
57
+ let g = row - n * GROUPS;
58
+ {% if phaseLayout %}
59
+ let chunk = lid.x % SEGMENT;
60
+ let sub_row = lid.x / SEGMENT;
61
+ let c0 = g * CPG + chunk * VEC;
62
+ let base_v = n * HW * CHUNKS + g * SEGMENT;
63
+ let shift_index = base_v;
64
+ {% else %}
65
+ let base_v = (n * CHANNELS + g * CPG) * PLANE_V;
66
+ let shift_index = base_v;
67
+ {% endif %}
68
+ let shift = {{ shift_at("shift_index", "shift_skip", "g * CPG") }};
69
+ var acc = vec2<f32>(0.0);
70
+ {% if phaseLayout %}
71
+ for (var hw = sub_row; hw < HW; hw = hw + SUB_ROWS) {
72
+ let idx = base_v + hw * CHUNKS + chunk;
73
+ let v = {{ pre_norm("idx", "skip_base + hw * CHUNKS + chunk") }};
74
+ {% else %}
75
+ for (var i = lid.x; i < SPAN_V; i = i + WG) {
76
+ let idx = base_v + i;
77
+ let v = {{ pre_norm("idx", "skip_base + i") }};
78
+ {% endif %}
79
+ let d = v - {{ vecF32 }}(shift);
80
+ {% if useVec4 %}
81
+ acc = acc + vec2<f32>(d.x + d.y + d.z + d.w, dot(d, d));
82
+ {% else %}
83
+ acc = acc + vec2<f32>(d, d * d);
84
+ {% endif %}
85
+ }
86
+ lane_sums[lid.x] = acc;
87
+ if (lid.x + WG < LANES) {
88
+ lane_sums[lid.x + WG] = vec2<f32>(0.0);
89
+ }
90
+ workgroupBarrier();
91
+ var stride = LANES / 2u;
92
+ loop {
93
+ if (stride == 0u) {
94
+ break;
95
+ }
96
+ if (lid.x < stride) {
97
+ lane_sums[lid.x] = lane_sums[lid.x] + lane_sums[lid.x + stride];
98
+ }
99
+ workgroupBarrier();
100
+ stride = stride / 2u;
101
+ }
102
+ let total = lane_sums[0];
103
+ let mean_d = total.x / GROUP_SIZE;
104
+ let variance = max(total.y / GROUP_SIZE - mean_d * mean_d, 0.0);
105
+ let mean = {{ vecF32 }}(shift + mean_d);
106
+ let inv_std = {{ vecF32 }}(inverseSqrt(variance + EPSILON));
107
+ {% if phaseLayout %}
108
+ {% if useVec4 %}
109
+ let gamma_v = vec4<f32>(f32(gamma[c0]), f32(gamma[c0 + 1u]), f32(gamma[c0 + 2u]), f32(gamma[c0 + 3u]));
110
+ let beta_v = vec4<f32>(f32(beta[c0]), f32(beta[c0 + 1u]), f32(beta[c0 + 2u]), f32(beta[c0 + 3u]));
111
+ {% else %}
112
+ let gamma_v = f32(gamma[c0]);
113
+ let beta_v = f32(beta[c0]);
114
+ {% endif %}
115
+ for (var hw = sub_row; hw < HW; hw = hw + SUB_ROWS) {
116
+ let idx = base_v + hw * CHUNKS + chunk;
117
+ {% else %}
118
+ for (var i = lid.x; i < SPAN_V; i = i + WG) {
119
+ let idx = base_v + i;
120
+ let channel = g * CPG + i / PLANE_V;
121
+ {% if useVec4 %}
122
+ let gamma_v = vec4<f32>(f32(gamma[channel]));
123
+ let beta_v = vec4<f32>(f32(beta[channel]));
124
+ {% else %}
125
+ let gamma_v = f32(gamma[channel]);
126
+ let beta_v = f32(beta[channel]);
127
+ {% endif %}
128
+ {% endif %}
129
+ let normalized = ({{ vecF32 }}({{ fusedSrc }}[idx]) - mean) * inv_std;
130
+ {% if activationSpec == 1 %}
131
+ Y[idx] = {{ storeElement }}(silu_e(gamma_v * normalized + beta_v));
132
+ {% else %}
133
+ Y[idx] = {{ storeElement }}(gamma_v * normalized + beta_v);
134
+ {% endif %}
135
+ }
136
+ }
build/webgpu/group-norm-slab.wgsl.jinja ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% set blockedStats = phase == "stats" and phaseLayout and (statsBlockedSpec | default(false)) %}
2
+ {% set useVector = vecSpec > 1 %}
3
+ {% set comp0 = ".x" if useVector else "" %}
4
+ {% macro channel_values(buffer, channel) %}
5
+ {{ vecF32 }}({% for i in range(vecSpec) %}f32({{ buffer }}[{{ channel }}{% if i > 0 %} + {{ i }}u{% endif %}]){% if not loop.last %}, {% endif %}{% endfor %}){% endmacro %}
6
+ {% macro component_sum(value) %}
7
+ {% for i in range(vecSpec) %}{{ value }}.{{ "xyzw"[i] }}{% if not loop.last %} + {% endif %}{% endfor %}{% endmacro %}
8
+ {% macro pre_norm(idx, sidx) %}
9
+ {% set x = vecF32 ~ "(X[" ~ idx ~ "])" %}
10
+ {% set skipTerm = "skip_v" if broadcastSkip else vecF32 ~ "(skip[" ~ sidx ~ "])" %}
11
+ {{ vecF32 ~ "(" ~ storeElement ~ "(" ~ x ~ " + " ~ skipTerm ~ " + bias_v))" if hasSkip else x }}{% endmacro %}
12
+ {% macro shift_at(idx, sidx, ch) %}
13
+ {% set x = "f32(X[" ~ idx ~ "]" ~ comp0 ~ ")" %}
14
+ {% set skipTerm = "f32(skip[" ~ sidx ~ "]" ~ ("" if broadcastSkip else comp0) ~ ")" %}
15
+ {% set biasTerm = " + f32(bias[" ~ ch ~ "])" if hasBias else "" %}
16
+ {{ "f32(" ~ tScalar ~ "(" ~ x ~ " + " ~ skipTerm ~ biasTerm ~ "))" if hasSkip else x }}{% endmacro %}
17
+ {{ env.wgsl.resourceDeclarations }}
18
+
19
+ // One phase of group normalization over a 4-D image tensor.
20
+ //
21
+ // A group is every spatial position of `CPG` adjacent channels of one batch item. The
22
+ // statistics phase accumulates the sum and the sum of squares of `value - shift`,
23
+ // where `shift` is the group's first element: subtracting one member of the group
24
+ // removes the common offset before the second moment is formed, so the variance the
25
+ // combine pass derives does not cancel against a large mean. The apply phase reads the
26
+ // combined mean and reciprocal standard deviation back and writes
27
+ // `gamma * (value - mean) * inv_std + beta`, optionally through SiLU.
28
+ //
29
+ // Every accumulation and the whole affine are f32 whatever the tensor type; only the
30
+ // store narrows.
31
+ const CPG: u32 = {{ cpgSpec }}u;
32
+ const GROUPS: u32 = {{ groupsSpec }}u;
33
+ const WG: u32 = {{ wgSpec }}u;
34
+ const UNITS: u32 = {{ unitsSpec }}u;
35
+ {% if phaseLayout %}
36
+ const HW: u32 = {{ dimHWSpec }}u;
37
+ const VEC: u32 = {{ vecSpec }}u;
38
+ const CHUNKS: u32 = {{ chunksSpec }}u;
39
+ const ROWS_PER_STEP: u32 = {{ rowsPerStepSpec }}u;
40
+ const ROWS_PER_SLAB: u32 = {{ rowsPerSlabSpec }}u;
41
+ const SLABS: u32 = {{ splitSpec }}u;
42
+ {% if blockedStats %}
43
+ const GROUP_BLOCKS: u32 = {{ statsGroupBlocksSpec }}u;
44
+ const GROUPS_PER_BLOCK: u32 = {{ statsGroupsPerBlockSpec }}u;
45
+ const TILE_CHUNKS: u32 = {{ statsTileChunksSpec }}u;
46
+ {% endif %}
47
+ {% else %}
48
+ const CHANNELS: u32 = {{ dimCSpec }}u;
49
+ const PLANE_V: u32 = {{ planeVecsSpec }}u;
50
+ const CHUNK_V: u32 = {{ chunkVecsSpec }}u;
51
+ const SPLITS: u32 = {{ splitSpec }}u;
52
+ {% endif %}
53
+ {% if phase == "stats" %}
54
+ {% if not phaseLayout %}
55
+ const PARTIALS_PER_ROW: u32 = {{ partialsPerRowSpec }}u;
56
+ {% endif %}
57
+
58
+ // Per-lane moments, combined below: the channels-last arm folds one contiguous
59
+ // segment per group out of this array, the channels-first arm folds all of it.
60
+ var<workgroup> lane_sums: array<vec2<f32>, WG>;
61
+ {% endif %}
62
+ {% if phase == "apply" and activationSpec == 1 %}
63
+
64
+ fn silu_e(v: {{ vecF32 }}) -> {{ vecF32 }} {
65
+ return v / ({{ vecF32 }}(1.0) + exp(-v));
66
+ }
67
+ {% endif %}
68
+
69
+ @compute @workgroup_size(WG)
70
+ fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
71
+ // 2D-folded work-unit index: wg.y carries the high bits past the dispatch's
72
+ // per-axis workgroup fold width.
73
+ let unit = wg.x + wg.y * {{ DISPATCH_FOLD_WIDTH }}u;
74
+ if (unit >= UNITS) {
75
+ return;
76
+ }
77
+ {% if phaseLayout %}
78
+ // Channels last. The workgroup owns one batch item and one slab of spatial rows and
79
+ // covers every channel of those rows: lane `lid.x` keeps channel chunk
80
+ // `lid.x % CHUNKS` and steps through the slab `ROWS_PER_STEP` rows at a time, so the
81
+ // workgroup reads whole contiguous channel rows and each lane's group is fixed.
82
+ {% if blockedStats %}
83
+ let spatial_unit = unit / GROUP_BLOCKS;
84
+ let group_begin = (unit % GROUP_BLOCKS) * GROUPS_PER_BLOCK;
85
+ let n = spatial_unit / SLABS;
86
+ let slab = spatial_unit - n * SLABS;
87
+ let chunk = group_begin * (CPG / VEC) + lid.x % TILE_CHUNKS;
88
+ let sub_row = lid.x / TILE_CHUNKS;
89
+ {% else %}
90
+ let n = unit / SLABS;
91
+ let slab = unit - n * SLABS;
92
+ let chunk = lid.x % CHUNKS;
93
+ let sub_row = lid.x / CHUNKS;
94
+ {% endif %}
95
+ let c0 = chunk * VEC;
96
+ {% if phase == "stats" or groupUniform %}
97
+ let g = c0 / CPG;
98
+ {% endif %}
99
+ let image_base = n * HW * CHUNKS;
100
+ let hw_begin = slab * ROWS_PER_SLAB;
101
+ let hw_end = min(hw_begin + ROWS_PER_SLAB, HW);
102
+ {% else %}
103
+ // Channels first. The workgroup owns one contiguous chunk of one (batch item,
104
+ // channel) plane, so every lane shares the channel and the group and the loads are
105
+ // contiguous across the workgroup. Adjacent units are adjacent chunks of one plane.
106
+ let plane = unit / SPLITS;
107
+ let part = unit - plane * SPLITS;
108
+ let n = plane / CHANNELS;
109
+ let c = plane % CHANNELS;
110
+ let g = c / CPG;
111
+ let base_v = plane * PLANE_V;
112
+ let chunk_begin = part * CHUNK_V;
113
+ let chunk_end = min(chunk_begin + CHUNK_V, PLANE_V);
114
+ {% endif %}
115
+ {% if phase == "stats" %}
116
+ {% if blockedStats %}
117
+ var acc = vec2<f32>(0.0);
118
+ // Keep tail lanes participating in the barrier without reading outside the group.
119
+ if (g < GROUPS) {
120
+ {% endif %}
121
+ {% if phaseLayout %}
122
+ let shift_index = image_base + g * (CPG / VEC);
123
+ {% else %}
124
+ let shift_index = (n * CHANNELS + g * CPG) * PLANE_V;
125
+ {% endif %}
126
+ let shift = {{ shift_at("shift_index", "shift_skip", "g * CPG") }};
127
+ {% if not blockedStats %}
128
+ var acc = vec2<f32>(0.0);
129
+ {% endif %}
130
+ {% if phaseLayout %}
131
+ for (var hw = hw_begin + sub_row; hw < hw_end; hw = hw + ROWS_PER_STEP) {
132
+ let idx = image_base + hw * CHUNKS + chunk;
133
+ let v = {{ pre_norm("idx", "skip_base + hw * CHUNKS + chunk") }};
134
+ {% else %}
135
+ for (var i = chunk_begin + lid.x; i < chunk_end; i = i + WG) {
136
+ let idx = base_v + i;
137
+ let v = {{ pre_norm("idx", "skip_base + i") }};
138
+ {% endif %}
139
+ let d = v - {{ vecF32 }}(shift);
140
+ {% if useVector %}
141
+ acc = acc + vec2<f32>({{ component_sum("d") }}, dot(d, d));
142
+ {% else %}
143
+ acc = acc + vec2<f32>(d, d * d);
144
+ {% endif %}
145
+ }
146
+ {% if blockedStats %}
147
+ }
148
+ {% endif %}
149
+ lane_sums[lid.x] = acc;
150
+ workgroupBarrier();
151
+ {% if phaseLayout %}
152
+ // One aligned segment of `CPG / VEC` lanes per group per sub-row. Each group's
153
+ // partial is summed in a fixed order by a single lane, so the value does not depend
154
+ // on how the workgroup was scheduled.
155
+ let segment = CPG / VEC;
156
+ for (var sel = lid.x; sel < {% if blockedStats %}min(GROUPS_PER_BLOCK, GROUPS - group_begin){% else %}GROUPS{% endif %}; sel = sel + WG) {
157
+ var total = vec2<f32>(0.0);
158
+ for (var r = 0u; r < ROWS_PER_STEP; r = r + 1u) {
159
+ let seg_base = r * {% if blockedStats %}TILE_CHUNKS{% else %}CHUNKS{% endif %} + sel * segment;
160
+ for (var t = 0u; t < segment; t = t + 1u) {
161
+ total = total + lane_sums[seg_base + t];
162
+ }
163
+ }
164
+ partials[(n * GROUPS + {% if blockedStats %}group_begin + {% endif %}sel) * SLABS + slab] = total;
165
+ }
166
+ {% else %}
167
+ // WG is a power of two on this arm, so the halving stride is uniform across the
168
+ // workgroup and every lane reaches every barrier.
169
+ var stride = WG / 2u;
170
+ loop {
171
+ if (stride == 0u) {
172
+ break;
173
+ }
174
+ if (lid.x < stride) {
175
+ lane_sums[lid.x] = lane_sums[lid.x] + lane_sums[lid.x + stride];
176
+ }
177
+ workgroupBarrier();
178
+ stride = stride / 2u;
179
+ }
180
+ if (lid.x == 0u) {
181
+ partials[(n * GROUPS + g) * PARTIALS_PER_ROW + (c - g * CPG) * SPLITS + part] = lane_sums[0];
182
+ }
183
+ {% endif %}
184
+ {% else %}
185
+ {% if groupUniform %}
186
+ let st = stats[n * GROUPS + g];
187
+ let mean = {{ vecF32 }}(st.x);
188
+ let inv_std = {{ vecF32 }}(st.y);
189
+ {% else %}
190
+ // The four channels this lane holds do not all belong to one group, which is what
191
+ // `channels_per_group % 4 != 0` means. The statistics are still read once per lane,
192
+ // as a component-wise gather over at most two adjacent groups.
193
+ let row_base = n * GROUPS;
194
+ let s0 = stats[row_base + c0 / CPG];
195
+ let s1 = stats[row_base + (c0 + 1u) / CPG];
196
+ let s2 = stats[row_base + (c0 + 2u) / CPG];
197
+ let s3 = stats[row_base + (c0 + 3u) / CPG];
198
+ let mean = vec4<f32>(s0.x, s1.x, s2.x, s3.x);
199
+ let inv_std = vec4<f32>(s0.y, s1.y, s2.y, s3.y);
200
+ {% endif %}
201
+ {% if useVector %}
202
+ {% if phaseLayout %}
203
+ let gamma_v = {{ channel_values("gamma", "c0") }};
204
+ let beta_v = {{ channel_values("beta", "c0") }};
205
+ {% else %}
206
+ let gamma_v = {{ vecF32 }}(f32(gamma[c]));
207
+ let beta_v = {{ vecF32 }}(f32(beta[c]));
208
+ {% endif %}
209
+ {% else %}
210
+ {% if phaseLayout %}
211
+ let gamma_v = f32(gamma[c0]);
212
+ let beta_v = f32(beta[c0]);
213
+ {% else %}
214
+ let gamma_v = f32(gamma[c]);
215
+ let beta_v = f32(beta[c]);
216
+ {% endif %}
217
+ {% endif %}
218
+ {% if phaseLayout %}
219
+ for (var hw = hw_begin + sub_row; hw < hw_end; hw = hw + ROWS_PER_STEP) {
220
+ let idx = image_base + hw * CHUNKS + chunk;
221
+ {% else %}
222
+ for (var i = chunk_begin + lid.x; i < chunk_end; i = i + WG) {
223
+ let idx = base_v + i;
224
+ {% endif %}
225
+ let normalized = ({{ vecF32 }}({{ applySrc }}[idx]) - mean) * inv_std;
226
+ {% if activationSpec == 1 %}
227
+ Y[idx] = {{ storeElement }}(silu_e(gamma_v * normalized + beta_v));
228
+ {% else %}
229
+ Y[idx] = {{ storeElement }}(gamma_v * normalized + beta_v);
230
+ {% endif %}
231
+ }
232
+ {% endif %}
233
+ }
build/webgpu/manifest.json ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "domain": "com.microsoft",
3
+ "name": "GroupNorm",
4
+ "sinceVersion": 1,
5
+ "inputs": {
6
+ "X": { "dtype": "T", "rank": 4 },
7
+ "gamma": { "dtype": "M", "rank": 1 },
8
+ "beta": { "dtype": "M", "rank": 1 }
9
+ },
10
+ "outputs": { "Y": { "dtype": "T", "rank": 4, "shape": "shapes.X" } },
11
+ "attributes": { "activation": {}, "groups": {}, "channels_last": { "default": 1 }, "epsilon": { "default": 0.00001 } },
12
+ "attributeConstraints": { "activation": { "required": true, "values": [0, 1] }, "groups": { "required": true } },
13
+ "typeConstraints": { "T": ["float32", "float16"], "M": ["float32", "float16"] },
14
+ "tunables": {
15
+ "WORKGROUP_SIZE": { "default": 256 },
16
+ "MAX_SPLITS": { "default": 256 },
17
+ "VALUES_PER_SPLIT": { "default": 4096 },
18
+ "SPLIT_MIN_ELEMENTS": { "default": 262144 },
19
+ "APPLY_SLAB_ELEMENTS": { "default": 8192 },
20
+ "FUSED_MIN_ROWS": { "default": 64 },
21
+ "FUSED_MAX_GROUP_BYTES": { "default": 131072 }
22
+ },
23
+ "derive": {
24
+ "attrsPresent": "has(attrs, \"groups\") and has(attrs, \"activation\")",
25
+ "numGroups": "attrs.groups if attrsPresent else 0",
26
+ "activationKind": "attrs.activation if attrsPresent else 0",
27
+ "channelsLast": "attrs.channels_last != 0",
28
+ "rankOk": "ranks.X == 4 and ranks.Y == 4 and ranks.gamma == 1 and ranks.beta == 1",
29
+ "dimN": "dim(shapes.X, 0) if rankOk else 0",
30
+ "dimC": "(dim(shapes.X, 3) if channelsLast else dim(shapes.X, 1)) if rankOk else 0",
31
+ "dimHW": "(dim(shapes.X, 1) * dim(shapes.X, 2) if channelsLast else dim(shapes.X, 2) * dim(shapes.X, 3)) if rankOk else 0",
32
+ "groupsOk": "numGroups >= 1 and dimC >= 1 and dimC % numGroups == 0",
33
+ "channelsPerGroup": "dimC / numGroups if groupsOk else 0",
34
+ "planeCount": "dimN * dimC",
35
+ "groupSize": "channelsPerGroup * dimHW",
36
+ "normRows": "dimN * numGroups",
37
+ "elementCount": "numel(shapes.X) if rankOk else 0",
38
+ "shapeOk": "rankOk and groupsOk and dimN > 0 and dimHW > 0 and groupSize > 0 and sameShape(shapes.Y, shapes.X) and dim(shapes.gamma, 0) == dimC and dim(shapes.beta, 0) == dimC",
39
+ "deviceWorkgroupCap": "min(device.limits.maxComputeInvocationsPerWorkgroup, device.limits.maxComputeWorkgroupSizeX)",
40
+ "storageBufferLimit": "min(device.limits.maxStorageBufferBindingSize, device.limits.maxBufferSize)",
41
+ "foldedDispatchCapacity": "min(device.limits.maxComputeWorkgroupsPerDimension, 65535) * min(device.limits.maxComputeWorkgroupsPerDimension, 65535)",
42
+ "statsVec4Ok": "(channelsPerGroup % 4 == 0 and dimC % 4 == 0) if channelsLast else (dimHW % 4 == 0)",
43
+ "applyVec4Ok": "(dimC % 4 == 0) if channelsLast else (dimHW % 4 == 0)",
44
+ "fusedVecWidth": "4 if statsVec4Ok else 1",
45
+ "statsVecWidth": "4 if statsVec4Ok else (2 if channelsLast and channelsPerGroup % 2 == 0 else 1)",
46
+ "applyVecWidth": "4 if applyVec4Ok else 1",
47
+ "workgroupCap": "max(1, floor(pow2ceil(min(tunables.WORKGROUP_SIZE, deviceWorkgroupCap) + 1) / 2))",
48
+ "statsChunks": "max(1, floor(dimC / statsVecWidth))",
49
+ "fusedRowChunks": "max(1, floor(dimC / fusedVecWidth))",
50
+ "statsSegment": "max(1, floor(channelsPerGroup / statsVecWidth))",
51
+ "statsGroupsPerBlock": "max(1, min(numGroups, floor(workgroupCap / statsSegment))) if channelsLast and statsChunks > deviceWorkgroupCap and statsSegment <= workgroupCap else max(1, numGroups)",
52
+ "statsGroupBlocks": "ceilDiv(max(1, numGroups), statsGroupsPerBlock)",
53
+ "statsTileChunks": "statsGroupsPerBlock * statsSegment if statsGroupBlocks > 1 else statsChunks",
54
+ "applyChunks": "max(1, floor(dimC / applyVecWidth))",
55
+ "statsRowsPerStep": "max(1, floor(workgroupCap / statsTileChunks))",
56
+ "applyRowsPerStep": "max(1, floor(workgroupCap / applyChunks))",
57
+ "planeVecs": "max(1, floor(dimHW / statsVecWidth))",
58
+ "slabCount": "ceilDiv(dimHW, ceilDiv(dimHW, max(1, min(tunables.MAX_SPLITS, dimHW, ceilDiv(dimHW * dimC, tunables.VALUES_PER_SPLIT)))))",
59
+ "planeSplits": "max(1, min(tunables.MAX_SPLITS, planeVecs, ceilDiv(dimHW, tunables.VALUES_PER_SPLIT)))",
60
+ "splitCount": "slabCount if channelsLast else planeSplits",
61
+ "partialsPerRow": "slabCount if channelsLast else max(1, channelsPerGroup) * planeSplits",
62
+ "partialElements": "max(1, normRows * partialsPerRow)",
63
+ "statsElements": "max(1, normRows)",
64
+ "statsWorkgroup": "(statsTileChunks * statsRowsPerStep) if channelsLast else min(workgroupCap, max(32, pow2ceil(ceilDiv(planeVecs, planeSplits))))",
65
+ "applyWorkgroup": "(applyChunks * applyRowsPerStep) if channelsLast else statsWorkgroup",
66
+ "applySlabCount": "ceilDiv(dimHW, ceilDiv(dimHW, max(1, min(dimHW, min(device.limits.maxComputeWorkgroupsPerDimension, 65535), ceilDiv(elementCount, tunables.APPLY_SLAB_ELEMENTS)))))",
67
+ "applySplit": "applySlabCount if channelsLast else planeSplits",
68
+ "applyUnits": "(dimN * applySlabCount) if channelsLast else (planeCount * planeSplits)",
69
+ "workUnits": "(dimN * slabCount * statsGroupBlocks) if channelsLast else (planeCount * planeSplits)",
70
+ "splitFits": "statsWorkgroup <= deviceWorkgroupCap and applyWorkgroup <= deviceWorkgroupCap and statsWorkgroup * 8 <= device.limits.maxComputeWorkgroupStorageSize and partialElements * 8 <= storageBufferLimit and workUnits > 0 and workUnits <= foldedDispatchCapacity and applyUnits <= foldedDispatchCapacity",
71
+ "fusedChunks": "max(1, floor(channelsPerGroup / fusedVecWidth)) if channelsLast else 1",
72
+ "fusedWorkgroup": "(fusedChunks * max(1, floor(workgroupCap / fusedChunks))) if channelsLast else min(workgroupCap, max(32, pow2ceil(max(1, floor(groupSize / fusedVecWidth)))))",
73
+ "fusedLanes": "pow2ceil(fusedWorkgroup)",
74
+ "fusedFits": "fusedWorkgroup <= deviceWorkgroupCap and fusedLanes * 8 <= device.limits.maxComputeWorkgroupStorageSize and normRows <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535) * min(device.limits.maxComputeWorkgroupsPerDimension, 65535)",
75
+ "combineWorkgroup": "min(workgroupCap, 64)",
76
+ "statsElement": "(\"vec\" ~ statsVecWidth ~ \"<\" ~ dtypes.T ~ \">\") if statsVecWidth > 1 else dtypes.T",
77
+ "fusedElement": "(\"vec4<\" ~ dtypes.T ~ \">\") if statsVec4Ok else dtypes.T",
78
+ "applyElement": "(\"vec4<\" ~ dtypes.T ~ \">\") if applyVec4Ok else dtypes.T",
79
+ "mScalar": "dtypes.M",
80
+ "tScalar": "dtypes.T",
81
+ "contractOk": "shapeOk and f16Ok(dtypes.T) and f16Ok(dtypes.M)"
82
+ },
83
+ "when": ["contractOk"],
84
+ "bindings": {
85
+ "xIn": { "arg": "X", "name": "X", "elementType": "$statsElement" },
86
+ "srcX": { "arg": "X", "name": "src", "elementType": "$statsElement" },
87
+ "gamma": { "elementType": "$mScalar" },
88
+ "beta": { "elementType": "$mScalar" },
89
+ "yOut": { "arg": "Y", "name": "Y", "elementType": "$applyElement" },
90
+ "partialsW": { "name": "partials", "buffer": "storage", "elementType": "vec2<f32>" },
91
+ "partialsR": { "name": "partials", "buffer": "read-only-storage", "elementType": "vec2<f32>" },
92
+ "statsW": { "name": "stats", "buffer": "storage", "elementType": "vec2<f32>" },
93
+ "statsR": { "name": "stats", "buffer": "read-only-storage", "elementType": "vec2<f32>" },
94
+ "srcApply": { "arg": "X", "name": "src", "elementType": "$applyElement" },
95
+ "yFused": { "arg": "Y", "name": "Y", "elementType": "$statsElement" }
96
+ },
97
+ "variants": [
98
+ {
99
+ "id": "split",
100
+ "priority": 20,
101
+ "when": ["splitFits"],
102
+ "demoteWhen": ["elementCount < tunables.SPLIT_MIN_ELEMENTS * statsGroupBlocks", "(not channelsLast) and normRows >= tunables.FUSED_MIN_ROWS and groupSize * dtypeBytes(tensorDtypes.X) <= tunables.FUSED_MAX_GROUP_BYTES"],
103
+ "derive": {
104
+ "phaseLayout": "channelsLast",
105
+ "hasSkip": "false",
106
+ "hasBias": "false",
107
+ "broadcastSkip": "false",
108
+ "vecSpec": "statsVecWidth",
109
+ "dimCSpec": "dimC",
110
+ "dimHWSpec": "dimHW",
111
+ "cpgSpec": "channelsPerGroup",
112
+ "groupsSpec": "numGroups",
113
+ "groupSizeSpec": "groupSize",
114
+ "normRowsSpec": "normRows",
115
+ "epsilonSpec": "attrs.epsilon",
116
+ "activationSpec": "activationKind",
117
+ "chunksSpec": "statsChunks",
118
+ "rowsPerStepSpec": "statsRowsPerStep",
119
+ "splitSpec": "splitCount",
120
+ "rowsPerSlabSpec": "ceilDiv(dimHW, slabCount)",
121
+ "planeVecsSpec": "planeVecs",
122
+ "chunkVecsSpec": "ceilDiv(planeVecs, planeSplits)",
123
+ "partialsPerRowSpec": "partialsPerRow",
124
+ "wgSpec": "statsWorkgroup",
125
+ "combineWgSpec": "combineWorkgroup",
126
+ "vecF32": "(\"vec\" ~ statsVecWidth ~ \"<f32>\") if statsVecWidth > 1 else \"f32\"",
127
+ "storeElement": "statsElement",
128
+ "applySrc": "\"src\"",
129
+ "groupUniform": "true"
130
+ },
131
+ "intermediates": [
132
+ { "id": "partials", "dtype": "float32", "shape": "[partialElements, 2]" },
133
+ { "id": "stats", "dtype": "float32", "shape": "[statsElements, 2]" }
134
+ ],
135
+ "passes": [
136
+ {
137
+ "id": "stats",
138
+ "name": "GroupNorm.Partials",
139
+ "shader": "group-norm-slab.wgsl.jinja",
140
+ "derive": {
141
+ "phase": "\"stats\"",
142
+ "unitsSpec": "workUnits",
143
+ "statsBlockedSpec": "channelsLast and statsGroupBlocks > 1",
144
+ "statsGroupsPerBlockSpec": "statsGroupsPerBlock",
145
+ "statsGroupBlocksSpec": "statsGroupBlocks",
146
+ "statsTileChunksSpec": "statsTileChunks"
147
+ },
148
+ "bindings": ["xIn", "partialsW"],
149
+ "dispatch": { "x": "min(workUnits, 65535)", "y": "ceilDiv(workUnits, 65535)", "z": 1 }
150
+ },
151
+ {
152
+ "id": "combine",
153
+ "name": "GroupNorm.Combine",
154
+ "shader": "group-norm-combine.wgsl.jinja",
155
+ "bindings": ["srcX", "partialsR", "statsW"],
156
+ "dispatch": {
157
+ "x": "min(ceilDiv((normRows), (combineWorkgroup)), 65535)",
158
+ "y": "ceilDiv(ceilDiv((normRows), (combineWorkgroup)), 65535)",
159
+ "z": 1
160
+ }
161
+ },
162
+ {
163
+ "id": "apply",
164
+ "name": "GroupNorm.Apply",
165
+ "shader": "group-norm-slab.wgsl.jinja",
166
+ "derive": {
167
+ "phase": "\"apply\"",
168
+ "vecSpec": "applyVecWidth",
169
+ "chunksSpec": "applyChunks",
170
+ "rowsPerStepSpec": "applyRowsPerStep",
171
+ "wgSpec": "applyWorkgroup",
172
+ "splitSpec": "applySplit",
173
+ "rowsPerSlabSpec": "ceilDiv(dimHW, applySlabCount)",
174
+ "groupUniform": "(channelsPerGroup % applyVecWidth == 0) if channelsLast else true",
175
+ "storeElement": "applyElement",
176
+ "vecF32": "\"vec4<f32>\" if applyVec4Ok else \"f32\"",
177
+ "unitsSpec": "applyUnits"
178
+ },
179
+ "bindings": ["srcApply", "gamma", "beta", "statsR", "yOut"],
180
+ "dispatch": { "x": "min(applyUnits, 65535)", "y": "ceilDiv(applyUnits, 65535)", "z": 1 }
181
+ }
182
+ ]
183
+ },
184
+ {
185
+ "id": "fused",
186
+ "priority": 10,
187
+ "when": ["fusedFits"],
188
+ "derive": {
189
+ "statsElement": "fusedElement",
190
+ "phaseLayout": "channelsLast",
191
+ "hasSkip": "false",
192
+ "hasBias": "false",
193
+ "broadcastSkip": "false",
194
+ "useVec4": "statsVec4Ok",
195
+ "vecSpec": "fusedVecWidth",
196
+ "dimCSpec": "dimC",
197
+ "dimHWSpec": "dimHW",
198
+ "cpgSpec": "channelsPerGroup",
199
+ "groupsSpec": "numGroups",
200
+ "groupSizeSpec": "groupSize",
201
+ "normRowsSpec": "normRows",
202
+ "epsilonSpec": "attrs.epsilon",
203
+ "activationSpec": "activationKind",
204
+ "chunksSpec": "fusedRowChunks",
205
+ "fusedChunksSpec": "fusedChunks",
206
+ "fusedRowsSpec": "max(1, floor(fusedWorkgroup / fusedChunks))",
207
+ "spanVecsSpec": "max(1, floor(groupSize / fusedVecWidth))",
208
+ "wgSpec": "fusedWorkgroup",
209
+ "ldsSpec": "fusedLanes",
210
+ "vecF32": "\"vec4<f32>\" if statsVec4Ok else \"f32\"",
211
+ "storeElement": "fusedElement",
212
+ "fusedSrc": "\"X\"",
213
+ "planeVecsSpec": "planeVecs"
214
+ },
215
+ "passes": [
216
+ {
217
+ "id": "main",
218
+ "name": "GroupNorm.Fused",
219
+ "shader": "group-norm-fused.wgsl.jinja",
220
+ "bindings": ["xIn", "gamma", "beta", "yFused"],
221
+ "dispatch": { "x": "min(normRows, 65535)", "y": "ceilDiv(normRows, 65535)", "z": 1 }
222
+ }
223
+ ]
224
+ }
225
+ ]
226
+ }
build/webgpu/metadata.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "com.microsoft.GroupNorm",
3
+ "id": "_com_microsoft_groupnorm_webgpu_3aa81a3",
4
+ "version": 1,
5
+ "license": "Apache-2.0",
6
+ "backend": { "type": "webgpu" },
7
+ "digest": {
8
+ "algorithm": "sha256",
9
+ "files": {
10
+ "bench.json": "jld33+mA1k/b0oKawSw9bnQxUcFLroArkzBwCnOdyCg=",
11
+ "group-norm-combine.wgsl.jinja": "weqcoPzdY5lQ8K/lZVEF03/sdbu1c/2jwdrW/KT9xr4=",
12
+ "group-norm-fused.wgsl.jinja": "uL3EIKVa0AnJbW7kB5MRPipXhZDr9KZ62soBHeu40K0=",
13
+ "group-norm-slab.wgsl.jinja": "T6ccs9TsauqsOjpC2hBzy7mRE/66CmLVI1F9JcBhk+w=",
14
+ "manifest.json": "U5sLYCEeiEBL842xP9mLAEgUQaJ3aioLBFcvJYpPMok=",
15
+ "test.json": "pEkAgZoOGQcSf9k/rHscjf21d18AlN/baC/QpVhOX08="
16
+ }
17
+ },
18
+ "provenance": { "kernel": { "sha": "6fdf6301e2bbcc2f03bf1eaf493b7ad55ef33afc", "dirty": false } },
19
+ "webgpu": {
20
+ "manifestSpec": "2.1",
21
+ "variants": {
22
+ "split": ["group-norm-combine.wgsl.jinja", "group-norm-slab.wgsl.jinja"],
23
+ "fused": ["group-norm-fused.wgsl.jinja"]
24
+ }
25
+ }
26
+ }
build/webgpu/test.json ADDED
The diff for this file is too large to render. See raw diff