Xenova HF Staff commited on
Commit
28e88e5
·
verified ·
1 Parent(s): 1f79789

sync 91d990483a17

Browse files
README.md CHANGED
@@ -18,17 +18,17 @@ See the [ONNX `GroupNormalization` spec](https://onnx.ai/onnx/operators/onnx__Gr
18
 
19
  ## Inputs
20
 
21
- | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
22
  | --- | --- | --- | --- | --- | --- | --- |
23
- | `X` | `x` | `T` | — | — | Input data tensor of shape `(N x C x D1 x ... x Dn)` where `N` is batch size and `C` is the number of channels. | required |
24
- | `scale` | `scale` | `T` | `1` | — | Scale tensor of shape `(C)`, one value per channel. | required |
25
- | `bias` | `bias` | `T` | `1` | — | Bias tensor of shape `(C)`, one value per channel. | required |
26
 
27
  ## Outputs
28
 
29
- | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
30
  | --- | --- | --- | --- | --- | --- | --- |
31
- | `Y` | `y` | `T` | same as `X` | same as `X` | Normalized output tensor of the same shape as `X`. | required |
32
 
33
  ## Attributes
34
 
@@ -37,8 +37,8 @@ Attributes and default values (overridable per request):
37
  | Attribute | Default | Description |
38
  | --- | --- | --- |
39
  | `epsilon` | `0.00001` | Small value added to the variance denominator to avoid division by zero. |
40
- | `stash_type` | `1` | TensorProto element type used for the normalization stage: `1` computes in float32, while `10` computes in float16. Normalized values are cast back to the input type before scale and bias are applied. |
41
  | `num_groups` | — | Required number of groups to divide the channels into; must be a divisor of `C`. |
 
42
 
43
  ## Type constraints
44
 
@@ -48,7 +48,7 @@ Attributes and default values (overridable per request):
48
 
49
  ## Files
50
 
51
- - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, provenance)
52
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
53
  - [`test.json`](build/webgpu/test.json) — correctness cases
54
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
@@ -59,10 +59,14 @@ Attributes and default values (overridable per request):
59
 
60
  ## Use with `@huggingface/kernels`
61
 
62
- The loader derives every required output's shape and logical dtype from the manifest contract and this call.
63
- It then allocates the result tensors automatically.
 
 
 
64
 
65
  The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
 
66
 
67
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
68
 
 
18
 
19
  ## Inputs
20
 
21
+ | Name | Upstream name | Logical dtype | Rank | Shape | Description | Presence |
22
  | --- | --- | --- | --- | --- | --- | --- |
23
+ | `x` | `X` | `T` | — | — | Input data tensor of shape `(N x C x D1 x ... x Dn)` where `N` is batch size and `C` is the number of channels. | required |
24
+ | `scale` | | `T` | `1` | — | Scale tensor of shape `(C)`, one value per channel. | required |
25
+ | `bias` | | `T` | `1` | — | Bias tensor of shape `(C)`, one value per channel. | required |
26
 
27
  ## Outputs
28
 
29
+ | Name | Upstream name | Logical dtype | Rank | Shape | Description | Presence |
30
  | --- | --- | --- | --- | --- | --- | --- |
31
+ | `y` | `Y` | `T` | same as `x` | same as `x` | Normalized output tensor of the same shape as `X`. | required |
32
 
33
  ## Attributes
34
 
 
37
  | Attribute | Default | Description |
38
  | --- | --- | --- |
39
  | `epsilon` | `0.00001` | Small value added to the variance denominator to avoid division by zero. |
 
40
  | `num_groups` | — | Required number of groups to divide the channels into; must be a divisor of `C`. |
41
+ | `stash_type` | `1` | TensorProto element type used for the normalization stage: `1` computes in float32, while `10` computes in float16. Normalized values are cast back to the input type before scale and bias are applied. |
42
 
43
  ## Type constraints
44
 
 
48
 
49
  ## Files
50
 
51
+ - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, per-variant templates, provenance)
52
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
53
  - [`test.json`](build/webgpu/test.json) — correctness cases
54
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
 
59
 
60
  ## Use with `@huggingface/kernels`
61
 
62
+ ```sh
63
+ npm install --save-exact @huggingface/kernels@0.0.1-preview.2
64
+ ```
65
+
66
+ Required output shapes and logical data types are inferred from the supplied inputs and attributes; result tensors are allocated automatically.
67
 
68
  The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
69
+ It follows the `v1` branch as fixes land. To pin exact artifact bytes, pass a 40-character commit `revision` instead of `version`.
70
 
71
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
72
 
