Xenova HF Staff commited on
Commit
1eadc80
·
verified ·
1 Parent(s): e152cdb

sync 91d990483a17

Browse files
README.md CHANGED
@@ -18,17 +18,17 @@ See the [ONNX `Clip` spec](https://onnx.ai/onnx/operators/onnx__Clip.html) for t
18
 
19
  ## Inputs
20
 
21
- | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
22
- | --- | --- | --- | --- | --- | --- | --- |
23
- | `input` | `input` | `T` | — | — | Input tensor whose elements are to be clipped. | required |
24
- | `min` | `min` | `T` | `0` | `[]` | Scalar lower bound; elements below this value are replaced by it. | optional |
25
- | `max` | `max` | `T` | `0` | `[]` | Scalar upper bound; elements above this value are replaced by it. | optional |
26
 
27
  ## Outputs
28
 
29
- | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
30
- | --- | --- | --- | --- | --- | --- | --- |
31
- | `output` | `output` | `T` | same as `input` | same as `input` | Output tensor with each element clipped to the specified interval. | required |
32
 
33
  ## Type constraints
34
 
@@ -38,7 +38,7 @@ See the [ONNX `Clip` spec](https://onnx.ai/onnx/operators/onnx__Clip.html) for t
38
 
39
  ## Files
40
 
41
- - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, provenance)
42
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
43
  - [`test.json`](build/webgpu/test.json) — correctness cases
44
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
@@ -47,10 +47,14 @@ See the [ONNX `Clip` spec](https://onnx.ai/onnx/operators/onnx__Clip.html) for t
47
 
48
  ## Use with `@huggingface/kernels`
49
 
50
- The loader derives every required output's shape and logical dtype from the manifest contract and this call.
51
- It then allocates the result tensors automatically.
 
 
 
52
 
53
  The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
 
54
 
55
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
56
 
 
18
 
19
  ## Inputs
20
 
21
+ | Name | Logical dtype | Rank | Shape | Description | Presence |
22
+ | --- | --- | --- | --- | --- | --- |
23
+ | `input` | `T` | — | — | Input tensor whose elements are to be clipped. | required |
24
+ | `min` | `T` | `0` | `[]` | Scalar lower bound; elements below this value are replaced by it. | optional |
25
+ | `max` | `T` | `0` | `[]` | Scalar upper bound; elements above this value are replaced by it. | optional |
26
 
27
  ## Outputs
28
 
29
+ | Name | Logical dtype | Rank | Shape | Description | Presence |
30
+ | --- | --- | --- | --- | --- | --- |
31
+ | `output` | `T` | same as `input` | same as `input` | Output tensor with each element clipped to the specified interval. | required |
32
 
33
  ## Type constraints
34
 
 
38
 
39
  ## Files
40
 
41
+ - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, per-variant templates, provenance)
42
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
43
  - [`test.json`](build/webgpu/test.json) — correctness cases
44
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
 
47
 
48
  ## Use with `@huggingface/kernels`
49
 
50
+ ```sh
51
+ npm install --save-exact @huggingface/kernels@0.0.1-preview.2
52
+ ```
53
+
54
+ Required output shapes and logical data types are inferred from the supplied inputs and attributes; result tensors are allocated automatically.
55
 
56
  The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
57
+ It follows the `v1` branch as fixes land. To pin exact artifact bytes, pass a 40-character commit `revision` instead of `version`.
58
 
59
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
60
 
build/webgpu/bench.json CHANGED
@@ -1,5 +1,4 @@
1
  {
2
- "op": "ai.onnx.Clip",
3
  "tunableSpace": { "WORKGROUP_SIZE": [64, 128, 256] },
4
  "cases": [
5
  {
@@ -38,8 +37,8 @@
38
  {
39
  "name": "clip-f32-input-bounds-large-scalar-path",
40
  "provenance": {
41
- "source": "authored for performance coverage",
42
- "notes": "Named for the scalar route it was authored to exercise. Its element count is divisible by four, so it now takes the vectorized input-bounds route; the scalar route stays live for counts that are not, and its fixtures still cover it."
43
  },
44
  "preset": "stress",
45
  "vars": { "dtype": "float32", "count": 8388608 },
@@ -54,8 +53,8 @@
54
  {
55
  "name": "clip-f16-8m-default-bounds-vec4",
56
  "provenance": {
57
- "source": "authored for performance coverage",
58
- "notes": "Clip with no min or max inputs, the only form that reaches the vec4 unary path. Nothing benchmarked that path, so the scalar input-bounds route had no vectorized sibling to be measured against."
59
  },
60
  "preset": "smoke",
61
  "vars": { "dtype": "float16", "count": 8388608 },
 
1
  {
 
2
  "tunableSpace": { "WORKGROUP_SIZE": [64, 128, 256] },
3
  "cases": [
4
  {
 
37
  {
38
  "name": "clip-f32-input-bounds-large-scalar-path",
39
  "provenance": {
40
+ "source": "local fixture",
41
+ "notes": "The 8,388,608-element float32 input is divisible by four and selects the vectorized input-bounds route."
42
  },
43
  "preset": "stress",
44
  "vars": { "dtype": "float32", "count": 8388608 },
 
53
  {
54
  "name": "clip-f16-8m-default-bounds-vec4",
55
  "provenance": {
56
+ "source": "local fixture",
57
+ "notes": "Clip without min or max inputs measures the vec4 unary path independently of scalar input-bound handling."
58
  },
59
  "preset": "smoke",
60
  "vars": { "dtype": "float16", "count": 8388608 },
build/webgpu/manifest.json CHANGED
@@ -2,43 +2,14 @@
2
  "domain": "ai.onnx",
3
  "name": "Clip",
4
  "sinceVersion": 13,
5
- "description": "Limits each element of the input tensor to the interval `[min, max]`, equivalent to `Min(max, Max(input, min))`. Each bound is an optional scalar input tensor; an omitted bound defaults to the input dtype's lowest or highest numeric value. When `min` exceeds `max`, all elements are set to `max`.",
6
- "inputs": [
7
- { "role": "input", "dtype": "T", "description": "Input tensor whose elements are to be clipped." },
8
- {
9
- "role": "min",
10
- "dtype": "T",
11
- "rank": 0,
12
- "optional": true,
13
- "shape": [],
14
- "description": "Scalar lower bound; elements below this value are replaced by it."
15
- },
16
- {
17
- "role": "max",
18
- "dtype": "T",
19
- "rank": 0,
20
- "optional": true,
21
- "shape": [],
22
- "description": "Scalar upper bound; elements above this value are replaced by it."
23
- }
24
- ],
25
- "outputs": [
26
- {
27
- "role": "output",
28
- "dtype": "T",
29
- "rank": "ranks.input",
30
- "shape": "shapes.input",
31
- "description": "Output tensor with each element clipped to the specified interval."
32
- }
33
- ],
34
- "typeConstraints": { "T": ["float32", "float16", "int32", "uint32", "int8", "uint8"] },
35
- "args": {
36
- "input": { "kind": "tensor", "semantic": "input", "role": "input" },
37
- "output": { "kind": "tensor", "semantic": "output", "role": "output" },
38
- "min": { "kind": "tensor", "semantic": "min", "role": "input1", "required": false },
39
- "max": { "kind": "tensor", "semantic": "max", "role": "input2", "required": false }
40
  },
41
- "tunables": { "WORKGROUP_SIZE": 256 },
 
 
42
  "derive": {
43
  "shapeContract": "numel(shapes.input) == numel(shapes.output) and f16Ok(dtypes.T)",
44
  "defaultBoundsContract": "shapeContract and not present.min and not present.max",
@@ -47,271 +18,49 @@
47
  "inputBoundsContract": "shapeContract and present.min and present.max and ranks.min == 0 and numel(shapes.min) == 1 and ranks.max == 0 and numel(shapes.max) == 1",
48
  "defaultMin": "-65504 if tensorDtypes.input == \"float16\" else (-340282346638528859811704183484516925440 if tensorDtypes.input == \"float32\" else (-128 if tensorDtypes.input == \"int8\" else (-2147483648 if tensorDtypes.input == \"int32\" else 0)))",
49
  "defaultMax": "65504 if tensorDtypes.input == \"float16\" else (340282346638528859811704183484516925440 if tensorDtypes.input == \"float32\" else (127 if tensorDtypes.input == \"int8\" else (2147483647 if tensorDtypes.input == \"int32\" else (255 if tensorDtypes.input == \"uint8\" else 4294967295))))",
50
- "wideVec4StorageOk": "dtypes.T != \"f32\" or device.features.has(\"subgroups\") or not (has(device.adapterInfo, \"subgroupMinSize\") and has(device.adapterInfo, \"subgroupMaxSize\") and device.adapterInfo.subgroupMinSize == 8 and device.adapterInfo.subgroupMaxSize <= 32)"
 
 
51
  },
52
- "constants": { "scalar": "dtypes.T", "usesF16": "dtypes.T == \"f16\"" },
53
- "bindingSets": {
54
- "defaultVector": [
55
- {
56
- "name": "x",
57
- "arg": "input",
58
- "semantic": "input",
59
- "buffer": { "type": "read-only-storage" },
60
- "elementType": "$vectorScalar"
61
- },
62
- {
63
- "name": "y",
64
- "arg": "output",
65
- "semantic": "output",
66
- "buffer": { "type": "storage" },
67
- "elementType": "$vectorScalar"
68
- },
69
- {
70
- "name": "params",
71
- "semantic": "kernel.params",
72
- "buffer": { "type": "uniform" },
73
- "struct": {
74
- "name": "Params",
75
- "fields": [{ "name": "count", "type": "u32", "value": "numel(shapes.output) / 4" }]
76
- }
77
- }
78
- ],
79
- "defaultScalar": [
80
- {
81
- "name": "input",
82
- "arg": "input",
83
- "semantic": "input",
84
- "buffer": { "type": "read-only-storage" },
85
- "elementType": "$scalar"
86
- },
87
- {
88
- "name": "output",
89
- "arg": "output",
90
- "semantic": "output",
91
- "buffer": { "type": "storage" },
92
- "elementType": "$scalar"
93
- },
94
- {
95
- "name": "params",
96
- "semantic": "kernel.params",
97
- "buffer": { "type": "uniform" },
98
- "struct": { "name": "Params", "fields": [{ "name": "count", "type": "u32", "value": "numel(shapes.output)" }] }
99
- }
100
- ],
101
- "inputBounds": [
102
- {
103
- "name": "input",
104
- "arg": "input",
105
- "semantic": "input",
106
- "buffer": { "type": "read-only-storage" },
107
- "elementType": "$scalar"
108
- },
109
- {
110
- "name": "output",
111
- "arg": "output",
112
- "semantic": "output",
113
- "buffer": { "type": "storage" },
114
- "elementType": "$scalar"
115
- },
116
- {
117
- "name": "clipMin",
118
- "arg": "min",
119
- "semantic": "min",
120
- "buffer": { "type": "read-only-storage" },
121
- "elementType": "$scalar"
122
- },
123
- {
124
- "name": "clipMax",
125
- "arg": "max",
126
- "semantic": "max",
127
- "buffer": { "type": "read-only-storage" },
128
- "elementType": "$scalar"
129
- },
130
- {
131
- "name": "params",
132
- "semantic": "kernel.params",
133
- "buffer": { "type": "uniform" },
134
- "struct": { "name": "Params", "fields": [{ "name": "count", "type": "u32", "value": "numel(shapes.output)" }] }
135
- }
136
- ],
137
- "minInput": [
138
- {
139
- "name": "input",
140
- "arg": "input",
141
- "semantic": "input",
142
- "buffer": { "type": "read-only-storage" },
143
- "elementType": "$scalar"
144
- },
145
- {
146
- "name": "output",
147
- "arg": "output",
148
- "semantic": "output",
149
- "buffer": { "type": "storage" },
150
- "elementType": "$scalar"
151
- },
152
- {
153
- "name": "clipMin",
154
- "arg": "min",
155
- "semantic": "min",
156
- "buffer": { "type": "read-only-storage" },
157
- "elementType": "$scalar"
158
- },
159
- {
160
- "name": "params",
161
- "semantic": "kernel.params",
162
- "buffer": { "type": "uniform" },
163
- "struct": { "name": "Params", "fields": [{ "name": "count", "type": "u32", "value": "numel(shapes.output)" }] }
164
- }
165
- ],
166
- "maxInput": [
167
- {
168
- "name": "input",
169
- "arg": "input",
170
- "semantic": "input",
171
- "buffer": { "type": "read-only-storage" },
172
- "elementType": "$scalar"
173
- },
174
- {
175
- "name": "output",
176
- "arg": "output",
177
- "semantic": "output",
178
- "buffer": { "type": "storage" },
179
- "elementType": "$scalar"
180
- },
181
- {
182
- "name": "clipMax",
183
- "arg": "max",
184
- "semantic": "max",
185
- "buffer": { "type": "read-only-storage" },
186
- "elementType": "$scalar"
187
- },
188
- {
189
- "name": "params",
190
- "semantic": "kernel.params",
191
- "buffer": { "type": "uniform" },
192
- "struct": { "name": "Params", "fields": [{ "name": "count", "type": "u32", "value": "numel(shapes.output)" }] }
193
- }
194
- ],
195
- "inputBoundsVector": [
196
- {
197
- "name": "x",
198
- "arg": "input",
199
- "semantic": "input",
200
- "buffer": { "type": "read-only-storage" },
201
- "elementType": "$vectorScalar"
202
- },
203
- {
204
- "name": "y",
205
- "arg": "output",
206
- "semantic": "output",
207
- "buffer": { "type": "storage" },
208
- "elementType": "$vectorScalar"
209
- },
210
- {
211
- "name": "clipMin",
212
- "arg": "min",
213
- "semantic": "min",
214
- "buffer": { "type": "read-only-storage" },
215
- "elementType": "$scalar"
216
- },
217
- {
218
- "name": "clipMax",
219
- "arg": "max",
220
- "semantic": "max",
221
- "buffer": { "type": "read-only-storage" },
222
- "elementType": "$scalar"
223
- },
224
- {
225
- "name": "params",
226
- "semantic": "kernel.params",
227
- "buffer": { "type": "uniform" },
228
- "struct": {
229
- "name": "Params",
230
- "fields": [{ "name": "count", "type": "u32", "value": "numel(shapes.output) / 4" }]
231
- }
232
- }
233
- ],
234
- "minInputVector": [
235
- {
236
- "name": "x",
237
- "arg": "input",
238
- "semantic": "input",
239
- "buffer": { "type": "read-only-storage" },
240
- "elementType": "$vectorScalar"
241
- },
242
- {
243
- "name": "y",
244
- "arg": "output",
245
- "semantic": "output",
246
- "buffer": { "type": "storage" },
247
- "elementType": "$vectorScalar"
248
- },
249
- {
250
- "name": "clipMin",
251
- "arg": "min",
252
- "semantic": "min",
253
- "buffer": { "type": "read-only-storage" },
254
- "elementType": "$scalar"
255
- },
256
- {
257
- "name": "params",
258
- "semantic": "kernel.params",
259
- "buffer": { "type": "uniform" },
260
- "struct": {
261
- "name": "Params",
262
- "fields": [{ "name": "count", "type": "u32", "value": "numel(shapes.output) / 4" }]
263
- }
264
- }
265
- ],
266
- "maxInputVector": [
267
- {
268
- "name": "x",
269
- "arg": "input",
270
- "semantic": "input",
271
- "buffer": { "type": "read-only-storage" },
272
- "elementType": "$vectorScalar"
273
- },
274
- {
275
- "name": "y",
276
- "arg": "output",
277
- "semantic": "output",
278
- "buffer": { "type": "storage" },
279
- "elementType": "$vectorScalar"
280
- },
281
- {
282
- "name": "clipMax",
283
- "arg": "max",
284
- "semantic": "max",
285
- "buffer": { "type": "read-only-storage" },
286
- "elementType": "$scalar"
287
- },
288
- {
289
- "name": "params",
290
- "semantic": "kernel.params",
291
- "buffer": { "type": "uniform" },
292
- "struct": {
293
- "name": "Params",
294
- "fields": [{ "name": "count", "type": "u32", "value": "numel(shapes.output) / 4" }]
295
- }
296
- }
297
- ]
298
  },
299
  "variants": [
300
  {
301
  "id": "default_bounds_vec4",
302
  "priority": 20,
303
  "when": ["defaultBoundsContract", "numel(shapes.input) > 0", "numel(shapes.input) % 4 == 0", "wideVec4StorageOk"],
304
- "constants": { "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"" },
305
  "passes": [
306
  {
307
  "id": "main",
308
  "name": "Clip.defaultBoundsVec4",
309
- "source": {
310
- "shader": "unary-vec4.wgsl.jinja",
311
- "inputs": { "op": "\"clip\"", "boundsFromInput": false, "minValue": "defaultMin", "maxValue": "defaultMax" }
 
 
 
 
312
  },
313
- "bindings": "defaultVector",
314
- "dispatch": { "threads": "numel(shapes.output) / 4", "workgroupSize": "tunables.WORKGROUP_SIZE" }
 
 
 
 
315
  }
316
  ]
317
  },
@@ -322,12 +71,20 @@
322
  {
323
  "id": "main",
324
  "name": "Clip.defaultBounds",
325
- "source": {
326
- "shader": "unary-scalar.wgsl.jinja",
327
- "inputs": { "op": "\"clip\"", "boundsFromInput": false, "minValue": "defaultMin", "maxValue": "defaultMax" }
 
 
 
 
328
  },
329
- "bindings": "defaultScalar",
330
- "dispatch": { "threads": "numel(shapes.output)", "workgroupSize": "tunables.WORKGROUP_SIZE" }
 
 
 
 
331
  }
332
  ]
333
  },
@@ -335,17 +92,25 @@
335
  "id": "input_bounds_vec4",
336
  "priority": 40,
337
  "when": ["inputBoundsContract", "numel(shapes.input) > 0", "numel(shapes.input) % 4 == 0", "wideVec4StorageOk"],
338
- "constants": { "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"" },
339
  "passes": [
340
  {
341
  "id": "main",
342
  "name": "Clip.inputBoundsVec4",
343
- "source": {
344
- "shader": "unary-vec4.wgsl.jinja",
345
- "inputs": { "op": "\"clip\"", "boundsFromInput": true, "clipMinFromInput": true, "clipMaxFromInput": true }
 
 
 
 
346
  },
347
- "bindings": "inputBoundsVector",
348
- "dispatch": { "threads": "numel(shapes.output) / 4", "workgroupSize": "tunables.WORKGROUP_SIZE" }
 
 
 
 
349
  }
350
  ]
351
  },
@@ -353,17 +118,25 @@
353
  "id": "min_input_vec4",
354
  "priority": 40,
355
  "when": ["minInputContract", "numel(shapes.input) > 0", "numel(shapes.input) % 4 == 0", "wideVec4StorageOk"],
356
- "constants": { "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"" },
357
  "passes": [
358
  {
359
  "id": "main",
360
  "name": "Clip.minInputVec4",
361
- "source": {
362
- "shader": "unary-vec4.wgsl.jinja",
363
- "inputs": { "op": "\"clip\"", "boundsFromInput": true, "clipMinFromInput": true, "clipMaxFromInput": false }
 
 
 
 
364
  },
365
- "bindings": "minInputVector",
366
- "dispatch": { "threads": "numel(shapes.output) / 4", "workgroupSize": "tunables.WORKGROUP_SIZE" }
 
 
 
 
367
  }
368
  ]
369
  },
@@ -371,17 +144,25 @@
371
  "id": "max_input_vec4",
372
  "priority": 40,
373
  "when": ["maxInputContract", "numel(shapes.input) > 0", "numel(shapes.input) % 4 == 0", "wideVec4StorageOk"],
374
- "constants": { "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"" },
375
  "passes": [
376
  {
377
  "id": "main",
378
  "name": "Clip.maxInputVec4",
379
- "source": {
380
- "shader": "unary-vec4.wgsl.jinja",
381
- "inputs": { "op": "\"clip\"", "boundsFromInput": true, "clipMinFromInput": false, "clipMaxFromInput": true }
 
 
 
 
382
  },
383
- "bindings": "maxInputVector",
384
- "dispatch": { "threads": "numel(shapes.output) / 4", "workgroupSize": "tunables.WORKGROUP_SIZE" }
 
 
 
 
385
  }
386
  ]
387
  },
@@ -393,12 +174,20 @@
393
  {
394
  "id": "main",
395
  "name": "Clip.inputBounds",
396
- "source": {
397
- "shader": "unary-scalar.wgsl.jinja",
398
- "inputs": { "op": "\"clip\"", "boundsFromInput": true, "clipMinFromInput": true, "clipMaxFromInput": true }
 
 
 
 
399
  },
400
- "bindings": "inputBounds",
401
- "dispatch": { "threads": "numel(shapes.output)", "workgroupSize": "tunables.WORKGROUP_SIZE" }
 
 
 
 
402
  }
403
  ]
404
  },
@@ -410,12 +199,20 @@
410
  {
411
  "id": "main",
412
  "name": "Clip.minInput",
413
- "source": {
414
- "shader": "unary-scalar.wgsl.jinja",
415
- "inputs": { "op": "\"clip\"", "boundsFromInput": true, "clipMinFromInput": true, "clipMaxFromInput": false }
 
 
 
 
416
  },
417
- "bindings": "minInput",
418
- "dispatch": { "threads": "numel(shapes.output)", "workgroupSize": "tunables.WORKGROUP_SIZE" }
 
 
 
 
419
  }
420
  ]
421
  },
@@ -427,12 +224,20 @@
427
  {
428
  "id": "main",
429
  "name": "Clip.maxInput",
430
- "source": {
431
- "shader": "unary-scalar.wgsl.jinja",
432
- "inputs": { "op": "\"clip\"", "boundsFromInput": true, "clipMinFromInput": false, "clipMaxFromInput": true }
 
 
 
 
433
  },
434
- "bindings": "maxInput",
435
- "dispatch": { "threads": "numel(shapes.output)", "workgroupSize": "tunables.WORKGROUP_SIZE" }
 
 
 
 
436
  }
437
  ]
438
  }
 
2
  "domain": "ai.onnx",
3
  "name": "Clip",
4
  "sinceVersion": 13,
5
+ "inputs": {
6
+ "input": { "dtype": "T" },
7
+ "min": { "dtype": "T", "rank": 0, "optional": true, "shape": [] },
8
+ "max": { "dtype": "T", "rank": 0, "optional": true, "shape": [] }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  },
10
+ "outputs": { "output": { "dtype": "T", "rank": "ranks.input", "shape": "shapes.input" } },
11
+ "typeConstraints": { "T": ["float32", "float16", "int32", "uint32", "int8", "uint8"] },
12
+ "tunables": { "WORKGROUP_SIZE": { "default": 256 } },
13
  "derive": {
14
  "shapeContract": "numel(shapes.input) == numel(shapes.output) and f16Ok(dtypes.T)",
15
  "defaultBoundsContract": "shapeContract and not present.min and not present.max",
 
18
  "inputBoundsContract": "shapeContract and present.min and present.max and ranks.min == 0 and numel(shapes.min) == 1 and ranks.max == 0 and numel(shapes.max) == 1",
19
  "defaultMin": "-65504 if tensorDtypes.input == \"float16\" else (-340282346638528859811704183484516925440 if tensorDtypes.input == \"float32\" else (-128 if tensorDtypes.input == \"int8\" else (-2147483648 if tensorDtypes.input == \"int32\" else 0)))",
20
  "defaultMax": "65504 if tensorDtypes.input == \"float16\" else (340282346638528859811704183484516925440 if tensorDtypes.input == \"float32\" else (127 if tensorDtypes.input == \"int8\" else (2147483647 if tensorDtypes.input == \"int32\" else (255 if tensorDtypes.input == \"uint8\" else 4294967295))))",
21
+ "wideVec4StorageOk": "dtypes.T != \"f32\" or device.features.has(\"subgroups\") or not (has(device.adapterInfo, \"subgroupMinSize\") and has(device.adapterInfo, \"subgroupMaxSize\") and device.adapterInfo.subgroupMinSize == 8 and device.adapterInfo.subgroupMaxSize <= 32)",
22
+ "scalar": "dtypes.T",
23
+ "usesF16": "dtypes.T == \"f16\""
24
  },
25
+ "bindings": {
26
+ "x": { "arg": "input", "buffer": "read-only-storage", "elementType": "$vectorScalar" },
27
+ "y": { "arg": "output", "buffer": "storage", "elementType": "$vectorScalar" },
28
+ "params": {
29
+ "buffer": "uniform",
30
+ "struct": [{ "name": "count", "type": "u32", "value": "numel(shapes.output) / 4" }]
31
+ },
32
+ "params_2": {
33
+ "name": "params",
34
+ "buffer": "uniform",
35
+ "struct": [{ "name": "count", "type": "u32", "value": "numel(shapes.output)" }]
36
+ },
37
+ "clipMin": { "arg": "min", "buffer": "read-only-storage", "elementType": "$scalar" },
38
+ "clipMax": { "arg": "max", "buffer": "read-only-storage", "elementType": "$scalar" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  },
40
  "variants": [
41
  {
42
  "id": "default_bounds_vec4",
43
  "priority": 20,
44
  "when": ["defaultBoundsContract", "numel(shapes.input) > 0", "numel(shapes.input) % 4 == 0", "wideVec4StorageOk"],
45
+ "derive": { "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"" },
46
  "passes": [
47
  {
48
  "id": "main",
49
  "name": "Clip.defaultBoundsVec4",
50
+ "shader": "unary-vec4.wgsl.jinja",
51
+ "derive": {
52
+ "op": "\"clip\"",
53
+ "boundsFromInput": false,
54
+ "minValue": "defaultMin",
55
+ "maxValue": "defaultMax",
56
+ "vec4PerThread": "4 if numel(shapes.output) * dtypeBytes(tensorDtypes.output) <= 16777216 else 1"
57
  },
58
+ "bindings": ["x", "y", "params"],
59
+ "dispatch": {
60
+ "x": "min(ceilDiv((ceilDiv(numel(shapes.output) / 4, 4 if numel(shapes.output) * dtypeBytes(tensorDtypes.output) <= 16777216 else 1)), (tunables.WORKGROUP_SIZE)), 65535)",
61
+ "y": "ceilDiv(ceilDiv((ceilDiv(numel(shapes.output) / 4, 4 if numel(shapes.output) * dtypeBytes(tensorDtypes.output) <= 16777216 else 1)), (tunables.WORKGROUP_SIZE)), 65535)",
62
+ "z": 1
63
+ }
64
  }
65
  ]
66
  },
 
71
  {
72
  "id": "main",
73
  "name": "Clip.defaultBounds",
74
+ "shader": "unary-scalar.wgsl.jinja",
75
+ "derive": {
76
+ "op": "\"clip\"",
77
+ "boundsFromInput": false,
78
+ "minValue": "defaultMin",
79
+ "maxValue": "defaultMax",
80
+ "itemsPerInvocation": 4
81
  },
82
+ "bindings": ["input", "output", "params_2"],
83
+ "dispatch": {
84
+ "x": "min(ceilDiv((ceilDiv(numel(shapes.output), 4)), (tunables.WORKGROUP_SIZE)), 65535)",
85
+ "y": "ceilDiv(ceilDiv((ceilDiv(numel(shapes.output), 4)), (tunables.WORKGROUP_SIZE)), 65535)",
86
+ "z": 1
87
+ }
88
  }
89
  ]
90
  },
 
92
  "id": "input_bounds_vec4",
93
  "priority": 40,
94
  "when": ["inputBoundsContract", "numel(shapes.input) > 0", "numel(shapes.input) % 4 == 0", "wideVec4StorageOk"],
95
+ "derive": { "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"" },
96
  "passes": [
97
  {
98
  "id": "main",
99
  "name": "Clip.inputBoundsVec4",
100
+ "shader": "unary-vec4.wgsl.jinja",
101
+ "derive": {
102
+ "op": "\"clip\"",
103
+ "boundsFromInput": true,
104
+ "clipMinFromInput": true,
105
+ "clipMaxFromInput": true,
106
+ "vec4PerThread": "4 if numel(shapes.output) * dtypeBytes(tensorDtypes.output) <= 16777216 else 1"
107
  },
108
+ "bindings": ["x", "y", "clipMin", "clipMax", "params"],
109
+ "dispatch": {
110
+ "x": "min(ceilDiv((ceilDiv(numel(shapes.output) / 4, 4 if numel(shapes.output) * dtypeBytes(tensorDtypes.output) <= 16777216 else 1)), (tunables.WORKGROUP_SIZE)), 65535)",
111
+ "y": "ceilDiv(ceilDiv((ceilDiv(numel(shapes.output) / 4, 4 if numel(shapes.output) * dtypeBytes(tensorDtypes.output) <= 16777216 else 1)), (tunables.WORKGROUP_SIZE)), 65535)",
112
+ "z": 1
113
+ }
114
  }
115
  ]
116
  },
 
118
  "id": "min_input_vec4",
119
  "priority": 40,
120
  "when": ["minInputContract", "numel(shapes.input) > 0", "numel(shapes.input) % 4 == 0", "wideVec4StorageOk"],
121
+ "derive": { "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"" },
122
  "passes": [
123
  {
124
  "id": "main",
125
  "name": "Clip.minInputVec4",
126
+ "shader": "unary-vec4.wgsl.jinja",
127
+ "derive": {
128
+ "op": "\"clip\"",
129
+ "boundsFromInput": true,
130
+ "clipMinFromInput": true,
131
+ "clipMaxFromInput": false,
132
+ "vec4PerThread": "4 if numel(shapes.output) * dtypeBytes(tensorDtypes.output) <= 16777216 else 1"
133
  },
134
+ "bindings": ["x", "y", "clipMin", "params"],
135
+ "dispatch": {
136
+ "x": "min(ceilDiv((ceilDiv(numel(shapes.output) / 4, 4 if numel(shapes.output) * dtypeBytes(tensorDtypes.output) <= 16777216 else 1)), (tunables.WORKGROUP_SIZE)), 65535)",
137
+ "y": "ceilDiv(ceilDiv((ceilDiv(numel(shapes.output) / 4, 4 if numel(shapes.output) * dtypeBytes(tensorDtypes.output) <= 16777216 else 1)), (tunables.WORKGROUP_SIZE)), 65535)",
138
+ "z": 1
139
+ }
140
  }
141
  ]
142
  },
 
144
  "id": "max_input_vec4",
145
  "priority": 40,
146
  "when": ["maxInputContract", "numel(shapes.input) > 0", "numel(shapes.input) % 4 == 0", "wideVec4StorageOk"],
147
+ "derive": { "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"" },
148
  "passes": [
149
  {
150
  "id": "main",
151
  "name": "Clip.maxInputVec4",
152
+ "shader": "unary-vec4.wgsl.jinja",
153
+ "derive": {
154
+ "op": "\"clip\"",
155
+ "boundsFromInput": true,
156
+ "clipMinFromInput": false,
157
+ "clipMaxFromInput": true,
158
+ "vec4PerThread": "4 if numel(shapes.output) * dtypeBytes(tensorDtypes.output) <= 16777216 else 1"
159
  },
160
+ "bindings": ["x", "y", "clipMax", "params"],
161
+ "dispatch": {
162
+ "x": "min(ceilDiv((ceilDiv(numel(shapes.output) / 4, 4 if numel(shapes.output) * dtypeBytes(tensorDtypes.output) <= 16777216 else 1)), (tunables.WORKGROUP_SIZE)), 65535)",
163
+ "y": "ceilDiv(ceilDiv((ceilDiv(numel(shapes.output) / 4, 4 if numel(shapes.output) * dtypeBytes(tensorDtypes.output) <= 16777216 else 1)), (tunables.WORKGROUP_SIZE)), 65535)",
164
+ "z": 1
165
+ }
166
  }
167
  ]
168
  },
 
174
  {
175
  "id": "main",
176
  "name": "Clip.inputBounds",
177
+ "shader": "unary-scalar.wgsl.jinja",
178
+ "derive": {
179
+ "op": "\"clip\"",
180
+ "boundsFromInput": true,
181
+ "clipMinFromInput": true,
182
+ "clipMaxFromInput": true,
183
+ "itemsPerInvocation": 4
184
  },
185
+ "bindings": ["input", "output", "clipMin", "clipMax", "params_2"],
186
+ "dispatch": {
187
+ "x": "min(ceilDiv((ceilDiv(numel(shapes.output), 4)), (tunables.WORKGROUP_SIZE)), 65535)",
188
+ "y": "ceilDiv(ceilDiv((ceilDiv(numel(shapes.output), 4)), (tunables.WORKGROUP_SIZE)), 65535)",
189
+ "z": 1
190
+ }
191
  }
192
  ]
193
  },
 
199
  {
200
  "id": "main",
201
  "name": "Clip.minInput",
202
+ "shader": "unary-scalar.wgsl.jinja",
203
+ "derive": {
204
+ "op": "\"clip\"",
205
+ "boundsFromInput": true,
206
+ "clipMinFromInput": true,
207
+ "clipMaxFromInput": false,
208
+ "itemsPerInvocation": 4
209
  },
210
+ "bindings": ["input", "output", "clipMin", "params_2"],
211
+ "dispatch": {
212
+ "x": "min(ceilDiv((ceilDiv(numel(shapes.output), 4)), (tunables.WORKGROUP_SIZE)), 65535)",
213
+ "y": "ceilDiv(ceilDiv((ceilDiv(numel(shapes.output), 4)), (tunables.WORKGROUP_SIZE)), 65535)",
214
+ "z": 1
215
+ }
216
  }
217
  ]
218
  },
 
224
  {
225
  "id": "main",
226
  "name": "Clip.maxInput",
227
+ "shader": "unary-scalar.wgsl.jinja",
228
+ "derive": {
229
+ "op": "\"clip\"",
230
+ "boundsFromInput": true,
231
+ "clipMinFromInput": false,
232
+ "clipMaxFromInput": true,
233
+ "itemsPerInvocation": 4
234
  },
235
+ "bindings": ["input", "output", "clipMax", "params_2"],
236
+ "dispatch": {
237
+ "x": "min(ceilDiv((ceilDiv(numel(shapes.output), 4)), (tunables.WORKGROUP_SIZE)), 65535)",
238
+ "y": "ceilDiv(ceilDiv((ceilDiv(numel(shapes.output), 4)), (tunables.WORKGROUP_SIZE)), 65535)",
239
+ "z": 1
240
+ }
241
  }
242
  ]
243
  }
