Xenova HF Staff commited on
Commit
02f8aff
·
verified ·
1 Parent(s): 386747d

sync 91d990483a17

Browse files
README.md CHANGED
@@ -18,15 +18,15 @@ See the [ONNX `MeanVarianceNormalization` spec](https://onnx.ai/onnx/operators/o
18
 
19
  ## Inputs
20
 
21
- | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
22
  | --- | --- | --- | --- | --- | --- | --- |
23
- | `X` | `x` | `T` | — | — | Input tensor to normalize. | required |
24
 
25
  ## Outputs
26
 
27
- | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
28
  | --- | --- | --- | --- | --- | --- | --- |
29
- | `Y` | `y` | `T` | same as `X` | same as `X` | Normalized tensor with the same shape as `X`. | required |
30
 
31
  ## Attributes
32
 
@@ -44,7 +44,7 @@ Default values (overridable per request):
44
 
45
  ## Files
46
 
47
- - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, provenance)
48
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
49
  - [`test.json`](build/webgpu/test.json) — correctness cases
50
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
@@ -57,10 +57,14 @@ Default values (overridable per request):
57
 
58
  ## Use with `@huggingface/kernels`
59
 
60
- The loader derives every required output's shape and logical dtype from the manifest contract and this call.
61
- It then allocates the result tensors automatically.
 
 
 
62
 
63
  The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
 
64
 
65
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
66
 
 
18
 
19
  ## Inputs
20
 
21
+ | Name | Upstream name | Logical dtype | Rank | Shape | Description | Presence |
22
  | --- | --- | --- | --- | --- | --- | --- |
23
+ | `x` | `X` | `T` | — | — | Input tensor to normalize. | required |
24
 
25
  ## Outputs
26
 
27
+ | Name | Upstream name | Logical dtype | Rank | Shape | Description | Presence |
28
  | --- | --- | --- | --- | --- | --- | --- |
29
+ | `y` | `Y` | `T` | same as `x` | same as `x` | Normalized tensor with the same shape as `X`. | required |
30
 
31
  ## Attributes
32
 
 
44
 
45
  ## Files
46
 
47
+ - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, per-variant templates, provenance)
48
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
49
  - [`test.json`](build/webgpu/test.json) — correctness cases
50
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
 
57
 
58
  ## Use with `@huggingface/kernels`
59
 
60
+ ```sh
61
+ npm install --save-exact @huggingface/kernels@0.0.1-preview.2
62
+ ```
63
+
64
+ Required output shapes and logical data types are inferred from the supplied inputs and attributes; result tensors are allocated automatically.
65
 
66
  The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
67
+ It follows the `v1` branch as fixes land. To pin exact artifact bytes, pass a 40-character commit `revision` instead of `version`.
68
 
69
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
70
 