build/webgpu/bench.json CHANGED
@@ -1,5 +1,4 @@
1
  {
2
- "op": "ai.onnx.GroupNormalization",
3
  "cases": [
4
  {
5
  "name": "4x64x128x128_g32",
 
1
  {
 
2
  "cases": [
3
  {
4
  "name": "4x64x128x128_g32",
build/webgpu/group-normalization-splitk-apply.wgsl.jinja CHANGED
@@ -1,6 +1,3 @@
1
- {% if usesF16 %}
2
- enable f16;
3
- {% endif %}
4
  {{ env.wgsl.resourceDeclarations }}
5
 
6
  const HIDDEN: u32 = {{ hiddenSize }}u;
 
 
 
 
1
  {{ env.wgsl.resourceDeclarations }}
2
 
3
  const HIDDEN: u32 = {{ hiddenSize }}u;
build/webgpu/group-normalization-splitk-partials.wgsl.jinja CHANGED
@@ -1,6 +1,3 @@
1
- {% if usesF16 %}
2
- enable f16;
3
- {% endif %}
4
  {{ env.wgsl.resourceDeclarations }}
5
 
6
  const HIDDEN: u32 = {{ hiddenSize }}u;
 
 
 
 
1
  {{ env.wgsl.resourceDeclarations }}
2
 
3
  const HIDDEN: u32 = {{ hiddenSize }}u;
build/webgpu/manifest.json CHANGED
@@ -2,55 +2,31 @@
2
  "domain": "ai.onnx",
3
  "name": "GroupNormalization",
4
  "sinceVersion": 21,
5
- "description": "Applies group normalization to the input: `y = scale * (x - mean) / sqrt(variance + epsilon) + bias`, where mean and variance are computed per instance per group of channels. The number of groups `num_groups` must divide the channel count `C` evenly; when `num_groups == C` this is equivalent to InstanceNormalization, and when `num_groups == 1` it is equivalent to LayerNormalization. The normalization stage supports TensorProto `stash_type` values `1` (float32) and `10` (float16).",
6
- "inputs": [
7
- {
8
- "role": "X",
9
- "dtype": "T",
10
- "description": "Input data tensor of shape `(N x C x D1 x ... x Dn)` where `N` is batch size and `C` is the number of channels."
11
- },
12
- { "role": "scale", "dtype": "T", "rank": 1, "description": "Scale tensor of shape `(C)`, one value per channel." },
13
- { "role": "bias", "dtype": "T", "rank": 1, "description": "Bias tensor of shape `(C)`, one value per channel." }
14
- ],
15
- "outputs": [
16
- {
17
- "role": "Y",
18
- "dtype": "T",
19
- "rank": "ranks.X",
20
- "description": "Normalized output tensor of the same shape as `X`.",
21
- "shape": "shapes.X"
22
- }
23
- ],
24
- "attributes": { "epsilon": 0.00001, "stash_type": 1 },
25
- "attributeDescriptions": {
26
- "epsilon": "Small value added to the variance denominator to avoid division by zero.",
27
- "num_groups": "Required number of groups to divide the channels into; must be a divisor of `C`.",
28
- "stash_type": "TensorProto element type used for the normalization stage: `1` computes in float32, while `10` computes in float16. Normalized values are cast back to the input type before scale and bias are applied."
29
  },
 
 
30
  "attributeConstraints": { "num_groups": { "required": true }, "stash_type": { "values": [1, 10] } },
31
  "typeConstraints": { "T": ["float32", "float16"] },
32
- "args": {
33
- "x": { "kind": "tensor", "semantic": "X", "role": "input" },
34
- "scale": { "kind": "tensor", "semantic": "scale", "role": "input" },
35
- "bias": { "kind": "tensor", "semantic": "bias", "role": "input" },
36
- "y": { "kind": "tensor", "semantic": "Y", "role": "output" }
37
- },
38
  "tunables": {
39
- "WORKGROUP_SIZE": 256,
40
- "MAX_STATS_SPLITS": 64,
41
- "STATS_VALUES_PER_SPLIT": 4096,
42
- "SPLIT_STATS_MIN_HIDDEN": 65536,
43
- "SPLIT_STATS_MAX_ROWS": 256
44
  },
45
  "derive": {
46
  "deviceWorkgroupCap": "min(device.limits.maxComputeInvocationsPerWorkgroup, device.limits.maxComputeWorkgroupSizeX)",
47
  "groupAttributesOk": "attrs.num_groups >= 1",
48
- "groupShapeOk": "groupAttributesOk and f16Ok(dtypes.T) and ranks.X >= 3 and ranks.scale == 1 and ranks.bias == 1 and ranks.Y == ranks.X and sameShape(shapes.Y, shapes.X) and dim(shapes.scale, 0) == dim(shapes.X, 1) and dim(shapes.bias, 0) == dim(shapes.X, 1) and dim(shapes.X, 1) % attrs.num_groups == 0",
49
  "groupContractOk": "groupShapeOk and attrs.stash_type == onnxDtypeCode(\"float32\")",
50
  "groupStashF16Ok": "groupShapeOk and attrs.stash_type == onnxDtypeCode(\"float16\")",
51
- "groupRows": "dim(shapes.X, 0) * attrs.num_groups if groupAttributesOk else 0",
52
- "groupSpatial": "inner(shapes.X, 1)",
53
- "groupChannelsPerGroup": "dim(shapes.X, 1) / attrs.num_groups if groupAttributesOk else 0",
54
  "groupHidden": "groupChannelsPerGroup * groupSpatial",
55
  "normDeviceWorkgroupCap": "min(tunables.WORKGROUP_SIZE, deviceWorkgroupCap)",
56
  "normWorkgroupCap": "max(1, pow2ceil(normDeviceWorkgroupCap + 1) / 2)",
@@ -59,98 +35,39 @@
59
  "hasSubgroupId": "device.features.has(\"subgroups\") and device.wgslLanguageFeatures.has(\"subgroup_id\")",
60
  "groupRowWorkgroupBytes": "normWorkgroupCap * 2 * 4",
61
  "groupRowCovered": "groupContractOk and groupRowWorkgroupBytes <= device.limits.maxComputeWorkgroupStorageSize",
62
- "groupSplitCount": "min(tunables.MAX_STATS_SPLITS, device.limits.maxComputeWorkgroupsPerDimension, pow2ceil(ceilDiv(groupHidden, tunables.STATS_VALUES_PER_SPLIT)))",
63
  "groupPartialBytes": "groupRows * groupSplitCount * 2 * 4",
64
- "groupSplitCovered": "groupRowCovered and groupRows <= tunables.SPLIT_STATS_MAX_ROWS and groupRows <= device.limits.maxComputeWorkgroupsPerDimension and groupHidden >= tunables.SPLIT_STATS_MIN_HIDDEN and groupPartialBytes <= device.limits.maxStorageBufferBindingSize and groupPartialBytes <= device.limits.maxBufferSize"
65
  },
66
- "bindingSets": {
67
- "norm": [
68
- {
69
- "name": "x",
70
- "arg": "x",
71
- "semantic": "X",
72
- "buffer": { "type": "read-only-storage" },
73
- "elementType": "$ioElement"
74
- },
75
- {
76
- "name": "scale",
77
- "arg": "scale",
78
- "semantic": "scale",
79
- "buffer": { "type": "read-only-storage" },
80
- "elementType": "$scalar"
81
- },
82
- {
83
- "name": "bias",
84
- "arg": "bias",
85
- "semantic": "bias",
86
- "buffer": { "type": "read-only-storage" },
87
- "elementType": "$scalar"
88
- },
89
- { "name": "y", "arg": "y", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$ioElement" },
90
- {
91
- "name": "params",
92
- "semantic": "kernel.params",
93
- "buffer": { "type": "uniform" },
94
- "struct": {
95
- "name": "Params",
96
- "fields": [
97
- { "name": "rows", "type": "u32", "value": "groupRows" },
98
- {
99
- "name": "rowStride",
100
- "type": "u32",
101
- "value": "max(1, min(groupRows, device.limits.maxComputeWorkgroupsPerDimension))"
102
- }
103
- ]
104
  }
105
- }
106
- ],
107
- "splitPartials": [
108
- { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
109
- { "name": "partials", "semantic": "partials", "buffer": { "type": "storage" }, "elementType": "vec2<f32>" },
110
- {
111
- "name": "params",
112
- "semantic": "kernel.params",
113
- "buffer": { "type": "uniform" },
114
- "struct": { "name": "Params", "fields": [{ "name": "rows", "type": "u32", "value": "groupRows" }] }
115
- }
116
- ],
117
- "splitApply": [
118
- { "name": "x", "arg": "x", "semantic": "X", "buffer": { "type": "read-only-storage" }, "elementType": "$scalar" },
119
- {
120
- "name": "scale",
121
- "arg": "scale",
122
- "semantic": "scale",
123
- "buffer": { "type": "read-only-storage" },
124
- "elementType": "$scalar"
125
- },
126
- {
127
- "name": "bias",
128
- "arg": "bias",
129
- "semantic": "bias",
130
- "buffer": { "type": "read-only-storage" },
131
- "elementType": "$scalar"
132
- },
133
- {
134
- "name": "partials",
135
- "semantic": "partials",
136
- "buffer": { "type": "read-only-storage" },
137
- "elementType": "vec2<f32>"
138
- },
139
- { "name": "y", "arg": "y", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$scalar" },
140
- {
141
- "name": "params",
142
- "semantic": "kernel.params",
143
- "buffer": { "type": "uniform" },
144
- "struct": { "name": "Params", "fields": [{ "name": "rows", "type": "u32", "value": "groupRows" }] }
145
- }
146
- ]
147
  },
148
  "variants": [
149
  {
150
  "id": "group_stash_f16_serial",
151
  "priority": 1000,
152
- "when": "groupStashF16Ok",
153
- "constants": {
154
  "scalar": "dtypes.T",
155
  "ioElement": "dtypes.T",
156
  "usesF16": "dtypes.T == \"f16\"",
@@ -165,8 +82,8 @@
165
  "id": "main",
166
  "name": "GroupNormalization.StashF16Serial",
167
  "shader": "group-normalization-stash-f16-serial.wgsl.jinja",
168
- "bindings": "norm",
169
- "dispatch": { "workgroups": "groupRows" }
170
  }
171
  ]
172
  },
@@ -174,7 +91,7 @@
174
  "id": "group_splitk",
175
  "priority": 120,
176
  "when": ["groupSplitCovered"],
177
- "constants": {
178
  "scalar": "dtypes.T",
179
  "usesF16": "dtypes.T == \"f16\"",
180
  "hiddenSize": "groupHidden",
@@ -191,15 +108,30 @@
191
  "id": "partials",
192
  "name": "GroupNormalization.SplitKPartials",
193
  "shader": "group-normalization-splitk-partials.wgsl.jinja",
194
- "bindings": "splitPartials",
195
- "dispatch": { "workgroups": "groupRows", "z": "groupSplitCount" }
 
 
 
 
196
  },
197
  {
198
  "id": "apply",
199
  "name": "GroupNormalization.SplitKApply",
200
  "shader": "group-normalization-splitk-apply.wgsl.jinja",
201
- "bindings": "splitApply",
202
- "dispatch": { "workgroups": "groupRows", "z": "groupSplitCount" }
 
 
 
 
 
 
 
 
 
 
 
203
  }
204
  ]
205
  },
@@ -207,36 +139,30 @@
207
  "id": "group_subgroup_vec4",
208
  "priority": 110,
209
  "when": ["groupRowCovered", "groupSpatial % 4 == 0"],
210
- "constants": {
211
- "scalar": "dtypes.T",
212
- "ioElement": "\"vec4<\" ~ dtypes.T ~ \">\"",
213
- "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\""
214
- },
215
  "passes": [
216
  {
217
  "id": "main",
218
  "name": "GroupNormalization.group_subgroup_vec4",
219
- "source": {
220
- "shader": "norm-row-stats.wgsl.jinja",
221
- "inputs": {
222
- "mode": "\"group\"",
223
- "vec4": true,
224
- "scalar": "dtypes.T",
225
- "usesF16": "dtypes.T == \"f16\"",
226
- "hidden": "groupHidden",
227
- "wg": "groupVec4Workgroup",
228
- "epsilon": "attrs.epsilon",
229
- "numGroups": "attrs.num_groups",
230
- "cpg": "groupChannelsPerGroup",
231
- "hiddenVec": "groupHidden / 4",
232
- "vecType": "\"vec4<\" ~ dtypes.T ~ \">\"",
233
- "spatialVec": "groupSpatial / 4",
234
- "combineSubgroups": "hasSubgroupId"
235
- }
236
  },
237
- "subgroupCollectivesWidth": "portable",
238
- "bindings": "norm",
239
- "dispatch": { "workgroups": "groupRows" }
240
  }
241
  ]
242
  },
@@ -244,30 +170,28 @@
244
  "id": "group_subgroup",
245
  "priority": 100,
246
  "when": ["groupRowCovered"],
247
- "constants": { "scalar": "dtypes.T", "ioElement": "dtypes.T" },
248
  "passes": [
249
  {
250
  "id": "main",
251
  "name": "GroupNormalization.group_subgroup",
252
- "source": {
253
- "shader": "norm-row-stats.wgsl.jinja",
254
- "inputs": {
255
- "mode": "\"group\"",
256
- "vec4": false,
257
- "scalar": "dtypes.T",
258
- "usesF16": "dtypes.T == \"f16\"",
259
- "hidden": "groupHidden",
260
- "wg": "groupScalarWorkgroup",
261
- "epsilon": "attrs.epsilon",
262
- "numGroups": "attrs.num_groups",
263
- "cpg": "groupChannelsPerGroup",
264
- "spatial": "groupSpatial",
265
- "combineSubgroups": "hasSubgroupId"
266
- }
267
  },
268
- "subgroupCollectivesWidth": "portable",
269
- "bindings": "norm",
270
- "dispatch": { "workgroups": "groupRows" }
271
  }
272
  ]
273
  }
 
2
  "domain": "ai.onnx",
3
  "name": "GroupNormalization",
4
  "sinceVersion": 21,
5
+ "inputs": {
6
+ "x": { "onnx": "X", "dtype": "T" },
7
+ "scale": { "dtype": "T", "rank": 1 },
8
+ "bias": { "dtype": "T", "rank": 1 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  },
10
+ "outputs": { "y": { "onnx": "Y", "dtype": "T", "rank": "ranks.x", "shape": "shapes.x" } },
11
+ "attributes": { "epsilon": { "default": 0.00001 }, "stash_type": { "default": 1 }, "num_groups": {} },
12
  "attributeConstraints": { "num_groups": { "required": true }, "stash_type": { "values": [1, 10] } },
13
  "typeConstraints": { "T": ["float32", "float16"] },
 
 
 
 
 
 
14
  "tunables": {
15
+ "WORKGROUP_SIZE": { "default": 256 },
16
+ "MAX_STATS_SPLITS": { "default": 64 },
17
+ "STATS_VALUES_PER_SPLIT": { "default": 4096 },
18
+ "SPLIT_STATS_MIN_HIDDEN": { "default": 65536 },
19
+ "SPLIT_STATS_MAX_ROWS": { "default": 256 }
20
  },
21
  "derive": {
22
  "deviceWorkgroupCap": "min(device.limits.maxComputeInvocationsPerWorkgroup, device.limits.maxComputeWorkgroupSizeX)",
23
  "groupAttributesOk": "attrs.num_groups >= 1",
24
+ "groupShapeOk": "groupAttributesOk and f16Ok(dtypes.T) and ranks.x >= 3 and ranks.scale == 1 and ranks.bias == 1 and ranks.y == ranks.x and sameShape(shapes.y, shapes.x) and dim(shapes.scale, 0) == dim(shapes.x, 1) and dim(shapes.bias, 0) == dim(shapes.x, 1) and dim(shapes.x, 1) % attrs.num_groups == 0",
25
  "groupContractOk": "groupShapeOk and attrs.stash_type == onnxDtypeCode(\"float32\")",
26
  "groupStashF16Ok": "groupShapeOk and attrs.stash_type == onnxDtypeCode(\"float16\")",
27
+ "groupRows": "dim(shapes.x, 0) * attrs.num_groups if groupAttributesOk else 0",
28
+ "groupSpatial": "inner(shapes.x, 1)",
29
+ "groupChannelsPerGroup": "dim(shapes.x, 1) / attrs.num_groups if groupAttributesOk else 0",
30
  "groupHidden": "groupChannelsPerGroup * groupSpatial",
31
  "normDeviceWorkgroupCap": "min(tunables.WORKGROUP_SIZE, deviceWorkgroupCap)",
32
  "normWorkgroupCap": "max(1, pow2ceil(normDeviceWorkgroupCap + 1) / 2)",
 
35
  "hasSubgroupId": "device.features.has(\"subgroups\") and device.wgslLanguageFeatures.has(\"subgroup_id\")",
36
  "groupRowWorkgroupBytes": "normWorkgroupCap * 2 * 4",
37
  "groupRowCovered": "groupContractOk and groupRowWorkgroupBytes <= device.limits.maxComputeWorkgroupStorageSize",
38
+ "groupSplitCount": "min(tunables.MAX_STATS_SPLITS, min(device.limits.maxComputeWorkgroupsPerDimension, 65535), pow2ceil(ceilDiv(groupHidden, tunables.STATS_VALUES_PER_SPLIT)))",
39
  "groupPartialBytes": "groupRows * groupSplitCount * 2 * 4",
40
+ "groupSplitCovered": "groupRowCovered and groupRows <= tunables.SPLIT_STATS_MAX_ROWS and groupRows <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535) and groupHidden >= tunables.SPLIT_STATS_MIN_HIDDEN and groupPartialBytes <= device.limits.maxStorageBufferBindingSize and groupPartialBytes <= device.limits.maxBufferSize"
41
  },
42
+ "bindings": {
43
+ "x": { "buffer": "read-only-storage", "elementType": "$ioElement" },
44
+ "scale": { "buffer": "read-only-storage", "elementType": "$scalar" },
45
+ "bias": { "buffer": "read-only-storage", "elementType": "$scalar" },
46
+ "y": { "buffer": "storage", "elementType": "$ioElement" },
47
+ "params": {
48
+ "buffer": "uniform",
49
+ "struct": [
50
+ { "name": "rows", "type": "u32", "value": "groupRows" },
51
+ {
52
+ "name": "rowStride",
53
+ "type": "u32",
54
+ "value": "max(1, min(groupRows, min(device.limits.maxComputeWorkgroupsPerDimension, 65535)))"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  }
56
+ ]
57
+ },
58
+ "x_2": { "name": "x", "buffer": "read-only-storage", "elementType": "$scalar" },
59
+ "params_2": {
60
+ "name": "params",
61
+ "buffer": "uniform",
62
+ "struct": [{ "name": "rows", "type": "u32", "value": "groupRows" }]
63
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  },
65
  "variants": [
66
  {
67
  "id": "group_stash_f16_serial",
68
  "priority": 1000,
69
+ "when": ["groupStashF16Ok"],
70
+ "derive": {
71
  "scalar": "dtypes.T",
72
  "ioElement": "dtypes.T",
73
  "usesF16": "dtypes.T == \"f16\"",
 
82
  "id": "main",
83
  "name": "GroupNormalization.StashF16Serial",
84
  "shader": "group-normalization-stash-f16-serial.wgsl.jinja",
85
+ "bindings": ["x", "scale", "bias", "y", "params"],
86
+ "dispatch": { "x": "min(groupRows, 65535)", "y": "ceilDiv(groupRows, 65535)", "z": 1 }
87
  }
88
  ]
89
  },
 
91
  "id": "group_splitk",
92
  "priority": 120,
93
  "when": ["groupSplitCovered"],
94
+ "derive": {
95
  "scalar": "dtypes.T",
96
  "usesF16": "dtypes.T == \"f16\"",
97
  "hiddenSize": "groupHidden",
 
108
  "id": "partials",
109
  "name": "GroupNormalization.SplitKPartials",
110
  "shader": "group-normalization-splitk-partials.wgsl.jinja",
111
+ "bindings": ["x_2", { "name": "partials", "buffer": "storage", "elementType": "vec2<f32>" }, "params_2"],
112
+ "dispatch": {
113
+ "x": "min(groupRows, DISPATCH_FOLD_WIDTH)",
114
+ "y": "ceilDiv(groupRows, DISPATCH_FOLD_WIDTH)",
115
+ "z": "groupSplitCount"
116
+ }
117
  },
118
  {
119
  "id": "apply",
120
  "name": "GroupNormalization.SplitKApply",
121
  "shader": "group-normalization-splitk-apply.wgsl.jinja",
122
+ "bindings": [
123
+ "x_2",
124
+ "scale",
125
+ "bias",
126
+ { "name": "partials", "buffer": "read-only-storage", "elementType": "vec2<f32>" },
127
+ { "arg": "y", "elementType": "$scalar" },
128
+ "params_2"
129
+ ],
130
+ "dispatch": {
131
+ "x": "min(groupRows, DISPATCH_FOLD_WIDTH)",
132
+ "y": "ceilDiv(groupRows, DISPATCH_FOLD_WIDTH)",
133
+ "z": "groupSplitCount"
134
+ }
135
  }
136
  ]
137
  },
 
139
  "id": "group_subgroup_vec4",
140
  "priority": 110,
141
  "when": ["groupRowCovered", "groupSpatial % 4 == 0"],
142
+ "derive": { "scalar": "dtypes.T", "ioElement": "\"vec4<\" ~ dtypes.T ~ \">\"" },
 
 
 
 
143
  "passes": [
144
  {
145
  "id": "main",
146
  "name": "GroupNormalization.group_subgroup_vec4",
147
+ "shader": "norm-row-stats.wgsl.jinja",
148
+ "derive": {
149
+ "modeSpec": "\"group\"",
150
+ "vec4": true,
151
+ "scalar": "dtypes.T",
152
+ "usesF16Spec": "dtypes.T == \"f16\"",
153
+ "hidden": "groupHidden",
154
+ "wg": "groupVec4Workgroup",
155
+ "epsilon": "attrs.epsilon",
156
+ "numGroupsSpec": "attrs.num_groups",
157
+ "cpg": "groupChannelsPerGroup",
158
+ "hiddenVec": "groupHidden / 4",
159
+ "vecType": "\"vec4<\" ~ dtypes.T ~ \">\"",
160
+ "spatialVec": "groupSpatial / 4",
161
+ "combineSubgroups": "hasSubgroupId"
 
 
162
  },
163
+ "bindings": ["x", "scale", "bias", "y", "params"],
164
+ "dispatch": { "x": "min(groupRows, 65535)", "y": "ceilDiv(groupRows, 65535)", "z": 1 },
165
+ "subgroupCollectivesWidth": "portable"
166
  }
167
  ]
168
  },
 
170
  "id": "group_subgroup",
171
  "priority": 100,
172
  "when": ["groupRowCovered"],
173
+ "derive": { "scalar": "dtypes.T", "ioElement": "dtypes.T" },
174
  "passes": [
175
  {
176
  "id": "main",
177
  "name": "GroupNormalization.group_subgroup",
178
+ "shader": "norm-row-stats.wgsl.jinja",
179
+ "derive": {
180
+ "modeSpec": "\"group\"",
181
+ "vec4": false,
182
+ "scalar": "dtypes.T",
183
+ "usesF16Spec": "dtypes.T == \"f16\"",
184
+ "hidden": "groupHidden",
185
+ "wg": "groupScalarWorkgroup",
186
+ "epsilon": "attrs.epsilon",
187
+ "numGroupsSpec": "attrs.num_groups",
188
+ "cpg": "groupChannelsPerGroup",
189
+ "spatial": "groupSpatial",
190
+ "combineSubgroups": "hasSubgroupId"
 
 
191
  },
192
+ "bindings": ["x", "scale", "bias", "y", "params"],
193
+ "dispatch": { "x": "min(groupRows, 65535)", "y": "ceilDiv(groupRows, 65535)", "z": 1 },
194
+ "subgroupCollectivesWidth": "portable"
195
  }
196
  ]
197
  }
build/webgpu/metadata.json CHANGED
@@ -1,21 +1,29 @@
1
  {
2
  "name": "ai.onnx.GroupNormalization",
3
- "id": "_ai_onnx_groupnormalization_webgpu_da50174",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
- "bench.json": "R60LEBSM13qdMqFSIdwpekWs9JTpBlgud7FNGLzPDdg=",
11
- "group-normalization-splitk-apply.wgsl.jinja": "7Q93kvTqmCixZN4Z7GpxYdlxwaxiAUMjH7AMKHq8pfc=",
12
- "group-normalization-splitk-partials.wgsl.jinja": "6FNPr81N83OW3DWYMjJln/B5zUsKTdinV610BRzzRvs=",
13
  "group-normalization-stash-f16-serial.wgsl.jinja": "Wez9kqS+lzZASbm2BpWZSsHuNTT4rqsus0NiyhmGmvY=",
14
- "manifest.json": "nWQpLNZpKQYFo8Pkd/yNT3xboHmv9HZJ5S28OKU86TE=",
15
- "norm-row-stats.wgsl.jinja": "RCAuBcGKN1mg+npi5UTIqD4zDk0+B8DQj6u2TH+4IXU=",
16
- "test.json": "KbF1vAUrTyrgdXUaq67pwqn41JjEAcHxnO2dImDHUiM="
17
  }
18
  },
19
- "provenance": { "kernel": { "sha": "2e7068faf55e7f43df740015f6d1ee49391a41c5", "dirty": false } },
20
- "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/ai.onnx.GroupNormalization" }
 
 
 
 
 
 
 
 
21
  }
 