build/webgpu/metadata.json CHANGED
@@ -1,19 +1,31 @@
1
  {
2
  "name": "ai.onnx.Clip",
3
- "id": "_ai_onnx_clip_webgpu_ea7567b",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
- "bench.json": "SiGZqqpALf6ubJItLzIkYagu90oP+HzScN4MdEyZgc8=",
11
- "manifest.json": "us9b3p/dfzQqD40BHqWMZiVp6xaoru4kvNbEXWhfevM=",
12
- "test.json": "ymGZ3BiNHy1Nye9xpVQCWrfPh5sbCvMpsaE5E8lWq/c=",
13
- "unary-scalar.wgsl.jinja": "NeWScwhOG4N4iqKUuLMB0vL0DbMQqgUCFSp+g+YoKCY=",
14
- "unary-vec4.wgsl.jinja": "QKAUBbXPhcNyTR7St0KyqUqJjuS++OgbomsrxdMCrTw="
15
  }
16
  },
17
- "provenance": { "kernel": { "sha": "2e7068faf55e7f43df740015f6d1ee49391a41c5", "dirty": false } },
18
- "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/ai.onnx.Clip" }
 
 
 
 
 
 
 
 
 
 
 
 
19
  }
 
1
  {
2
  "name": "ai.onnx.Clip",
3
+ "id": "_ai_onnx_clip_webgpu_de72431",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
+ "bench.json": "F12x3+s84ktuo167e5515+F2hnUMtcowAYHmEgoDQOg=",
11
+ "manifest.json": "kwVE+OfpodbDYkXNlYpHzflNPmAefXobA+ugNpwKfME=",
12
+ "test.json": "a604ZK+4B6A13Q4yccOjW3dVe84WzyJ57YjvMttgjuQ=",
13
+ "unary-scalar.wgsl.jinja": "rsQQXT2OWXvamkOuvViLDBj3K2kZY/CpmMOvsDNXU7Y=",
14
+ "unary-vec4.wgsl.jinja": "/UXGg3mvbuFOUdyjrpM1npD9tJ5fovKFZ6dt0LFgRRc="
15
  }