build/webgpu/bench.json CHANGED
@@ -1,5 +1,4 @@
1
  {
2
- "op": "ai.onnx.MeanVarianceNormalization",
3
  "cases": [
4
  {
5
  "name": "1x32x32x32_default_axes",
 
1
  {
 
2
  "cases": [
3
  {
4
  "name": "1x32x32x32_default_axes",
build/webgpu/manifest.json CHANGED
@@ -2,44 +2,30 @@
2
  "domain": "ai.onnx",
3
  "name": "MeanVarianceNormalization",
4
  "sinceVersion": 13,
5
- "description": "Normalizes each group as `(X - mean) / sqrt(variance)`, reducing over `axes` (default `[0, 2, 3]`).",
6
- "inputs": [{ "role": "X", "dtype": "T", "description": "Input tensor to normalize." }],
7
- "outputs": [
8
- {
9
- "role": "Y",
10
- "dtype": "T",
11
- "rank": "ranks.X",
12
- "description": "Normalized tensor with the same shape as `X`.",
13
- "shape": "shapes.X"
14
- }
15
- ],
16
- "attributes": { "axes": [0, 2, 3] },
17
- "attributeDescriptions": { "axes": "Axes that share a mean and variance; negative values count from the back." },
18
  "typeConstraints": { "T": ["float32", "float16"] },
19
- "args": {
20
- "x": { "kind": "tensor", "semantic": "X", "role": "input" },
21
- "y": { "kind": "tensor", "semantic": "Y", "role": "output" }
22
- },
23
  "tunables": {
24
- "WORKGROUP_SIZE": 256,
25
- "SERIAL_WORKGROUP_SIZE": 256,
26
- "SERIAL_TINY_WORKGROUP_SIZE": 64,
27
- "SERIAL_MAX_REDUCTION": 128,
28
- "SERIAL_MIN_ROWS": 256,
29
- "TREE_MEDIUM_WORKGROUP_SIZE": 64,
30
- "VEC4_MIN_REDUCTION": 8,
31
- "FLAT_SPLIT_MIN_ELEMENTS": 65536,
32
- "FLAT_SPLIT_TARGET_ELEMENTS": 4096,
33
- "MAX_FLAT_SPLITS": 256
34
  },
35
  "derive": {
36
  "deviceWorkgroupCap": "min(device.limits.maxComputeInvocationsPerWorkgroup, device.limits.maxComputeWorkgroupSizeX)",
37
- "foldedDispatchCapacity": "device.limits.maxComputeWorkgroupsPerDimension * device.limits.maxComputeWorkgroupsPerDimension",
38
- "shapeContract": "ranks.X >= 3 and ranks.Y == ranks.X and sameShape(shapes.Y, shapes.X) and f16Ok(dtypes.T)",
39
- "reduceCount": "(dim(shapes.X, 0) if hasAxis(attrs.axes, 0, ranks.X) else 1) * (dim(shapes.X, 1) if hasAxis(attrs.axes, 1, ranks.X) else 1) * (dim(shapes.X, 2) if hasAxis(attrs.axes, 2, ranks.X) else 1) * (dim(shapes.X, 3) if ranks.X >= 4 and hasAxis(attrs.axes, 3, ranks.X) else 1) * (dim(shapes.X, 4) if ranks.X >= 5 and hasAxis(attrs.axes, 4, ranks.X) else 1) * (dim(shapes.X, 5) if ranks.X >= 6 and hasAxis(attrs.axes, 5, ranks.X) else 1)",
40
- "rowCount": "numel(shapes.X) / max(1, reduceCount)",
41
- "allAxesReduced": "hasAxis(attrs.axes, 0, ranks.X) and hasAxis(attrs.axes, 1, ranks.X) and hasAxis(attrs.axes, 2, ranks.X) and (ranks.X < 4 or hasAxis(attrs.axes, 3, ranks.X)) and (ranks.X < 5 or hasAxis(attrs.axes, 4, ranks.X)) and (ranks.X < 6 or hasAxis(attrs.axes, 5, ranks.X))",
42
- "vec4Eligible": "((ranks.X == 3 and hasAxis(attrs.axes, 2, 3) and (dim(shapes.X, 2) % 4 == 0 or (hasAxis(attrs.axes, 1, 3) and dim(shapes.X, 1) * dim(shapes.X, 2) % 4 == 0) or (hasAxis(attrs.axes, 0, 3) and hasAxis(attrs.axes, 1, 3) and numel(shapes.X) % 4 == 0))) or (ranks.X == 4 and hasAxis(attrs.axes, 3, 4) and (dim(shapes.X, 3) % 4 == 0 or (hasAxis(attrs.axes, 2, 4) and dim(shapes.X, 2) * dim(shapes.X, 3) % 4 == 0) or (hasAxis(attrs.axes, 1, 4) and hasAxis(attrs.axes, 2, 4) and dim(shapes.X, 1) * dim(shapes.X, 2) * dim(shapes.X, 3) % 4 == 0) or (hasAxis(attrs.axes, 0, 4) and hasAxis(attrs.axes, 1, 4) and hasAxis(attrs.axes, 2, 4) and numel(shapes.X) % 4 == 0))) or (ranks.X == 5 and hasAxis(attrs.axes, 4, 5) and (dim(shapes.X, 4) % 4 == 0 or (hasAxis(attrs.axes, 3, 5) and dim(shapes.X, 3) * dim(shapes.X, 4) % 4 == 0) or (hasAxis(attrs.axes, 2, 5) and hasAxis(attrs.axes, 3, 5) and dim(shapes.X, 2) * dim(shapes.X, 3) * dim(shapes.X, 4) % 4 == 0) or (hasAxis(attrs.axes, 1, 5) and hasAxis(attrs.axes, 2, 5) and hasAxis(attrs.axes, 3, 5) and dim(shapes.X, 1) * dim(shapes.X, 2) * dim(shapes.X, 3) * dim(shapes.X, 4) % 4 == 0) or (allAxesReduced and numel(shapes.X) % 4 == 0))) or (ranks.X == 6 and hasAxis(attrs.axes, 5, 6) and (dim(shapes.X, 5) % 4 == 0 or (hasAxis(attrs.axes, 4, 6) and dim(shapes.X, 4) * dim(shapes.X, 5) % 4 == 0) or (hasAxis(attrs.axes, 3, 6) and hasAxis(attrs.axes, 4, 6) and dim(shapes.X, 3) * dim(shapes.X, 4) * dim(shapes.X, 5) % 4 == 0) or (hasAxis(attrs.axes, 2, 6) and hasAxis(attrs.axes, 3, 6) and hasAxis(attrs.axes, 4, 6) and dim(shapes.X, 2) * dim(shapes.X, 3) * dim(shapes.X, 4) * dim(shapes.X, 5) % 4 == 0) or (hasAxis(attrs.axes, 1, 6) and hasAxis(attrs.axes, 2, 6) and hasAxis(attrs.axes, 3, 6) and hasAxis(attrs.axes, 4, 6) and dim(shapes.X, 1) * dim(shapes.X, 2) * dim(shapes.X, 3) * dim(shapes.X, 4) * dim(shapes.X, 5) % 4 == 0) or (allAxesReduced and numel(shapes.X) % 4 == 0))))",
43
  "maxWorkgroupSize": "min(tunables.WORKGROUP_SIZE, deviceWorkgroupCap)",
44
  "minSubgroupSize": "device.adapterInfo.subgroupMinSize if has(device.adapterInfo, \"subgroupMinSize\") else 1",
45
  "useSubgroups": "device.features.has(\"subgroups\") and has(device.adapterInfo, \"subgroupMinSize\") and minSubgroupSize > 0",
@@ -48,93 +34,36 @@
48
  "serialWorkgroupSize": "min(device.limits.maxComputeInvocationsPerWorkgroup, device.limits.maxComputeWorkgroupSizeX, (tunables.SERIAL_TINY_WORKGROUP_SIZE if reduceCount <= 4 else tunables.SERIAL_WORKGROUP_SIZE))",
49
  "rowDispatchFits": "rowCount <= foldedDispatchCapacity",
50
  "serialDispatchFits": "ceilDiv(rowCount, serialWorkgroupSize) <= foldedDispatchCapacity",
51
- "applyDispatchFits": "ceilDiv(numel(shapes.Y), maxWorkgroupSize) <= foldedDispatchCapacity",
52
  "scalarStorageFits": "scalarWorkgroupSize * 8 <= device.limits.maxComputeWorkgroupStorageSize",
53
  "vectorStorageFits": "vectorWorkgroupSize * 8 <= device.limits.maxComputeWorkgroupStorageSize",
54
- "flatSplit": "min(tunables.MAX_FLAT_SPLITS, pow2ceil(ceilDiv(numel(shapes.X), tunables.FLAT_SPLIT_TARGET_ELEMENTS)))",
55
  "flatScratchBytes": "flatSplit * 8",
56
- "flatPathFits": "flatSplit <= device.limits.maxComputeWorkgroupsPerDimension and flatScratchBytes <= device.limits.maxStorageBufferBindingSize and flatScratchBytes <= device.limits.maxBufferSize and maxWorkgroupSize * 8 <= device.limits.maxComputeWorkgroupStorageSize and applyDispatchFits"
57
  },
58
- "bindingSets": {
59
- "rows": [
60
- {
61
- "name": "x",
62
- "arg": "x",
63
- "semantic": "X",
64
- "buffer": { "type": "read-only-storage" },
65
- "elementType": "$ioElement"
66
- },
67
- { "name": "y", "arg": "y", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$ioElement" },
68
- {
69
- "name": "params",
70
- "semantic": "kernel.params",
71
- "buffer": { "type": "uniform" },
72
- "struct": { "name": "Params", "fields": [{ "name": "rows", "type": "u32", "value": "rowCount" }] }
73
- }
74
- ],
75
- "flatPartials": [
76
- { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
77
- { "name": "partials", "semantic": "partials", "buffer": { "type": "storage" }, "elementType": "vec2<f32>" },
78
- {
79
- "name": "params",
80
- "semantic": "kernel.params",
81
- "buffer": { "type": "uniform" },
82
- "struct": { "name": "Params", "fields": [{ "name": "count", "type": "u32", "value": "numel(shapes.X)" }] }
83
- }
84
- ],
85
- "flatCombine": [
86
- { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
87
- {
88
- "name": "partials",
89
- "semantic": "partials",
90
- "buffer": { "type": "read-only-storage" },
91
- "elementType": "vec2<f32>"
92
- },
93
- { "name": "stats", "semantic": "stats", "buffer": { "type": "storage" }, "elementType": "f32", "length": 2 },
94
- {
95
- "name": "params",
96
- "semantic": "kernel.params",
97
- "buffer": { "type": "uniform" },
98
- "struct": { "name": "Params", "fields": [{ "name": "count", "type": "u32", "value": "numel(shapes.X)" }] }
99
- }
100
- ],
101
- "flatApply": [
102
- { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
103
- {
104
- "name": "stats",
105
- "semantic": "stats",
106
- "buffer": { "type": "read-only-storage" },
107
- "elementType": "f32",
108
- "length": 2
109
- },
110
- { "name": "y", "arg": "y", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$scalar" },
111
- {
112
- "name": "params",
113
- "semantic": "kernel.params",
114
- "buffer": { "type": "uniform" },
115
- "struct": { "name": "Params", "fields": [{ "name": "count", "type": "u32", "value": "numel(shapes.Y)" }] }
116
- }
117
- ],
118
- "noopParams": [
119
- {
120
- "name": "params",
121
- "semantic": "kernel.params",
122
- "buffer": { "type": "uniform" },
123
- "struct": { "name": "Params", "fields": [{ "name": "dummy", "type": "u32", "value": 0 }] }
124
- }
125
- ]
126
  },
127
  "variants": [
128
  {
129
  "id": "empty_noop",
130
  "priority": 200,
131
- "when": ["shapeContract", "numel(shapes.X) == 0"],
132
  "passes": [
133
  {
134
  "id": "noop",
135
  "name": "MeanVarianceNormalization.Empty",
136
  "shader": "noop.wgsl.jinja",
137
- "bindings": "noopParams",
138
  "dispatch": { "x": 0 }
139
  }
140
  ]
@@ -142,8 +71,8 @@
142
  {
143
  "id": "all_axes_flat_split",
144
  "priority": 120,
145
- "when": ["shapeContract", "numel(shapes.X) > 0", "allAxesReduced", "numel(shapes.X) >= tunables.FLAT_SPLIT_MIN_ELEMENTS", "flatPathFits"],
146
- "constants": { "scalar": "dtypes.T" },
147
  "intermediates": [
148
  { "id": "partials", "dtype": "float32", "shape": "[flatSplit, 2]" },
149
  { "id": "stats", "dtype": "float32", "shape": "[2]" }
@@ -152,120 +81,123 @@
152
  {
153
  "id": "partials",
154
  "name": "MeanVarianceNormalization.FlatPartials",
155
- "source": {
156
- "shader": "norm-flat-splitk-partials.wgsl.jinja",
157
- "inputs": { "workgroupSize": "maxWorkgroupSize", "split": "flatSplit", "usesF16": "dtypes.T == \"f16\"" }
 
 
158
  },
159
- "bindings": "flatPartials",
160
- "dispatch": { "workgroups": "flatSplit" }
161
  },
162
  {
163
  "id": "combine",
164
  "name": "MeanVarianceNormalization.FlatCombine",
165
- "source": {
166
- "shader": "norm-flat-splitk-combine.wgsl.jinja",
167
- "inputs": { "split": "flatSplit", "usesF16": "dtypes.T == \"f16\"" }
168
- },
169
- "bindings": "flatCombine",
170
- "dispatch": { "workgroups": 1 }
 
 
 
171
  },
172
  {
173
  "id": "apply",
174
  "name": "MeanVarianceNormalization.FlatApply",
175
- "source": {
176
- "shader": "norm-flat-apply.wgsl.jinja",
177
- "inputs": { "workgroupSize": "maxWorkgroupSize", "scalar": "dtypes.T", "usesF16": "dtypes.T == \"f16\"" }
 
 
178
  },
179
- "bindings": "flatApply",
180
- "dispatch": { "threads": "numel(shapes.Y)", "workgroupSize": "maxWorkgroupSize" }
 
 
 
 
 
 
 
 
 
181
  }
182
  ]
183
  },
184
  {
185
  "id": "serial_rows",
186
  "priority": 115,
187
- "when": ["shapeContract", "numel(shapes.X) > 0", "reduceCount <= tunables.SERIAL_MAX_REDUCTION", "rowCount >= tunables.SERIAL_MIN_ROWS", "serialDispatchFits"],
188
- "constants": { "scalar": "dtypes.T", "ioElement": "dtypes.T" },
189
  "passes": [
190
  {
191
  "id": "main",
192
  "name": "MeanVarianceNormalization.SerialRows",
193
- "source": {
194
- "shader": "mean-variance-normalization-serial-rows.wgsl.jinja",
195
- "inputs": {
196
- "xShape": "shapes.X",
197
- "reduce": ["hasAxis(attrs.axes, 0, ranks.X)", "hasAxis(attrs.axes, 1, ranks.X)", "hasAxis(attrs.axes, 2, ranks.X)", "hasAxis(attrs.axes, 3, ranks.X)", "hasAxis(attrs.axes, 4, ranks.X)", "hasAxis(attrs.axes, 5, ranks.X)"],
198
- "reduceCount": "reduceCount",
199
- "workgroupSize": "serialWorkgroupSize",
200
- "scalar": "dtypes.T",
201
- "usesF16": "dtypes.T == \"f16\""
202
- }
203
  },
204
- "bindings": "rows",
205
- "dispatch": { "threads": "rowCount", "workgroupSize": "serialWorkgroupSize" }
 
 
 
 
206
  }
207
  ]
208
  },
209
  {
210
  "id": "cooperative_vec4",
211
  "priority": 110,
212
- "when": ["shapeContract", "numel(shapes.X) > 0", "reduceCount >= tunables.VEC4_MIN_REDUCTION", "vec4Eligible", "rowDispatchFits", "vectorStorageFits"],
213
- "constants": {
214
- "scalar": "dtypes.T",
215
- "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
216
- "ioElement": "\"vec4<\" ~ dtypes.T ~ \">\""
217
- },
218
  "passes": [
219
  {
220
  "id": "main",
221
  "name": "MeanVarianceNormalization.CooperativeVec4",
222
- "source": {
223
- "shader": "mean-variance-normalization-subgroup.wgsl.jinja",
224
- "inputs": {
225
- "xShape": "shapes.X",
226
- "reduce": ["hasAxis(attrs.axes, 0, ranks.X)", "hasAxis(attrs.axes, 1, ranks.X)", "hasAxis(attrs.axes, 2, ranks.X)", "hasAxis(attrs.axes, 3, ranks.X)", "hasAxis(attrs.axes, 4, ranks.X)", "hasAxis(attrs.axes, 5, ranks.X)"],
227
- "reduceCount": "reduceCount",
228
- "wg": "vectorWorkgroupSize",
229
- "minSubgroupSize": "minSubgroupSize",
230
- "maxSubgroups": "ceilDiv(vectorWorkgroupSize, minSubgroupSize)",
231
- "scalar": "dtypes.T",
232
- "vecType": "\"vec4<\" ~ dtypes.T ~ \">\"",
233
- "usesF16": "dtypes.T == \"f16\"",
234
- "useSubgroups": "useSubgroups",
235
- "vectorized": true
236
- }
237
  },
238
- "bindings": "rows",
239
- "dispatch": { "workgroups": "rowCount" }
240
  }
241
  ]
242
  },
243
  {
244
  "id": "cooperative_scalar",
245
  "priority": 100,
246
- "when": ["shapeContract", "numel(shapes.X) > 0", "rowDispatchFits", "scalarStorageFits"],
247
- "constants": { "scalar": "dtypes.T", "ioElement": "dtypes.T" },
248
  "passes": [
249
  {
250
  "id": "main",
251
  "name": "MeanVarianceNormalization.CooperativeScalar",
252
- "source": {
253
- "shader": "mean-variance-normalization-subgroup.wgsl.jinja",
254
- "inputs": {
255
- "xShape": "shapes.X",
256
- "reduce": ["hasAxis(attrs.axes, 0, ranks.X)", "hasAxis(attrs.axes, 1, ranks.X)", "hasAxis(attrs.axes, 2, ranks.X)", "hasAxis(attrs.axes, 3, ranks.X)", "hasAxis(attrs.axes, 4, ranks.X)", "hasAxis(attrs.axes, 5, ranks.X)"],
257
- "reduceCount": "reduceCount",
258
- "wg": "scalarWorkgroupSize",
259
- "minSubgroupSize": "minSubgroupSize",
260
- "maxSubgroups": "ceilDiv(scalarWorkgroupSize, minSubgroupSize)",
261
- "scalar": "dtypes.T",
262
- "usesF16": "dtypes.T == \"f16\"",
263
- "useSubgroups": "useSubgroups",
264
- "vectorized": false
265
- }
266
  },
267
- "bindings": "rows",
268
- "dispatch": { "workgroups": "rowCount" }
269
  }
270
  ]
271
  }
 
2
  "domain": "ai.onnx",
3
  "name": "MeanVarianceNormalization",
4
  "sinceVersion": 13,
5
+ "inputs": { "x": { "onnx": "X", "dtype": "T" } },
6
+ "outputs": { "y": { "onnx": "Y", "dtype": "T", "rank": "ranks.x", "shape": "shapes.x" } },
7
+ "attributes": { "axes": { "default": [0, 2, 3] } },
 
 
 
 
 
 
 
 
 
 
8
  "typeConstraints": { "T": ["float32", "float16"] },
 
 
 
 
9
  "tunables": {
10
+ "WORKGROUP_SIZE": { "default": 256 },
11
+ "SERIAL_WORKGROUP_SIZE": { "default": 256 },
12
+ "SERIAL_TINY_WORKGROUP_SIZE": { "default": 64 },
13
+ "SERIAL_MAX_REDUCTION": { "default": 128 },
14
+ "SERIAL_MIN_ROWS": { "default": 256 },
15
+ "TREE_MEDIUM_WORKGROUP_SIZE": { "default": 64 },
16
+ "VEC4_MIN_REDUCTION": { "default": 8 },
17
+ "FLAT_SPLIT_MIN_ELEMENTS": { "default": 65536 },
18
+ "FLAT_SPLIT_TARGET_ELEMENTS": { "default": 4096 },
19
+ "MAX_FLAT_SPLITS": { "default": 256 }
20
  },
21
  "derive": {
22
  "deviceWorkgroupCap": "min(device.limits.maxComputeInvocationsPerWorkgroup, device.limits.maxComputeWorkgroupSizeX)",
23
+ "foldedDispatchCapacity": "min(device.limits.maxComputeWorkgroupsPerDimension, 65535) * min(device.limits.maxComputeWorkgroupsPerDimension, 65535)",
24
+ "shapeContract": "ranks.x >= 3 and ranks.x <= 8 and ranks.y == ranks.x and sameShape(shapes.y, shapes.x) and f16Ok(dtypes.T)",
25
+ "reduceCount": "(dim(shapes.x, 0) if hasAxis(attrs.axes, 0, ranks.x) else 1) * (dim(shapes.x, 1) if hasAxis(attrs.axes, 1, ranks.x) else 1) * (dim(shapes.x, 2) if hasAxis(attrs.axes, 2, ranks.x) else 1) * (dim(shapes.x, 3) if ranks.x >= 4 and hasAxis(attrs.axes, 3, ranks.x) else 1) * (dim(shapes.x, 4) if ranks.x >= 5 and hasAxis(attrs.axes, 4, ranks.x) else 1) * (dim(shapes.x, 5) if ranks.x >= 6 and hasAxis(attrs.axes, 5, ranks.x) else 1) * (dim(shapes.x, 6) if ranks.x >= 7 and hasAxis(attrs.axes, 6, ranks.x) else 1) * (dim(shapes.x, 7) if ranks.x >= 8 and hasAxis(attrs.axes, 7, ranks.x) else 1)",
26
+ "rowCount": "numel(shapes.x) / max(1, reduceCount)",
27
+ "allAxesReduced": "hasAxis(attrs.axes, 0, ranks.x) and hasAxis(attrs.axes, 1, ranks.x) and hasAxis(attrs.axes, 2, ranks.x) and (ranks.x < 4 or hasAxis(attrs.axes, 3, ranks.x)) and (ranks.x < 5 or hasAxis(attrs.axes, 4, ranks.x)) and (ranks.x < 6 or hasAxis(attrs.axes, 5, ranks.x)) and (ranks.x < 7 or hasAxis(attrs.axes, 6, ranks.x)) and (ranks.x < 8 or hasAxis(attrs.axes, 7, ranks.x))",
28
+ "vec4Eligible": "((ranks.x == 3 and hasAxis(attrs.axes, 2, 3) and (dim(shapes.x, 2) % 4 == 0 or (hasAxis(attrs.axes, 1, 3) and dim(shapes.x, 1) * dim(shapes.x, 2) % 4 == 0) or (hasAxis(attrs.axes, 0, 3) and hasAxis(attrs.axes, 1, 3) and numel(shapes.x) % 4 == 0))) or (ranks.x == 4 and hasAxis(attrs.axes, 3, 4) and (dim(shapes.x, 3) % 4 == 0 or (hasAxis(attrs.axes, 2, 4) and dim(shapes.x, 2) * dim(shapes.x, 3) % 4 == 0) or (hasAxis(attrs.axes, 1, 4) and hasAxis(attrs.axes, 2, 4) and dim(shapes.x, 1) * dim(shapes.x, 2) * dim(shapes.x, 3) % 4 == 0) or (hasAxis(attrs.axes, 0, 4) and hasAxis(attrs.axes, 1, 4) and hasAxis(attrs.axes, 2, 4) and numel(shapes.x) % 4 == 0))) or (ranks.x == 5 and hasAxis(attrs.axes, 4, 5) and (dim(shapes.x, 4) % 4 == 0 or (hasAxis(attrs.axes, 3, 5) and dim(shapes.x, 3) * dim(shapes.x, 4) % 4 == 0) or (hasAxis(attrs.axes, 2, 5) and hasAxis(attrs.axes, 3, 5) and dim(shapes.x, 2) * dim(shapes.x, 3) * dim(shapes.x, 4) % 4 == 0) or (hasAxis(attrs.axes, 1, 5) and hasAxis(attrs.axes, 2, 5) and hasAxis(attrs.axes, 3, 5) and dim(shapes.x, 1) * dim(shapes.x, 2) * dim(shapes.x, 3) * dim(shapes.x, 4) % 4 == 0) or (allAxesReduced and numel(shapes.x) % 4 == 0))) or (ranks.x == 6 and hasAxis(attrs.axes, 5, 6) and (dim(shapes.x, 5) % 4 == 0 or (hasAxis(attrs.axes, 4, 6) and dim(shapes.x, 4) * dim(shapes.x, 5) % 4 == 0) or (hasAxis(attrs.axes, 3, 6) and hasAxis(attrs.axes, 4, 6) and dim(shapes.x, 3) * dim(shapes.x, 4) * dim(shapes.x, 5) % 4 == 0) or (hasAxis(attrs.axes, 2, 6) and hasAxis(attrs.axes, 3, 6) and hasAxis(attrs.axes, 4, 6) and dim(shapes.x, 2) * dim(shapes.x, 3) * dim(shapes.x, 4) * dim(shapes.x, 5) % 4 == 0) or (hasAxis(attrs.axes, 1, 6) and hasAxis(attrs.axes, 2, 6) and hasAxis(attrs.axes, 3, 6) and hasAxis(attrs.axes, 4, 6) and dim(shapes.x, 1) * dim(shapes.x, 2) * dim(shapes.x, 3) * dim(shapes.x, 4) * dim(shapes.x, 5) % 4 == 0) or (allAxesReduced and numel(shapes.x) % 4 == 0))))",
29
  "maxWorkgroupSize": "min(tunables.WORKGROUP_SIZE, deviceWorkgroupCap)",
30
  "minSubgroupSize": "device.adapterInfo.subgroupMinSize if has(device.adapterInfo, \"subgroupMinSize\") else 1",
31
  "useSubgroups": "device.features.has(\"subgroups\") and has(device.adapterInfo, \"subgroupMinSize\") and minSubgroupSize > 0",
 
34
  "serialWorkgroupSize": "min(device.limits.maxComputeInvocationsPerWorkgroup, device.limits.maxComputeWorkgroupSizeX, (tunables.SERIAL_TINY_WORKGROUP_SIZE if reduceCount <= 4 else tunables.SERIAL_WORKGROUP_SIZE))",
35
  "rowDispatchFits": "rowCount <= foldedDispatchCapacity",
36
  "serialDispatchFits": "ceilDiv(rowCount, serialWorkgroupSize) <= foldedDispatchCapacity",
37
+ "applyDispatchFits": "ceilDiv(numel(shapes.y), maxWorkgroupSize) <= foldedDispatchCapacity",
38
  "scalarStorageFits": "scalarWorkgroupSize * 8 <= device.limits.maxComputeWorkgroupStorageSize",
39
  "vectorStorageFits": "vectorWorkgroupSize * 8 <= device.limits.maxComputeWorkgroupStorageSize",
40
+ "flatSplit": "min(tunables.MAX_FLAT_SPLITS, pow2ceil(ceilDiv(numel(shapes.x), tunables.FLAT_SPLIT_TARGET_ELEMENTS)))",
41
  "flatScratchBytes": "flatSplit * 8",
42
+ "flatPathFits": "flatSplit <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535) and flatScratchBytes <= device.limits.maxStorageBufferBindingSize and flatScratchBytes <= device.limits.maxBufferSize and maxWorkgroupSize * 8 <= device.limits.maxComputeWorkgroupStorageSize and applyDispatchFits"
43
  },
44
+ "when": ["shapeContract"],
45
+ "bindings": {
46
+ "x": { "buffer": "read-only-storage", "elementType": "$scalar" },
47
+ "params": { "buffer": "uniform", "struct": [{ "name": "count", "type": "u32", "value": "numel(shapes.x)" }] },
48
+ "x_2": { "name": "x", "buffer": "read-only-storage", "elementType": "$ioElement" },
49
+ "y_2": { "name": "y", "buffer": "storage", "elementType": "$ioElement" },
50
+ "params_4": {
51
+ "name": "params",
52
+ "buffer": "uniform",
53
+ "struct": [{ "name": "rows", "type": "u32", "value": "rowCount" }]
54
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  },
56
  "variants": [
57
  {
58
  "id": "empty_noop",
59
  "priority": 200,
60
+ "when": ["numel(shapes.x) == 0"],
61
  "passes": [
62
  {
63
  "id": "noop",
64
  "name": "MeanVarianceNormalization.Empty",
65
  "shader": "noop.wgsl.jinja",
66
+ "bindings": [{ "name": "params", "struct": [{ "name": "dummy", "type": "u32", "value": 0 }] }],
67
  "dispatch": { "x": 0 }
68
  }
69
  ]
 
71
  {
72
  "id": "all_axes_flat_split",
73
  "priority": 120,
74
+ "when": ["numel(shapes.x) > 0", "allAxesReduced", "numel(shapes.x) >= tunables.FLAT_SPLIT_MIN_ELEMENTS", "flatPathFits"],
75
+ "derive": { "scalar": "dtypes.T" },
76
  "intermediates": [
77
  { "id": "partials", "dtype": "float32", "shape": "[flatSplit, 2]" },
78
  { "id": "stats", "dtype": "float32", "shape": "[2]" }
 
81
  {
82
  "id": "partials",
83
  "name": "MeanVarianceNormalization.FlatPartials",
84
+ "shader": "norm-flat-splitk-partials.wgsl.jinja",
85
+ "derive": {
86
+ "workgroupSizeSpec": "maxWorkgroupSize",
87
+ "splitSpec": "flatSplit",
88
+ "usesF16Spec": "dtypes.T == \"f16\""
89
  },
90
+ "bindings": ["x", { "name": "partials", "buffer": "storage", "elementType": "vec2<f32>" }, "params"],
91
+ "dispatch": { "x": "min(flatSplit, 65535)", "y": "ceilDiv(flatSplit, 65535)", "z": 1 }
92
  },
93
  {
94
  "id": "combine",
95
  "name": "MeanVarianceNormalization.FlatCombine",
96
+ "shader": "norm-flat-splitk-combine.wgsl.jinja",
97
+ "derive": { "splitSpec": "flatSplit", "usesF16Spec": "dtypes.T == \"f16\"" },
98
+ "bindings": [
99
+ "x",
100
+ { "name": "partials", "buffer": "read-only-storage", "elementType": "vec2<f32>" },
101
+ { "name": "stats", "buffer": "storage", "elementType": "f32", "length": 2 },
102
+ "params"
103
+ ],
104
+ "dispatch": { "x": "min(1, 65535)", "y": "ceilDiv(1, 65535)", "z": 1 }
105
  },
106
  {
107
  "id": "apply",
108
  "name": "MeanVarianceNormalization.FlatApply",
109
+ "shader": "norm-flat-apply.wgsl.jinja",
110
+ "derive": {
111
+ "workgroupSizeSpec": "maxWorkgroupSize",
112
+ "scalar": "dtypes.T",
113
+ "usesF16Spec": "dtypes.T == \"f16\""
114
  },
115
+ "bindings": [
116
+ "x",
117
+ { "name": "stats", "buffer": "read-only-storage", "elementType": "f32", "length": 2 },
118
+ { "arg": "y", "elementType": "$scalar" },
119
+ { "name": "params", "struct": [{ "name": "count", "type": "u32", "value": "numel(shapes.y)" }] }
120
+ ],
121
+ "dispatch": {
122
+ "x": "min(ceilDiv((numel(shapes.y)), (maxWorkgroupSize)), 65535)",
123
+ "y": "ceilDiv(ceilDiv((numel(shapes.y)), (maxWorkgroupSize)), 65535)",
124
+ "z": 1
125
+ }
126
  }
127
  ]
128
  },
129
  {
130
  "id": "serial_rows",
131
  "priority": 115,
132
+ "when": ["numel(shapes.x) > 0", "reduceCount <= tunables.SERIAL_MAX_REDUCTION", "rowCount >= tunables.SERIAL_MIN_ROWS", "serialDispatchFits"],
133
+ "derive": { "scalar": "dtypes.T", "ioElement": "dtypes.T" },
134
  "passes": [
135
  {
136
  "id": "main",
137
  "name": "MeanVarianceNormalization.SerialRows",
138
+ "shader": "mean-variance-normalization-serial-rows.wgsl.jinja",
139
+ "derive": {
140
+ "xShape": "shapes.x",
141
+ "reduce": ["hasAxis(attrs.axes, 0, ranks.x)", "hasAxis(attrs.axes, 1, ranks.x)", "hasAxis(attrs.axes, 2, ranks.x)", "hasAxis(attrs.axes, 3, ranks.x)", "hasAxis(attrs.axes, 4, ranks.x)", "hasAxis(attrs.axes, 5, ranks.x)", "hasAxis(attrs.axes, 6, ranks.x)", "hasAxis(attrs.axes, 7, ranks.x)"],
142
+ "workgroupSizeSpec": "serialWorkgroupSize",
143
+ "scalar": "dtypes.T",
144
+ "usesF16Spec": "dtypes.T == \"f16\""
 
 
 
145
  },
146
+ "bindings": ["x_2", "y_2", "params_4"],
147
+ "dispatch": {
148
+ "x": "min(ceilDiv((rowCount), (serialWorkgroupSize)), 65535)",
149
+ "y": "ceilDiv(ceilDiv((rowCount), (serialWorkgroupSize)), 65535)",
150
+ "z": 1
151
+ }
152
  }
153
  ]
154
  },
155
  {
156
  "id": "cooperative_vec4",
157
  "priority": 110,
158
+ "when": ["numel(shapes.x) > 0", "reduceCount >= tunables.VEC4_MIN_REDUCTION", "vec4Eligible", "rowDispatchFits", "vectorStorageFits"],
159
+ "derive": { "scalar": "dtypes.T", "ioElement": "\"vec4<\" ~ dtypes.T ~ \">\"" },
 
 
 
 
160
  "passes": [
161
  {
162
  "id": "main",
163
  "name": "MeanVarianceNormalization.CooperativeVec4",
164
+ "shader": "mean-variance-normalization-subgroup.wgsl.jinja",
165
+ "subgroupCollectivesWidth": "portable",
166
+ "derive": {
167
+ "xShape": "shapes.x",
168
+ "reduce": ["hasAxis(attrs.axes, 0, ranks.x)", "hasAxis(attrs.axes, 1, ranks.x)", "hasAxis(attrs.axes, 2, ranks.x)", "hasAxis(attrs.axes, 3, ranks.x)", "hasAxis(attrs.axes, 4, ranks.x)", "hasAxis(attrs.axes, 5, ranks.x)", "hasAxis(attrs.axes, 6, ranks.x)", "hasAxis(attrs.axes, 7, ranks.x)"],
169
+ "wg": "vectorWorkgroupSize",
170
+ "scalar": "dtypes.T",
171
+ "vecType": "\"vec4<\" ~ dtypes.T ~ \">\"",
172
+ "usesF16Spec": "dtypes.T == \"f16\"",
173
+ "vectorizedSpec": true
 
 
 
 
 
174
  },
175
+ "bindings": ["x_2", "y_2", "params_4"],
176
+ "dispatch": { "x": "min(rowCount, 65535)", "y": "ceilDiv(rowCount, 65535)", "z": 1 }
177
  }
178
  ]
179
  },
180
  {
181
  "id": "cooperative_scalar",
182
  "priority": 100,
183
+ "when": ["numel(shapes.x) > 0", "rowDispatchFits", "scalarStorageFits"],
184
+ "derive": { "scalar": "dtypes.T", "ioElement": "dtypes.T" },
185
  "passes": [
186
  {
187
  "id": "main",
188
  "name": "MeanVarianceNormalization.CooperativeScalar",
189
+ "shader": "mean-variance-normalization-subgroup.wgsl.jinja",
190
+ "subgroupCollectivesWidth": "portable",
191
+ "derive": {
192
+ "xShape": "shapes.x",
193
+ "reduce": ["hasAxis(attrs.axes, 0, ranks.x)", "hasAxis(attrs.axes, 1, ranks.x)", "hasAxis(attrs.axes, 2, ranks.x)", "hasAxis(attrs.axes, 3, ranks.x)", "hasAxis(attrs.axes, 4, ranks.x)", "hasAxis(attrs.axes, 5, ranks.x)", "hasAxis(attrs.axes, 6, ranks.x)", "hasAxis(attrs.axes, 7, ranks.x)"],
194
+ "wg": "scalarWorkgroupSize",
195
+ "scalar": "dtypes.T",
196
+ "usesF16Spec": "dtypes.T == \"f16\"",
197
+ "vectorizedSpec": false
 
 
 
 
 
198
  },
199
+ "bindings": ["x_2", "y_2", "params_4"],
200
+ "dispatch": { "x": "min(rowCount, 65535)", "y": "ceilDiv(rowCount, 65535)", "z": 1 }
201
  }
202
  ]
203
  }
build/webgpu/mean-variance-normalization-serial-rows.wgsl.jinja CHANGED
@@ -1,4 +1,4 @@
1
- {% if source.usesF16 %}
2
  enable f16;
3
  {% endif %}
4
  {{ env.wgsl.resourceDeclarations }}
@@ -10,42 +10,41 @@ enable f16;
10
  // The reduction is shifted by its first value before accumulating moments. This
11
  // stabilizes the variance calculation while retaining the zero-variance ONNX
12
  // result as 0/0 = NaN.
13
- {% macro nd_offset(target, index, reduced, indent) %}
14
- {% set rank = source.xShape | length %}
15
  {% for i in range(rank) %}
16
- {% if source.reduce[i] if reduced else not source.reduce[i] %}
17
  {% set pa = namespace(v=1) %}
18
  {% set st = namespace(v=1) %}
19
  {% for j in range(i + 1, rank) %}
20
- {% set st.v = st.v * source.xShape[j] %}
21
- {% if source.reduce[j] if reduced else not source.reduce[j] %}{% set pa.v = pa.v * source.xShape[j] %}{% endif %}
22
  {% endfor %}
23
- {{ indent }}{{ target }} = {{ target }} + (({{ index }} / {{ pa.v }}u) % {{ source.xShape[i] }}u) * {{ st.v }}u;
24
  {% endif %}
25
  {% endfor %}
26
  {% endmacro %}
27
- const WG: u32 = {{ source.workgroupSize }}u;
28
- const R: u32 = {{ source.reduceCount }}u;
29
 
30
  @compute @workgroup_size(WG, 1, 1)
31
  fn main(
32
- @builtin(global_invocation_id) gid: vec3<u32>,
33
- @builtin(num_workgroups) nwg: vec3<u32>
34
  ) {
35
  // `threads` dispatches fold past the WebGPU x-dimension limit into y.
36
- let row = gid.x + gid.y * nwg.x * WG;
37
  if (row >= params.rows) {
38
  return;
39
  }
40
 
41
  var base_off = 0u;
42
- {{ nd_offset("base_off", "row", 0, " ") }}
43
  let shift = f32(x[base_off]);
44
  var sum_d = 0.0;
45
  var sum_d2 = 0.0;
46
  for (var r = 0u; r < R; r = r + 1u) {
47
  var off = base_off;
48
- {{ nd_offset("off", "r", 1, " ") }} let d = f32(x[off]) - shift;
49
  sum_d = sum_d + d;
50
  sum_d2 = sum_d2 + d * d;
51
  }
@@ -57,6 +56,6 @@ fn main(
57
 
58
  for (var r = 0u; r < R; r = r + 1u) {
59
  var off = base_off;
60
- {{ nd_offset("off", "r", 1, " ") }} y[off] = {{ source.scalar }}((f32(x[off]) - mean) / denom);
61
  }
62
  }
 
1
+ {% if usesF16Spec %}
2
  enable f16;
3
  {% endif %}
4
  {{ env.wgsl.resourceDeclarations }}
 
10
  // The reduction is shifted by its first value before accumulating moments. This
11
  // stabilizes the variance calculation while retaining the zero-variance ONNX
12
  // result as 0/0 = NaN.
13
+ {% macro nd_offset(target, index, reduced) %}
14
+ {% set rank = xShape | length %}
15
  {% for i in range(rank) %}
16
+ {% if reduce[i] if reduced else not reduce[i] %}
17
  {% set pa = namespace(v=1) %}
18
  {% set st = namespace(v=1) %}
19
  {% for j in range(i + 1, rank) %}
20
+ {% set st.v = st.v * xShape[j] %}
21
+ {% if reduce[j] if reduced else not reduce[j] %}{% set pa.v = pa.v * xShape[j] %}{% endif %}
22
  {% endfor %}
23
+ {{ target }} = {{ target }} + (({{ index }} / {{ pa.v }}u) % {{ xShape[i] }}u) * {{ st.v }}u;
24
  {% endif %}
25
  {% endfor %}
26
  {% endmacro %}
27
+ const WG: u32 = {{ workgroupSizeSpec }}u;
28
+ const R: u32 = {{ reduceCount }}u;
29
 
30
  @compute @workgroup_size(WG, 1, 1)
31
  fn main(
32
+ @builtin(global_invocation_id) gid: vec3<u32>
 
33
  ) {
34
  // `threads` dispatches fold past the WebGPU x-dimension limit into y.
35
+ let row = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * WG;
36
  if (row >= params.rows) {
37
  return;
38
  }
39
 
40
  var base_off = 0u;
41
+ {{ nd_offset("base_off", "row", 0) | indent(2, true) }}
42
  let shift = f32(x[base_off]);
43
  var sum_d = 0.0;
44
  var sum_d2 = 0.0;
45
  for (var r = 0u; r < R; r = r + 1u) {
46
  var off = base_off;
47
+ {{ nd_offset("off", "r", 1) | indent(4, true) }} let d = f32(x[off]) - shift;
48
  sum_d = sum_d + d;
49
  sum_d2 = sum_d2 + d * d;
50
  }
 
56
 
57
  for (var r = 0u; r < R; r = r + 1u) {
58
  var off = base_off;
59
+ {{ nd_offset("off", "r", 1) | indent(4, true) }} y[off] = {{ scalar }}((f32(x[off]) - mean) / denom);
60
  }
61
  }
build/webgpu/mean-variance-normalization-subgroup.wgsl.jinja CHANGED
@@ -1,58 +1,67 @@
1
- {% if source.usesF16 %}
2
  enable f16;
3
  {% endif %}
4
- {% if source.useSubgroups %}
5
  enable subgroups;
6
  {% endif %}
7
  {{ env.wgsl.resourceDeclarations }}
8
 
9
- {% macro nd_offset(target, index, reduced, indent) %}
10
- {% set rank = source.xShape | length %}
11
  {% for i in range(rank) %}
12
- {% if source.reduce[i] if reduced else not source.reduce[i] %}
13
  {% set pa = namespace(v=1) %}
14
  {% set st = namespace(v=1) %}
15
  {% for j in range(i + 1, rank) %}
16
- {% set st.v = st.v * source.xShape[j] %}
17
- {% if source.reduce[j] if reduced else not source.reduce[j] %}{% set pa.v = pa.v * source.xShape[j] %}{% endif %}
18
  {% endfor %}
19
- {{ indent }}{{ target }} = {{ target }} + (({{ index }} / {{ pa.v }}u) % {{ source.xShape[i] }}u) * {{ st.v }}u;
20
  {% endif %}
21
  {% endfor %}
22
  {% endmacro %}
23
- const WG: u32 = {{ source.wg }}u;
24
- const R: u32 = {{ source.reduceCount }}u;
25
- {% if source.vectorized %}
26
  const RV: u32 = R / 4u;
27
 
28
  {% endif %}
29
- {% if source.useSubgroups %}
30
- {% if source.wg > source.minSubgroupSize %}
31
- // The device-reported minimum subgroup width bounds cross-subgroup partials.
32
- const MAX_SG: u32 = {{ source.maxSubgroups }}u;
33
- var<workgroup> sg_partials: array<vec2<f32>, MAX_SG>;
34
- {% endif %}
35
  {% else %}
36
  var<workgroup> wg_red: array<vec2<f32>, WG>;
37
  {% endif %}
38
 
39
- fn reduce_pair(value: vec2<f32>{% if not source.useSubgroups or source.wg > source.minSubgroupSize %}, tid: u32{% endif %}{% if source.useSubgroups and source.wg > source.minSubgroupSize %}, sg_lane: u32, sg_size: u32{% endif %}) -> vec2<f32> {
40
- {% if source.useSubgroups %}
41
  let s = vec2<f32>(subgroupAdd(value.x), subgroupAdd(value.y));
42
- {% if source.wg > source.minSubgroupSize %}
43
- if (sg_lane == 0u) {
44
- sg_partials[tid / sg_size] = s;
 
 
 
45
  }
 
 
 
 
 
 
 
 
 
 
 
46
  workgroupBarrier();
47
- let num_sg = (WG + sg_size - 1u) / sg_size;
48
  var total = vec2<f32>(0.0, 0.0);
49
- for (var i = 0u; i < num_sg; i = i + 1u) {
50
  total = total + sg_partials[i];
51
  }
52
- return total;
53
- {% else %}
54
- return s;
55
- {% endif %}
56
  {% else %}
57
  wg_red[tid] = value;
58
  workgroupBarrier();
@@ -69,14 +78,12 @@ fn reduce_pair(value: vec2<f32>{% if not source.useSubgroups or source.wg > sour
69
  @compute @workgroup_size(WG, 1, 1)
70
  fn main(
71
  @builtin(workgroup_id) wg_id: vec3<u32>,
72
- @builtin(local_invocation_id) lid: vec3<u32>,
73
- @builtin(num_workgroups) nwg: vec3<u32>
74
- {%- if source.useSubgroups and source.wg > source.minSubgroupSize %},
75
- @builtin(subgroup_invocation_id) sg_lane: u32,
76
  @builtin(subgroup_size) sg_size: u32
77
  {%- endif %}
78
  ) {
79
- let row = wg_id.x + wg_id.y * nwg.x;
80
  if (row >= params.rows) {
81
  return;
82
  }
@@ -84,21 +91,21 @@ fn main(
84
 
85
  // Base offset from the kept-axis coordinates.
86
  var base_off = 0u;
87
- {{ nd_offset("base_off", "row", 0, " ") }}
88
- {% if source.vectorized %}
89
  let shift = f32(x[base_off / 4u].x);
90
  {% else %}
91
  let shift = f32(x[base_off]);
92
  {% endif %}
93
  var acc = vec2<f32>(0.0, 0.0);
94
- {% if source.vectorized %}
95
  for (var q = tid; q < RV; q = q + WG) {
96
  let r = q * 4u;
97
  {% else %}
98
  for (var r = tid; r < R; r = r + WG) {
99
  {% endif %}
100
  var off = base_off;
101
- {{ nd_offset("off", "r", 1, " ") }}{% if source.vectorized %}
102
  let d = vec4<f32>(x[off / 4u]) - vec4<f32>(shift);
103
  acc.x = acc.x + d.x + d.y + d.z + d.w;
104
  acc.y = acc.y + dot(d, d);
@@ -109,28 +116,24 @@ fn main(
109
  {% endif %}
110
  }
111
 
112
- {% if source.useSubgroups %}
113
- let totals = reduce_pair(acc{% if source.wg > source.minSubgroupSize %}, tid, sg_lane, sg_size{% endif %});
114
- {% else %}
115
- let totals = reduce_pair(acc, tid);
116
- {% endif %}
117
  let mean_d = totals.x / f32(R);
118
  let variance = max(totals.y / f32(R) - mean_d * mean_d, 0.0);
119
  let mean = shift + mean_d;
120
  let denom = sqrt(variance);
121
 
122
- {% if source.vectorized %}
123
  for (var q = tid; q < RV; q = q + WG) {
124
  let r = q * 4u;
125
  {% else %}
126
  for (var r = tid; r < R; r = r + WG) {
127
  {% endif %}
128
  var off = base_off;
129
- {{ nd_offset("off", "r", 1, " ") }}{% if source.vectorized %}
130
  let v = vec4<f32>(x[off / 4u]);
131
- y[off / 4u] = {{ source.vecType }}((v - vec4<f32>(mean)) / vec4<f32>(denom));
132
  {% else %}
133
- y[off] = {{ source.scalar }}((f32(x[off]) - mean) / denom);
134
  {% endif %}
135
  }
136
  }
 
1
+ {% if usesF16Spec %}
2
  enable f16;
3
  {% endif %}
4
+ {% if useSubgroups %}
5
  enable subgroups;
6
  {% endif %}
7
  {{ env.wgsl.resourceDeclarations }}
8
 
9
+ {% macro nd_offset(target, index, reduced) %}
10
+ {% set rank = xShape | length %}
11
  {% for i in range(rank) %}
12
+ {% if reduce[i] if reduced else not reduce[i] %}
13
  {% set pa = namespace(v=1) %}
14
  {% set st = namespace(v=1) %}
15
  {% for j in range(i + 1, rank) %}
16
+ {% set st.v = st.v * xShape[j] %}
17
+ {% if reduce[j] if reduced else not reduce[j] %}{% set pa.v = pa.v * xShape[j] %}{% endif %}
18
  {% endfor %}
19
+ {{ target }} = {{ target }} + (({{ index }} / {{ pa.v }}u) % {{ xShape[i] }}u) * {{ st.v }}u;
20
  {% endif %}
21
  {% endfor %}
22
  {% endmacro %}
23
+ const WG: u32 = {{ wg }}u;
24
+ const R: u32 = {{ reduceCount }}u;
25
+ {% if vectorizedSpec %}
26
  const RV: u32 = R / 4u;
27
 
28
  {% endif %}
29
+ {% if useSubgroups %}
30
+ // One slot per invocation: the cross-subgroup fold indexes by
31
+ // local_invocation_id, never by a subgroup ordinal, so no subgroup width or
32
+ // partition can overflow or alias it.
33
+ var<workgroup> sg_partials: array<vec2<f32>, WG>;
 
34
  {% else %}
35
  var<workgroup> wg_red: array<vec2<f32>, WG>;
36
  {% endif %}
37
 
38
+ fn reduce_pair(value: vec2<f32>, tid: u32{% if useSubgroups %}, sg_size: u32{% endif %}) -> vec2<f32> {
39
+ {% if useSubgroups %}
40
  let s = vec2<f32>(subgroupAdd(value.x), subgroupAdd(value.y));
41
+ // The whole workgroup is one subgroup: the collective above already covers it
42
+ // (no shared memory, no barrier). The test reads the `subgroup_size` builtin,
43
+ // which is uniform; a collective's result is not uniform to WGSL's analysis
44
+ // and may not guard a barrier.
45
+ if (sg_size == WG) {
46
+ return s;
47
  }
48
+ // Cross-subgroup fold that assumes nothing about which invocations share a
49
+ // subgroup, how many subgroups there are, or which of a subgroup's lanes are
50
+ // active: every invocation owns the slot at its own index, the elected lane
51
+ // publishes its subgroup pair there and every other lane publishes the sum
52
+ // identity. Each subgroup then folds all WG slots — lane `rank`, its dense
53
+ // position among the active lanes, walks slots rank, rank + count, ... — and
54
+ // one more collective merges the lane partials, so every slot is added
55
+ // exactly once at any legal width and partition.
56
+ let rank = subgroupExclusiveAdd(1u);
57
+ let count = subgroupAdd(1u);
58
+ sg_partials[tid] = select(vec2<f32>(0.0, 0.0), s, rank == 0u);
59
  workgroupBarrier();
 
60
  var total = vec2<f32>(0.0, 0.0);
61
+ for (var i = rank; i < WG; i = i + count) {
62
  total = total + sg_partials[i];
63
  }
64
+ return vec2<f32>(subgroupAdd(total.x), subgroupAdd(total.y));
 
 
 
65
  {% else %}
66
  wg_red[tid] = value;
67
  workgroupBarrier();
 
78
  @compute @workgroup_size(WG, 1, 1)
79
  fn main(
80
  @builtin(workgroup_id) wg_id: vec3<u32>,
81
+ @builtin(local_invocation_id) lid: vec3<u32>
82
+ {%- if useSubgroups %},
 
 
83
  @builtin(subgroup_size) sg_size: u32
84
  {%- endif %}
85
  ) {
86
+ let row = wg_id.x + wg_id.y * {{ DISPATCH_FOLD_WIDTH }}u;
87
  if (row >= params.rows) {
88
  return;
89
  }
 
91
 
92
  // Base offset from the kept-axis coordinates.
93
  var base_off = 0u;
94
+ {{ nd_offset("base_off", "row", 0) | indent(2, true) }}
95
+ {% if vectorizedSpec %}
96
  let shift = f32(x[base_off / 4u].x);
97
  {% else %}
98
  let shift = f32(x[base_off]);
99
  {% endif %}
100
  var acc = vec2<f32>(0.0, 0.0);
101
+ {% if vectorizedSpec %}
102
  for (var q = tid; q < RV; q = q + WG) {
103
  let r = q * 4u;
104
  {% else %}
105
  for (var r = tid; r < R; r = r + WG) {
106
  {% endif %}
107
  var off = base_off;
108
+ {{ nd_offset("off", "r", 1) | indent(4, true) }}{% if vectorizedSpec %}
109
  let d = vec4<f32>(x[off / 4u]) - vec4<f32>(shift);
110
  acc.x = acc.x + d.x + d.y + d.z + d.w;
111
  acc.y = acc.y + dot(d, d);
 
116
  {% endif %}
117
  }
118
 
119
+ let totals = reduce_pair(acc, tid{% if useSubgroups %}, sg_size{% endif %});
 
 
 
 
120
  let mean_d = totals.x / f32(R);
121
  let variance = max(totals.y / f32(R) - mean_d * mean_d, 0.0);
122
  let mean = shift + mean_d;
123
  let denom = sqrt(variance);
124
 
125
+ {% if vectorizedSpec %}
126
  for (var q = tid; q < RV; q = q + WG) {
127
  let r = q * 4u;
128
  {% else %}
129
  for (var r = tid; r < R; r = r + WG) {
130
  {% endif %}
131
  var off = base_off;
132
+ {{ nd_offset("off", "r", 1) | indent(4, true) }}{% if vectorizedSpec %}
133
  let v = vec4<f32>(x[off / 4u]);
134
+ y[off / 4u] = {{ vecType }}((v - vec4<f32>(mean)) / vec4<f32>(denom));
135
  {% else %}
136
+ y[off] = {{ scalar }}((f32(x[off]) - mean) / denom);
137
  {% endif %}
138
  }
139
  }
build/webgpu/metadata.json CHANGED
@@ -1,23 +1,32 @@
1
  {
2
  "name": "ai.onnx.MeanVarianceNormalization",
3
- "id": "_ai_onnx_meanvariancenormalization_webgpu_09532e8",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
- "bench.json": "Z7dtgwBXEEaVuYjwvsi+nzdPnqr+T3GbPX6F4A4qUyQ=",
11
- "manifest.json": "8549q1hb8wFCRzXlrsGTXzEyEVO5WtP64OtKI3Hq6Ug=",
12
- "mean-variance-normalization-serial-rows.wgsl.jinja": "C/TVvmYhaLeq+yKB4nAK535avgH9kcYaeSZKnRY/Df0=",
13
- "mean-variance-normalization-subgroup.wgsl.jinja": "eFY8+N5f0/V/npXvheysb002bNEWtJLxNE//2aGzIr0=",
14
  "noop.wgsl.jinja": "k/5BMD6UO81N7XlF+t4iSKyt3dbtcqNMCru5aUKNBKE=",
15
- "norm-flat-apply.wgsl.jinja": "O8G9eyv748OmW3GMQjqSFbgsiJ/pW/plJAzAbFGvGNc=",
16
- "norm-flat-splitk-combine.wgsl.jinja": "uBFuaqUxbf6Qr2uE5huMcdP2+S2nHF+zM3RH0tzAx9o=",
17
- "norm-flat-splitk-partials.wgsl.jinja": "YwepfH5ztse2kwIz4xhl80Gw5GEnWNMue0oGZUD47p8=",
18
- "test.json": "OAgHM6FyfQCVSV8h47D92KbimR901cmqk1fTTm4g3q8="
19
  }
20
  },
21
- "provenance": { "kernel": { "sha": "2e7068faf55e7f43df740015f6d1ee49391a41c5", "dirty": false } },
22
- "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/ai.onnx.MeanVarianceNormalization" }
 
 
 
 
 
 
 
 
 
23
  }
 
1
  {
2
  "name": "ai.onnx.MeanVarianceNormalization",
3
+ "id": "_ai_onnx_meanvariancenormalization_webgpu_b716a78",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
+ "bench.json": "zH0PcLLzlKTcTGM2WzPi8keFUjvDcCnttgcQLxJ9WuU=",
11
+ "manifest.json": "Snfz0B5Kkd7enbnP+ETHCh/vyME+C3vu4puxVB1xF3c=",
12
+ "mean-variance-normalization-serial-rows.wgsl.jinja": "qcoGxwmc0jShVeozos6da6awjOJbbIIKLfoMCTyzNuk=",
13
+ "mean-variance-normalization-subgroup.wgsl.jinja": "GTbYa5JP9Z186OoP6/BZ6w9CyZD2iihXHHl2Cil91pE=",
14
  "noop.wgsl.jinja": "k/5BMD6UO81N7XlF+t4iSKyt3dbtcqNMCru5aUKNBKE=",
15
+ "norm-flat-apply.wgsl.jinja": "fJkwUp+SOc5+EV2M+skzaoOyf4cCAj6RE/l9EFpRzlw=",
16
+ "norm-flat-splitk-combine.wgsl.jinja": "qrWEZGCTfaNnkDSBX7p6j+pnJHKCaAwFmHIXe0DJQC0=",
17
+ "norm-flat-splitk-partials.wgsl.jinja": "mQFvRpYjvQsoFwdFiShP0nMYFrWbaGmWxYIMXrSi4Sc=",
18
+ "test.json": "bGHJmclwK4mv21Rm4JYKnWsrwe5GeHdDnBXy+q8fIj8="
19
  }
20
  },
21
+ "provenance": { "kernel": { "sha": "91d990483a174128daf7673f3f37a7c890493ae1", "dirty": false } },
22
+ "webgpu": {
23
+ "manifestSpec": "2.0",
24
+ "variants": {
25
+ "empty_noop": ["noop.wgsl.jinja"],
26
+ "all_axes_flat_split": ["norm-flat-apply.wgsl.jinja", "norm-flat-splitk-combine.wgsl.jinja", "norm-flat-splitk-partials.wgsl.jinja"],
27
+ "serial_rows": ["mean-variance-normalization-serial-rows.wgsl.jinja"],
28
+ "cooperative_vec4": ["mean-variance-normalization-subgroup.wgsl.jinja"],
29
+ "cooperative_scalar": ["mean-variance-normalization-subgroup.wgsl.jinja"]
30
+ }
31
+ }
32
  }
build/webgpu/norm-flat-apply.wgsl.jinja CHANGED
@@ -1,14 +1,13 @@
1
- {% if source.usesF16 %}
2
  enable f16;
3
  {% endif %}
4
  {{ env.wgsl.resourceDeclarations }}
5
 
6
- @compute @workgroup_size({{ source.workgroupSize }}, 1, 1)
7
- fn main(@builtin(global_invocation_id) gid: vec3<u32>,
8
- @builtin(num_workgroups) nwg: vec3<u32>) {
9
- let i = gid.x + gid.y * nwg.x * {{ source.workgroupSize }}u;
10
  if (i >= params.count) {
11
  return;
12
  }
13
- y[i] = {{ source.scalar }}((f32(x[i]) - stats[0]) / stats[1]);
14
  }
 
1
+ {% if usesF16Spec %}
2
  enable f16;
3
  {% endif %}
4
  {{ env.wgsl.resourceDeclarations }}
5
 
6
+ @compute @workgroup_size({{ workgroupSizeSpec }}, 1, 1)
7
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
8
+ let i = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ workgroupSizeSpec }}u;
 
9
  if (i >= params.count) {
10
  return;
11
  }
12
+ y[i] = {{ scalar }}((f32(x[i]) - stats[0]) / stats[1]);
13
  }
build/webgpu/norm-flat-splitk-combine.wgsl.jinja CHANGED
@@ -1,9 +1,9 @@
1
- {% if source.usesF16 %}
2
  enable f16;
3
  {% endif %}
4
  {{ env.wgsl.resourceDeclarations }}
5
 
6
- const SPLIT: u32 = {{ source.split }}u;
7
 
8
  @compute @workgroup_size(1, 1, 1)
9
  fn main() {
 
1
+ {% if usesF16Spec %}
2
  enable f16;
3
  {% endif %}
4
  {{ env.wgsl.resourceDeclarations }}
5
 
6
+ const SPLIT: u32 = {{ splitSpec }}u;
7
 
8
  @compute @workgroup_size(1, 1, 1)
9
  fn main() {
build/webgpu/norm-flat-splitk-partials.wgsl.jinja CHANGED
@@ -1,10 +1,10 @@
1
- {% if source.usesF16 %}
2
  enable f16;
3
  {% endif %}
4
  {{ env.wgsl.resourceDeclarations }}
5
 
6
- const WG: u32 = {{ source.workgroupSize }}u;
7
- const SPLIT: u32 = {{ source.split }}u;
8
 
9
  var<workgroup> reduction: array<vec2<f32>, WG>;
10
 
 
1
+ {% if usesF16Spec %}
2
  enable f16;
3
  {% endif %}
4
  {{ env.wgsl.resourceDeclarations }}
5
 
6
+ const WG: u32 = {{ workgroupSizeSpec }}u;
7
+ const SPLIT: u32 = {{ splitSpec }}u;
8
 
9
  var<workgroup> reduction: array<vec2<f32>, WG>;
10
 
build/webgpu/test.json CHANGED
@@ -1,5 +1,4 @@
1
  {
2
- "op": "ai.onnx.MeanVarianceNormalization",
3
  "fixtureArrays": {
4
  "ort_axes_subset_rank5_0_2_4_input_x": [0.6369617, 0.2697867, 0.0409735, 0.0165276, 0.8132702, 0.9127556, 0.6066358, 0.7294966, 0.543625, 0.9350724, 0.8158536, 0.0027385, 0.8574043, 0.0335856, 0.7296554, 0.1756556, 0.8631789, 0.5414612, 0.2997119, 0.4226872, 0.0283197, 0.1242833, 0.6706244, 0.6471895, 0.6153851, 0.3836776, 0.9972099, 0.9808353, 0.685542, 0.6504593, 0.6884467, 0.3889214]
5
  },
@@ -144,7 +143,7 @@
144
  "provenance": {
145
  "source": "onnxruntime/test/providers/cpu/tensor/mean_variance_normalization_test.cc",
146
  "test": "MeanVarianceNormalizationTest.DefaultAxes",
147
- "notes": "Rank-4 default/spatial axes companion: the variance is positive but subnormal, so the normalized output should be finite."
148
  },
149
  "attrs": { "axes": [0, 2, 3] },
150
  "inputs": {
@@ -165,7 +164,7 @@
165
  "provenance": {
166
  "source": "onnxruntime/test/providers/cpu/tensor/mean_variance_normalization_test.cc",
167
  "test": "MeanVarianceNormalizationTest.DefaultAxes",
168
- "notes": "Rank-5 companion: the reduced variance is positive but subnormal, so normalization should produce finite values."
169
  },
170
  "attrs": { "axes": [0, 2, 3, 4] },
171
  "inputs": {
@@ -259,7 +258,7 @@
259
  {
260
  "name": "f32_spatial_axes_2x8x32x32",
261
  "provenance": {
262
- "notes": "Compact sibling for the spatial-axes MVN benchmark; preserves axes=[2,3] over many channel planes without benchmark-scale tensors."
263
  },
264
  "attrs": { "axes": [2, 3] },
265
  "inputs": {
@@ -566,7 +565,7 @@
566
  {
567
  "name": "rank5_serial_rows_channel_axis_f16",
568
  "provenance": {
569
- "notes": "float16 on the coalesced serial-row route, which needs at least SERIAL_MIN_ROWS independent rows and a reduction within SERIAL_MAX_REDUCTION. Only f32 cases and a bench had reached it."
570
  },
571
  "attrs": { "axes": [1] },
572
  "inputs": {
@@ -577,6 +576,23 @@
577
  }
578
  },
579
  "outputs": { "y": { "dtype": "float16", "shape": [2, 32, 8, 8, 4], "tolerance": 0.02 } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
580
  }
581
  ]
582
  }
 
1
  {
 
2
  "fixtureArrays": {
3
  "ort_axes_subset_rank5_0_2_4_input_x": [0.6369617, 0.2697867, 0.0409735, 0.0165276, 0.8132702, 0.9127556, 0.6066358, 0.7294966, 0.543625, 0.9350724, 0.8158536, 0.0027385, 0.8574043, 0.0335856, 0.7296554, 0.1756556, 0.8631789, 0.5414612, 0.2997119, 0.4226872, 0.0283197, 0.1242833, 0.6706244, 0.6471895, 0.6153851, 0.3836776, 0.9972099, 0.9808353, 0.685542, 0.6504593, 0.6884467, 0.3889214]
4
  },
 
143
  "provenance": {
144
  "source": "onnxruntime/test/providers/cpu/tensor/mean_variance_normalization_test.cc",
145
  "test": "MeanVarianceNormalizationTest.DefaultAxes",
146
+ "notes": "With rank-4 default spatial axes, a positive subnormal variance must produce a finite normalized output."
147
  },
148
  "attrs": { "axes": [0, 2, 3] },
149
  "inputs": {
 
164
  "provenance": {
165
  "source": "onnxruntime/test/providers/cpu/tensor/mean_variance_normalization_test.cc",
166
  "test": "MeanVarianceNormalizationTest.DefaultAxes",
167
+ "notes": "With a rank-5 input, a positive subnormal reduced variance must produce finite normalized values."
168
  },
169
  "attrs": { "axes": [0, 2, 3, 4] },
170
  "inputs": {
 
258
  {
259
  "name": "f32_spatial_axes_2x8x32x32",
260
  "provenance": {
261
+ "notes": "A compact input exercises axes [2,3] over many channel planes without benchmark-scale tensors."
262
  },
263
  "attrs": { "axes": [2, 3] },
264
  "inputs": {
 
565
  {
566
  "name": "rank5_serial_rows_channel_axis_f16",
567
  "provenance": {
568
+ "notes": "Float16 data exercises the coalesced serial-row path with at least SERIAL_MIN_ROWS independent rows and a reduction within SERIAL_MAX_REDUCTION."
569
  },
570
  "attrs": { "axes": [1] },
571
  "inputs": {
 
576
  }
577
  },
578
  "outputs": { "y": { "dtype": "float16", "shape": [2, 32, 8, 8, 4], "tolerance": 0.02 } }
579
+ },
580
+ {
581
+ "name": "rank7_last_axis_above_axis_table",
582
+ "provenance": {
583
+ "notes": "A rank-7 input reduced along its final axis exercises axis 6. Each independent 64-element row must be normalized across that last dimension."
584
+ },
585
+ "attrs": { "axes": [-1] },
586
+ "inputs": {
587
+ "x": {
588
+ "dtype": "float32",
589
+ "shape": [1, 4, 2, 2, 2, 2, 64],
590
+ "data": { "kind": "fillFloat32", "sinStep": 0.013, "cosStep": 0.029, "scale": 1.0 }
591
+ }
592
+ },
593
+ "outputs": {
594
+ "y": { "dtype": "float32", "shape": [1, 4, 2, 2, 2, 2, 64], "tolerance": 0.00002, "relTolerance": 0.00002 }
595
+ }
596
  }
597
  ]
598
  }