1
  {
2
  "name": "ai.onnx.GroupNormalization",
3
+ "id": "_ai_onnx_groupnormalization_webgpu_a85b638",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
+ "bench.json": "gttctaT32ACO8eeDLTBubmz2KmY+JbRp40eLp4BLAiw=",
11
+ "group-normalization-splitk-apply.wgsl.jinja": "bJQ3aD6iCak7YjlH5yZZGPo8FIGY8CuCz3BwVnTnUPE=",
12
+ "group-normalization-splitk-partials.wgsl.jinja": "DVgljhomjpQ4XizLxIEh8NckAby7lIb6+plUaGh812A=",
13
  "group-normalization-stash-f16-serial.wgsl.jinja": "Wez9kqS+lzZASbm2BpWZSsHuNTT4rqsus0NiyhmGmvY=",
14
+ "manifest.json": "A56uOwydeaOGyMd1ulwyeOrEUd7G8uLRbxougyW6s8g=",
15
+ "norm-row-stats.wgsl.jinja": "CyRuHHc7bYmXEhtvfCxLRvjhidAMvicRA5nuJJESwxg=",
16
+ "test.json": "jfHQT3aDoszhxWXbi2YtXb9bqMfNRQyaM3HFdPdOWA0="
17
  }
18
  },
19
+ "provenance": { "kernel": { "sha": "91d990483a174128daf7673f3f37a7c890493ae1", "dirty": false } },
20
+ "webgpu": {
21
+ "manifestSpec": "2.0",
22
+ "variants": {
23
+ "group_stash_f16_serial": ["group-normalization-stash-f16-serial.wgsl.jinja"],
24
+ "group_splitk": ["group-normalization-splitk-apply.wgsl.jinja", "group-normalization-splitk-partials.wgsl.jinja"],
25
+ "group_subgroup_vec4": ["norm-row-stats.wgsl.jinja"],
26
+ "group_subgroup": ["norm-row-stats.wgsl.jinja"]
27
+ }
28
+ }
29
  }