16
  },
17
+ "provenance": { "kernel": { "sha": "91d990483a174128daf7673f3f37a7c890493ae1", "dirty": false } },
18
+ "webgpu": {
19
+ "manifestSpec": "2.0",
20
+ "variants": {
21
+ "default_bounds_vec4": ["unary-vec4.wgsl.jinja"],
22
+ "default_bounds": ["unary-scalar.wgsl.jinja"],
23
+ "input_bounds_vec4": ["unary-vec4.wgsl.jinja"],
24
+ "min_input_vec4": ["unary-vec4.wgsl.jinja"],
25
+ "max_input_vec4": ["unary-vec4.wgsl.jinja"],
26
+ "input_bounds": ["unary-scalar.wgsl.jinja"],
27
+ "min_input": ["unary-scalar.wgsl.jinja"],
28
+ "max_input": ["unary-scalar.wgsl.jinja"]
29
+ }
30
+ }
31
  }
build/webgpu/test.json CHANGED
@@ -1,5 +1,4 @@
1
  {
2
- "op": "ai.onnx.Clip",
3
  "fixtureArrays": {
4
  "onnx_backend_clip_input": [1.764052391052246, 0.40015721321105957, 0.978738009929657, 2.2408931255340576, 1.8675580024719238, -0.9772778749465942, 0.9500884413719177, -0.15135720372200012, -0.10321885347366333, 0.4105985164642334, 0.14404356479644775, 1.4542734622955322, 0.7610377073287964, 0.12167501449584961, 0.44386324286460876, 0.3336743414402008, 1.4940791130065918, -0.2051582634449005, 0.3130677044391632, -0.8540957570075989, -2.5529897212982178, 0.653618574142456, 0.8644362092018127, -0.7421650290489197, 2.269754648208618, -1.4543657302856445, 0.04575851559638977, -0.18718385696411133, 1.5327792167663574, 1.4693588018417358, 0.154947429895401, 0.37816253304481506, -0.8877857327461243, -1.980796456336975, -0.34791216254234314, 0.15634897351264954, 1.2302906513214111, 1.202379822731018, -0.38732680678367615, -0.302302747964859, -1.0485529899597168, -1.420017957687378, -1.7062702178955078, 1.950775384902954, -0.5096521973609924, -0.4380742907524109, -1.2527953386306763, 0.7774903774261475, -1.6138978004455566, -0.21274028718471527, -0.8954665660858154, 0.38690251111984253, -0.5108051300048828, -1.18063223361969, -0.02818222902715206, 0.4283318817615509, 0.06651721894741058, 0.30247190594673157, -0.6343221068382263, -0.3627411723136902]
5
  },
@@ -35,7 +34,7 @@
35
  "provenance": {
36
  "source": "onnxruntime/test/providers/cpu/math/clip_test.cc",
37
  "test": "MathOpTest.Clip_Relu",
38
- "notes": "Scalar-path companion: Clip with min=0 should pass through positive subnormal inputs instead of flushing them to zero."
39
  },
40
  "inputs": {
41
  "input": { "dtype": "float32", "shape": [3], "data": { "kind": "values", "values": [-1e-40, 0.0, 1e-40] } },
@@ -370,16 +369,6 @@
370
  },
371
  "outputs": { "output": { "dtype": "float32", "shape": [4], "tolerance": 0.000001, "allowNaN": true } }
372
  },
