Xenova HF Staff commited on
Commit
2f706e2
·
verified ·
1 Parent(s): 1496de8

sync 91d990483a17

Browse files
README.md CHANGED
@@ -18,15 +18,15 @@ See the [ONNX `LpNormalization` spec](https://onnx.ai/onnx/operators/onnx__LpNor
18
 
19
  ## Inputs
20
 
21
- | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
22
- | --- | --- | --- | --- | --- | --- | --- |
23
- | `input` | `input` | `T` | — | — | Input tensor to normalize. | required |
24
 
25
  ## Outputs
26
 
27
- | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
28
- | --- | --- | --- | --- | --- | --- | --- |
29
- | `output` | `output` | `T` | same as `input` | same as `input` | Tensor after Lp-normalization; same shape as the input. | required |
30
 
31
  ## Attributes
32
 
@@ -45,7 +45,7 @@ Default values (overridable per request):
45
 
46
  ## Files
47
 
48
- - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, provenance)
49
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
50
  - [`test.json`](build/webgpu/test.json) — correctness cases
51
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
@@ -56,10 +56,14 @@ Default values (overridable per request):
56
 
57
  ## Use with `@huggingface/kernels`
58
 
59
- The loader derives every required output's shape and logical dtype from the manifest contract and this call.
60
- It then allocates the result tensors automatically.
 
 
 
61
 
62
  The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
 
63
 
64
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
65
 
 
18
 
19
  ## Inputs
20
 
21
+ | Name | Logical dtype | Rank | Shape | Description | Presence |
22
+ | --- | --- | --- | --- | --- | --- |
23
+ | `input` | `T` | — | — | Input tensor to normalize. | required |
24
 
25
  ## Outputs
26
 
27
+ | Name | Logical dtype | Rank | Shape | Description | Presence |
28
+ | --- | --- | --- | --- | --- | --- |
29
+ | `output` | `T` | same as `input` | same as `input` | Tensor after Lp-normalization; same shape as the input. | required |
30
 
31
  ## Attributes
32
 
 
45
 
46
  ## Files
47
 
48
+ - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, per-variant templates, provenance)
49
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
50
  - [`test.json`](build/webgpu/test.json) — correctness cases
51
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
 
56
 
57
  ## Use with `@huggingface/kernels`
58
 
59
+ ```sh
60
+ npm install --save-exact @huggingface/kernels@0.0.1-preview.2
61
+ ```
62
+
63
+ Required output shapes and logical data types are inferred from the supplied inputs and attributes; result tensors are allocated 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
+ It follows the `v1` branch as fixes land. To pin exact artifact bytes, pass a 40-character commit `revision` instead of `version`.
67
 
68
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
69
 