build/webgpu/norm-row-stats.wgsl.jinja CHANGED
@@ -1,8 +1,18 @@
1
- {% if source.usesF16 %}
2
  enable f16;
3
  {% endif %}
4
- {% set combineSubgroups = source.combineSubgroups %}
5
- {% set scalarIo = source.scalarIo if source.scalarIo is defined else false %}
 
 
 
 
 
 
 
 
 
 
6
  {% set reduceThreadParameters = ", sg_lane: u32, sg_id: u32, num_sg: u32"
7
  if combineSubgroups else ", tid: u32" %}
8
  {% set reduceThreadArguments = ", sg_lane, sg_id, num_sg"
@@ -22,21 +32,64 @@ enable subgroups;
22
  //
23
  // Shifted moments avoid cancellation from a large common offset; normalize as
24
  // (x - mean) / sqrt(variance + EPSILON).
25
- const HIDDEN: u32 = {{ source.hidden }}u;
26
- {% if source.vec4 %}
27
- const HIDDEN_V: u32 = {{ source.hiddenVec }}u;
28
- {% endif %}
29
- const WG: u32 = {{ source.wg }}u;
30
- const EPSILON: f32 = {{ source.epsilon }};
31
- const NUM_GROUPS: u32 = {{ source.numGroups }}u;
32
- const CPG: u32 = {{ source.cpg }}u;
33
- {% if source.vec4 %}
34
- const SPATIAL_V: u32 = {{ source.spatialVec }}u;
 
 
 
 
 
 
 
35
  {% else %}
36
- const SPATIAL: u32 = {{ source.spatial }}u;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  {% endif %}
38
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
 
 
 
 
 
40
 
41
  {% if combineSubgroups %}
42
  var<workgroup> sg_partials: array<vec2<f32>, WG>;
@@ -95,29 +148,52 @@ fn main(
95
  return;
96
  }
97
  let tid = lid.x;
98
- {% if source.vec4 and not scalarIo %}
 
 
 
 
 
 
 
99
  let base = row * HIDDEN_V;
100
  {% else %}
101
  let base = row * HIDDEN;
102
  {% endif %}
103
 
104
- {% if source.vec4 %}
 
 
 
105
  let shift = f32(x[base].x);
 
106
  {% else %}
107
  let shift = f32(x[base]);
108
  {% endif %}
109
 
110
  var acc = vec2<f32>(0.0, 0.0);
111
- {% if source.vec4 %}
112
  for (var i = tid; i < HIDDEN_V; i = i + WG) {
 
 
 
 
 
 
113
  let v = vec4<f32>(x[base + i]);
 
114
  let d = v - vec4<f32>(shift);
115
  acc.x = acc.x + d.x + d.y + d.z + d.w;
116
  acc.y = acc.y + dot(d, d);
117
  }
118
  {% else %}
119
  for (var i = tid; i < HIDDEN; i = i + WG) {
 
 
 
 
120
  let v = f32(x[base + i]);
 
121
  let d = v - shift;
122
  acc.x = acc.x + d;
123
  acc.y = acc.y + d * d;
@@ -132,21 +208,51 @@ fn main(
132
  let row_mean = shift + mean_d;
133
  let g_ch_base = (row % NUM_GROUPS) * CPG;
134
 
135
- {% if source.vec4 %}
 
 
 
136
  for (var i = tid; i < HIDDEN_V; i = i + WG) {
 
 
 
 
 
 
 
137
  let idx = base + i;
138
  let v = vec4<f32>(x[idx]);
 
139
  let ch = g_ch_base + i / SPATIAL_V;
140
  let normed = (v - vec4<f32>(row_mean)) / vec4<f32>(denom);
141
- y[idx] = {{ source.vecType }}(normed * vec4<f32>(f32(scale[ch])) + vec4<f32>(f32(bias[ch])));
142
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  {% else %}
144
  for (var i = tid; i < HIDDEN; i = i + WG) {
145
  let idx = base + i;
 
 
 
146
  let v = f32(x[idx]);
 
147
  let ch = g_ch_base + i / SPATIAL;
148
  let normed = (v - row_mean) / denom;
149
- y[idx] = {{ source.scalar }}(normed * f32(scale[ch]) + f32(bias[ch]));
150
  }
151
  {% endif %}
152
  }
 
1
+ {% if usesF16Spec %}
2
  enable f16;
3
  {% endif %}
4
+ {% set combineSubgroups = combineSubgroups %}
5
+ {% set scalarIo = scalarIo if scalarIo is defined else false %}
6
+ {% set packedBf16Embedding = packedBf16Embedding if packedBf16Embedding is defined else false %}
7
+ {% set rmsChainNorm = rmsChainNorm if rmsChainNorm is defined else false %}
8
+ {% set hiddenPairs = hiddenPairs | default(0) %}
9
+ {% set numRows = numRows | default(0) %}
10
+ {% set epsilon = epsilon | default("0.0") %}
11
+ {% set epsilon2 = epsilon2 | default("0.0") %}
12
+ {% set numGroupsSpec = numGroupsSpec | default(0) %}
13
+ {% set cpg = cpg | default(0) %}
14
+ {% set spatialVec = spatialVec | default(0) %}
15
+ {% set spatial = spatial | default(0) %}
16
  {% set reduceThreadParameters = ", sg_lane: u32, sg_id: u32, num_sg: u32"
17
  if combineSubgroups else ", tid: u32" %}
18
  {% set reduceThreadArguments = ", sg_lane, sg_id, num_sg"
 
32
  //
33
  // Shifted moments avoid cancellation from a large common offset; normalize as
34
  // (x - mean) / sqrt(variance + EPSILON).
35
+ const HIDDEN: u32 = {{ hidden }}u;
36
+ {% if vec4 %}
37
+ const HIDDEN_V: u32 = {{ hiddenVec }}u;
38
+ {% endif %}
39
+ {% if packedBf16Embedding %}
40
+ const HIDDEN_PAIRS: u32 = {{ hiddenPairs }}u;
41
+ const NUM_ROWS: u32 = {{ numRows }}u;
42
+ {% endif %}
43
+ const WG: u32 = {{ wg }}u;
44
+ const EPSILON: f32 = {{ epsilon }};
45
+ {% if rmsChainNorm %}
46
+ const EPSILON2: f32 = {{ epsilon2 }};
47
+ {% endif %}
48
+ const NUM_GROUPS: u32 = {{ numGroupsSpec }}u;
49
+ const CPG: u32 = {{ cpg }}u;
50
+ {% if vec4 %}
51
+ const SPATIAL_V: u32 = {{ spatialVec }}u;
52
  {% else %}
53
+ const SPATIAL: u32 = {{ spatial }}u;
54
+ {% endif %}
55
+
56
+ {% if packedBf16Embedding %}
57
+ {% if vec4 %}
58
+ fn unpack_bf16_pair(word: u32) -> vec2<f32> {
59
+ let bits = vec2<u32>(word & 0xffffu, word >> 16u);
60
+ return bitcast<vec2<f32>>(bits << vec2<u32>(16u));
61
+ }
62
+ {% endif %}
63
+
64
+ {% if not vec4 %}
65
+ fn embedding_scalar(source_row: u32, hidden: u32) -> f32 {
66
+ if (source_row >= NUM_ROWS) {
67
+ return 0.0;
68
+ }
69
+ let word = x[source_row * HIDDEN_PAIRS + (hidden >> 1u)];
70
+ let bits = select(word & 0xffffu, word >> 16u, (hidden & 1u) != 0u);
71
+ return bitcast<f32>(bits << 16u);
72
+ }
73
  {% endif %}
74
 
75
+ {% if vec4 %}
76
+ fn embedding_vec4(source_row: u32, hidden_vec: u32) -> vec4<f32> {
77
+ if (source_row >= NUM_ROWS) {
78
+ return vec4<f32>(0.0);
79
+ }
80
+ let base = source_row * HIDDEN_PAIRS + hidden_vec * 2u;
81
+ let low = unpack_bf16_pair(x[base]);
82
+ let high = unpack_bf16_pair(x[base + 1u]);
83
+ return vec4<f32>(low, high);
84
+ }
85
+ {% endif %}
86
+ {% endif %}
87
 
88
+ {% if vec4 and scalarIo %}
89
+ fn load_vec4(index: u32) -> vec4<f32> {
90
+ return vec4<f32>(x[index], x[index + 1u], x[index + 2u], x[index + 3u]);
91
+ }
92
+ {% endif %}
93
 
94
  {% if combineSubgroups %}
95
  var<workgroup> sg_partials: array<vec2<f32>, WG>;
 
148
  return;
149
  }
150
  let tid = lid.x;
151
+ {% if packedBf16Embedding %}
152
+ let source_row = indices[row];
153
+ {% if vec4 %}
154
+ let base = row * HIDDEN_V;
155
+ {% else %}
156
+ let base = row * HIDDEN;
157
+ {% endif %}
158
+ {% elif vec4 and not scalarIo %}
159
  let base = row * HIDDEN_V;
160
  {% else %}
161
  let base = row * HIDDEN;
162
  {% endif %}
163
 
164
+ {% if vec4 %}
165
+ {% if scalarIo %}
166
+ let shift = f32(x[base]);
167
+ {% else %}
168
  let shift = f32(x[base].x);
169
+ {% endif %}
170
  {% else %}
171
  let shift = f32(x[base]);
172
  {% endif %}
173
 
174
  var acc = vec2<f32>(0.0, 0.0);
175
+ {% if vec4 %}
176
  for (var i = tid; i < HIDDEN_V; i = i + WG) {
177
+ {% if packedBf16Embedding %}
178
+ let v = embedding_vec4(source_row, i);
179
+ embedding_out[base + i] = v;
180
+ {% elif scalarIo %}
181
+ let v = load_vec4(base + i * 4u);
182
+ {% else %}
183
  let v = vec4<f32>(x[base + i]);
184
+ {% endif %}
185
  let d = v - vec4<f32>(shift);
186
  acc.x = acc.x + d.x + d.y + d.z + d.w;
187
  acc.y = acc.y + dot(d, d);
188
  }
189
  {% else %}
190
  for (var i = tid; i < HIDDEN; i = i + WG) {
191
+ {% if packedBf16Embedding %}
192
+ let v = embedding_scalar(source_row, i);
193
+ embedding_out[base + i] = v;
194
+ {% else %}
195
  let v = f32(x[base + i]);
196
+ {% endif %}
197
  let d = v - shift;
198
  acc.x = acc.x + d;
199
  acc.y = acc.y + d * d;
 
208
  let row_mean = shift + mean_d;
209
  let g_ch_base = (row % NUM_GROUPS) * CPG;
210
 
211
+ {% if rmsChainNorm %}
212
+ var acc2 = 0.0;
213
+ {% endif %}
214
+ {% if vec4 %}
215
  for (var i = tid; i < HIDDEN_V; i = i + WG) {
216
+ {% if packedBf16Embedding %}
217
+ let idx = base + i;
218
+ let v = embedding_vec4(source_row, i);
219
+ {% elif scalarIo %}
220
+ let idx = base + i * 4u;
221
+ let v = load_vec4(idx);
222
+ {% else %}
223
  let idx = base + i;
224
  let v = vec4<f32>(x[idx]);
225
+ {% endif %}
226
  let ch = g_ch_base + i / SPATIAL_V;
227
  let normed = (v - vec4<f32>(row_mean)) / vec4<f32>(denom);
228
+ y[idx] = {{ vecType }}(normed * vec4<f32>(f32(scale[ch])) + vec4<f32>(f32(bias[ch])));
229
  }
230
+ {% if rmsChainNorm %}
231
+
232
+ // The chained second norm reads the residual row this loop just stored. This
233
+ // barrier completes those stores and any preceding shared-scratch use before
234
+ // the next reduction reuses its scratch; each lane then re-reads only the
235
+ // elements it wrote itself.
236
+ workgroupBarrier();
237
+ let total2 = reduce_scalar(acc2{{ reduceThreadArguments }});
238
+ let inv2 = inverseSqrt(total2 / f32(HIDDEN) + EPSILON2);
239
+ for (var i = tid; i < HIDDEN_V; i = i + WG) {
240
+ let idx = base + i;
241
+ let hv = vec4<f32>(y[idx]);
242
+ normed2[idx] = {{ vecType }}(hv * inv2 * vec4<f32>(scale2[i]));
243
+ }
244
+ {% endif %}
245
  {% else %}
246
  for (var i = tid; i < HIDDEN; i = i + WG) {
247
  let idx = base + i;
248
+ {% if packedBf16Embedding %}
249
+ let v = embedding_scalar(source_row, i);
250
+ {% else %}
251
  let v = f32(x[idx]);
252
+ {% endif %}
253
  let ch = g_ch_base + i / SPATIAL;
254
  let normed = (v - row_mean) / denom;
255
+ y[idx] = {{ scalar }}(normed * f32(scale[ch]) + f32(bias[ch]));
256
  }
257
  {% endif %}
258
  }
build/webgpu/test.json CHANGED
@@ -1,5 +1,4 @@
1
  {
2
- "op": "ai.onnx.GroupNormalization",
3
  "fixtureArrays": {
4
  "batch2_groups2_f32_input_x": [1, 2, 3, 4, 5, 6, 7, 8, -1, -2, -3, -4, 8, 7, 6, 5],
5
  "ort_rank3_equivalent_instance_norm_groups_equal_channels_input_x": [3.1513367, 9.283596, 1.4546119, 5.4617004, 8.519701, 1.2382338, 1.7930176, 5.1099434, 7.9195533, 7.638727, 8.065445, 3.8082376, 2.3667817, 2.8248506, 3.7754705, 5.861325, 5.058735, 3.2787242, 3.6843839, 9.755121, 2.7902672, 7.3974323, 8.283609, 8.488337],
@@ -107,7 +106,7 @@
107
  "provenance": {
108
  "source": "onnxruntime/test/providers/cpu/nn/group_norm_op_test.cc",
109
  "test": "GroupNormalizationOpTest.GroupSize_N",
110
- "notes": "Larger grouped companion: subnormal scales should survive across channels and spatial positions."
111
  },
112
  "attrs": { "num_groups": 2, "epsilon": 0.00001 },
113
  "inputs": {
@@ -693,7 +692,7 @@
693
  {
694
  "name": "group_splitk_layernorm_equiv_65536_f16",
695
  "provenance": {
696
- "notes": "float16 twin of the split-K group reduction. Only f32 cases reached the split-K partial and apply shaders, so neither had ever emitted its f16 declaration."
697
  },
698
  "attrs": { "num_groups": 1, "epsilon": 0.00001 },
699
  "inputs": {
 
1
  {
 
2
  "fixtureArrays": {
3
  "batch2_groups2_f32_input_x": [1, 2, 3, 4, 5, 6, 7, 8, -1, -2, -3, -4, 8, 7, 6, 5],
4
  "ort_rank3_equivalent_instance_norm_groups_equal_channels_input_x": [3.1513367, 9.283596, 1.4546119, 5.4617004, 8.519701, 1.2382338, 1.7930176, 5.1099434, 7.9195533, 7.638727, 8.065445, 3.8082376, 2.3667817, 2.8248506, 3.7754705, 5.861325, 5.058735, 3.2787242, 3.6843839, 9.755121, 2.7902672, 7.3974323, 8.283609, 8.488337],
 
106
  "provenance": {
107
  "source": "onnxruntime/test/providers/cpu/nn/group_norm_op_test.cc",
108
  "test": "GroupNormalizationOpTest.GroupSize_N",
109
+ "notes": "A larger grouped input requires subnormal scales to survive across channels and spatial positions."
110
  },
111
  "attrs": { "num_groups": 2, "epsilon": 0.00001 },
112
  "inputs": {
 
692
  {
693
  "name": "group_splitk_layernorm_equiv_65536_f16",
694
  "provenance": {
695
+ "notes": "A float16 group of 65,536 elements exercises both split-K partial reduction and the float16 apply pass."
696
  },
697
  "attrs": { "num_groups": 1, "epsilon": 0.00001 },
698
  "inputs": {