373
- {
374
- "name": "backend_min_greater_than_max",
375
- "inputs": {
376
- "input": { "dtype": "float32", "shape": [3], "data": { "kind": "values", "values": [-2.0, 0.0, 6.0] } },
377
- "min": { "dtype": "float32", "shape": [], "data": { "kind": "values", "values": [2.0] } },
378
- "max": { "dtype": "float32", "shape": [], "data": { "kind": "values", "values": [1.0] } }
379
- },
380
- "outputs": { "output": { "dtype": "float32", "shape": [3], "tolerance": 0 } },
381
- "provenance": { "source": "cmake/external/onnx/onnx/backend/test/data/node/test_clip_min_greater_than_max" }
382
- },
383
  {
384
  "name": "default_max_only",
385
  "inputs": {
@@ -844,14 +833,14 @@
844
  {
845
  "name": "onnx_backend_clip_min_greater_than_max",
846
  "inputs": {
847
- "input": { "dtype": "float32", "shape": [3], "data": { "kind": "values", "values": [-2.0, 0.0, 6.0] } },
848
  "min": { "dtype": "float32", "shape": [], "data": { "kind": "values", "values": [2.0] } },
849
  "max": { "dtype": "float32", "shape": [], "data": { "kind": "values", "values": [1.0] } }
850
  },
851
  "outputs": { "output": { "dtype": "float32", "shape": [3] } },
852
  "provenance": {
853
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_clip_min_greater_than_max",
854
- "notes": "ONNX Clip min/max bounds are supplied as scalar tensor inputs."
855
  }
856
  },
857
  {
@@ -931,11 +920,7 @@
931
  {
932
  "name": "input_bounds_i32_min_greater_than_max",
933
  "inputs": {
934
- "input": {
935
- "dtype": "int32",
936
- "shape": [6],
937
- "data": { "kind": "values", "values": [-2147483648, -10, 0, 5, 10, 2147483647] }
938
- },
939
  "min": { "dtype": "int32", "shape": [], "data": { "kind": "values", "values": [5] } },
940
  "max": { "dtype": "int32", "shape": [], "data": { "kind": "values", "values": [-5] } }
941
  },
@@ -946,6 +931,9 @@
946
  "data": { "kind": "values", "values": [-5, -5, -5, -5, -5, -5] },
947
  "tolerance": 0
948
  }
 
 
 
949
  }
950
  },
951
  {
@@ -968,7 +956,7 @@
968
  {
969
  "name": "uint8_input_bounds_min_greater_than_max",
970
  "inputs": {
971
- "input": { "dtype": "uint8", "shape": [5], "data": { "kind": "values", "values": [0, 50, 100, 150, 255] } },
972
  "min": { "dtype": "uint8", "shape": [], "data": { "kind": "values", "values": [200] } },
973
  "max": { "dtype": "uint8", "shape": [], "data": { "kind": "values", "values": [100] } }
974
  },
@@ -979,6 +967,9 @@
979
  "data": { "kind": "values", "values": [100, 100, 100, 100, 100] },
980
  "tolerance": 0
981
  }
 
 
 
982
  }
983
  }