build/webgpu/bench.json CHANGED
@@ -1,5 +1,4 @@
1
  {
2
- "op": "ai.onnx.LpNormalization",
3
  "tunableSpace": {
4
  "WORKGROUP_SIZE": [64, 128, 256],
5
  "SPLIT_TARGET_DIM": [128, 256, 512, 1024],
 
1
  {
 
2
  "tunableSpace": {
3
  "WORKGROUP_SIZE": [64, 128, 256],
4
  "SPLIT_TARGET_DIM": [128, 256, 512, 1024],
build/webgpu/lp-norm-divide.wgsl.jinja CHANGED
@@ -1,6 +1,3 @@
1
- {% if usesF16 %}
2
- enable f16;
3
- {% endif %}
4
  {{ env.wgsl.resourceDeclarations }}
5
 
6
  const WG: u32 = {{ workgroupSize }}u;
 
 
 
 
1
  {{ env.wgsl.resourceDeclarations }}
2
 
3
  const WG: u32 = {{ workgroupSize }}u;
build/webgpu/lp-norm-reduce.wgsl.jinja CHANGED
@@ -1,14 +1,11 @@
1
- {% if usesF16 %}
2
- enable f16;
3
- {% endif %}
4
  {{ env.wgsl.resourceDeclarations }}
5
 
6
  const WG: u32 = {{ workgroupSize }}u;
7
- {% if source.segmented %}
8
  const SPLIT: u32 = {{ split }}u;
9
 
10
  {% endif %}
11
- {% if source.segmented %}
12
  // Reduce a strided normalization axis in segments. Workgroup rows divide a long
13
  // axis and write partial sums for the final reduction.
14
  {% else %}
@@ -18,14 +15,14 @@ const SPLIT: u32 = {{ split }}u;
18
  // tensor.
19
  @compute @workgroup_size(WG)
20
  fn main(
21
- @builtin(global_invocation_id) gid: vec3<u32>{% if source.segmented %},
22
  @builtin(workgroup_id) wid: vec3<u32>,
23
  {% else %},
24
  {% endif %}
25
  @builtin(num_workgroups) nwg: vec3<u32>
26
  ) {
27
  let stride = nwg.x * WG;
28
- {% if source.segmented %}
29
  let segment = wid.y;
30
  let chunk = (params.dim + SPLIT - 1u) / SPLIT;
31
  let begin = segment * chunk;
@@ -43,7 +40,7 @@ fn main(
43
  let v = f32(input[base + j * params.inner]);
44
  norm += select(v * v, abs(v), params.p == 1u);
45
  }
46
- {% if source.segmented %}
47
  normOutput[segment * params.rows + r] = norm;
48
  {% else %}
49
  if (params.p == 2u) { norm = sqrt(norm); }
 
 
 
 
1
  {{ env.wgsl.resourceDeclarations }}
2
 
3
  const WG: u32 = {{ workgroupSize }}u;
4
+ {% if segmented %}
5
  const SPLIT: u32 = {{ split }}u;
6
 
7
  {% endif %}
8
+ {% if segmented %}
9
  // Reduce a strided normalization axis in segments. Workgroup rows divide a long
10
  // axis and write partial sums for the final reduction.
11
  {% else %}
 
15
  // tensor.
16
  @compute @workgroup_size(WG)
17
  fn main(
18
+ @builtin(global_invocation_id) gid: vec3<u32>{% if segmented %},
19
  @builtin(workgroup_id) wid: vec3<u32>,
20
  {% else %},
21
  {% endif %}
22
  @builtin(num_workgroups) nwg: vec3<u32>
23
  ) {
24
  let stride = nwg.x * WG;
25
+ {% if segmented %}
26
  let segment = wid.y;
27
  let chunk = (params.dim + SPLIT - 1u) / SPLIT;
28
  let begin = segment * chunk;
 
40
  let v = f32(input[base + j * params.inner]);
41
  norm += select(v * v, abs(v), params.p == 1u);
42
  }
43
+ {% if segmented %}
44
  normOutput[segment * params.rows + r] = norm;
45
  {% else %}
46
  if (params.p == 2u) { norm = sqrt(norm); }
build/webgpu/manifest.json CHANGED
@@ -2,185 +2,61 @@
2
  "domain": "ai.onnx",
3
  "name": "LpNormalization",
4
  "sinceVersion": 1,
5
- "description": "Applies Lp-normalization to the input tensor along the specified axis: `output = input / Lp_norm(input, axis)`. Supports L1 (`p=1`) and L2 (`p=2`) norms. Where the Lp norm is zero, the output is defined as zero to avoid division by zero.",
6
- "inputs": [{ "role": "input", "dtype": "T", "description": "Input tensor to normalize." }],
7
- "outputs": [
8
- {
9
- "role": "output",
10
- "dtype": "T",
11
- "rank": "ranks.input",
12
- "description": "Tensor after Lp-normalization; same shape as the input.",
13
- "shape": "shapes.input"
14
- }
15
- ],
16
- "attributes": { "axis": -1, "p": 2 },
17
- "attributeDescriptions": {
18
- "axis": "The axis along which normalization is applied; `-1` means the last axis.",
19
- "p": "The order of the Lp norm to use; only `1` (L1) or `2` (L2) are supported."
20
- },
21
  "attributeConstraints": { "p": { "values": [1, 2] } },
22
  "typeConstraints": { "T": ["float32", "float16"] },
23
- "args": {
24
- "input": { "kind": "tensor", "semantic": "input", "role": "input" },
25
- "output": { "kind": "tensor", "semantic": "output", "role": "output" }
26
- },
27
  "tunables": {
28
- "WORKGROUP_SIZE": 256,
29
- "SPLIT_MIN_DIM": 512,
30
- "SPLIT_MIN_ROWS": 32,
31
- "SPLIT_TARGET_DIM": 256,
32
- "MAX_SPLITS": 128
33
  },
34
  "derive": {
35
  "deviceWorkgroupCap": "min(device.limits.maxComputeInvocationsPerWorkgroup, device.limits.maxComputeWorkgroupSizeX)",
36
  "workgroupSize": "min(tunables.WORKGROUP_SIZE, deviceWorkgroupCap)",
37
- "hasSubgroupId": "device.features.has(\"subgroups\") and device.wgslLanguageFeatures.has(\"subgroup_id\")"
 
38
  },
39
- "constants": { "scalar": "dtypes.T" },
40
- "bindingSets": {
41
- "rowStats": [
42
- {
43
- "name": "x",
44
- "arg": "input",
45
- "semantic": "input",
46
- "buffer": { "type": "read-only-storage" },
47
- "elementType": "$ioElement"
48
- },
49
- {
50
- "name": "y",
51
- "arg": "output",
52
- "semantic": "output",
53
- "buffer": { "type": "storage" },
54
- "elementType": "$ioElement"
55
- },
56
- {
57
- "name": "params",
58
- "semantic": "kernel.params",
59
- "buffer": { "type": "uniform" },
60
- "struct": {
61
- "name": "Params",
62
- "fields": [
63
- { "name": "rows", "type": "u32", "value": "numel(shapes.input) / dim(shapes.input, -1)" },
64
- {
65
- "name": "rowStride",
66
- "type": "u32",
67
- "value": "max(1, min(numel(shapes.input) / dim(shapes.input, -1), device.limits.maxComputeWorkgroupsPerDimension))"
68
- }
69
- ]
70
- }
71
- }
72
- ],
73
- "axisReduce": [
74
- {
75
- "name": "input",
76
- "arg": "input",
77
- "semantic": "input",
78
- "buffer": { "type": "read-only-storage" },
79
- "elementType": "$scalar"
80
- },
81
- { "name": "normOutput", "semantic": "rowNorms", "buffer": { "type": "storage" }, "elementType": "f32" },
82
- {
83
- "name": "params",
84
- "semantic": "kernel.params",
85
- "buffer": { "type": "uniform" },
86
- "struct": {
87
- "name": "Params",
88
- "fields": [
89
- { "name": "rows", "type": "u32", "value": "axisRows" },
90
- { "name": "dim", "type": "u32", "value": "axisDim" },
91
- { "name": "inner", "type": "u32", "value": "axisInner" },
92
- { "name": "p", "type": "u32", "value": "attrs.p" }
93
- ]
94
- }
95
- }
96
- ],
97
- "splitReduce": [
98
- {
99
- "name": "input",
100
- "arg": "input",
101
- "semantic": "input",
102
- "buffer": { "type": "read-only-storage" },
103
- "elementType": "$scalar"
104
- },
105
- { "name": "normOutput", "semantic": "partials", "buffer": { "type": "storage" }, "elementType": "f32" },
106
- {
107
- "name": "params",
108
- "semantic": "kernel.params",
109
- "buffer": { "type": "uniform" },
110
- "struct": {
111
- "name": "Params",
112
- "fields": [
113
- { "name": "rows", "type": "u32", "value": "axisRows" },
114
- { "name": "dim", "type": "u32", "value": "axisDim" },
115
- { "name": "inner", "type": "u32", "value": "axisInner" },
116
- { "name": "p", "type": "u32", "value": "attrs.p" }
117
- ]
118
- }
119
- }
120
- ],
121
- "splitCombine": [
122
- { "name": "partials", "semantic": "partials", "buffer": { "type": "read-only-storage" }, "elementType": "f32" },
123
- { "name": "rowNorms", "semantic": "rowNorms", "buffer": { "type": "storage" }, "elementType": "f32" },
124
- {
125
- "name": "params",
126
- "semantic": "kernel.params",
127
- "buffer": { "type": "uniform" },
128
- "struct": {
129
- "name": "Params",
130
- "fields": [
131
- { "name": "rows", "type": "u32", "value": "axisRows" },
132
- { "name": "p", "type": "u32", "value": "attrs.p" }
133
- ]
134
- }
135
- }
136
- ],
137
- "axisDivide": [
138
- {
139
- "name": "input",
140
- "arg": "input",
141
- "semantic": "input",
142
- "buffer": { "type": "read-only-storage" },
143
- "elementType": "$divElem"
144
- },
145
- { "name": "rowNorms", "semantic": "rowNorms", "buffer": { "type": "read-only-storage" }, "elementType": "f32" },
146
- {
147
- "name": "output",
148
- "arg": "output",
149
- "semantic": "output",
150
- "buffer": { "type": "storage" },
151
- "elementType": "$divElem"
152
- },
153
- {
154
- "name": "params",
155
- "semantic": "kernel.params",
156
- "buffer": { "type": "uniform" },
157
- "struct": {
158
- "name": "Params",
159
- "fields": [
160
- { "name": "count", "type": "u32", "value": "divideCount" },
161
- { "name": "dim", "type": "u32", "value": "axisDim" },
162
- { "name": "inner", "type": "u32", "value": "axisInner" }
163
- ]
164
- }
165
- }
166
- ]
167
  },
168
  "variants": [
169
  {
170
  "id": "axis_splitk",
171
  "priority": 20,
 
172
  "derive": {
173
  "axisDim": "dim(shapes.input, attrs.axis)",
174
  "axisInner": "inner(shapes.input, attrs.axis)",
175
  "axisRows": "numel(shapes.input) / axisDim",
176
  "split": "min(tunables.MAX_SPLITS, pow2ceil(ceilDiv(axisDim, tunables.SPLIT_TARGET_DIM)))",
177
  "divideVec4": "numel(shapes.output) % 4 == 0 and axisInner % 4 == 0",
178
- "divideCount": "numel(shapes.output) / 4 if divideVec4 else numel(shapes.output)"
179
- },
180
- "when": ["ranks.input >= 2", "ranks.output == ranks.input", "numel(shapes.input) == numel(shapes.output)", "inner(shapes.input, attrs.axis) > 1", "dim(shapes.input, attrs.axis) >= tunables.SPLIT_MIN_DIM", "numel(shapes.input) / dim(shapes.input, attrs.axis) >= tunables.SPLIT_MIN_ROWS", "min(tunables.MAX_SPLITS, pow2ceil(ceilDiv(dim(shapes.input, attrs.axis), tunables.SPLIT_TARGET_DIM))) <= device.limits.maxComputeWorkgroupsPerDimension", "numel(shapes.input) / dim(shapes.input, attrs.axis) * 4 <= device.limits.maxStorageBufferBindingSize", "numel(shapes.input) / dim(shapes.input, attrs.axis) * 4 <= device.limits.maxBufferSize", "numel(shapes.input) / dim(shapes.input, attrs.axis) * min(tunables.MAX_SPLITS, pow2ceil(ceilDiv(dim(shapes.input, attrs.axis), tunables.SPLIT_TARGET_DIM))) * 4 <= device.limits.maxStorageBufferBindingSize", "numel(shapes.input) / dim(shapes.input, attrs.axis) * min(tunables.MAX_SPLITS, pow2ceil(ceilDiv(dim(shapes.input, attrs.axis), tunables.SPLIT_TARGET_DIM))) * 4 <= device.limits.maxBufferSize", "f16Ok(dtypes.T)"],
181
- "constants": {
182
- "usesF16": "dtypes.T == \"f16\"",
183
- "split": "split",
184
  "divVec4": "divideVec4",
185
  "divElem": "(\"vec4<\" ~ dtypes.T ~ \">\") if divideVec4 else dtypes.T"
186
  },
@@ -192,114 +68,176 @@
192
  {
193
  "id": "split_reduce",
194
  "name": "LpNormalization.SplitReduce",
195
- "source": { "shader": "lp-norm-reduce.wgsl.jinja", "inputs": { "segmented": true } },
196
- "bindings": "splitReduce",
 
197
  "dispatch": {
198
- "x": "min(ceilDiv(axisRows, workgroupSize), device.limits.maxComputeWorkgroupsPerDimension)",
199
  "y": "split"
200
  }
201
  },
202
  {
203
  "id": "combine",
204
  "name": "LpNormalization.SplitCombine",
205
- "source": { "shader": "lp-norm-split-combine.wgsl.jinja", "inputs": { "split": "split" } },
206
- "bindings": "splitCombine",
207
- "dispatch": { "gridStride": "axisRows", "workgroupSize": "workgroupSize" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  },
209
  {
210
  "id": "divide",
211
  "name": "LpNormalization.Divide",
212
  "shader": "lp-norm-divide.wgsl.jinja",
213
- "bindings": "axisDivide",
214
- "dispatch": { "gridStride": "divideCount", "workgroupSize": "workgroupSize" }
 
 
 
 
215
  }
216
  ]
217
  },
218
  {
219
  "id": "axis",
 
220
  "derive": {
221
  "axisDim": "dim(shapes.input, attrs.axis)",
222
  "axisInner": "inner(shapes.input, attrs.axis)",
223
  "axisRows": "numel(shapes.input) / axisDim",
224
- "divideCount": "numel(shapes.output)"
 
 
225
  },
226
- "when": ["ranks.input >= 1", "ranks.output == ranks.input", "numel(shapes.input) == numel(shapes.output)", "numel(shapes.input) / dim(shapes.input, attrs.axis) * 4 <= device.limits.maxStorageBufferBindingSize", "numel(shapes.input) / dim(shapes.input, attrs.axis) * 4 <= device.limits.maxBufferSize", "f16Ok(dtypes.T)"],
227
- "constants": { "usesF16": "dtypes.T == \"f16\"", "divVec4": false, "divElem": "dtypes.T" },
228
  "intermediates": [{ "id": "rowNorms", "dtype": "float32", "shape": "[axisRows]" }],
229
  "passes": [
230
  {
231
  "id": "reduce",
232
  "name": "LpNormalization.RowReduce",
233
- "source": { "shader": "lp-norm-reduce.wgsl.jinja", "inputs": { "segmented": false } },
234
- "bindings": "axisReduce",
235
- "dispatch": { "gridStride": "axisRows", "workgroupSize": "workgroupSize" }
 
 
 
 
 
236
  },
237
  {
238
  "id": "divide",
239
  "name": "LpNormalization.Divide",
240
  "shader": "lp-norm-divide.wgsl.jinja",
241
- "bindings": "axisDivide",
242
- "dispatch": { "gridStride": "divideCount", "workgroupSize": "workgroupSize" }
 
 
 
 
243
  }
244
  ]
245
  },
246
  {
247
  "id": "last_axis_row_vec4",
248
  "priority": 110,
249
- "when": ["ranks.input >= 1", "ranks.output == ranks.input", "numel(shapes.input) == numel(shapes.output)", "(attrs.axis == -1 or attrs.axis == ranks.input - 1)", "dim(shapes.input, -1) % 4 == 0", "f16Ok(dtypes.T)"],
250
- "constants": { "ioElement": "\"vec4<\" ~ dtypes.T ~ \">\"", "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"" },
251
  "passes": [
252
  {
253
  "id": "main",
254
  "name": "LpNormalization.LastAxisRow",
255
- "source": {
256
- "shader": "norm-row-stats.wgsl.jinja",
257
- "inputs": {
258
- "mode": "\"lp\"",
259
- "vec4": true,
260
- "scalar": "dtypes.T",
261
- "usesF16": "dtypes.T == \"f16\"",
262
- "hidden": "dim(shapes.input, -1)",
263
- "wg": "min(workgroupSize, pow2ceil(dim(shapes.input, -1) / 4))",
264
- "p": "attrs.p",
265
- "hiddenVec": "dim(shapes.input, -1) / 4",
266
- "vecType": "\"vec4<\" ~ dtypes.T ~ \">\"",
267
- "combineSubgroups": "hasSubgroupId"
 
 
 
 
 
 
 
 
 
 
 
 
 
268
  }
 
 
 
 
 
269
  },
270
- "subgroupCollectivesWidth": "portable",
271
- "bindings": "rowStats",
272
- "dispatch": { "workgroups": "numel(shapes.input) / dim(shapes.input, -1)" }
273
  }
274
  ]
275
  },
276
  {
277
  "id": "last_axis_row",
278
  "priority": 100,
279
- "when": ["ranks.input >= 1", "ranks.output == ranks.input", "numel(shapes.input) == numel(shapes.output)", "(attrs.axis == -1 or attrs.axis == ranks.input - 1)", "true", "f16Ok(dtypes.T)"],
280
- "constants": { "ioElement": "dtypes.T", "vectorScalar": "dtypes.T" },
281
  "passes": [
282
  {
283
  "id": "main",
284
  "name": "LpNormalization.LastAxisRow",
285
- "source": {
286
- "shader": "norm-row-stats.wgsl.jinja",
287
- "inputs": {
288
- "mode": "\"lp\"",
289
- "vec4": false,
290
- "scalar": "dtypes.T",
291
- "usesF16": "dtypes.T == \"f16\"",
292
- "hidden": "dim(shapes.input, -1)",
293
- "wg": "min(workgroupSize, pow2ceil(dim(shapes.input, -1)))",
294
- "p": "attrs.p",
295
- "hiddenVec": 1,
296
- "vecType": "\"vec4<\" ~ dtypes.T ~ \">\"",
297
- "combineSubgroups": "hasSubgroupId"
 
 
 
 
 
 
 
 
 
 
 
 
 
298
  }
 
 
 
 
 
299
  },
300
- "subgroupCollectivesWidth": "portable",
301
- "bindings": "rowStats",
302
- "dispatch": { "workgroups": "numel(shapes.input) / dim(shapes.input, -1)" }
303
  }
304
  ]
305
  }
 
2
  "domain": "ai.onnx",
3
  "name": "LpNormalization",
4
  "sinceVersion": 1,
5
+ "inputs": { "input": { "dtype": "T" } },
6
+ "outputs": { "output": { "dtype": "T", "rank": "ranks.input", "shape": "shapes.input" } },
7
+ "attributes": { "axis": { "default": -1 }, "p": { "default": 2 } },
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  "attributeConstraints": { "p": { "values": [1, 2] } },
9
  "typeConstraints": { "T": ["float32", "float16"] },
 
 
 
 
10
  "tunables": {
11
+ "WORKGROUP_SIZE": { "default": 256 },
12
+ "SPLIT_MIN_DIM": { "default": 512 },
13
+ "SPLIT_MIN_ROWS": { "default": 32 },
14
+ "SPLIT_TARGET_DIM": { "default": 256 },
15
+ "MAX_SPLITS": { "default": 128 }
16
  },
17
  "derive": {
18
  "deviceWorkgroupCap": "min(device.limits.maxComputeInvocationsPerWorkgroup, device.limits.maxComputeWorkgroupSizeX)",
19
  "workgroupSize": "min(tunables.WORKGROUP_SIZE, deviceWorkgroupCap)",
20
+ "hasSubgroupId": "device.features.has(\"subgroups\") and device.wgslLanguageFeatures.has(\"subgroup_id\")",
21
+ "scalar": "dtypes.T"
22
  },
23
+ "when": ["ranks.output == ranks.input", "numel(shapes.input) == numel(shapes.output)", "f16Ok(dtypes.T)"],
24
+ "bindings": {
25
+ "input": { "buffer": "read-only-storage", "elementType": "$scalar" },
26
+ "params": {
27
+ "buffer": "uniform",
28
+ "struct": [
29
+ { "name": "rows", "type": "u32", "value": "axisRows" },
30
+ { "name": "dim", "type": "u32", "value": "axisDim" },
31
+ { "name": "inner", "type": "u32", "value": "axisInner" },
32
+ { "name": "p", "type": "u32", "value": "attrs.p" }
33
+ ]
34
+ },
35
+ "input_2": { "name": "input", "buffer": "read-only-storage", "elementType": "$divElem" },
36
+ "rowNorms": { "buffer": "read-only-storage", "elementType": "f32" },
37
+ "output": { "buffer": "storage", "elementType": "$divElem" },
38
+ "params_3": {
39
+ "name": "params",
40
+ "buffer": "uniform",
41
+ "struct": [
42
+ { "name": "count", "type": "u32", "value": "divideCount" },
43
+ { "name": "dim", "type": "u32", "value": "axisDim" },
44
+ { "name": "inner", "type": "u32", "value": "axisInner" }
45
+ ]
46
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  },
48
  "variants": [
49
  {
50
  "id": "axis_splitk",
51
  "priority": 20,
52
+ "when": ["ranks.input >= 2", "inner(shapes.input, attrs.axis) > 1", "dim(shapes.input, attrs.axis) >= tunables.SPLIT_MIN_DIM", "numel(shapes.input) / dim(shapes.input, attrs.axis) >= tunables.SPLIT_MIN_ROWS", "min(tunables.MAX_SPLITS, pow2ceil(ceilDiv(dim(shapes.input, attrs.axis), tunables.SPLIT_TARGET_DIM))) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535)", "numel(shapes.input) / dim(shapes.input, attrs.axis) * 4 <= device.limits.maxStorageBufferBindingSize", "numel(shapes.input) / dim(shapes.input, attrs.axis) * 4 <= device.limits.maxBufferSize", "numel(shapes.input) / dim(shapes.input, attrs.axis) * min(tunables.MAX_SPLITS, pow2ceil(ceilDiv(dim(shapes.input, attrs.axis), tunables.SPLIT_TARGET_DIM))) * 4 <= device.limits.maxStorageBufferBindingSize", "numel(shapes.input) / dim(shapes.input, attrs.axis) * min(tunables.MAX_SPLITS, pow2ceil(ceilDiv(dim(shapes.input, attrs.axis), tunables.SPLIT_TARGET_DIM))) * 4 <= device.limits.maxBufferSize"],
53
  "derive": {
54
  "axisDim": "dim(shapes.input, attrs.axis)",
55
  "axisInner": "inner(shapes.input, attrs.axis)",
56
  "axisRows": "numel(shapes.input) / axisDim",
57
  "split": "min(tunables.MAX_SPLITS, pow2ceil(ceilDiv(axisDim, tunables.SPLIT_TARGET_DIM)))",
58
  "divideVec4": "numel(shapes.output) % 4 == 0 and axisInner % 4 == 0",
59
+ "divideCount": "numel(shapes.output) / 4 if divideVec4 else numel(shapes.output)",
 
 
 
 
 
60
  "divVec4": "divideVec4",
61
  "divElem": "(\"vec4<\" ~ dtypes.T ~ \">\") if divideVec4 else dtypes.T"
62
  },
 
68
  {
69
  "id": "split_reduce",
70
  "name": "LpNormalization.SplitReduce",
71
+ "shader": "lp-norm-reduce.wgsl.jinja",
72
+ "derive": { "segmented": true },
73
+ "bindings": ["input", { "scratch": "partials", "name": "normOutput", "elementType": "f32" }, "params"],
74
  "dispatch": {
75
+ "x": "min(ceilDiv(axisRows, workgroupSize), min(device.limits.maxComputeWorkgroupsPerDimension, 65535))",
76
  "y": "split"
77
  }
78
  },
79
  {
80
  "id": "combine",
81
  "name": "LpNormalization.SplitCombine",
82
+ "shader": "lp-norm-split-combine.wgsl.jinja",
83
+ "derive": {},
84
+ "bindings": [
85
+ { "name": "partials", "buffer": "read-only-storage", "elementType": "f32" },
86
+ { "name": "rowNorms", "buffer": "storage", "elementType": "f32" },
87
+ {
88
+ "name": "params",
89
+ "struct": [
90
+ { "name": "rows", "type": "u32", "value": "axisRows" },
91
+ { "name": "p", "type": "u32", "value": "attrs.p" }
92
+ ]
93
+ }
94
+ ],
95
+ "dispatch": {
96
+ "x": "min(ceilDiv((axisRows), (workgroupSize)), min(device.limits.maxComputeWorkgroupsPerDimension, 65535))",
97
+ "y": 1,
98
+ "z": 1
99
+ }
100
  },
101
  {
102
  "id": "divide",
103
  "name": "LpNormalization.Divide",
104
  "shader": "lp-norm-divide.wgsl.jinja",
105
+ "bindings": ["input_2", "rowNorms", "output", "params_3"],
106
+ "dispatch": {
107
+ "x": "min(ceilDiv((divideCount), (workgroupSize)), min(device.limits.maxComputeWorkgroupsPerDimension, 65535))",
108
+ "y": 1,
109
+ "z": 1
110
+ }
111
  }
112
  ]
113
  },
114
  {
115
  "id": "axis",
116
+ "when": ["ranks.input >= 1", "numel(shapes.input) / dim(shapes.input, attrs.axis) * 4 <= device.limits.maxStorageBufferBindingSize", "numel(shapes.input) / dim(shapes.input, attrs.axis) * 4 <= device.limits.maxBufferSize"],
117
  "derive": {
118
  "axisDim": "dim(shapes.input, attrs.axis)",
119
  "axisInner": "inner(shapes.input, attrs.axis)",
120
  "axisRows": "numel(shapes.input) / axisDim",
121
+ "divideCount": "numel(shapes.output)",
122
+ "divVec4": false,
123
+ "divElem": "dtypes.T"
124
  },
 
 
125
  "intermediates": [{ "id": "rowNorms", "dtype": "float32", "shape": "[axisRows]" }],
126
  "passes": [
127
  {
128
  "id": "reduce",
129
  "name": "LpNormalization.RowReduce",
130
+ "shader": "lp-norm-reduce.wgsl.jinja",
131
+ "derive": { "segmented": false },
132
+ "bindings": ["input", { "scratch": "rowNorms", "name": "normOutput", "elementType": "f32" }, "params"],
133
+ "dispatch": {
134
+ "x": "min(ceilDiv((axisRows), (workgroupSize)), min(device.limits.maxComputeWorkgroupsPerDimension, 65535))",
135
+ "y": 1,
136
+ "z": 1
137
+ }
138
  },
139
  {
140
  "id": "divide",
141
  "name": "LpNormalization.Divide",
142
  "shader": "lp-norm-divide.wgsl.jinja",
143
+ "bindings": ["input_2", "rowNorms", "output", "params_3"],
144
+ "dispatch": {
145
+ "x": "min(ceilDiv((divideCount), (workgroupSize)), min(device.limits.maxComputeWorkgroupsPerDimension, 65535))",
146
+ "y": 1,
147
+ "z": 1
148
+ }
149
  }
150
  ]
151
  },
152
  {
153
  "id": "last_axis_row_vec4",
154
  "priority": 110,
155
+ "when": ["ranks.input >= 1", "(attrs.axis == -1 or attrs.axis == ranks.input - 1)", "dim(shapes.input, -1) % 4 == 0"],
156
+ "derive": { "ioElement": "\"vec4<\" ~ dtypes.T ~ \">\"" },
157
  "passes": [
158
  {
159
  "id": "main",
160
  "name": "LpNormalization.LastAxisRow",
161
+ "shader": "norm-row-stats.wgsl.jinja",
162
+ "derive": {
163
+ "modeSpec": "\"lp\"",
164
+ "vec4": true,
165
+ "scalar": "dtypes.T",
166
+ "usesF16Spec": "dtypes.T == \"f16\"",
167
+ "hidden": "dim(shapes.input, -1)",
168
+ "wg": "min(workgroupSize, pow2ceil(dim(shapes.input, -1) / 4))",
169
+ "p": "attrs.p",
170
+ "hiddenVec": "dim(shapes.input, -1) / 4",
171
+ "vecType": "\"vec4<\" ~ dtypes.T ~ \">\"",
172
+ "combineSubgroups": "hasSubgroupId"
173
+ },
174
+ "bindings": [
175
+ { "arg": "input", "name": "x", "elementType": "$ioElement" },
176
+ { "arg": "output", "name": "y", "elementType": "$ioElement" },
177
+ {
178
+ "name": "params",
179
+ "struct": [
180
+ { "name": "rows", "type": "u32", "value": "numel(shapes.input) / dim(shapes.input, -1)" },
181
+ {
182
+ "name": "rowStride",
183
+ "type": "u32",
184
+ "value": "max(1, min(numel(shapes.input) / dim(shapes.input, -1), min(device.limits.maxComputeWorkgroupsPerDimension, 65535)))"
185
+ }
186
+ ]
187
  }
188
+ ],
189
+ "dispatch": {
190
+ "x": "min(numel(shapes.input) / dim(shapes.input, -1), 65535)",
191
+ "y": "ceilDiv(numel(shapes.input) / dim(shapes.input, -1), 65535)",
192
+ "z": 1
193
  },
194
+ "subgroupCollectivesWidth": "portable"
 
 
195
  }
196
  ]
197
  },
198
  {
199
  "id": "last_axis_row",
200
  "priority": 100,
201
+ "when": ["ranks.input >= 1", "(attrs.axis == -1 or attrs.axis == ranks.input - 1)", "true"],
202
+ "derive": { "ioElement": "dtypes.T" },
203
  "passes": [
204
  {
205
  "id": "main",
206
  "name": "LpNormalization.LastAxisRow",
207
+ "shader": "norm-row-stats.wgsl.jinja",
208
+ "derive": {
209
+ "modeSpec": "\"lp\"",
210
+ "vec4": false,
211
+ "scalar": "dtypes.T",
212
+ "usesF16Spec": "dtypes.T == \"f16\"",
213
+ "hidden": "dim(shapes.input, -1)",
214
+ "wg": "min(workgroupSize, pow2ceil(dim(shapes.input, -1)))",
215
+ "p": "attrs.p",
216
+ "hiddenVec": 1,
217
+ "vecType": "\"vec4<\" ~ dtypes.T ~ \">\"",
218
+ "combineSubgroups": "hasSubgroupId"
219
+ },
220
+ "bindings": [
221
+ { "arg": "input", "name": "x", "elementType": "$ioElement" },
222
+ { "arg": "output", "name": "y", "elementType": "$ioElement" },
223
+ {
224
+ "name": "params",
225
+ "struct": [
226
+ { "name": "rows", "type": "u32", "value": "numel(shapes.input) / dim(shapes.input, -1)" },
227
+ {
228
+ "name": "rowStride",
229
+ "type": "u32",
230
+ "value": "max(1, min(numel(shapes.input) / dim(shapes.input, -1), min(device.limits.maxComputeWorkgroupsPerDimension, 65535)))"
231
+ }
232
+ ]
233
  }
234
+ ],
235
+ "dispatch": {
236
+ "x": "min(numel(shapes.input) / dim(shapes.input, -1), 65535)",
237
+ "y": "ceilDiv(numel(shapes.input) / dim(shapes.input, -1), 65535)",
238
+ "z": 1
239
  },
240
+ "subgroupCollectivesWidth": "portable"
 
 
241
  }
242
  ]
243
  }
build/webgpu/metadata.json CHANGED
@@ -1,21 +1,29 @@
1
  {
2
  "name": "ai.onnx.LpNormalization",
3
- "id": "_ai_onnx_lpnormalization_webgpu_4171c72",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
- "bench.json": "4Qg+bNUooWdz8Aj8xcupfNIzsDtFpPO/oVyDuhMdCKQ=",
11
- "lp-norm-divide.wgsl.jinja": "27WmHX7/sfxmHRGh068Q06O+5Up+UA1yNukXGXUmCLE=",
12
- "lp-norm-reduce.wgsl.jinja": "XTvPbUz+NedeLvmFx2JZ0VnbBdx0t1TJ8QdaQlvhw+w=",
13
  "lp-norm-split-combine.wgsl.jinja": "PzQVkT5bUQ3+l2lqn3MgA2P0MbokIMa8Q6fVT4lFq9Q=",
14
- "manifest.json": "rdr9JjuddPq9Xhf4rXnzFfMglX1ORDsP1HvkriIryy8=",
15
- "norm-row-stats.wgsl.jinja": "Ru5DVZqQDj7YfQjw02snVOuPdbiVeFOxafk1dQmKQoU=",
16
- "test.json": "7CtVpdOA+xLNbzNOVKMe12xYpagIX0a1f5YABmQa1Hc="
17
  }
18
  },
19
- "provenance": { "kernel": { "sha": "2e7068faf55e7f43df740015f6d1ee49391a41c5", "dirty": false } },
20
- "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/ai.onnx.LpNormalization" }
 
 
 
 
 
 
 
 
21
  }
 
1
  {
2
  "name": "ai.onnx.LpNormalization",
3
+ "id": "_ai_onnx_lpnormalization_webgpu_7afe9b5",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
+ "bench.json": "Zqjg8X2eQ7EWdSXd2uSW+U227BKDhM9aUvyzA2Fd75o=",
11
+ "lp-norm-divide.wgsl.jinja": "bIMXO9qn/IyThJQr5tapg04aNYkQqYt6LK/yz9D9ZBo=",
12
+ "lp-norm-reduce.wgsl.jinja": "Yu7Q9BM+uAAsEfA4VC1PQR7VOxdTyn+A+mjIryfLJwc=",
13
  "lp-norm-split-combine.wgsl.jinja": "PzQVkT5bUQ3+l2lqn3MgA2P0MbokIMa8Q6fVT4lFq9Q=",
14
+ "manifest.json": "EVQo8vZnPKz7vHj4+ZebBiS7UI2rAlpagBmiNdNEp6c=",
15
+ "norm-row-stats.wgsl.jinja": "iwey3jfLc6FXtfY4bBqo5+YG3ALWvo0RHLn12HHeJ0Q=",
16
+ "test.json": "7Vll6iQ9EAVx7E3pLedW2baT6dCaQLOfgy3qgY3xzyQ="
17
  }
18
  },
19
+ "provenance": { "kernel": { "sha": "91d990483a174128daf7673f3f37a7c890493ae1", "dirty": false } },
20
+ "webgpu": {
21
+ "manifestSpec": "2.0",
22
+ "variants": {
23
+ "axis_splitk": ["lp-norm-divide.wgsl.jinja", "lp-norm-reduce.wgsl.jinja", "lp-norm-split-combine.wgsl.jinja"],
24
+ "axis": ["lp-norm-divide.wgsl.jinja", "lp-norm-reduce.wgsl.jinja"],
25
+ "last_axis_row_vec4": ["norm-row-stats.wgsl.jinja"],
26
+ "last_axis_row": ["norm-row-stats.wgsl.jinja"]
27
+ }
28
+ }
29
  }
build/webgpu/norm-row-stats.wgsl.jinja CHANGED
@@ -1,9 +1,15 @@
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 lpOrder = source.p if source.p is defined else 0 %}
 
 
 
 
 
 
7
  {% set reduceThreadParameters = ", sg_lane: u32, sg_id: u32, num_sg: u32"
8
  if combineSubgroups else ", tid: u32" %}
9
  {% set reduceThreadArguments = ", sg_lane, sg_id, num_sg"
@@ -22,15 +28,58 @@ enable subgroups;
22
  // tree, then every thread applies the fused normalize + affine write.
23
  //
24
  // Lp mode divides by the norm and maps a zero norm to zero without epsilon.
25
- {% if source.mode != "lp" or not source.vec4 %}
26
- const HIDDEN: u32 = {{ source.hidden }}u;
27
  {% endif %}
28
- {% if source.vec4 %}
29
- const HIDDEN_V: u32 = {{ source.hiddenVec }}u;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  {% endif %}
31
- const WG: u32 = {{ source.wg }}u;
32
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
 
 
 
 
 
34
 
35
  {% if combineSubgroups %}
36
  var<workgroup> sg_partials: array<f32, WG>;
@@ -84,7 +133,14 @@ fn main(
84
  return;
85
  }
86
  let tid = lid.x;
87
- {% if source.vec4 and not scalarIo %}
 
 
 
 
 
 
 
88
  let base = row * HIDDEN_V;
89
  {% else %}
90
  let base = row * HIDDEN;
@@ -92,9 +148,16 @@ fn main(
92
 
93
 
94
  var acc = 0.0;
95
- {% if source.vec4 %}
96
  for (var i = tid; i < HIDDEN_V; i = i + WG) {
 
 
 
 
 
 
97
  let v = vec4<f32>(x[base + i]);
 
98
  {% if lpOrder == 1 %}
99
  let a = abs(v);
100
  acc = acc + a.x + a.y + a.z + a.w;
@@ -104,7 +167,12 @@ fn main(
104
  }
105
  {% else %}
106
  for (var i = tid; i < HIDDEN; i = i + WG) {
 
 
 
 
107
  let v = f32(x[base + i]);
 
108
  {% if lpOrder == 1 %}
109
  acc = acc + abs(v);
110
  {% else %}
@@ -121,19 +189,49 @@ fn main(
121
  let norm = total;
122
  {% endif %}
123
 
124
- {% if source.vec4 %}
 
 
 
125
  for (var i = tid; i < HIDDEN_V; i = i + WG) {
 
 
 
 
 
 
 
126
  let idx = base + i;
127
  let v = vec4<f32>(x[idx]);
 
128
  let normalized = select(v / vec4<f32>(norm), vec4<f32>(0.0), vec4<bool>(norm == 0.0));
129
- y[idx] = {{ source.vecType }}(normalized);
130
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  {% else %}
132
  for (var i = tid; i < HIDDEN; i = i + WG) {
133
  let idx = base + i;
 
 
 
134
  let v = f32(x[idx]);
 
135
  let normalized = select(v / norm, 0.0, norm == 0.0);
136
- y[idx] = {{ source.scalar }}(normalized);
137
  }
138
  {% endif %}
139
  }
 
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 lpOrder = p if p is defined else 0 %}
8
+ {% set rmsChainNorm = rmsChainNorm if rmsChainNorm is defined else false %}
9
+ {% set hiddenPairs = hiddenPairs | default(0) %}
10
+ {% set numRows = numRows | default(0) %}
11
+ {% set epsilon = epsilon | default("0.0") %}
12
+ {% set epsilon2 = epsilon2 | default("0.0") %}
13
  {% set reduceThreadParameters = ", sg_lane: u32, sg_id: u32, num_sg: u32"
14
  if combineSubgroups else ", tid: u32" %}
15
  {% set reduceThreadArguments = ", sg_lane, sg_id, num_sg"
 
28
  // tree, then every thread applies the fused normalize + affine write.
29
  //
30
  // Lp mode divides by the norm and maps a zero norm to zero without epsilon.
31
+ {% if modeSpec != "lp" or not vec4 %}
32
+ const HIDDEN: u32 = {{ hidden }}u;
33
  {% endif %}
34
+ {% if vec4 %}
35
+ const HIDDEN_V: u32 = {{ hiddenVec }}u;
36
+ {% endif %}
37
+ {% if packedBf16Embedding %}
38
+ const HIDDEN_PAIRS: u32 = {{ hiddenPairs }}u;
39
+ const NUM_ROWS: u32 = {{ numRows }}u;
40
+ {% endif %}
41
+ const WG: u32 = {{ wg }}u;
42
+ {% if rmsChainNorm %}
43
+ const EPSILON2: f32 = {{ epsilon2 }};
44
+ {% endif %}
45
+
46
+ {% if packedBf16Embedding %}
47
+ {% if vec4 %}
48
+ fn unpack_bf16_pair(word: u32) -> vec2<f32> {
49
+ let bits = vec2<u32>(word & 0xffffu, word >> 16u);
50
+ return bitcast<vec2<f32>>(bits << vec2<u32>(16u));
51
+ }
52
+ {% endif %}
53
+
54
+ {% if not vec4 %}
55
+ fn embedding_scalar(source_row: u32, hidden: u32) -> f32 {
56
+ if (source_row >= NUM_ROWS) {
57
+ return 0.0;
58
+ }
59
+ let word = x[source_row * HIDDEN_PAIRS + (hidden >> 1u)];
60
+ let bits = select(word & 0xffffu, word >> 16u, (hidden & 1u) != 0u);
61
+ return bitcast<f32>(bits << 16u);
62
+ }
63
  {% endif %}
 
64
 
65
+ {% if vec4 %}
66
+ fn embedding_vec4(source_row: u32, hidden_vec: u32) -> vec4<f32> {
67
+ if (source_row >= NUM_ROWS) {
68
+ return vec4<f32>(0.0);
69
+ }
70
+ let base = source_row * HIDDEN_PAIRS + hidden_vec * 2u;
71
+ let low = unpack_bf16_pair(x[base]);
72
+ let high = unpack_bf16_pair(x[base + 1u]);
73
+ return vec4<f32>(low, high);
74
+ }
75
+ {% endif %}
76
+ {% endif %}
77
 
78
+ {% if vec4 and scalarIo %}
79
+ fn load_vec4(index: u32) -> vec4<f32> {
80
+ return vec4<f32>(x[index], x[index + 1u], x[index + 2u], x[index + 3u]);
81
+ }
82
+ {% endif %}
83
 
84
  {% if combineSubgroups %}
85
  var<workgroup> sg_partials: array<f32, WG>;
 
133
  return;
134
  }
135
  let tid = lid.x;
136
+ {% if packedBf16Embedding %}
137
+ let source_row = indices[row];
138
+ {% if vec4 %}
139
+ let base = row * HIDDEN_V;
140
+ {% else %}
141
+ let base = row * HIDDEN;
142
+ {% endif %}
143
+ {% elif vec4 and not scalarIo %}
144
  let base = row * HIDDEN_V;
145
  {% else %}
146
  let base = row * HIDDEN;
 
148
 
149
 
150
  var acc = 0.0;
151
+ {% if vec4 %}
152
  for (var i = tid; i < HIDDEN_V; i = i + WG) {
153
+ {% if packedBf16Embedding %}
154
+ let v = embedding_vec4(source_row, i);
155
+ embedding_out[base + i] = v;
156
+ {% elif scalarIo %}
157
+ let v = load_vec4(base + i * 4u);
158
+ {% else %}
159
  let v = vec4<f32>(x[base + i]);
160
+ {% endif %}
161
  {% if lpOrder == 1 %}
162
  let a = abs(v);
163
  acc = acc + a.x + a.y + a.z + a.w;
 
167
  }
168
  {% else %}
169
  for (var i = tid; i < HIDDEN; i = i + WG) {
170
+ {% if packedBf16Embedding %}
171
+ let v = embedding_scalar(source_row, i);
172
+ embedding_out[base + i] = v;
173
+ {% else %}
174
  let v = f32(x[base + i]);
175
+ {% endif %}
176
  {% if lpOrder == 1 %}
177
  acc = acc + abs(v);
178
  {% else %}
 
189
  let norm = total;
190
  {% endif %}
191
 
192
+ {% if rmsChainNorm %}
193
+ var acc2 = 0.0;
194
+ {% endif %}
195
+ {% if vec4 %}
196
  for (var i = tid; i < HIDDEN_V; i = i + WG) {
197
+ {% if packedBf16Embedding %}
198
+ let idx = base + i;
199
+ let v = embedding_vec4(source_row, i);
200
+ {% elif scalarIo %}
201
+ let idx = base + i * 4u;
202
+ let v = load_vec4(idx);
203
+ {% else %}
204
  let idx = base + i;
205
  let v = vec4<f32>(x[idx]);
206
+ {% endif %}
207
  let normalized = select(v / vec4<f32>(norm), vec4<f32>(0.0), vec4<bool>(norm == 0.0));
208
+ y[idx] = {{ vecType }}(normalized);
209
  }
210
+ {% if rmsChainNorm %}
211
+
212
+ // The chained second norm reads the residual row this loop just stored. This
213
+ // barrier completes those stores and any preceding shared-scratch use before
214
+ // the next reduction reuses its scratch; each lane then re-reads only the
215
+ // elements it wrote itself.
216
+ workgroupBarrier();
217
+ let total2 = reduce_scalar(acc2{{ reduceThreadArguments }});
218
+ let inv2 = inverseSqrt(total2 / f32(HIDDEN) + EPSILON2);
219
+ for (var i = tid; i < HIDDEN_V; i = i + WG) {
220
+ let idx = base + i;
221
+ let hv = vec4<f32>(y[idx]);
222
+ normed2[idx] = {{ vecType }}(hv * inv2 * vec4<f32>(scale2[i]));
223
+ }
224
+ {% endif %}
225
  {% else %}
226
  for (var i = tid; i < HIDDEN; i = i + WG) {
227
  let idx = base + i;
228
+ {% if packedBf16Embedding %}
229
+ let v = embedding_scalar(source_row, i);
230
+ {% else %}
231
  let v = f32(x[idx]);
232
+ {% endif %}
233
  let normalized = select(v / norm, 0.0, norm == 0.0);
234
+ y[idx] = {{ scalar }}(normalized);
235
  }
236
  {% endif %}
237
  }
build/webgpu/test.json CHANGED
@@ -1,5 +1,4 @@
1
  {
2
- "op": "ai.onnx.LpNormalization",
3
  "fixtureArrays": {
4
  "ort_l1_axis1_rank3_input_input": [5.93932154, 7.4367043, 6.42487038, 5.90394865, 4.81289319, 6.81304702, 4.9382849, 9.02595701, 9.67296484, 4.45097367, 8.12552534, 5.76005428, 6.11240105, 9.33036974, 1.63932452, 1.7841637, 1.18196558, 8.49357861, 8.00341076, 8.83010933, 9.80756508, 8.19242708, 5.15331426, 8.02476259]
5
  },
@@ -72,7 +71,7 @@
72
  "provenance": {
73
  "source": "onnxruntime/test/providers/cpu/nn/lp_norm_op_test.cc",
74
  "test": "LpNormalizationTest.L2NormalizationWithZeroNorm",
75
- "notes": "Axis-0 companion: normal inputs whose squared L2 sums are finite subnormals must still produce nonzero normalized values."
76
  },
77
  "attrs": { "axis": 0, "p": 2 },
78
  "inputs": {
@@ -93,7 +92,7 @@
93
  "provenance": {
94
  "source": "onnxruntime/test/providers/cpu/nn/lp_norm_op_test.cc",
95
  "test": "LpNormalizationTest.L2NormalizationWithZeroNorm",
96
- "notes": "Vec4 last-axis companion: the L2 norm is tiny but nonzero, so the direction of the vector must survive normalization."
97
  },
98
  "attrs": { "axis": -1, "p": 2 },
99
  "inputs": {
@@ -142,7 +141,7 @@
142
  "provenance": {
143
  "source": "onnxruntime/test/providers/cpu/nn/lp_norm_op_test.cc",
144
  "test": "LpNormalizationTest.L1NormalizationWithZeroNorm",
145
- "notes": "Axis-0 companion: the strided two-pass p1 path must not collapse finite subnormal L1 norms to zero."
146
  },
147
  "attrs": { "axis": 0, "p": 1 },
148
  "inputs": {
@@ -170,7 +169,7 @@
170
  "provenance": {
171
  "source": "onnxruntime/test/providers/cpu/nn/lp_norm_op_test.cc",
172
  "test": "LpNormalizationTest.L1NormalizationWithZeroNorm",
173
- "notes": "Vec4 last-axis companion: finite subnormal L1 totals should still produce stable signed ratios."
174
  },
175
  "attrs": { "axis": -1, "p": 1 },
176
  "inputs": {
@@ -207,7 +206,7 @@
207
  "provenance": {
208
  "source": "onnxruntime/test/providers/cpu/nn/lp_norm_op_test.cc",
209
  "test": "LpNormalizationTest.L2Normalization_FP16",
210
- "notes": "Uses large f16 values to preserve ORT's float-accumulation regression coverage."
211
  },
212
  "attrs": { "axis": -1, "p": 2 },
213
  "inputs": { "input": { "dtype": "float16", "shape": [2, 128], "data": { "kind": "constant", "value": 100.0 } } },
@@ -219,17 +218,16 @@
219
  "provenance": {
220
  "source": "onnxruntime/test/providers/cpu/nn/lp_norm_op_test.cc",
221
  "test": "LpNormalizationTest.L1Normalization_FP16",
222
- "notes": "Uses large f16 values to preserve ORT's float-accumulation regression coverage."
223
  },
224
  "attrs": { "axis": -1, "p": 1 },
225
  "inputs": { "input": { "dtype": "float16", "shape": [2, 128], "data": { "kind": "constant", "value": 200.0 } } },
226
- "outputs": { "output": { "dtype": "float16", "shape": [2, 128] } },
227
- "tolerance": 0.002
228
  },
229
  {
230
  "name": "f16_p1_last_axis_constant200_norm_scale_lock",
231
  "provenance": {
232
- "notes": "Mirrors the ORT-derived ort_f16_p1_last_axis_large_values_float_accumulation case, whose 0.002 tolerance against a 0.0078125 output is a min detectable uniform scale error of 0.256 - a 25% error in the L1 norm passes. The ORT case is left as imported; this one keeps the identical input (128 columns of 200.0, so the answer 200/25600 = 2^-7 is exact in f16) and asserts it to 1e-6, which pins the divisor to the column count."
233
  },
234
  "attrs": { "axis": -1, "p": 1 },
235
  "inputs": { "input": { "dtype": "float16", "shape": [2, 128], "data": { "kind": "constant", "value": 200.0 } } },
@@ -459,16 +457,8 @@
459
  },
460
  {
461
  "name": "f16_p2_axis0_splitk_decode",
462
- "attrs": { "axis": 0, "p": 2 },
463
- "inputs": {
464
- "input": { "dtype": "float16", "shape": [512, 256], "data": { "kind": "linspace", "start": -1.0, "end": 1.0 } }
465
- },
466
- "outputs": { "output": { "dtype": "float16", "shape": [512, 256], "tolerance": 0.01 } }
467
- },
468
- {
469
- "name": "f16_p2_axis0_splitk_decode_norm_scale_lock",
470
  "provenance": {
471
- "notes": "The p=2 half of the axis_splitk route: f16_p2_axis0_splitk_decode's 0.01 absolute tolerance against a 0.0765 maximum output is a min detectable uniform scale error of 0.131, so a partial sum-of-squares combined without the square root, or a rsqrt applied per split instead of once, is within tolerance. Same shape/attrs/data with a tolerance at f16 output resolution."
472
  },
473
  "attrs": { "axis": 0, "p": 2 },
474
  "inputs": {
@@ -478,20 +468,8 @@
478
  },
479
  {
480
  "name": "f16_p1_rank3_axis0_splitk_scalardiv",
481
- "attrs": { "axis": 0, "p": 1 },
482
- "inputs": {
483
- "input": {
484
- "dtype": "float16",
485
- "shape": [512, 86, 3],
486
- "data": { "kind": "linspace", "start": -2.0, "end": 3.0 }
487
- }
488
- },
489
- "outputs": { "output": { "dtype": "float16", "shape": [512, 86, 3], "tolerance": 0.01 } }
490
- },
491
- {
492
- "name": "f16_p1_rank3_axis0_splitk_scalardiv_norm_scale_lock",
493
  "provenance": {
494
- "notes": "LpNormalization output is scale-invariant in the input, so no data choice can lift it above ~1/n along the normalized axis; the only way a fixture sees a denominator error is a tolerance commensurate with that magnitude. f16_p1_rank3_axis0_splitk_scalardiv pairs a 0.01 absolute tolerance with a 4.5e-3 maximum output (min detectable uniform scale error 2.22), so the split-k L1 partial sum, its combine, and the scalar divide could each be off by any factor and pass. Same shape, axis, p and data on the axis_splitk route, with the tolerance retightened to a few f16 ulp at the true output scale."
495
  },
496
  "attrs": { "axis": 0, "p": 1 },
497
  "inputs": {
@@ -507,16 +485,8 @@
507
  },
508
  {
509
  "name": "f16_p2_last_axis_hidden2048",
510
- "attrs": { "axis": -1, "p": 2 },
511
- "inputs": {
512
- "input": { "dtype": "float16", "shape": [256, 2048], "data": { "kind": "linspace", "start": -1.0, "end": 1.0 } }
513
- },
514
- "outputs": { "output": { "dtype": "float16", "shape": [256, 2048], "tolerance": 0.01 } }
515
- },
516
- {
517
- "name": "f16_p2_last_axis_hidden2048_norm_scale_lock",
518
  "provenance": {
519
- "notes": "last_axis_row_vec4 over a 2048-wide row: the sibling's 0.01 absolute tolerance against a 0.0383 maximum output is a min detectable uniform scale error of 0.261, so the row norm could be a quarter or four times too large and pass. Same shape/attrs/data with the tolerance scaled to the output."
520
  },
521
  "attrs": { "axis": -1, "p": 2 },
522
  "inputs": {
@@ -546,7 +516,12 @@
546
  "data": { "kind": "linspace", "start": -0.5, "end": 0.5 }
547
  }
548
  },
549
- "outputs": { "output": { "dtype": "float16", "shape": [8, 1024, 768], "tolerance": 0.01 } }
 
 
 
 
 
550
  },
551
  {
552
  "name": "f16_vec4_last_axis_zero_norm_guard",
 
1
  {
 
2
  "fixtureArrays": {
3
  "ort_l1_axis1_rank3_input_input": [5.93932154, 7.4367043, 6.42487038, 5.90394865, 4.81289319, 6.81304702, 4.9382849, 9.02595701, 9.67296484, 4.45097367, 8.12552534, 5.76005428, 6.11240105, 9.33036974, 1.63932452, 1.7841637, 1.18196558, 8.49357861, 8.00341076, 8.83010933, 9.80756508, 8.19242708, 5.15331426, 8.02476259]
4
  },
 
71
  "provenance": {
72
  "source": "onnxruntime/test/providers/cpu/nn/lp_norm_op_test.cc",
73
  "test": "LpNormalizationTest.L2NormalizationWithZeroNorm",
74
+ "notes": "Along axis 0, normal inputs whose squared L2 sums are finite subnormals must still produce nonzero normalized values."
75
  },
76
  "attrs": { "axis": 0, "p": 2 },
77
  "inputs": {
 
92
  "provenance": {
93
  "source": "onnxruntime/test/providers/cpu/nn/lp_norm_op_test.cc",
94
  "test": "LpNormalizationTest.L2NormalizationWithZeroNorm",
95
+ "notes": "On the vec4 last-axis path, a tiny nonzero L2 norm must preserve the vector's direction."
96
  },
97
  "attrs": { "axis": -1, "p": 2 },
98
  "inputs": {
 
141
  "provenance": {
142
  "source": "onnxruntime/test/providers/cpu/nn/lp_norm_op_test.cc",
143
  "test": "LpNormalizationTest.L1NormalizationWithZeroNorm",
144
+ "notes": "Along axis 0, the strided two-pass p=1 path must not collapse finite subnormal L1 norms to zero."
145
  },
146
  "attrs": { "axis": 0, "p": 1 },
147
  "inputs": {
 
169
  "provenance": {
170
  "source": "onnxruntime/test/providers/cpu/nn/lp_norm_op_test.cc",
171
  "test": "LpNormalizationTest.L1NormalizationWithZeroNorm",
172
+ "notes": "On the vec4 last-axis path, finite subnormal L1 totals must produce stable signed ratios."
173
  },
174
  "attrs": { "axis": -1, "p": 1 },
175
  "inputs": {
 
206
  "provenance": {
207
  "source": "onnxruntime/test/providers/cpu/nn/lp_norm_op_test.cc",
208
  "test": "LpNormalizationTest.L2Normalization_FP16",
209
+ "notes": "Large float16 values require float32 accumulation for the p=2 norm."
210
  },
211
  "attrs": { "axis": -1, "p": 2 },
212
  "inputs": { "input": { "dtype": "float16", "shape": [2, 128], "data": { "kind": "constant", "value": 100.0 } } },
 
218
  "provenance": {
219
  "source": "onnxruntime/test/providers/cpu/nn/lp_norm_op_test.cc",
220
  "test": "LpNormalizationTest.L1Normalization_FP16",
221
+ "notes": "Large float16 values require float32 accumulation for the p=1 norm."
222
  },
223
  "attrs": { "axis": -1, "p": 1 },
224
  "inputs": { "input": { "dtype": "float16", "shape": [2, 128], "data": { "kind": "constant", "value": 200.0 } } },
225
+ "outputs": { "output": { "dtype": "float16", "shape": [2, 128], "tolerance": 0.00002, "relTolerance": 0.002 } }
 
226
  },
227
  {
228
  "name": "f16_p1_last_axis_constant200_norm_scale_lock",
229
  "provenance": {
230
+ "notes": "Each 128-element row contains 200, so its L1 norm is 25,600 and every normalized value is exactly 2^-7 in float16. A 1e-6 tolerance makes an incorrect divisor observable."
231
  },
232
  "attrs": { "axis": -1, "p": 1 },
233
  "inputs": { "input": { "dtype": "float16", "shape": [2, 128], "data": { "kind": "constant", "value": 200.0 } } },
 
457
  },
458
  {
459
  "name": "f16_p2_axis0_splitk_decode",
 
 
 
 
 
 
 
 
460
  "provenance": {
461
+ "notes": "Shape [512,256] uses a split reduction along axis 0 for the p=2 norm. Float16-resolution tolerances expose an omitted final square root or a reciprocal square root applied separately to partial sums."
462
  },
463
  "attrs": { "axis": 0, "p": 2 },
464
  "inputs": {
 
468
  },
469
  {
470
  "name": "f16_p1_rank3_axis0_splitk_scalardiv",
 
 
 
 
 
 
 
 
 
 
 
 
471
  "provenance": {
472
+ "notes": "Shape [512,86,3] uses a split L1 reduction along axis 0 followed by scalar division. The normalized outputs peak near 4.5e-3, so float16-resolution tolerances make partial-sum, combine, and divisor errors observable."
473
  },
474
  "attrs": { "axis": 0, "p": 1 },
475
  "inputs": {
 
485
  },
486
  {
487
  "name": "f16_p2_last_axis_hidden2048",
 
 
 
 
 
 
 
 
488
  "provenance": {
489
+ "notes": "A 2,048-wide row exercises vec4 last-axis normalization with tolerance scaled to the approximately 0.0383 output magnitude, making uniform norm-scale errors observable."
490
  },
491
  "attrs": { "axis": -1, "p": 2 },
492
  "inputs": {
 
516
  "data": { "kind": "linspace", "start": -0.5, "end": 0.5 }
517
  }
518
  },
519
+ "outputs": {
520
+ "output": { "dtype": "float16", "shape": [8, 1024, 768], "tolerance": 0.0001, "relTolerance": 0.002 }
521
+ },
522
+ "provenance": {
523
+ "notes": "A rank-3 p=2 norm over 1,024 elements of axis 1 runs split partials and a combine. Float16-resolution tolerances scaled to the roughly 0.05 outputs expose an omitted square root or a rescaled divisor."
524
+ }
525
  },
526
  {
527
  "name": "f16_vec4_last_axis_zero_norm_guard",