984
  ]
 
1
  {
 
2
  "fixtureArrays": {
3
  "onnx_backend_clip_input": [1.764052391052246, 0.40015721321105957, 0.978738009929657, 2.2408931255340576, 1.8675580024719238, -0.9772778749465942, 0.9500884413719177, -0.15135720372200012, -0.10321885347366333, 0.4105985164642334, 0.14404356479644775, 1.4542734622955322, 0.7610377073287964, 0.12167501449584961, 0.44386324286460876, 0.3336743414402008, 1.4940791130065918, -0.2051582634449005, 0.3130677044391632, -0.8540957570075989, -2.5529897212982178, 0.653618574142456, 0.8644362092018127, -0.7421650290489197, 2.269754648208618, -1.4543657302856445, 0.04575851559638977, -0.18718385696411133, 1.5327792167663574, 1.4693588018417358, 0.154947429895401, 0.37816253304481506, -0.8877857327461243, -1.980796456336975, -0.34791216254234314, 0.15634897351264954, 1.2302906513214111, 1.202379822731018, -0.38732680678367615, -0.302302747964859, -1.0485529899597168, -1.420017957687378, -1.7062702178955078, 1.950775384902954, -0.5096521973609924, -0.4380742907524109, -1.2527953386306763, 0.7774903774261475, -1.6138978004455566, -0.21274028718471527, -0.8954665660858154, 0.38690251111984253, -0.5108051300048828, -1.18063223361969, -0.02818222902715206, 0.4283318817615509, 0.06651721894741058, 0.30247190594673157, -0.6343221068382263, -0.3627411723136902]
4
  },
 
34
  "provenance": {
35
  "source": "onnxruntime/test/providers/cpu/math/clip_test.cc",
36
  "test": "MathOpTest.Clip_Relu",
37
+ "notes": "On an unaligned scalar path, Clip with min=0 must preserve positive subnormal inputs."
38
  },
39
  "inputs": {
40
  "input": { "dtype": "float32", "shape": [3], "data": { "kind": "values", "values": [-1e-40, 0.0, 1e-40] } },
 
369
  },
370
  "outputs": { "output": { "dtype": "float32", "shape": [4], "tolerance": 0.000001, "allowNaN": true } }
371
  },
 
 
 
 
 
 
 
 
 
 
372
  {
373
  "name": "default_max_only",
374
  "inputs": {
 
833
  {
834
  "name": "onnx_backend_clip_min_greater_than_max",
835
  "inputs": {
836
+ "input": { "dtype": "float32", "shape": [3], "data": { "kind": "constant", "value": -2.0 } },
837
  "min": { "dtype": "float32", "shape": [], "data": { "kind": "values", "values": [2.0] } },
838
  "max": { "dtype": "float32", "shape": [], "data": { "kind": "values", "values": [1.0] } }
839
  },
840
  "outputs": { "output": { "dtype": "float32", "shape": [3] } },
841
  "provenance": {
842
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_clip_min_greater_than_max",
843
+ "notes": "Diverges from the upstream test's inputs (inputs.input values [-2.0, 0.0, 6.0] -> constant -2.0); the expected output is recomputed by the CPU reference for the new inputs. ONNX Clip min/max bounds are supplied as scalar tensor inputs. With min above max every element becomes max whatever the input holds, so the input is uniform at a value below both bounds: min(x,max) would return -2 and max(x,min) would return 2."
844
  }
845
  },
846
  {
 
920
  {
921
  "name": "input_bounds_i32_min_greater_than_max",
922
  "inputs": {
923
+ "input": { "dtype": "int32", "shape": [6], "data": { "kind": "constant", "value": -2147483648 } },
 
 
 
 
924
  "min": { "dtype": "int32", "shape": [], "data": { "kind": "values", "values": [5] } },
925
  "max": { "dtype": "int32", "shape": [], "data": { "kind": "values", "values": [-5] } }
926
  },
 
931
  "data": { "kind": "values", "values": [-5, -5, -5, -5, -5, -5] },
932
  "tolerance": 0
933
  }
934
+ },
935
+ "provenance": {
936
+ "notes": "With min above max every element becomes max whatever the input holds, so the input is uniform at int32 min: clamping in the wrong order returns -2147483648 or 5 instead of -5."
937
  }
938
  },
939
  {
 
956
  {
957
  "name": "uint8_input_bounds_min_greater_than_max",
958
  "inputs": {
959
+ "input": { "dtype": "uint8", "shape": [5], "data": { "kind": "constant", "value": 0 } },
960
  "min": { "dtype": "uint8", "shape": [], "data": { "kind": "values", "values": [200] } },
961
  "max": { "dtype": "uint8", "shape": [], "data": { "kind": "values", "values": [100] } }
962
  },
 
967
  "data": { "kind": "values", "values": [100, 100, 100, 100, 100] },
968
  "tolerance": 0
969
  }
970
+ },
971
+ "provenance": {
972
+ "notes": "With min above max every element becomes max whatever the input holds, so the input is uniform at 0: clamping in the wrong order returns 0 or 200 instead of 100."
973
  }
974
  }
975
  ]
build/webgpu/unary-scalar.wgsl.jinja CHANGED
@@ -1,18 +1,23 @@
1
  {% macro flat_tail_open() %}
2
  @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
3
- fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) nwg: vec3<u32>) {
4
  // 2D-folded flat index: gid.y carries the high bits when the element count exceeds the
5
- // maxComputeWorkgroupsPerDimension limit (the dispatch caps x and spills the rest into y).
6
- let invocation = gid.x + gid.y * nwg.x * {{ tunables.WORKGROUP_SIZE }}u;
7
- let i = invocation;
8
- if (i >= params.count) {
9
- return;
10
- }{% endmacro %}
 
 
 
11
  {% macro flat_tail_close() %}
 
12
  {% endmacro %}
13
 
14
- // Scalar unary fallback. Each branch retains the operation's numeric hardening,
15
- // including Payne-Hanek trigonometric range reduction and NaN/overflow guards.
 
16
  {% if usesF16 %}
17
  enable f16;
18
  {% endif %}
@@ -22,18 +27,18 @@ fn is_nan_f32(value: f32) -> bool {
22
  let bits = bitcast<u32>(value);
23
  return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
24
  }{% endmacro %}
25
- {% if source.op == "clip" and scalar != "i32" and scalar != "u32" %}
26
  {{ emit_is_nan_f32() }}
27
  {% endif %}
28
  {{ flat_tail_open() }}
29
- {% set clipMinFromInput = source.clipMinFromInput | default(false) %}
30
- {% set clipMaxFromInput = source.clipMaxFromInput | default(false) %}
31
- {% set clipMinValue = source.minValue | default(0) %}
32
- {% set clipMaxValue = source.maxValue | default(0) %}
33
  {% set clipScalar = scalar | default("f32") %}
34
  {% if scalar == "i32" or scalar == "u32" %}
35
  let value = input[i];
36
- {% if source.boundsFromInput %}
37
  {% if clipMinFromInput and clipMaxFromInput %}
38
  output[i] = min(max(value, clipMin[0]), clipMax[0]);
39
  {% elif clipMinFromInput %}
@@ -46,7 +51,7 @@ fn is_nan_f32(value: f32) -> bool {
46
  {% endif %}
47
  {% else %}
48
  let value = f32(input[i]);
49
- {% if source.boundsFromInput %}
50
  {% if clipMinFromInput and clipMaxFromInput %}
51
  var out = min(max(value, f32(clipMin[0])), f32(clipMax[0]));
52
  {% elif clipMinFromInput %}
 
1
  {% macro flat_tail_open() %}
2
  @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
3
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
4
  // 2D-folded flat index: gid.y carries the high bits when the element count exceeds the
5
+ // dispatch's per-axis workgroup fold width (the dispatch caps x and spills the rest into y).
6
+ let invocation = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ tunables.WORKGROUP_SIZE }}u;
7
+ // Tail-safe scalar x4 keeps vector-like dispatch density without requiring
8
+ // the logical tensor length (or its storage binding) to be vec4 aligned.
9
+ {% set itemsPerInvocation = itemsPerInvocation if itemsPerInvocation is defined else 4 %}
10
+ let begin = invocation * {{ itemsPerInvocation }}u;
11
+ let end = min(begin + {{ itemsPerInvocation }}u, params.count);
12
+ for (var i = begin; i < end; i = i + 1u) {
13
+ {%- endmacro %}
14
  {% macro flat_tail_close() %}
15
+ }
16
  {% endmacro %}
17
 
18
+ {% set boundsFromInput = boundsFromInput is defined and boundsFromInput %}
19
+ // Scalar unary elementwise implementation. Specialization emits only the
20
+ // selected operation and any numerical helper it requires.
21
  {% if usesF16 %}
22
  enable f16;
23
  {% endif %}
 
27
  let bits = bitcast<u32>(value);
28
  return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
29
  }{% endmacro %}
30
+ {% if op == "clip" and scalar != "i32" and scalar != "u32" %}
31
  {{ emit_is_nan_f32() }}
32
  {% endif %}
33
  {{ flat_tail_open() }}
34
+ {% set clipMinFromInput = clipMinFromInput | default(false) %}
35
+ {% set clipMaxFromInput = clipMaxFromInput | default(false) %}
36
+ {% set clipMinValue = minValue | default(0) %}
37
+ {% set clipMaxValue = maxValue | default(0) %}
38
  {% set clipScalar = scalar | default("f32") %}
39
  {% if scalar == "i32" or scalar == "u32" %}
40
  let value = input[i];
41
+ {% if boundsFromInput %}
42
  {% if clipMinFromInput and clipMaxFromInput %}
43
  output[i] = min(max(value, clipMin[0]), clipMax[0]);
44
  {% elif clipMinFromInput %}
 
51
  {% endif %}
52
  {% else %}
53
  let value = f32(input[i]);
54
+ {% if boundsFromInput %}
55
  {% if clipMinFromInput and clipMaxFromInput %}
56
  var out = min(max(value, f32(clipMin[0])), f32(clipMax[0]));
57
  {% elif clipMinFromInput %}
build/webgpu/unary-vec4.wgsl.jinja CHANGED
@@ -1,8 +1,5 @@
1
- // Loads and stores vec4<T> (128 bits) while retaining scalar per-component
2
- // arithmetic, including per-component helper calls for guard-heavy operations.
3
- {% if usesF16 %}
4
- enable f16;
5
- {% endif %}
6
  {{ env.wgsl.resourceDeclarations }}
7
 
8
  {% macro emit_is_nan_f32() %}
@@ -10,12 +7,12 @@ fn is_nan_f32(value: f32) -> bool {
10
  let bits = bitcast<u32>(value);
11
  return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
12
  }{% endmacro %}
13
- {% if (source.op == "clip" and scalar != "i32" and scalar != "u32") or (source.op == "relu" and scalar != "i32") or source.op == "sin" or source.op == "tan" %}
14
  {{ emit_is_nan_f32() }}
15
  {% endif %}
16
- {% if not (source.boundsFromInput | default(false)) and scalar != "i32" and scalar != "u32" %}
17
- {% set clipMinValue = source.minValue | default(0) %}
18
- {% set clipMaxValue = source.maxValue | default(0) %}
19
  fn clip_value(v: f32) -> f32 {
20
  var out = min(max(v, f32({{ clipMinValue }})), f32({{ clipMaxValue }}));
21
  if (is_nan_f32(v)) {
@@ -25,20 +22,39 @@ fn clip_value(v: f32) -> f32 {
25
  }
26
  {% endif %}
27
 
 
 
 
 
 
28
  @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
29
- fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) nwg: vec3<u32>) {
30
  // 2D-folded flat index: gid.y carries the high bits when the element count exceeds the
31
- // maxComputeWorkgroupsPerDimension limit (the dispatch caps x and spills the rest into y).
32
- let i = gid.x + gid.y * nwg.x * {{ tunables.WORKGROUP_SIZE }}u;
 
 
 
 
 
 
 
 
 
 
 
 
33
  if (i >= params.count) {
34
  return;
35
  }
 
 
36
  let xv = x[i];
37
- {% set clipMinValue = source.minValue | default(0) %}
38
- {% set clipMaxValue = source.maxValue | default(0) %}
39
- {% if source.boundsFromInput | default(false) %}
40
- {% set clipMinFromInput = source.clipMinFromInput | default(false) %}
41
- {% set clipMaxFromInput = source.clipMaxFromInput | default(false) %}
42
  {% if scalar == "i32" or scalar == "u32" %}
43
  {% if clipMinFromInput and clipMaxFromInput %}
44
  y[i] = min(max(xv, vec4<{{ scalar }}>(clipMin[0])), vec4<{{ scalar }}>(clipMax[0]));
@@ -70,4 +86,7 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups)
70
  let fv = vec4<f32>(xv);
71
  y[i] = {{ vectorScalar }}(vec4<f32>(clip_value(fv.x), clip_value(fv.y), clip_value(fv.z), clip_value(fv.w)));
72
  {% endif %}
 
 
 
73
  }
 
1
+ // Loads and stores vec4<T> while evaluating the selected unary operation per
2
+ // component.
 
 
 
3
  {{ env.wgsl.resourceDeclarations }}
4
 
5
  {% macro emit_is_nan_f32() %}
 
7
  let bits = bitcast<u32>(value);
8
  return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
9
  }{% endmacro %}
10
+ {% if (op == "clip" and scalar != "i32" and scalar != "u32") or (op == "relu" and scalar != "i32") or op == "sin" or op == "tan" %}
11
  {{ emit_is_nan_f32() }}
12
  {% endif %}
13
+ {% if not (boundsFromInput | default(false)) and scalar != "i32" and scalar != "u32" %}
14
+ {% set clipMinValue = minValue | default(0) %}
15
+ {% set clipMaxValue = maxValue | default(0) %}
16
  fn clip_value(v: f32) -> f32 {
17
  var out = min(max(v, f32({{ clipMinValue }})), f32({{ clipMaxValue }}));
18
  if (is_nan_f32(v)) {
 
22
  }
23
  {% endif %}
24
 
25
+ {% set vec4PerThread = vec4PerThread %}
26
+ {% if vec4PerThread > 1 %}
27
+ const ITEMS: u32 = {{ vec4PerThread }}u;
28
+ {% endif %}
29
+
30
  @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
31
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
32
  // 2D-folded flat index: gid.y carries the high bits when the element count exceeds the
33
+ // per-axis dispatch fold width (the dispatch caps x and spills the rest into y).
34
+ {% if vec4PerThread > 1 %}
35
+ // Each invocation walks ITEMS vec4 groups a span apart. Consecutive lanes
36
+ // access consecutive words on every step, while each lane can keep several
37
+ // independent loads in flight.
38
+ let tid = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ tunables.WORKGROUP_SIZE }}u;
39
+ let span = (params.count + ITEMS - 1u) / ITEMS;
40
+ for (var j = 0u; j < ITEMS; j = j + 1u) {
41
+ let i = tid + j * span;
42
+ if (i >= params.count) {
43
+ break;
44
+ }
45
+ {% else %}
46
+ let i = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ tunables.WORKGROUP_SIZE }}u;
47
  if (i >= params.count) {
48
  return;
49
  }
50
+ {% endif %}
51
+
52
  let xv = x[i];
53
+ {% set clipMinValue = minValue | default(0) %}
54
+ {% set clipMaxValue = maxValue | default(0) %}
55
+ {% if boundsFromInput | default(false) %}
56
+ {% set clipMinFromInput = clipMinFromInput | default(false) %}
57
+ {% set clipMaxFromInput = clipMaxFromInput | default(false) %}
58
  {% if scalar == "i32" or scalar == "u32" %}
59
  {% if clipMinFromInput and clipMaxFromInput %}
60
  y[i] = min(max(xv, vec4<{{ scalar }}>(clipMin[0])), vec4<{{ scalar }}>(clipMax[0]));
 
86
  let fv = vec4<f32>(xv);
87
  y[i] = {{ vectorScalar }}(vec4<f32>(clip_value(fv.x), clip_value(fv.y), clip_value(fv.z), clip_value(fv.w)));
88
  {% endif %}
89
+ {% if vec4PerThread > 1 %}
90
+ }
91
+ {% endif %}
92
  }