Xenova HF Staff commited on
Commit
e9d0b24
·
verified ·
1 Parent(s): 7c6615c

sync 91d990483a17

Browse files
README.md CHANGED
@@ -18,15 +18,15 @@ See the [ONNX `ArgMin` spec](https://onnx.ai/onnx/operators/onnx__ArgMin.html) f
18
 
19
  ## Inputs
20
 
21
- | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
22
  | --- | --- | --- | --- | --- | --- | --- |
23
- | `data` | `x` | `T` | — | — | Values whose minimum index is selected along `axis`. | required |
24
 
25
  ## Outputs
26
 
27
- | Name | Bind key | Logical dtype | WebGPU storage | Rank | Shape | Description | Presence |
28
  | --- | --- | --- | --- | --- | --- | --- | --- |
29
- | `reduced` | `y` | `I` | `uint32` | derived | derived; see description | Logical int64 indices of the minimum values along the reduced axis; WebGPU stores these bounded indices as uint32. | required |
30
 
31
  ## Attributes
32
 
@@ -51,7 +51,7 @@ Some implementation variants require `subgroups`. These are route-specific capab
51
 
52
  ## Files
53
 
54
- - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, provenance)
55
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
56
  - [`test.json`](build/webgpu/test.json) — correctness cases
57
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
@@ -65,10 +65,14 @@ Some implementation variants require `subgroups`. These are route-specific capab
65
 
66
  ## Use with `@huggingface/kernels`
67
 
68
- The loader derives every required output's shape and logical dtype from the manifest contract and this call.
69
- It then allocates the result tensors automatically.
 
 
 
70
 
71
  The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
 
72
 
73
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
74
 
 
18
 
19
  ## Inputs
20
 
21
+ | Name | Upstream name | Logical dtype | Rank | Shape | Description | Presence |
22
  | --- | --- | --- | --- | --- | --- | --- |
23
+ | `x` | `data` | `T` | — | — | Values whose minimum index is selected along `axis`. | required |
24
 
25
  ## Outputs
26
 
27
+ | Name | Upstream name | Logical dtype | WebGPU storage | Rank | Shape | Description | Presence |
28
  | --- | --- | --- | --- | --- | --- | --- | --- |
29
+ | `y` | `reduced` | `I` | `uint32` | derived | derived | Logical int64 indices of the minimum values along the reduced axis; WebGPU stores these bounded indices as uint32. | required |
30
 
31
  ## Attributes
32
 
 
51
 
52
  ## Files
53
 
54
+ - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, per-variant templates, provenance)
55
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
56
  - [`test.json`](build/webgpu/test.json) — correctness cases
57
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
 
65
 
66
  ## Use with `@huggingface/kernels`
67
 
68
+ ```sh
69
+ npm install --save-exact @huggingface/kernels@0.0.1-preview.2
70
+ ```
71
+
72
+ Required output shapes and logical data types are inferred from the supplied inputs and attributes; result tensors are allocated automatically.
73
 
74
  The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
75
+ It follows the `v1` branch as fixes land. To pin exact artifact bytes, pass a 40-character commit `revision` instead of `version`.
76
 
77
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
78
 
build/webgpu/bench.json CHANGED
@@ -1,5 +1,4 @@
1
  {
2
- "op": "ai.onnx.ArgMin",
3
  "cases": [
4
  {
5
  "name": "argmin-axis0-f32-1024x512",
 
1
  {
 
2
  "cases": [
3
  {
4
  "name": "argmin-axis0-f32-1024x512",
build/webgpu/manifest.json CHANGED
@@ -2,57 +2,47 @@
2
  "domain": "ai.onnx",
3
  "name": "ArgMin",
4
  "sinceVersion": 13,
5
- "description": "Returns the index of the minimum value along an axis, choosing the first equal value unless `select_last_index` is enabled.",
6
- "inputs": [{ "role": "data", "dtype": "T", "description": "Values whose minimum index is selected along `axis`." }],
7
- "outputs": [
8
- {
9
- "role": "reduced",
10
  "dtype": "I",
11
- "rank": "ranks.data if attrs.keepdims == 1 else ranks.data - 1",
12
- "description": "Logical int64 indices of the minimum values along the reduced axis; WebGPU stores these bounded indices as uint32.",
13
- "shape": "prefix(shapes.data, normalizedAxis) + ([1] if attrs.keepdims == 1 else []) + suffix(shapes.data, normalizedAxis + 1)"
14
  }
15
- ],
16
- "attributes": { "axis": 0, "keepdims": 1, "select_last_index": 0 },
17
- "attributeDescriptions": {
18
- "axis": "Axis to reduce; negative values count from the back.",
19
- "keepdims": "Retain the reduced dimension with length one when non-zero.",
20
- "select_last_index": "Choose the last equal minimum instead of the first when non-zero."
21
  },
 
22
  "attributeConstraints": { "keepdims": { "values": [0, 1] }, "select_last_index": { "values": [0, 1] } },
23
  "typeConstraints": { "T": ["float32", "float16", "int32", "uint32", "int16", "int8", "uint8"], "I": ["int64"] },
24
- "args": {
25
- "x": { "kind": "tensor", "semantic": "data", "role": "input" },
26
- "y": { "kind": "tensor", "semantic": "reduced", "role": "output", "dtype": "uint32" }
27
- },
28
  "tunables": {
29
- "WORKGROUP_SIZE": 256,
30
- "TILE_COLUMNS": 16,
31
- "TILED_MIN_AXIS": 64,
32
- "TILED_MIN_OUTPUTS": 16,
33
- "SPLIT_MIN_AXIS": 8192,
34
- "SPLIT_MAX_OUTPUTS": 4096,
35
- "SPLIT_TARGET_AXIS": 256,
36
- "SPLIT_TILE_COLUMNS": 8,
37
- "MAX_SPLITS": 128,
38
- "ROW_SPLIT_MIN_AXIS": 32768,
39
- "ROW_SPLIT_MAX_OUTPUTS": 32,
40
- "ROW_SPLIT_TARGET_AXIS": 2048,
41
- "ROW_SPLIT_MAX_SPLITS": 64
42
  },
43
  "derive": {
44
  "deviceWorkgroupCap": "min(device.limits.maxComputeInvocationsPerWorkgroup, device.limits.maxComputeWorkgroupSizeX)",
45
- "foldedDispatchCapacity": "device.limits.maxComputeWorkgroupsPerDimension * device.limits.maxComputeWorkgroupsPerDimension",
46
  "narrowSubgroupRange": "has(device.adapterInfo, \"subgroupMinSize\") and has(device.adapterInfo, \"subgroupMaxSize\") and device.adapterInfo.subgroupMinSize < device.adapterInfo.subgroupMaxSize and device.adapterInfo.subgroupMaxSize <= 16",
47
- "normalizedAxis": "attrs.axis if attrs.axis >= 0 else attrs.axis + ranks.data",
48
- "axisInRange": "ranks.data >= 1 and normalizedAxis >= 0 and normalizedAxis < ranks.data",
49
- "axisDim": "dim(shapes.data, normalizedAxis)",
50
- "axisInner": "inner(shapes.data, normalizedAxis)",
51
- "outputCount": "numel(shapes.reduced)",
52
- "outputRankOk": "(attrs.keepdims == 1 and ranks.reduced == ranks.data) or (attrs.keepdims == 0 and ranks.reduced == ranks.data - 1)",
53
- "outputShapeOk": "outputRankOk and outputCount * axisDim == numel(shapes.data)",
54
  "baseContract": "axisInRange and axisDim > 0 and outputShapeOk and f16Ok(dtypes.T)",
55
- "lastAxis": "normalizedAxis == ranks.data - 1",
56
  "nativeParallelType": "dtypes.T == \"f32\" or dtypes.T == \"i32\" or dtypes.T == \"u32\"",
57
  "rowParallelType": "nativeParallelType or dtypes.T == \"f16\"",
58
  "workgroupSize": "min(tunables.WORKGROUP_SIZE, deviceWorkgroupCap)",
@@ -65,190 +55,84 @@
65
  "splitCount": "min(tunables.MAX_SPLITS, pow2ceil(ceilDiv(axisDim, tunables.SPLIT_TARGET_AXIS)))",
66
  "splitScratchBytes": "splitCount * outputCount * 8",
67
  "splitScratchFits": "splitScratchBytes <= device.limits.maxStorageBufferBindingSize and splitScratchBytes <= device.limits.maxBufferSize",
68
- "splitDispatchFits": "splitCount <= device.limits.maxComputeWorkgroupsPerDimension and ceilDiv(ceilDiv(outputCount, workgroupSize), device.limits.maxComputeWorkgroupsPerDimension) <= device.limits.maxComputeWorkgroupsPerDimension",
69
- "splitTileDispatchFits": "splitCount <= device.limits.maxComputeWorkgroupsPerDimension and ceilDiv(outputCount, tunables.SPLIT_TILE_COLUMNS) <= device.limits.maxComputeWorkgroupsPerDimension",
70
  "rowSplitCount": "min(tunables.ROW_SPLIT_MAX_SPLITS, pow2ceil(ceilDiv(axisDim, tunables.ROW_SPLIT_TARGET_AXIS)))",
71
  "rowSplitScratchBytes": "rowSplitCount * outputCount * 8",
72
  "rowSplitScratchFits": "rowSplitScratchBytes <= device.limits.maxStorageBufferBindingSize and rowSplitScratchBytes <= device.limits.maxBufferSize",
73
- "rowSplitDispatchFits": "outputCount <= device.limits.maxComputeWorkgroupsPerDimension and rowSplitCount <= device.limits.maxComputeWorkgroupsPerDimension",
74
  "tileDispatchFits": "ceilDiv(outputCount, tunables.TILE_COLUMNS) <= foldedDispatchCapacity"
75
  },
76
- "bindingSets": {
77
- "contiguousVec4": [
78
- {
79
- "name": "x",
80
- "arg": "x",
81
- "semantic": "data",
82
- "buffer": { "type": "read-only-storage" },
83
- "elementType": "$vectorScalar"
84
- },
85
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "u32" },
86
- {
87
- "name": "params",
88
- "semantic": "kernel.params",
89
- "buffer": { "type": "uniform" },
90
- "struct": {
91
- "name": "Params",
92
- "fields": [
93
- { "name": "rows", "type": "u32", "value": "outputCount" },
94
- { "name": "chunkCount", "type": "u32", "value": "axisDim / 4" },
95
- {
96
- "name": "rowStride",
97
- "type": "u32",
98
- "value": "max(1, min(outputCount, device.limits.maxComputeWorkgroupsPerDimension))"
99
- }
100
- ]
101
- }
102
- }
103
- ],
104
- "contiguousScalar": [
105
- {
106
- "name": "x",
107
- "arg": "x",
108
- "semantic": "data",
109
- "buffer": { "type": "read-only-storage" },
110
- "elementType": "$scalar"
111
- },
112
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "u32" },
113
- {
114
- "name": "params",
115
- "semantic": "kernel.params",
116
- "buffer": { "type": "uniform" },
117
- "struct": {
118
- "name": "Params",
119
- "fields": [
120
- { "name": "rows", "type": "u32", "value": "outputCount" },
121
- { "name": "chunkCount", "type": "u32", "value": "axisDim" },
122
- {
123
- "name": "rowStride",
124
- "type": "u32",
125
- "value": "max(1, min(outputCount, device.limits.maxComputeWorkgroupsPerDimension))"
126
- }
127
- ]
128
- }
129
- }
130
- ],
131
- "axisGeometry": [
132
- {
133
- "name": "x",
134
- "arg": "x",
135
- "semantic": "data",
136
- "buffer": { "type": "read-only-storage" },
137
- "elementType": "$scalar"
138
- },
139
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "u32" },
140
- {
141
- "name": "params",
142
- "semantic": "kernel.params",
143
- "buffer": { "type": "uniform" },
144
- "struct": {
145
- "name": "Params",
146
- "fields": [
147
- { "name": "axisDim", "type": "u32", "value": "axisDim" },
148
- { "name": "outputCount", "type": "u32", "value": "outputCount" },
149
- { "name": "innerSize", "type": "u32", "value": "axisInner" }
150
- ]
151
- }
152
- }
153
- ],
154
- "rowSplitVec4": [
155
- {
156
- "name": "x",
157
- "arg": "x",
158
- "semantic": "data",
159
- "buffer": { "type": "read-only-storage" },
160
- "elementType": "$vectorScalar"
161
- },
162
- { "name": "partials_val", "semantic": "partials_val", "buffer": { "type": "storage" }, "elementType": "u32" },
163
- { "name": "partials_idx", "semantic": "partials_idx", "buffer": { "type": "storage" }, "elementType": "u32" },
164
- {
165
- "name": "params",
166
- "semantic": "kernel.params",
167
- "buffer": { "type": "uniform" },
168
- "struct": {
169
- "name": "Params",
170
- "fields": [
171
- { "name": "rows", "type": "u32", "value": "outputCount" },
172
- { "name": "chunkCount", "type": "u32", "value": "axisDim / 4" }
173
- ]
174
- }
175
- }
176
- ],
177
- "rowSplitScalar": [
178
- {
179
- "name": "x",
180
- "arg": "x",
181
- "semantic": "data",
182
- "buffer": { "type": "read-only-storage" },
183
- "elementType": "$scalar"
184
- },
185
- { "name": "partials_val", "semantic": "partials_val", "buffer": { "type": "storage" }, "elementType": "u32" },
186
- { "name": "partials_idx", "semantic": "partials_idx", "buffer": { "type": "storage" }, "elementType": "u32" },
187
- {
188
- "name": "params",
189
- "semantic": "kernel.params",
190
- "buffer": { "type": "uniform" },
191
- "struct": {
192
- "name": "Params",
193
- "fields": [
194
- { "name": "rows", "type": "u32", "value": "outputCount" },
195
- { "name": "chunkCount", "type": "u32", "value": "axisDim" }
196
- ]
197
  }
198
- }
199
- ],
200
- "splitReduce": [
201
- {
202
- "name": "x",
203
- "arg": "x",
204
- "semantic": "data",
205
- "buffer": { "type": "read-only-storage" },
206
- "elementType": "$scalar"
207
- },
208
- { "name": "partials_val", "semantic": "partials_val", "buffer": { "type": "storage" }, "elementType": "u32" },
209
- { "name": "partials_idx", "semantic": "partials_idx", "buffer": { "type": "storage" }, "elementType": "u32" },
210
- {
211
- "name": "params",
212
- "semantic": "kernel.params",
213
- "buffer": { "type": "uniform" },
214
- "struct": {
215
- "name": "Params",
216
- "fields": [
217
- { "name": "axisDim", "type": "u32", "value": "axisDim" },
218
- { "name": "outputCount", "type": "u32", "value": "outputCount" },
219
- { "name": "innerSize", "type": "u32", "value": "axisInner" }
220
- ]
221
  }
222
- }
223
- ],
224
- "splitCombine": [
225
- {
226
- "name": "partials_val",
227
- "semantic": "partials_val",
228
- "buffer": { "type": "read-only-storage" },
229
- "elementType": "u32"
230
- },
231
- {
232
- "name": "partials_idx",
233
- "semantic": "partials_idx",
234
- "buffer": { "type": "read-only-storage" },
235
- "elementType": "u32"
236
- },
237
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "u32" },
238
- {
239
- "name": "params",
240
- "semantic": "kernel.params",
241
- "buffer": { "type": "uniform" },
242
- "struct": { "name": "Params", "fields": [{ "name": "outputCount", "type": "u32", "value": "outputCount" }] }
243
- }
244
- ]
245
  },
246
  "variants": [
247
  {
248
  "id": "last_axis_split_vec4",
249
  "priority": 41,
250
  "when": ["baseContract", "rowParallelType", "lastAxis", "axisDim >= tunables.ROW_SPLIT_MIN_AXIS", "axisDim % 4 == 0", "outputCount <= tunables.ROW_SPLIT_MAX_OUTPUTS", "rowSplitScratchFits", "rowSplitDispatchFits", "rowStorageFits"],
251
- "constants": {
252
  "scalar": "dtypes.T",
253
  "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
254
  "usesF16": "dtypes.T == \"f16\"",
@@ -264,19 +148,23 @@
264
  {
265
  "id": "split_reduce",
266
  "name": "ArgMin.LastAxisSplitVec4Reduce",
267
- "source": {
268
- "shader": "reduce-arg-row-split.wgsl.jinja",
269
- "inputs": { "mode": "\"min\"", "vec4": true, "useSubgroups": "device.features.has(\"subgroups\")" }
270
- },
271
- "bindings": "rowSplitVec4",
272
  "dispatch": { "x": "outputCount", "y": "rowSplitCount" }
273
  },
274
  {
275
  "id": "combine",
276
  "name": "ArgMin.LastAxisSplitCombine",
277
- "source": { "shader": "reduce-arg-axis-split-combine.wgsl.jinja", "inputs": { "mode": "\"min\"" } },
278
- "bindings": "splitCombine",
279
- "dispatch": { "threads": "outputCount", "workgroupSize": "workgroupSize" }
 
 
 
 
 
280
  }
281
  ]
282
  },
@@ -284,7 +172,7 @@
284
  "id": "last_axis_split",
285
  "priority": 41,
286
  "when": ["baseContract", "rowParallelType", "lastAxis", "axisDim >= tunables.ROW_SPLIT_MIN_AXIS", "axisDim % 4 != 0", "outputCount <= tunables.ROW_SPLIT_MAX_OUTPUTS", "rowSplitScratchFits", "rowSplitDispatchFits", "rowStorageFits"],
287
- "constants": {
288
  "scalar": "dtypes.T",
289
  "usesF16": "dtypes.T == \"f16\"",
290
  "selectLastIndex": "attrs.select_last_index != 0",
@@ -299,19 +187,23 @@
299
  {
300
  "id": "split_reduce",
301
  "name": "ArgMin.LastAxisSplitReduce",
302
- "source": {
303
- "shader": "reduce-arg-row-split.wgsl.jinja",
304
- "inputs": { "mode": "\"min\"", "vec4": false, "useSubgroups": "device.features.has(\"subgroups\")" }
305
- },
306
- "bindings": "rowSplitScalar",
307
  "dispatch": { "x": "outputCount", "y": "rowSplitCount" }
308
  },
309
  {
310
  "id": "combine",
311
  "name": "ArgMin.LastAxisSplitCombine",
312
- "source": { "shader": "reduce-arg-axis-split-combine.wgsl.jinja", "inputs": { "mode": "\"min\"" } },
313
- "bindings": "splitCombine",
314
- "dispatch": { "threads": "outputCount", "workgroupSize": "workgroupSize" }
 
 
 
 
 
315
  }
316
  ]
317
  },
@@ -319,7 +211,7 @@
319
  "id": "axis_split_tiled_narrow",
320
  "priority": 31,
321
  "when": ["baseContract", "nativeParallelType", "not lastAxis", "axisDim >= tunables.SPLIT_MIN_AXIS", "outputCount >= tunables.SPLIT_TILE_COLUMNS", "outputCount <= 2 * tunables.SPLIT_TILE_COLUMNS", "workgroupSize % tunables.SPLIT_TILE_COLUMNS == 0", "splitScratchFits", "splitTileDispatchFits"],
322
- "constants": {
323
  "scalar": "dtypes.T",
324
  "selectLastIndex": "attrs.select_last_index != 0",
325
  "workgroupSize": "workgroupSize",
@@ -334,25 +226,31 @@
334
  {
335
  "id": "split_reduce",
336
  "name": "ArgMin.AxisSplitTiledReduce",
337
- "source": { "shader": "reduce-arg-axis-split-tiled.wgsl.jinja", "inputs": { "mode": "\"min\"" } },
338
- "bindings": "splitReduce",
 
339
  "dispatch": { "x": "ceilDiv(outputCount, tunables.SPLIT_TILE_COLUMNS)", "y": "splitCount" }
340
  },
341
  {
342
  "id": "combine",
343
  "name": "ArgMin.AxisSplitCombine",
344
- "source": { "shader": "reduce-arg-axis-split-combine.wgsl.jinja", "inputs": { "mode": "\"min\"" } },
345
- "bindings": "splitCombine",
346
- "dispatch": { "threads": "outputCount", "workgroupSize": "workgroupSize" }
 
 
 
 
 
347
  }
348
  ]
349
  },
350
  {
351
  "id": "subgroup_min_last_axis_vec4",
352
  "priority": 41,
353
- "requires": { "features": ["subgroups"] },
354
  "when": ["baseContract", "rowParallelType", "lastAxis", "axisDim >= 4", "axisDim % 4 == 0", "has(device.adapterInfo, \"subgroupMinSize\")", "has(device.adapterInfo, \"subgroupMaxSize\")", "device.adapterInfo.subgroupMinSize >= 4", "device.adapterInfo.subgroupMinSize <= device.adapterInfo.subgroupMaxSize", "device.adapterInfo.subgroupMinSize <= device.limits.maxComputeInvocationsPerWorkgroup", "device.adapterInfo.subgroupMinSize <= device.limits.maxComputeWorkgroupSizeX", "axisDim >= device.adapterInfo.subgroupMinSize * 4", "ceilDiv(axisDim, 4) <= device.adapterInfo.subgroupMinSize * 16", "outputCount >= device.adapterInfo.subgroupMinSize * 4", "rowDispatchFits"],
355
- "constants": {
 
356
  "scalar": "dtypes.T",
357
  "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
358
  "usesF16": "dtypes.T == \"f16\"",
@@ -363,21 +261,20 @@
363
  {
364
  "id": "main",
365
  "name": "ArgMin.SubgroupMinLastAxisVec4",
366
- "source": {
367
- "shader": "reduce-arg-row-subgroup.wgsl.jinja",
368
- "inputs": { "mode": "\"min\"", "vec4": true, "singleSubgroup": true }
369
- },
370
- "bindings": "contiguousVec4",
371
- "dispatch": { "workgroups": "outputCount" }
372
  }
373
  ]
374
  },
375
  {
376
  "id": "subgroup_min_last_axis",
377
  "priority": 41,
378
- "requires": { "features": ["subgroups"] },
379
  "when": ["baseContract", "rowParallelType", "lastAxis", "axisDim % 4 != 0", "has(device.adapterInfo, \"subgroupMinSize\")", "has(device.adapterInfo, \"subgroupMaxSize\")", "device.adapterInfo.subgroupMinSize >= 4", "device.adapterInfo.subgroupMinSize <= device.adapterInfo.subgroupMaxSize", "device.adapterInfo.subgroupMinSize <= device.limits.maxComputeInvocationsPerWorkgroup", "device.adapterInfo.subgroupMinSize <= device.limits.maxComputeWorkgroupSizeX", "axisDim >= device.adapterInfo.subgroupMinSize * 4", "axisDim <= device.adapterInfo.subgroupMinSize * 64", "outputCount >= device.adapterInfo.subgroupMinSize * 4", "rowDispatchFits"],
380
- "constants": {
 
381
  "scalar": "dtypes.T",
382
  "usesF16": "dtypes.T == \"f16\"",
383
  "selectLastIndex": "attrs.select_last_index != 0",
@@ -387,12 +284,11 @@
387
  {
388
  "id": "main",
389
  "name": "ArgMin.SubgroupMinLastAxis",
390
- "source": {
391
- "shader": "reduce-arg-row-subgroup.wgsl.jinja",
392
- "inputs": { "mode": "\"min\"", "vec4": false, "singleSubgroup": true }
393
- },
394
- "bindings": "contiguousScalar",
395
- "dispatch": { "workgroups": "outputCount" }
396
  }
397
  ]
398
  },
@@ -400,7 +296,7 @@
400
  "id": "last_axis_vec4",
401
  "priority": 40,
402
  "when": ["baseContract", "rowParallelType", "lastAxis", "axisDim >= 4", "axisDim % 4 == 0", "parallelRowWorthwhile", "rowDispatchFits", "rowStorageFits"],
403
- "constants": {
404
  "scalar": "dtypes.T",
405
  "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
406
  "usesF16": "dtypes.T == \"f16\"",
@@ -411,12 +307,11 @@
411
  {
412
  "id": "main",
413
  "name": "ArgMin.LastAxisVec4",
414
- "source": {
415
- "shader": "reduce-arg-row-subgroup.wgsl.jinja",
416
- "inputs": { "mode": "\"min\"", "vec4": true, "useSubgroups": "device.features.has(\"subgroups\")" }
417
- },
418
- "bindings": "contiguousVec4",
419
- "dispatch": { "workgroups": "outputCount" }
420
  }
421
  ]
422
  },
@@ -424,7 +319,7 @@
424
  "id": "last_axis",
425
  "priority": 39,
426
  "when": ["baseContract", "rowParallelType", "lastAxis", "axisDim % 4 != 0", "parallelRowWorthwhile", "rowDispatchFits", "rowStorageFits"],
427
- "constants": {
428
  "scalar": "dtypes.T",
429
  "usesF16": "dtypes.T == \"f16\"",
430
  "selectLastIndex": "attrs.select_last_index != 0",
@@ -434,25 +329,24 @@
434
  {
435
  "id": "main",
436
  "name": "ArgMin.LastAxis",
437
- "source": {
438
- "shader": "reduce-arg-row-subgroup.wgsl.jinja",
439
- "inputs": {
440
- "mode": "\"min\"",
441
- "vec4": false,
442
- "useSubgroups": "device.features.has(\"subgroups\") and not narrowOddRowPortablePreferred"
443
- }
444
  },
445
- "bindings": "contiguousScalar",
446
- "dispatch": { "workgroups": "outputCount" }
447
  }
448
  ]
449
  },
450
  {
451
  "id": "axis_split",
452
  "priority": 30,
453
- "demoteWhen": ["narrowLargeAxisTilePreferred"],
454
  "when": ["baseContract", "nativeParallelType", "not lastAxis", "axisDim >= tunables.SPLIT_MIN_AXIS", "outputCount >= 1", "outputCount <= tunables.SPLIT_MAX_OUTPUTS", "splitScratchFits", "splitDispatchFits", "threadDispatchFits"],
455
- "constants": {
 
456
  "scalar": "dtypes.T",
457
  "selectLastIndex": "attrs.select_last_index != 0",
458
  "workgroupSize": "workgroupSize",
@@ -466,16 +360,26 @@
466
  {
467
  "id": "split_reduce",
468
  "name": "ArgMin.AxisSplitReduce",
469
- "source": { "shader": "reduce-arg-axis-split-reduce.wgsl.jinja", "inputs": { "mode": "\"min\"" } },
470
- "bindings": "splitReduce",
471
- "dispatch": { "threads": "outputCount", "workgroupSize": "workgroupSize", "y": "splitCount" }
 
 
 
 
 
472
  },
473
  {
474
  "id": "combine",
475
  "name": "ArgMin.AxisSplitCombine",
476
- "source": { "shader": "reduce-arg-axis-split-combine.wgsl.jinja", "inputs": { "mode": "\"min\"" } },
477
- "bindings": "splitCombine",
478
- "dispatch": { "threads": "outputCount", "workgroupSize": "workgroupSize" }
 
 
 
 
 
479
  }
480
  ]
481
  },
@@ -483,7 +387,7 @@
483
  "id": "axis_tiled",
484
  "priority": 20,
485
  "when": ["baseContract", "nativeParallelType", "not lastAxis", "axisDim >= tunables.TILED_MIN_AXIS", "outputCount >= tunables.TILED_MIN_OUTPUTS", "workgroupSize % tunables.TILE_COLUMNS == 0", "rowStorageFits", "tileDispatchFits"],
486
- "constants": {
487
  "scalar": "dtypes.T",
488
  "selectLastIndex": "attrs.select_last_index != 0",
489
  "workgroupSize": "workgroupSize",
@@ -493,18 +397,23 @@
493
  {
494
  "id": "main",
495
  "name": "ArgMin.AxisTiled",
496
- "source": { "shader": "reduce-arg-axis-tiled.wgsl.jinja", "inputs": { "mode": "\"min\"" } },
497
- "bindings": "axisGeometry",
498
- "dispatch": { "workgroups": "ceilDiv((outputCount), (tunables.TILE_COLUMNS))" }
 
 
 
 
 
499
  }
500
  ]
501
  },
502
  {
503
  "id": "axis_serial",
504
  "priority": 0,
505
- "supersededBy": ["axis_tiled"],
506
  "when": ["baseContract", "threadDispatchFits"],
507
- "constants": {
 
508
  "scalar": "dtypes.T",
509
  "usesF16": "dtypes.T == \"f16\"",
510
  "selectLastIndex": "attrs.select_last_index != 0",
@@ -514,9 +423,14 @@
514
  {
515
  "id": "main",
516
  "name": "ArgMin.AxisSerial",
517
- "source": { "shader": "reduce-arg-axis.wgsl.jinja", "inputs": { "mode": "\"min\"" } },
518
- "bindings": "axisGeometry",
519
- "dispatch": { "threads": "outputCount", "workgroupSize": "workgroupSize" }
 
 
 
 
 
520
  }
521
  ]
522
  }
 
2
  "domain": "ai.onnx",
3
  "name": "ArgMin",
4
  "sinceVersion": 13,
5
+ "inputs": { "x": { "onnx": "data", "dtype": "T" } },
6
+ "outputs": {
7
+ "y": {
8
+ "onnx": "reduced",
 
9
  "dtype": "I",
10
+ "rank": "ranks.x if attrs.keepdims == 1 else ranks.x - 1",
11
+ "shape": "prefix(shapes.x, normalizedAxis) + ([1] if attrs.keepdims == 1 else []) + suffix(shapes.x, normalizedAxis + 1)",
12
+ "storage": "uint32"
13
  }
 
 
 
 
 
 
14
  },
15
+ "attributes": { "axis": { "default": 0 }, "keepdims": { "default": 1 }, "select_last_index": { "default": 0 } },
16
  "attributeConstraints": { "keepdims": { "values": [0, 1] }, "select_last_index": { "values": [0, 1] } },
17
  "typeConstraints": { "T": ["float32", "float16", "int32", "uint32", "int16", "int8", "uint8"], "I": ["int64"] },
 
 
 
 
18
  "tunables": {
19
+ "WORKGROUP_SIZE": { "default": 256 },
20
+ "TILE_COLUMNS": { "default": 16 },
21
+ "TILED_MIN_AXIS": { "default": 64 },
22
+ "TILED_MIN_OUTPUTS": { "default": 16 },
23
+ "SPLIT_MIN_AXIS": { "default": 8192 },
24
+ "SPLIT_MAX_OUTPUTS": { "default": 4096 },
25
+ "SPLIT_TARGET_AXIS": { "default": 256 },
26
+ "SPLIT_TILE_COLUMNS": { "default": 8 },
27
+ "MAX_SPLITS": { "default": 128 },
28
+ "ROW_SPLIT_MIN_AXIS": { "default": 32768 },
29
+ "ROW_SPLIT_MAX_OUTPUTS": { "default": 32 },
30
+ "ROW_SPLIT_TARGET_AXIS": { "default": 2048 },
31
+ "ROW_SPLIT_MAX_SPLITS": { "default": 64 }
32
  },
33
  "derive": {
34
  "deviceWorkgroupCap": "min(device.limits.maxComputeInvocationsPerWorkgroup, device.limits.maxComputeWorkgroupSizeX)",
35
+ "foldedDispatchCapacity": "min(device.limits.maxComputeWorkgroupsPerDimension, 65535) * min(device.limits.maxComputeWorkgroupsPerDimension, 65535)",
36
  "narrowSubgroupRange": "has(device.adapterInfo, \"subgroupMinSize\") and has(device.adapterInfo, \"subgroupMaxSize\") and device.adapterInfo.subgroupMinSize < device.adapterInfo.subgroupMaxSize and device.adapterInfo.subgroupMaxSize <= 16",
37
+ "normalizedAxis": "attrs.axis if attrs.axis >= 0 else attrs.axis + ranks.x",
38
+ "axisInRange": "ranks.x >= 1 and normalizedAxis >= 0 and normalizedAxis < ranks.x",
39
+ "axisDim": "dim(shapes.x, normalizedAxis)",
40
+ "axisInner": "inner(shapes.x, normalizedAxis)",
41
+ "outputCount": "numel(shapes.y)",
42
+ "outputRankOk": "(attrs.keepdims == 1 and ranks.y == ranks.x) or (attrs.keepdims == 0 and ranks.y == ranks.x - 1)",
43
+ "outputShapeOk": "outputRankOk and outputCount * axisDim == numel(shapes.x)",
44
  "baseContract": "axisInRange and axisDim > 0 and outputShapeOk and f16Ok(dtypes.T)",
45
+ "lastAxis": "normalizedAxis == ranks.x - 1",
46
  "nativeParallelType": "dtypes.T == \"f32\" or dtypes.T == \"i32\" or dtypes.T == \"u32\"",
47
  "rowParallelType": "nativeParallelType or dtypes.T == \"f16\"",
48
  "workgroupSize": "min(tunables.WORKGROUP_SIZE, deviceWorkgroupCap)",
 
55
  "splitCount": "min(tunables.MAX_SPLITS, pow2ceil(ceilDiv(axisDim, tunables.SPLIT_TARGET_AXIS)))",
56
  "splitScratchBytes": "splitCount * outputCount * 8",
57
  "splitScratchFits": "splitScratchBytes <= device.limits.maxStorageBufferBindingSize and splitScratchBytes <= device.limits.maxBufferSize",
58
+ "splitDispatchFits": "splitCount <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535) and ceilDiv(ceilDiv(outputCount, workgroupSize), min(device.limits.maxComputeWorkgroupsPerDimension, 65535)) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535)",
59
+ "splitTileDispatchFits": "splitCount <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535) and ceilDiv(outputCount, tunables.SPLIT_TILE_COLUMNS) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535)",
60
  "rowSplitCount": "min(tunables.ROW_SPLIT_MAX_SPLITS, pow2ceil(ceilDiv(axisDim, tunables.ROW_SPLIT_TARGET_AXIS)))",
61
  "rowSplitScratchBytes": "rowSplitCount * outputCount * 8",
62
  "rowSplitScratchFits": "rowSplitScratchBytes <= device.limits.maxStorageBufferBindingSize and rowSplitScratchBytes <= device.limits.maxBufferSize",
63
+ "rowSplitDispatchFits": "outputCount <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535) and rowSplitCount <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535)",
64
  "tileDispatchFits": "ceilDiv(outputCount, tunables.TILE_COLUMNS) <= foldedDispatchCapacity"
65
  },
66
+ "bindings": {
67
+ "x": { "buffer": "read-only-storage", "elementType": "$vectorScalar" },
68
+ "partials_val": { "buffer": "storage", "elementType": "u32" },
69
+ "partials_idx": { "buffer": "storage", "elementType": "u32" },
70
+ "params": {
71
+ "buffer": "uniform",
72
+ "struct": [
73
+ { "name": "rows", "type": "u32", "value": "outputCount" },
74
+ { "name": "chunkCount", "type": "u32", "value": "axisDim / 4" }
75
+ ]
76
+ },
77
+ "partials_val_2": { "name": "partials_val", "buffer": "read-only-storage", "elementType": "u32" },
78
+ "partials_idx_2": { "name": "partials_idx", "buffer": "read-only-storage", "elementType": "u32" },
79
+ "y": { "buffer": "storage", "elementType": "u32" },
80
+ "params_2": {
81
+ "name": "params",
82
+ "buffer": "uniform",
83
+ "struct": [{ "name": "outputCount", "type": "u32", "value": "outputCount" }]
84
+ },
85
+ "x_2": { "name": "x", "buffer": "read-only-storage", "elementType": "$scalar" },
86
+ "params_3": {
87
+ "name": "params",
88
+ "buffer": "uniform",
89
+ "struct": [
90
+ { "name": "rows", "type": "u32", "value": "outputCount" },
91
+ { "name": "chunkCount", "type": "u32", "value": "axisDim" }
92
+ ]
93
+ },
94
+ "params_4": {
95
+ "name": "params",
96
+ "buffer": "uniform",
97
+ "struct": [
98
+ { "name": "axisDim", "type": "u32", "value": "axisDim" },
99
+ { "name": "outputCount", "type": "u32", "value": "outputCount" },
100
+ { "name": "innerSize", "type": "u32", "value": "axisInner" }
101
+ ]
102
+ },
103
+ "params_5": {
104
+ "name": "params",
105
+ "buffer": "uniform",
106
+ "struct": [
107
+ { "name": "rows", "type": "u32", "value": "outputCount" },
108
+ { "name": "chunkCount", "type": "u32", "value": "axisDim / 4" },
109
+ {
110
+ "name": "rowStride",
111
+ "type": "u32",
112
+ "value": "max(1, min(outputCount, min(device.limits.maxComputeWorkgroupsPerDimension, 65535)))"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  }
114
+ ]
115
+ },
116
+ "params_6": {
117
+ "name": "params",
118
+ "buffer": "uniform",
119
+ "struct": [
120
+ { "name": "rows", "type": "u32", "value": "outputCount" },
121
+ { "name": "chunkCount", "type": "u32", "value": "axisDim" },
122
+ {
123
+ "name": "rowStride",
124
+ "type": "u32",
125
+ "value": "max(1, min(outputCount, min(device.limits.maxComputeWorkgroupsPerDimension, 65535)))"
 
 
 
 
 
 
 
 
 
 
 
126
  }
127
+ ]
128
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  },
130
  "variants": [
131
  {
132
  "id": "last_axis_split_vec4",
133
  "priority": 41,
134
  "when": ["baseContract", "rowParallelType", "lastAxis", "axisDim >= tunables.ROW_SPLIT_MIN_AXIS", "axisDim % 4 == 0", "outputCount <= tunables.ROW_SPLIT_MAX_OUTPUTS", "rowSplitScratchFits", "rowSplitDispatchFits", "rowStorageFits"],
135
+ "derive": {
136
  "scalar": "dtypes.T",
137
  "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
138
  "usesF16": "dtypes.T == \"f16\"",
 
148
  {
149
  "id": "split_reduce",
150
  "name": "ArgMin.LastAxisSplitVec4Reduce",
151
+ "shader": "reduce-arg-row-split.wgsl.jinja",
152
+ "subgroupCollectivesWidth": "portable",
153
+ "derive": { "modeSpec": "\"min\"", "vec4": true, "useSubgroups": "device.features.has(\"subgroups\")" },
154
+ "bindings": ["x", "partials_val", "partials_idx", "params"],
 
155
  "dispatch": { "x": "outputCount", "y": "rowSplitCount" }
156
  },
157
  {
158
  "id": "combine",
159
  "name": "ArgMin.LastAxisSplitCombine",
160
+ "shader": "reduce-arg-axis-split-combine.wgsl.jinja",
161
+ "derive": { "modeSpec": "\"min\"" },
162
+ "bindings": ["partials_val_2", "partials_idx_2", "y", "params_2"],
163
+ "dispatch": {
164
+ "x": "min(ceilDiv((outputCount), (workgroupSize)), 65535)",
165
+ "y": "ceilDiv(ceilDiv((outputCount), (workgroupSize)), 65535)",
166
+ "z": 1
167
+ }
168
  }
169
  ]
170
  },
 
172
  "id": "last_axis_split",
173
  "priority": 41,
174
  "when": ["baseContract", "rowParallelType", "lastAxis", "axisDim >= tunables.ROW_SPLIT_MIN_AXIS", "axisDim % 4 != 0", "outputCount <= tunables.ROW_SPLIT_MAX_OUTPUTS", "rowSplitScratchFits", "rowSplitDispatchFits", "rowStorageFits"],
175
+ "derive": {
176
  "scalar": "dtypes.T",
177
  "usesF16": "dtypes.T == \"f16\"",
178
  "selectLastIndex": "attrs.select_last_index != 0",
 
187
  {
188
  "id": "split_reduce",
189
  "name": "ArgMin.LastAxisSplitReduce",
190
+ "shader": "reduce-arg-row-split.wgsl.jinja",
191
+ "subgroupCollectivesWidth": "portable",
192
+ "derive": { "modeSpec": "\"min\"", "vec4": false, "useSubgroups": "device.features.has(\"subgroups\")" },
193
+ "bindings": ["x_2", "partials_val", "partials_idx", "params_3"],
 
194
  "dispatch": { "x": "outputCount", "y": "rowSplitCount" }
195
  },
196
  {
197
  "id": "combine",
198
  "name": "ArgMin.LastAxisSplitCombine",
199
+ "shader": "reduce-arg-axis-split-combine.wgsl.jinja",
200
+ "derive": { "modeSpec": "\"min\"" },
201
+ "bindings": ["partials_val_2", "partials_idx_2", "y", "params_2"],
202
+ "dispatch": {
203
+ "x": "min(ceilDiv((outputCount), (workgroupSize)), 65535)",
204
+ "y": "ceilDiv(ceilDiv((outputCount), (workgroupSize)), 65535)",
205
+ "z": 1
206
+ }
207
  }
208
  ]
209
  },
 
211
  "id": "axis_split_tiled_narrow",
212
  "priority": 31,
213
  "when": ["baseContract", "nativeParallelType", "not lastAxis", "axisDim >= tunables.SPLIT_MIN_AXIS", "outputCount >= tunables.SPLIT_TILE_COLUMNS", "outputCount <= 2 * tunables.SPLIT_TILE_COLUMNS", "workgroupSize % tunables.SPLIT_TILE_COLUMNS == 0", "splitScratchFits", "splitTileDispatchFits"],
214
+ "derive": {
215
  "scalar": "dtypes.T",
216
  "selectLastIndex": "attrs.select_last_index != 0",
217
  "workgroupSize": "workgroupSize",
 
226
  {
227
  "id": "split_reduce",
228
  "name": "ArgMin.AxisSplitTiledReduce",
229
+ "shader": "reduce-arg-axis-split-tiled.wgsl.jinja",
230
+ "derive": { "modeSpec": "\"min\"" },
231
+ "bindings": ["x_2", "partials_val", "partials_idx", "params_4"],
232
  "dispatch": { "x": "ceilDiv(outputCount, tunables.SPLIT_TILE_COLUMNS)", "y": "splitCount" }
233
  },
234
  {
235
  "id": "combine",
236
  "name": "ArgMin.AxisSplitCombine",
237
+ "shader": "reduce-arg-axis-split-combine.wgsl.jinja",
238
+ "derive": { "modeSpec": "\"min\"" },
239
+ "bindings": ["partials_val_2", "partials_idx_2", "y", "params_2"],
240
+ "dispatch": {
241
+ "x": "min(ceilDiv((outputCount), (workgroupSize)), 65535)",
242
+ "y": "ceilDiv(ceilDiv((outputCount), (workgroupSize)), 65535)",
243
+ "z": 1
244
+ }
245
  }
246
  ]
247
  },
248
  {
249
  "id": "subgroup_min_last_axis_vec4",
250
  "priority": 41,
 
251
  "when": ["baseContract", "rowParallelType", "lastAxis", "axisDim >= 4", "axisDim % 4 == 0", "has(device.adapterInfo, \"subgroupMinSize\")", "has(device.adapterInfo, \"subgroupMaxSize\")", "device.adapterInfo.subgroupMinSize >= 4", "device.adapterInfo.subgroupMinSize <= device.adapterInfo.subgroupMaxSize", "device.adapterInfo.subgroupMinSize <= device.limits.maxComputeInvocationsPerWorkgroup", "device.adapterInfo.subgroupMinSize <= device.limits.maxComputeWorkgroupSizeX", "axisDim >= device.adapterInfo.subgroupMinSize * 4", "ceilDiv(axisDim, 4) <= device.adapterInfo.subgroupMinSize * 16", "outputCount >= device.adapterInfo.subgroupMinSize * 4", "rowDispatchFits"],
252
+ "requires": { "features": ["subgroups"] },
253
+ "derive": {
254
  "scalar": "dtypes.T",
255
  "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
256
  "usesF16": "dtypes.T == \"f16\"",
 
261
  {
262
  "id": "main",
263
  "name": "ArgMin.SubgroupMinLastAxisVec4",
264
+ "shader": "reduce-arg-row-subgroup.wgsl.jinja",
265
+ "subgroupCollectivesWidth": "portable",
266
+ "derive": { "modeSpec": "\"min\"", "vec4": true, "singleSubgroup": true },
267
+ "bindings": ["x", "y", "params_5"],
268
+ "dispatch": { "x": "min(outputCount, 65535)", "y": "ceilDiv(outputCount, 65535)", "z": 1 }
 
269
  }
270
  ]
271
  },
272
  {
273
  "id": "subgroup_min_last_axis",
274
  "priority": 41,
 
275
  "when": ["baseContract", "rowParallelType", "lastAxis", "axisDim % 4 != 0", "has(device.adapterInfo, \"subgroupMinSize\")", "has(device.adapterInfo, \"subgroupMaxSize\")", "device.adapterInfo.subgroupMinSize >= 4", "device.adapterInfo.subgroupMinSize <= device.adapterInfo.subgroupMaxSize", "device.adapterInfo.subgroupMinSize <= device.limits.maxComputeInvocationsPerWorkgroup", "device.adapterInfo.subgroupMinSize <= device.limits.maxComputeWorkgroupSizeX", "axisDim >= device.adapterInfo.subgroupMinSize * 4", "axisDim <= device.adapterInfo.subgroupMinSize * 64", "outputCount >= device.adapterInfo.subgroupMinSize * 4", "rowDispatchFits"],
276
+ "requires": { "features": ["subgroups"] },
277
+ "derive": {
278
  "scalar": "dtypes.T",
279
  "usesF16": "dtypes.T == \"f16\"",
280
  "selectLastIndex": "attrs.select_last_index != 0",
 
284
  {
285
  "id": "main",
286
  "name": "ArgMin.SubgroupMinLastAxis",
287
+ "shader": "reduce-arg-row-subgroup.wgsl.jinja",
288
+ "subgroupCollectivesWidth": "portable",
289
+ "derive": { "modeSpec": "\"min\"", "vec4": false, "singleSubgroup": true },
290
+ "bindings": ["x_2", "y", "params_6"],
291
+ "dispatch": { "x": "min(outputCount, 65535)", "y": "ceilDiv(outputCount, 65535)", "z": 1 }
 
292
  }
293
  ]
294
  },
 
296
  "id": "last_axis_vec4",
297
  "priority": 40,
298
  "when": ["baseContract", "rowParallelType", "lastAxis", "axisDim >= 4", "axisDim % 4 == 0", "parallelRowWorthwhile", "rowDispatchFits", "rowStorageFits"],
299
+ "derive": {
300
  "scalar": "dtypes.T",
301
  "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
302
  "usesF16": "dtypes.T == \"f16\"",
 
307
  {
308
  "id": "main",
309
  "name": "ArgMin.LastAxisVec4",
310
+ "shader": "reduce-arg-row-subgroup.wgsl.jinja",
311
+ "subgroupCollectivesWidth": "portable",
312
+ "derive": { "modeSpec": "\"min\"", "vec4": true, "useSubgroups": "device.features.has(\"subgroups\")" },
313
+ "bindings": ["x", "y", "params_5"],
314
+ "dispatch": { "x": "min(outputCount, 65535)", "y": "ceilDiv(outputCount, 65535)", "z": 1 }
 
315
  }
316
  ]
317
  },
 
319
  "id": "last_axis",
320
  "priority": 39,
321
  "when": ["baseContract", "rowParallelType", "lastAxis", "axisDim % 4 != 0", "parallelRowWorthwhile", "rowDispatchFits", "rowStorageFits"],
322
+ "derive": {
323
  "scalar": "dtypes.T",
324
  "usesF16": "dtypes.T == \"f16\"",
325
  "selectLastIndex": "attrs.select_last_index != 0",
 
329
  {
330
  "id": "main",
331
  "name": "ArgMin.LastAxis",
332
+ "shader": "reduce-arg-row-subgroup.wgsl.jinja",
333
+ "subgroupCollectivesWidth": "portable",
334
+ "derive": {
335
+ "modeSpec": "\"min\"",
336
+ "vec4": false,
337
+ "useSubgroups": "device.features.has(\"subgroups\") and not narrowOddRowPortablePreferred"
 
338
  },
339
+ "bindings": ["x_2", "y", "params_6"],
340
+ "dispatch": { "x": "min(outputCount, 65535)", "y": "ceilDiv(outputCount, 65535)", "z": 1 }
341
  }
342
  ]
343
  },
344
  {
345
  "id": "axis_split",
346
  "priority": 30,
 
347
  "when": ["baseContract", "nativeParallelType", "not lastAxis", "axisDim >= tunables.SPLIT_MIN_AXIS", "outputCount >= 1", "outputCount <= tunables.SPLIT_MAX_OUTPUTS", "splitScratchFits", "splitDispatchFits", "threadDispatchFits"],
348
+ "demoteWhen": ["narrowLargeAxisTilePreferred"],
349
+ "derive": {
350
  "scalar": "dtypes.T",
351
  "selectLastIndex": "attrs.select_last_index != 0",
352
  "workgroupSize": "workgroupSize",
 
360
  {
361
  "id": "split_reduce",
362
  "name": "ArgMin.AxisSplitReduce",
363
+ "shader": "reduce-arg-axis-split-reduce.wgsl.jinja",
364
+ "derive": { "modeSpec": "\"min\"" },
365
+ "bindings": ["x_2", "partials_val", "partials_idx", "params_4"],
366
+ "dispatch": {
367
+ "x": "min(ceilDiv((outputCount), (workgroupSize)), DISPATCH_FOLD_WIDTH)",
368
+ "y": "splitCount",
369
+ "z": "ceilDiv(ceilDiv((outputCount), (workgroupSize)), DISPATCH_FOLD_WIDTH)"
370
+ }
371
  },
372
  {
373
  "id": "combine",
374
  "name": "ArgMin.AxisSplitCombine",
375
+ "shader": "reduce-arg-axis-split-combine.wgsl.jinja",
376
+ "derive": { "modeSpec": "\"min\"" },
377
+ "bindings": ["partials_val_2", "partials_idx_2", "y", "params_2"],
378
+ "dispatch": {
379
+ "x": "min(ceilDiv((outputCount), (workgroupSize)), 65535)",
380
+ "y": "ceilDiv(ceilDiv((outputCount), (workgroupSize)), 65535)",
381
+ "z": 1
382
+ }
383
  }
384
  ]
385
  },
 
387
  "id": "axis_tiled",
388
  "priority": 20,
389
  "when": ["baseContract", "nativeParallelType", "not lastAxis", "axisDim >= tunables.TILED_MIN_AXIS", "outputCount >= tunables.TILED_MIN_OUTPUTS", "workgroupSize % tunables.TILE_COLUMNS == 0", "rowStorageFits", "tileDispatchFits"],
390
+ "derive": {
391
  "scalar": "dtypes.T",
392
  "selectLastIndex": "attrs.select_last_index != 0",
393
  "workgroupSize": "workgroupSize",
 
397
  {
398
  "id": "main",
399
  "name": "ArgMin.AxisTiled",
400
+ "shader": "reduce-arg-axis-tiled.wgsl.jinja",
401
+ "derive": { "modeSpec": "\"min\"" },
402
+ "bindings": ["x_2", "y", "params_4"],
403
+ "dispatch": {
404
+ "x": "min(ceilDiv((outputCount), (tunables.TILE_COLUMNS)), 65535)",
405
+ "y": "ceilDiv(ceilDiv((outputCount), (tunables.TILE_COLUMNS)), 65535)",
406
+ "z": 1
407
+ }
408
  }
409
  ]
410
  },
411
  {
412
  "id": "axis_serial",
413
  "priority": 0,
 
414
  "when": ["baseContract", "threadDispatchFits"],
415
+ "supersededBy": ["axis_tiled"],
416
+ "derive": {
417
  "scalar": "dtypes.T",
418
  "usesF16": "dtypes.T == \"f16\"",
419
  "selectLastIndex": "attrs.select_last_index != 0",
 
423
  {
424
  "id": "main",
425
  "name": "ArgMin.AxisSerial",
426
+ "shader": "reduce-arg-axis.wgsl.jinja",
427
+ "derive": { "modeSpec": "\"min\"" },
428
+ "bindings": ["x_2", "y", "params_4"],
429
+ "dispatch": {
430
+ "x": "min(ceilDiv((outputCount), (workgroupSize)), 65535)",
431
+ "y": "ceilDiv(ceilDiv((outputCount), (workgroupSize)), 65535)",
432
+ "z": 1
433
+ }
434
  }
435
  ]
436
  }
build/webgpu/metadata.json CHANGED
@@ -1,24 +1,38 @@
1
  {
2
  "name": "ai.onnx.ArgMin",
3
- "id": "_ai_onnx_argmin_webgpu_a0fbb5d",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
- "bench.json": "mm/x1LvCBXT2wlKgCEbxB5Oc506zsg2sfh5+8s0SzyI=",
11
- "manifest.json": "6YtVbWUVmqO89IGSyNQYIvaRetZCne1BxSikonXTKaQ=",
12
- "reduce-arg-axis-split-combine.wgsl.jinja": "rdvAqkyd5MkxXLWQtVzpTxqqKwANhk2L1h/SMW9P2qU=",
13
- "reduce-arg-axis-split-reduce.wgsl.jinja": "VC4pBZyx0kVu5zmny2clKWdMU2D6HWpM/qiym+b664o=",
14
- "reduce-arg-axis-split-tiled.wgsl.jinja": "/CtqlF3QH7OIQi9q0WzSOJC6K/KDUzcH4sDHdrcGTLg=",
15
- "reduce-arg-axis-tiled.wgsl.jinja": "382jqrusVAtzqbRnn0b++oLtQK53ezOx203+9kyWs34=",
16
- "reduce-arg-axis.wgsl.jinja": "LzoRz8NbCeL+4FA0A6K/5xmPw6jr1n3rqnq3h8lXXFs=",
17
- "reduce-arg-row-split.wgsl.jinja": "t2dWKMUzJLdeIBMtllRHEKE+V5YQDSry6dxe/WmwiUg=",
18
- "reduce-arg-row-subgroup.wgsl.jinja": "IkTnxOCG5uHuSYBWOr8vUW7MfqnJKlBY8ByKbA4+K60=",
19
- "test.json": "stypKi/CuonXgtb+pFhpPyL6q5ymL6WWV0M9V27ANfc="
20
  }
21
  },
22
- "provenance": { "kernel": { "sha": "2e7068faf55e7f43df740015f6d1ee49391a41c5", "dirty": false } },
23
- "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/ai.onnx.ArgMin" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  }
 
1
  {
2
  "name": "ai.onnx.ArgMin",
3
+ "id": "_ai_onnx_argmin_webgpu_e54f586",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
+ "bench.json": "eUbzwtoY0FL3vKlPzo8M3cBq2mhnew0DQY8yNp6XVJE=",
11
+ "manifest.json": "b/mmulEG774GnpjcB/ZKqlAMwgzQSVLE/hPpLeDjJjA=",
12
+ "reduce-arg-axis-split-combine.wgsl.jinja": "vNqRyvgMuCTgGo/a3v+7SKnhIWNU/SHjGEGOYSIeo6o=",
13
+ "reduce-arg-axis-split-reduce.wgsl.jinja": "3GqAQKBSBkxqVaKQg2ycI9Vq3xVteDHQ3PJBMCJqulM=",
14
+ "reduce-arg-axis-split-tiled.wgsl.jinja": "Z3JRqGsy97pIswTbs/9DR3QgTJblOetszM3RiAhhvb8=",
15
+ "reduce-arg-axis-tiled.wgsl.jinja": "ndfXWO62yzI5tgZUJeFg9tFmQYm9bRw3QRq0EwgVxTo=",
16
+ "reduce-arg-axis.wgsl.jinja": "RUQfKNZwkYusxT68lh2FcrAxsx4x/ENaaQ7MXWaV2Ow=",
17
+ "reduce-arg-row-split.wgsl.jinja": "YQ6LPhs3SyGX/XSXRk9g0lEW91scGJn9qE4OcHi9W/s=",
18
+ "reduce-arg-row-subgroup.wgsl.jinja": "dz9xdylOz8BK6INB0dcwdKa2UpJSwvkqQejLkllg7to=",
19
+ "test.json": "a/in/WTnKwukbD6HmiZkClzWll8kawFe503uUr81g6U="
20
  }
21
  },
22
+ "provenance": { "kernel": { "sha": "91d990483a174128daf7673f3f37a7c890493ae1", "dirty": false } },
23
+ "webgpu": {
24
+ "manifestSpec": "2.0",
25
+ "variants": {
26
+ "last_axis_split_vec4": ["reduce-arg-axis-split-combine.wgsl.jinja", "reduce-arg-row-split.wgsl.jinja"],
27
+ "last_axis_split": ["reduce-arg-axis-split-combine.wgsl.jinja", "reduce-arg-row-split.wgsl.jinja"],
28
+ "axis_split_tiled_narrow": ["reduce-arg-axis-split-combine.wgsl.jinja", "reduce-arg-axis-split-tiled.wgsl.jinja"],
29
+ "subgroup_min_last_axis_vec4": ["reduce-arg-row-subgroup.wgsl.jinja"],
30
+ "subgroup_min_last_axis": ["reduce-arg-row-subgroup.wgsl.jinja"],
31
+ "last_axis_vec4": ["reduce-arg-row-subgroup.wgsl.jinja"],
32
+ "last_axis": ["reduce-arg-row-subgroup.wgsl.jinja"],
33
+ "axis_split": ["reduce-arg-axis-split-combine.wgsl.jinja", "reduce-arg-axis-split-reduce.wgsl.jinja"],
34
+ "axis_tiled": ["reduce-arg-axis-tiled.wgsl.jinja"],
35
+ "axis_serial": ["reduce-arg-axis.wgsl.jinja"]
36
+ }
37
+ }
38
  }
build/webgpu/reduce-arg-axis-split-combine.wgsl.jinja CHANGED
@@ -7,7 +7,7 @@
7
  // The fold compares (value, index), never segment order. Equal values keep the
8
  // smaller index for first-index mode and the larger index for last-index mode.
9
  // Empty segments carry SENTINEL_IDX and are skipped.
10
- {% set cmp = ">" if source.mode == "max" else "<" %}
11
  {% set is_int = scalar == "i32" or scalar == "u32" %}
12
  {% set work = scalar if is_int else "f32" %}
13
  {{ env.wgsl.resourceDeclarations }}
@@ -17,9 +17,8 @@ const SPLIT: u32 = {{ split }}u;
17
  const SENTINEL_IDX: u32 = 4294967295u;
18
 
19
  @compute @workgroup_size(WG, 1, 1)
20
- fn main(@builtin(global_invocation_id) gid: vec3<u32>,
21
- @builtin(num_workgroups) nwg: vec3<u32>) {
22
- let output_index = gid.x + gid.y * nwg.x * WG;
23
  if (output_index >= params.outputCount) {
24
  return;
25
  }
 
7
  // The fold compares (value, index), never segment order. Equal values keep the
8
  // smaller index for first-index mode and the larger index for last-index mode.
9
  // Empty segments carry SENTINEL_IDX and are skipped.
10
+ {% set cmp = ">" if modeSpec == "max" else "<" %}
11
  {% set is_int = scalar == "i32" or scalar == "u32" %}
12
  {% set work = scalar if is_int else "f32" %}
13
  {{ env.wgsl.resourceDeclarations }}
 
17
  const SENTINEL_IDX: u32 = 4294967295u;
18
 
19
  @compute @workgroup_size(WG, 1, 1)
20
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
21
+ let output_index = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * WG;
 
22
  if (output_index >= params.outputCount) {
23
  return;
24
  }
build/webgpu/reduce-arg-axis-split-reduce.wgsl.jinja CHANGED
@@ -1,4 +1,4 @@
1
- // Pass 1 of a rank-independent split-axis ArgMax/ArgMin reduction. The tensor
2
  // is flattened as (outer, axis, inner). Each thread scans one contiguous segment
3
  // of the reduced axis for one output position.
4
  //
@@ -6,7 +6,7 @@
6
  // layout without losing integer precision. The combine pass restores the native
7
  // comparison type. Equal values keep the lowest index by default or the highest
8
  // index when select_last_index is enabled.
9
- {% set cmp = ">" if source.mode == "max" else "<" %}
10
  {% set is_int = scalar == "i32" or scalar == "u32" %}
11
  {% set work = scalar if is_int else "f32" %}
12
  {{ env.wgsl.resourceDeclarations }}
@@ -20,22 +20,21 @@ const SENTINEL_IDX: u32 = 4294967295u;
20
  // is not folded into an unrepresentable infinity literal.
21
  fn lane_identity() -> {{ work }} {
22
  {% if scalar == "i32" %}
23
- return bitcast<i32>({{ "0x80000000u" if source.mode == "max" else "0x7fffffffu" }});
24
  {% elif scalar == "u32" %}
25
- return {{ "0u" if source.mode == "max" else "0xffffffffu" }};
26
  {% else %}
27
- var bits = {{ "0xff800000u" if source.mode == "max" else "0x7f800000u" }};
28
  return bitcast<f32>(bits);
29
  {% endif %}
30
  }
31
 
32
  @compute @workgroup_size(WG, 1, 1)
33
  fn main(@builtin(global_invocation_id) gid: vec3<u32>,
34
- @builtin(workgroup_id) wg: vec3<u32>,
35
- @builtin(num_workgroups) nwg: vec3<u32>) {
36
- // 2D-folded column index: wg.z carries the high bits past the maxComputeWorkgroupsPerDimension
37
- // workgroup-per-dimension dispatch limit on the x dimension.
38
- let output_index = (wg.x + wg.z * nwg.x) * WG + (gid.x % WG);
39
  let split_index = wg.y;
40
  if (output_index >= params.outputCount) { return; }
41
  let outer_index = output_index / params.innerSize;
 
1
+ // Pass 1 of a rank-independent split-axis arg reduction. The tensor
2
  // is flattened as (outer, axis, inner). Each thread scans one contiguous segment
3
  // of the reduced axis for one output position.
4
  //
 
6
  // layout without losing integer precision. The combine pass restores the native
7
  // comparison type. Equal values keep the lowest index by default or the highest
8
  // index when select_last_index is enabled.
9
+ {% set cmp = ">" if modeSpec == "max" else "<" %}
10
  {% set is_int = scalar == "i32" or scalar == "u32" %}
11
  {% set work = scalar if is_int else "f32" %}
12
  {{ env.wgsl.resourceDeclarations }}
 
20
  // is not folded into an unrepresentable infinity literal.
21
  fn lane_identity() -> {{ work }} {
22
  {% if scalar == "i32" %}
23
+ return bitcast<i32>({{ "0x80000000u" if modeSpec == "max" else "0x7fffffffu" }});
24
  {% elif scalar == "u32" %}
25
+ return {{ "0u" if modeSpec == "max" else "0xffffffffu" }};
26
  {% else %}
27
+ var bits = {{ "0xff800000u" if modeSpec == "max" else "0x7f800000u" }};
28
  return bitcast<f32>(bits);
29
  {% endif %}
30
  }
31
 
32
  @compute @workgroup_size(WG, 1, 1)
33
  fn main(@builtin(global_invocation_id) gid: vec3<u32>,
34
+ @builtin(workgroup_id) wg: vec3<u32>) {
35
+ // 2D-folded column index: wg.z carries the high bits past the per-axis dispatch
36
+ // fold width on the x dimension.
37
+ let output_index = (wg.x + wg.z * {{ DISPATCH_FOLD_WIDTH }}u) * WG + (gid.x % WG);
 
38
  let split_index = wg.y;
39
  if (output_index >= params.outputCount) { return; }
40
  let outer_index = output_index / params.innerSize;
build/webgpu/reduce-arg-axis-split-tiled.wgsl.jinja CHANGED
@@ -7,7 +7,7 @@
7
  //
8
  // The final fold is intentionally subgroup-free: it is small, deterministic,
9
  // and preserves identical first/last-index tie semantics on every feature tier.
10
- {% set cmp = ">" if source.mode == "max" else "<" %}
11
  {% set is_int = scalar == "i32" or scalar == "u32" %}
12
  {% set work = scalar if is_int else "f32" %}
13
  {{ env.wgsl.resourceDeclarations }}
@@ -20,11 +20,11 @@ const SENTINEL_IDX: u32 = 4294967295u;
20
 
21
  fn lane_identity() -> {{ work }} {
22
  {% if scalar == "i32" %}
23
- return bitcast<i32>({{ "0x80000000u" if source.mode == "max" else "0x7fffffffu" }});
24
  {% elif scalar == "u32" %}
25
- return {{ "0u" if source.mode == "max" else "0xffffffffu" }};
26
  {% else %}
27
- var bits = {{ "0xff800000u" if source.mode == "max" else "0x7f800000u" }};
28
  return bitcast<f32>(bits);
29
  {% endif %}
30
  }
 
7
  //
8
  // The final fold is intentionally subgroup-free: it is small, deterministic,
9
  // and preserves identical first/last-index tie semantics on every feature tier.
10
+ {% set cmp = ">" if modeSpec == "max" else "<" %}
11
  {% set is_int = scalar == "i32" or scalar == "u32" %}
12
  {% set work = scalar if is_int else "f32" %}
13
  {{ env.wgsl.resourceDeclarations }}
 
20
 
21
  fn lane_identity() -> {{ work }} {
22
  {% if scalar == "i32" %}
23
+ return bitcast<i32>({{ "0x80000000u" if modeSpec == "max" else "0x7fffffffu" }});
24
  {% elif scalar == "u32" %}
25
+ return {{ "0u" if modeSpec == "max" else "0xffffffffu" }};
26
  {% else %}
27
+ var bits = {{ "0xff800000u" if modeSpec == "max" else "0x7f800000u" }};
28
  return bitcast<f32>(bits);
29
  {% endif %}
30
  }
build/webgpu/reduce-arg-axis-tiled.wgsl.jinja CHANGED
@@ -1,10 +1,10 @@
1
- // Tiled strided-axis ArgMax/ArgMin reduction. f32 compares directly; i32 and
2
  // u32 compare natively, so large-magnitude integers keep full precision. The
3
  // workgroup partials carry the native comparison type without a cross-pass
4
  // bitcast round trip. Flattening the output as (outer, inner) lets one coalesced
5
  // column tile handle every non-contiguous axis. f16 uses the serial or
6
  // contiguous routes because this kernel has no widened-f16 scratch path.
7
- {% set cmp = ">" if source.mode == "max" else "<" %}
8
  {% set is_int = scalar == "i32" or scalar == "u32" %}
9
  {% set work = scalar if is_int else "f32" %}
10
  {{ env.wgsl.resourceDeclarations }}
@@ -19,11 +19,11 @@ const SENTINEL_IDX: u32 = 4294967295u;
19
  // is not folded into an unrepresentable infinity literal.
20
  fn lane_identity() -> {{ work }} {
21
  {% if scalar == "i32" %}
22
- return bitcast<i32>({{ "0x80000000u" if source.mode == "max" else "0x7fffffffu" }});
23
  {% elif scalar == "u32" %}
24
- return {{ "0u" if source.mode == "max" else "0xffffffffu" }};
25
  {% else %}
26
- var bits = {{ "0xff800000u" if source.mode == "max" else "0x7f800000u" }};
27
  return bitcast<f32>(bits);
28
  {% endif %}
29
  }
@@ -32,15 +32,14 @@ var<workgroup> partial_val: array<{{ work }}, WG>;
32
  var<workgroup> partial_idx: array<u32, WG>;
33
 
34
  @compute @workgroup_size(WG, 1, 1)
35
- fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>,
36
- @builtin(num_workgroups) nwg: vec3<u32>) {
37
  let tid = lid.x;
38
  let col_lane = tid % TILE_COLS;
39
  let row_lane = tid / TILE_COLS;
40
- // 2D-folded column-tile index: wg.y carries the high bits past the maxComputeWorkgroupsPerDimension
41
- // workgroup-per-dimension dispatch limit. Reduces to wg.x when nwg.y == 1;
42
  // the bounds guard drops the over-dispatched tail.
43
- let output_index = (wg.x + wg.y * nwg.x) * TILE_COLS + col_lane;
44
  let in_bounds = output_index < params.outputCount;
45
  let outer_index = output_index / params.innerSize;
46
  let inner_index = output_index % params.innerSize;
 
1
+ // Tiled strided-axis arg reduction. f32 compares directly; i32 and
2
  // u32 compare natively, so large-magnitude integers keep full precision. The
3
  // workgroup partials carry the native comparison type without a cross-pass
4
  // bitcast round trip. Flattening the output as (outer, inner) lets one coalesced
5
  // column tile handle every non-contiguous axis. f16 uses the serial or
6
  // contiguous routes because this kernel has no widened-f16 scratch path.
7
+ {% set cmp = ">" if modeSpec == "max" else "<" %}
8
  {% set is_int = scalar == "i32" or scalar == "u32" %}
9
  {% set work = scalar if is_int else "f32" %}
10
  {{ env.wgsl.resourceDeclarations }}
 
19
  // is not folded into an unrepresentable infinity literal.
20
  fn lane_identity() -> {{ work }} {
21
  {% if scalar == "i32" %}
22
+ return bitcast<i32>({{ "0x80000000u" if modeSpec == "max" else "0x7fffffffu" }});
23
  {% elif scalar == "u32" %}
24
+ return {{ "0u" if modeSpec == "max" else "0xffffffffu" }};
25
  {% else %}
26
+ var bits = {{ "0xff800000u" if modeSpec == "max" else "0x7f800000u" }};
27
  return bitcast<f32>(bits);
28
  {% endif %}
29
  }
 
32
  var<workgroup> partial_idx: array<u32, WG>;
33
 
34
  @compute @workgroup_size(WG, 1, 1)
35
+ fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
 
36
  let tid = lid.x;
37
  let col_lane = tid % TILE_COLS;
38
  let row_lane = tid / TILE_COLS;
39
+ // 2D-folded column-tile index: wg.y carries the high bits past the per-axis dispatch fold width.
40
+ // Reduces to wg.x when the dispatch does not fold;
41
  // the bounds guard drops the over-dispatched tail.
42
+ let output_index = (wg.x + wg.y * {{ DISPATCH_FOLD_WIDTH }}u) * TILE_COLS + col_lane;
43
  let in_bounds = output_index < params.outputCount;
44
  let outer_index = output_index / params.innerSize;
45
  let inner_index = output_index % params.innerSize;
build/webgpu/reduce-arg-axis.wgsl.jinja CHANGED
@@ -1,18 +1,17 @@
1
- // Rank-independent serial arg reduction for ArgMax and ArgMin. Flattening a
2
  // tensor around the reduced axis into (outer, axis, inner) gives one
3
  // rank-independent coordinate calculation. One thread owns one output position
4
  // and walks the reduced axis at innerSize stride.
5
- {% set cmp = ">" if source.mode == "max" else "<" %}
6
  {% if usesF16 %}
7
  enable f16;
8
  {%- endif %}
9
  {{ env.wgsl.resourceDeclarations }}
10
 
11
  @compute @workgroup_size({{ workgroupSize }})
12
- fn main(@builtin(global_invocation_id) gid: vec3<u32>,
13
- @builtin(num_workgroups) nwg: vec3<u32>) {
14
  // Fold the output grid into x/y when one dispatch dimension is insufficient.
15
- let output_index = gid.x + gid.y * nwg.x * {{ workgroupSize }}u;
16
  if (output_index >= params.outputCount) {
17
  return;
18
  }
 
1
+ // Rank-independent serial arg reduction. Flattening a
2
  // tensor around the reduced axis into (outer, axis, inner) gives one
3
  // rank-independent coordinate calculation. One thread owns one output position
4
  // and walks the reduced axis at innerSize stride.
5
+ {% set cmp = ">" if modeSpec == "max" else "<" %}
6
  {% if usesF16 %}
7
  enable f16;
8
  {%- endif %}
9
  {{ env.wgsl.resourceDeclarations }}
10
 
11
  @compute @workgroup_size({{ workgroupSize }})
12
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
 
13
  // Fold the output grid into x/y when one dispatch dimension is insufficient.
14
+ let output_index = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ workgroupSize }}u;
15
  if (output_index >= params.outputCount) {
16
  return;
17
  }
build/webgpu/reduce-arg-row-split.wgsl.jinja CHANGED
@@ -1,4 +1,4 @@
1
- {% set useSubgroups = source.useSubgroups %}
2
  {% if usesF16 %}
3
  enable f16;
4
  {%- endif %}
@@ -17,15 +17,15 @@ const SENTINEL_IDX: u32 = 4294967295u;
17
  {% else %}
18
  {% set work = "f32" %}
19
  {% endif %}
20
- {% set cmp = ">" if source.mode == "max" else "<" %}
21
 
22
  fn lane_identity() -> {{ work }} {
23
  {%- if scalar == "i32" %}
24
- return bitcast<i32>({{ "0x80000000u" if source.mode == "max" else "0x7fffffffu" }});
25
  {%- elif scalar == "u32" %}
26
- return {{ "0u" if source.mode == "max" else "0xffffffffu" }};
27
  {%- else %}
28
- var bits = {{ "0xff800000u" if source.mode == "max" else "0x7f800000u" }};
29
  return bitcast<f32>(bits);
30
  {%- endif %}
31
  }
@@ -36,7 +36,6 @@ var<workgroup> wg_idx: array<u32, WG>;
36
  @compute @workgroup_size(WG, 1, 1)
37
  fn main(@builtin(workgroup_id) wg: vec3<u32>,
38
  @builtin(local_invocation_id) lid: vec3<u32>{% if useSubgroups %},
39
- @builtin(subgroup_invocation_id) subgroup_lane: u32,
40
  @builtin(subgroup_size) subgroup_size: u32{% endif %}) {
41
  let row = wg.x;
42
  let split_index = wg.y;
@@ -49,7 +48,7 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>,
49
  var best_val = lane_identity();
50
  var best_idx = SENTINEL_IDX;
51
  for (var chunk = segment_begin + tid; chunk < segment_end; chunk += WG) {
52
- {%- if source.vec4 %}
53
  let value4 = x[base + chunk];
54
  {%- for component in ["x", "y", "z", "w"] %}
55
  {
@@ -93,31 +92,51 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>,
93
  let subgroup_candidate = select(SENTINEL_IDX, best_idx, best_val == subgroup_val);
94
  let subgroup_idx = subgroupMin(subgroup_candidate);
95
  {%- endif %}
96
- let safe_subgroup_size = max(subgroup_size, 1u);
97
- // The final subgroup may be partial on devices whose native subgroup size
98
- // does not divide WG. Merge that leader as well.
99
- let subgroup_count = max(1u, (WG + safe_subgroup_size - 1u) / safe_subgroup_size);
100
- if (subgroup_lane == 0u) {
101
- let slot = min(tid / safe_subgroup_size, WG - 1u);
102
- wg_val[slot] = subgroup_val;
103
- wg_idx[slot] = subgroup_idx;
104
- }
105
- workgroupBarrier();
106
- if (tid == 0u) {
107
- var output_val = wg_val[0];
108
- var output_idx = wg_idx[0];
109
- for (var slot = 1u; slot < subgroup_count; slot++) {
 
 
 
 
 
 
 
 
 
 
 
110
  let candidate_val = wg_val[slot];
111
  let candidate_idx = wg_idx[slot];
112
  {%- if selectLastIndex %}
113
- if (candidate_idx != SENTINEL_IDX && (output_idx == SENTINEL_IDX || candidate_val {{ cmp }} output_val || (candidate_val == output_val && candidate_idx > output_idx))) {
114
  {%- else %}
115
- if (candidate_idx != SENTINEL_IDX && (output_idx == SENTINEL_IDX || candidate_val {{ cmp }} output_val || (candidate_val == output_val && candidate_idx < output_idx))) {
116
  {%- endif %}
117
- output_val = candidate_val;
118
- output_idx = candidate_idx;
119
  }
120
  }
 
 
 
 
 
 
 
 
 
121
  let scratch_index = split_index * params.rows + row;
122
  {%- if scalar == "u32" %}
123
  partials_val[scratch_index] = output_val;
 
1
+ {% set useSubgroups = useSubgroups %}
2
  {% if usesF16 %}
3
  enable f16;
4
  {%- endif %}
 
17
  {% else %}
18
  {% set work = "f32" %}
19
  {% endif %}
20
+ {% set cmp = ">" if modeSpec == "max" else "<" %}
21
 
22
  fn lane_identity() -> {{ work }} {
23
  {%- if scalar == "i32" %}
24
+ return bitcast<i32>({{ "0x80000000u" if modeSpec == "max" else "0x7fffffffu" }});
25
  {%- elif scalar == "u32" %}
26
+ return {{ "0u" if modeSpec == "max" else "0xffffffffu" }};
27
  {%- else %}
28
+ var bits = {{ "0xff800000u" if modeSpec == "max" else "0x7f800000u" }};
29
  return bitcast<f32>(bits);
30
  {%- endif %}
31
  }
 
36
  @compute @workgroup_size(WG, 1, 1)
37
  fn main(@builtin(workgroup_id) wg: vec3<u32>,
38
  @builtin(local_invocation_id) lid: vec3<u32>{% if useSubgroups %},
 
39
  @builtin(subgroup_size) subgroup_size: u32{% endif %}) {
40
  let row = wg.x;
41
  let split_index = wg.y;
 
48
  var best_val = lane_identity();
49
  var best_idx = SENTINEL_IDX;
50
  for (var chunk = segment_begin + tid; chunk < segment_end; chunk += WG) {
51
+ {%- if vec4 %}
52
  let value4 = x[base + chunk];
53
  {%- for component in ["x", "y", "z", "w"] %}
54
  {
 
92
  let subgroup_candidate = select(SENTINEL_IDX, best_idx, best_val == subgroup_val);
93
  let subgroup_idx = subgroupMin(subgroup_candidate);
94
  {%- endif %}
95
+ // The cross-subgroup fold assumes nothing about which invocations share a
96
+ // subgroup, how many subgroups there are, or which of a subgroup's lanes are
97
+ // active: every invocation owns the slot at its own index, the elected lane
98
+ // publishes its subgroup pair there and every other lane publishes the
99
+ // identity pair (the sentinel index, which the merge skips). Each subgroup
100
+ // then folds all WG slots — lane `rank`, its dense position among the active
101
+ // lanes, walks slots rank, rank + count, ... — and one more collective merges
102
+ // the lane partials, so every slot is merged exactly once at any legal width
103
+ // and partition. The merge (best value, then the tie-break index) is
104
+ // commutative and associative, so the fold order does not change the result.
105
+ var output_val = subgroup_val;
106
+ var output_idx = subgroup_idx;
107
+ // A one-subgroup workgroup is already fully reduced by the collectives above.
108
+ // The test reads the `subgroup_size` builtin, which is uniform; a collective's
109
+ // result is not uniform to WGSL's analysis and may not guard a barrier.
110
+ if (subgroup_size != WG) {
111
+ let rank = subgroupExclusiveAdd(1u);
112
+ let count = subgroupAdd(1u);
113
+ let leader = rank == 0u;
114
+ wg_val[tid] = select(lane_identity(), subgroup_val, leader);
115
+ wg_idx[tid] = select(SENTINEL_IDX, subgroup_idx, leader);
116
+ workgroupBarrier();
117
+ var fold_val = lane_identity();
118
+ var fold_idx = SENTINEL_IDX;
119
+ for (var slot = rank; slot < WG; slot = slot + count) {
120
  let candidate_val = wg_val[slot];
121
  let candidate_idx = wg_idx[slot];
122
  {%- if selectLastIndex %}
123
+ if (candidate_idx != SENTINEL_IDX && (fold_idx == SENTINEL_IDX || candidate_val {{ cmp }} fold_val || (candidate_val == fold_val && candidate_idx > fold_idx))) {
124
  {%- else %}
125
+ if (candidate_idx != SENTINEL_IDX && (fold_idx == SENTINEL_IDX || candidate_val {{ cmp }} fold_val || (candidate_val == fold_val && candidate_idx < fold_idx))) {
126
  {%- endif %}
127
+ fold_val = candidate_val;
128
+ fold_idx = candidate_idx;
129
  }
130
  }
131
+ output_val = subgroupMin(fold_val);{% if selectLastIndex %}
132
+ let fold_candidate = select(0u, fold_idx, fold_val == output_val && fold_idx != SENTINEL_IDX);
133
+ output_idx = subgroupMax(fold_candidate);
134
+ {%- else %}
135
+ let fold_candidate = select(SENTINEL_IDX, fold_idx, fold_val == output_val);
136
+ output_idx = subgroupMin(fold_candidate);
137
+ {%- endif %}
138
+ }
139
+ if (tid == 0u) {
140
  let scratch_index = split_index * params.rows + row;
141
  {%- if scalar == "u32" %}
142
  partials_val[scratch_index] = output_val;
build/webgpu/reduce-arg-row-subgroup.wgsl.jinja CHANGED
@@ -1,16 +1,17 @@
1
- // Subgroup (value, index) row reduction for ArgMax and ArgMin over a contiguous
2
  // last axis, with one workgroup per output row. Each thread scans a strided
3
  // chunk, carrying a (best value, best index) pair with ONNX tie semantics.
4
  // Subgroup combination first reduces the value, then reduces the matching
5
- // lanes' indices. Leaders deposit per-subgroup pairs in shared memory for a
6
- // final fold. A one-subgroup workgroup writes its collective result directly,
7
- // avoiding shared memory and the workgroup barrier.
 
8
  //
9
  // Float inputs compare in f32. Sentinels are runtime-bitcast infinities or the
10
  // integer bounds; ties against the sentinel use the same index rule, so a row
11
  // filled with the sentinel still produces the correct index. NaNs are never
12
- // accepted, and an all-NaN row falls back to index zero like the serial scan.
13
- {% if source.useSubgroups is defined %}{% set useSubgroups = source.useSubgroups %}{% else %}{% set useSubgroups = true %}{% endif %}
14
  {% if usesF16 %}
15
  enable f16;
16
  {%- endif %}
@@ -21,7 +22,7 @@ enable subgroups;
21
 
22
  const WG: u32 = {{ workgroupSize }}u;
23
  const SENTINEL_IDX: u32 = 4294967295u;
24
- {% if source.mode == "min" and scalar != "i32" and scalar != "u32" %}
25
  fn pos_inf_f32() -> f32 {
26
  var bits = 0x7f800000u;
27
  return bitcast<f32>(bits);
@@ -35,16 +36,22 @@ fn pos_inf_f32() -> f32 {
35
  {% set cmpType = "f32" %}
36
  {% endif %}
37
  {% set cmp = "<" %}
38
- {%- if not (useSubgroups and source.singleSubgroup is defined and source.singleSubgroup) %}
 
 
 
 
 
 
 
39
  var<workgroup> wgVal: array<{{ cmpType }}, WG>;
40
  var<workgroup> wgIdx: array<u32, WG>;
41
  {%- endif %}
42
 
43
  @compute @workgroup_size(WG, 1, 1)
44
  fn main(@builtin(workgroup_id) wg: vec3<u32>,
45
- @builtin(local_invocation_id) lid: vec3<u32>{% if useSubgroups %},
46
- @builtin(subgroup_invocation_id) sgLid: u32{% if not (source.singleSubgroup is defined and source.singleSubgroup) %},
47
- @builtin(subgroup_size) sgSize: u32{% endif %}{% endif %}) {
48
  let row = wg.x + wg.y * params.rowStride;
49
  if (row >= params.rows) {
50
  return;
@@ -58,7 +65,7 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>,
58
  var bestVal: f32 = pos_inf_f32();
59
  {%- endif %}
60
  var bestIdx: u32 = SENTINEL_IDX;
61
- {%- if source.vec4 %}
62
  for (var c = tid; c < params.chunkCount; c = c + WG) {
63
  let v4 = x[base + c];
64
  {%- for comp in ["x", "y", "z", "w"] %}
@@ -120,23 +127,37 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>,
120
  let cand = select(SENTINEL_IDX, bestIdx, bestVal == m);
121
  let sgIdx = subgroupMin(cand);
122
  {%- endif %}
123
- {% if source.singleSubgroup is defined and source.singleSubgroup %}
124
- if (sgLid == 0u) {
 
 
125
  y[row] = select(sgIdx, 0u, sgIdx == SENTINEL_IDX);
126
  }
127
  {%- else %}
128
- let safeSg = max(sgSize, 1u);
129
- let slotCount = max(1u, (WG + safeSg - 1u) / safeSg);
130
- if (sgLid == 0u) {
131
- let slot = min(tid / safeSg, WG - 1u);
132
- wgVal[slot] = m;
133
- wgIdx[slot] = sgIdx;
134
- }
135
- workgroupBarrier();
136
- if (tid == 0u) {
137
- var outVal = wgVal[0];
138
- var outIdx = wgIdx[0];
139
- for (var i = 1u; i < slotCount; i = i + 1u) {
 
 
 
 
 
 
 
 
 
 
 
 
140
  let v = wgVal[i];
141
  let vi = wgIdx[i];
142
  {%- if selectLastIndex %}
@@ -148,7 +169,14 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>,
148
  outIdx = vi;
149
  }
150
  }
151
- y[row] = select(outIdx, 0u, outIdx == SENTINEL_IDX);
 
 
 
 
 
 
 
152
  }
153
  {%- endif %}
154
  {% else %}
 
1
+ // Subgroup (value, index) arg reduction over a contiguous
2
  // last axis, with one workgroup per output row. Each thread scans a strided
3
  // chunk, carrying a (best value, best index) pair with ONNX tie semantics.
4
  // Subgroup combination first reduces the value, then reduces the matching
5
+ // lanes' indices. Every invocation then publishes one shared-memory slot (its
6
+ // subgroup's pair from the elected lane, the identity pair from every other
7
+ // lane) and each subgroup folds all of them. A one-subgroup workgroup writes
8
+ // its collective result directly, avoiding shared memory and the barrier.
9
  //
10
  // Float inputs compare in f32. Sentinels are runtime-bitcast infinities or the
11
  // integer bounds; ties against the sentinel use the same index rule, so a row
12
  // filled with the sentinel still produces the correct index. NaNs are never
13
+ // accepted, and an all-NaN row returns index zero.
14
+ {% if useSubgroups is defined %}{% set useSubgroups = useSubgroups %}{% else %}{% set useSubgroups = true %}{% endif %}
15
  {% if usesF16 %}
16
  enable f16;
17
  {%- endif %}
 
22
 
23
  const WG: u32 = {{ workgroupSize }}u;
24
  const SENTINEL_IDX: u32 = 4294967295u;
25
+ {% if modeSpec == "min" and scalar != "i32" and scalar != "u32" %}
26
  fn pos_inf_f32() -> f32 {
27
  var bits = 0x7f800000u;
28
  return bitcast<f32>(bits);
 
36
  {% set cmpType = "f32" %}
37
  {% endif %}
38
  {% set cmp = "<" %}
39
+ {%- if cmpType == "i32" %}
40
+ {% set identityVal = "2147483647i" %}
41
+ {%- elif cmpType == "u32" %}
42
+ {% set identityVal = "4294967295u" %}
43
+ {%- else %}
44
+ {% set identityVal = "pos_inf_f32()" %}
45
+ {%- endif %}
46
+ {%- if not (useSubgroups and singleSubgroup is defined and singleSubgroup) %}
47
  var<workgroup> wgVal: array<{{ cmpType }}, WG>;
48
  var<workgroup> wgIdx: array<u32, WG>;
49
  {%- endif %}
50
 
51
  @compute @workgroup_size(WG, 1, 1)
52
  fn main(@builtin(workgroup_id) wg: vec3<u32>,
53
+ @builtin(local_invocation_id) lid: vec3<u32>{% if useSubgroups and not (singleSubgroup is defined and singleSubgroup) %},
54
+ @builtin(subgroup_size) sgSize: u32{% endif %}) {
 
55
  let row = wg.x + wg.y * params.rowStride;
56
  if (row >= params.rows) {
57
  return;
 
65
  var bestVal: f32 = pos_inf_f32();
66
  {%- endif %}
67
  var bestIdx: u32 = SENTINEL_IDX;
68
+ {%- if vec4 %}
69
  for (var c = tid; c < params.chunkCount; c = c + WG) {
70
  let v4 = x[base + c];
71
  {%- for comp in ["x", "y", "z", "w"] %}
 
127
  let cand = select(SENTINEL_IDX, bestIdx, bestVal == m);
128
  let sgIdx = subgroupMin(cand);
129
  {%- endif %}
130
+ {% if singleSubgroup is defined and singleSubgroup %}
131
+ // The workgroup is one (possibly partial) subgroup: elect the writer rather
132
+ // than assuming lane 0 is active.
133
+ if (subgroupElect()) {
134
  y[row] = select(sgIdx, 0u, sgIdx == SENTINEL_IDX);
135
  }
136
  {%- else %}
137
+ // Cross-subgroup fold that assumes nothing about which invocations share a
138
+ // subgroup, how many subgroups there are, or which of a subgroup's lanes are
139
+ // active: every invocation owns the slot at its own index, the elected lane
140
+ // publishes its subgroup pair there and every other lane publishes the
141
+ // identity pair (the sentinel index, which the merge skips). Each subgroup
142
+ // then folds all WG slots — lane `rank`, its dense position among the active
143
+ // lanes, walks slots rank, rank + count, ... — and one more collective merges
144
+ // the lane partials, so every slot is merged exactly once at any legal width
145
+ // and partition. The merge is commutative and associative (best value, then
146
+ // the tie-break index), so the fold order does not change the result.
147
+ var winnerIdx = sgIdx;
148
+ // A one-subgroup workgroup is already fully reduced by the collectives above.
149
+ // The test reads the `subgroup_size` builtin, which is uniform; a collective's
150
+ // result is not uniform to WGSL's analysis and may not guard a barrier.
151
+ if (sgSize != WG) {
152
+ let rank = subgroupExclusiveAdd(1u);
153
+ let count = subgroupAdd(1u);
154
+ let leader = rank == 0u;
155
+ wgVal[tid] = select({{ identityVal }}, m, leader);
156
+ wgIdx[tid] = select(SENTINEL_IDX, sgIdx, leader);
157
+ workgroupBarrier();
158
+ var outVal = {{ identityVal }};
159
+ var outIdx = SENTINEL_IDX;
160
+ for (var i = rank; i < WG; i = i + count) {
161
  let v = wgVal[i];
162
  let vi = wgIdx[i];
163
  {%- if selectLastIndex %}
 
169
  outIdx = vi;
170
  }
171
  }
172
+ let foldVal = subgroupMin(outVal);{% if selectLastIndex %}
173
+ winnerIdx = subgroupMax(select(0u, outIdx, outVal == foldVal && outIdx != SENTINEL_IDX));
174
+ {%- else %}
175
+ winnerIdx = subgroupMin(select(SENTINEL_IDX, outIdx, outVal == foldVal));
176
+ {%- endif %}
177
+ }
178
+ if (tid == 0u) {
179
+ y[row] = select(winnerIdx, 0u, winnerIdx == SENTINEL_IDX);
180
  }
181
  {%- endif %}
182
  {% else %}
build/webgpu/test.json CHANGED
@@ -1,9 +1,8 @@
1
  {
2
- "op": "ai.onnx.ArgMin",
3
  "fixtureArrays": {
4
  "onnx_backend_argmin_input_x": [0.9762700796127319, 4.3037872314453125, 2.055267572402954, 0.8976636528968811, -1.5269039869308472, 2.917882204055786, -1.248255729675293, 7.835460186004639, 9.273255348205566, -2.331169605255127, 5.834500789642334, 0.577898383140564, 1.3608912229537964, 8.511932373046875, -8.579278945922852, -8.257413864135742, -9.59563159942627, 6.6523966789245605, 5.563135147094727, 7.400242805480957, 9.572366714477539, 5.983171463012695, -0.7704127430915833, 5.610583305358887],
5
  "axis0_splitk_8192x32_ties_first_index_input_x": [2, -3, -3, 1, 2, -3, -3, 1, 2, -3, -3, 1, 2, -3, -3, 1, 2, -3, -3, 1, 2, -3, -3, 1, 2, -3, -3, 1, 2, -3, -3, 1, 2],
6
- "subgroup_vec4_ties_first_index_input_x": [1, 5, 5, 2, 5, 0, 1, 0, 7, 7, 7, 7, 7, 7, 7, 7],
7
  "axis0_splitk_i32_8192x16_precision_min_input_x": [16777217, 16777217, 16777217, 16777217, 16777217, 16777217, 16777217, 16777217, 16777216, 16777216, 16777216, 16777216, 16777216, 16777216, 16777216, 16777216, 16777216]
8
  },
9
  "cases": [
@@ -26,7 +25,7 @@
26
  "provenance": {
27
  "source": "https://onnx.ai/onnx/operators/onnx__ArgMin.html",
28
  "test": "ArgMin axis default",
29
- "notes": "Omitting axis must reduce the leading dimension (ONNX default 0), not axis 1. The rank-2 output shape and per-column indices distinguish the two semantics. ONNX int64 indices use the framework's representable uint32 slot."
30
  },
31
  "inputs": {
32
  "x": {
@@ -84,7 +83,7 @@
84
  "provenance": {
85
  "source": "onnxruntime/test/providers/cpu/reduction/reduction_ops_test.cc",
86
  "test": "ArgMin",
87
- "notes": "Axis-0 companion for finite subnormal ordering: a negative subnormal is strictly less than zero and must not be tie-broken as flushed zero."
88
  },
89
  "attrs": { "axis": 0, "keepdims": 0 },
90
  "inputs": {
@@ -95,9 +94,7 @@
95
  {
96
  "name": "dispatch_cliff_axis1_16777216x1",
97
  "attrs": { "axis": 1, "keepdims": 0 },
98
- "inputs": {
99
- "x": { "dtype": "float32", "shape": [16777216, 1], "data": { "kind": "linspace", "start": -1.0, "end": 1.0 } }
100
- },
101
  "outputs": { "y": { "dtype": "uint32", "shape": [16777216], "tolerance": 0 } }
102
  },
103
  {
@@ -200,12 +197,12 @@
200
  "name": "ort_axis1_nan_first_incumbent_gpu_gap",
201
  "skipGpu": {
202
  "category": "todo",
203
- "reason": "The parallel min/max reduction routes do not yet preserve the reference's first-element NaN incumbent semantics; explicit NaN and index tracking is implementable in WGSL."
204
  },
205
  "provenance": {
206
  "source": "onnxruntime/test/providers/cpu/reduction/reduction_ops_test.cc",
207
  "test": "ReductionOpTest.ArgMin",
208
- "notes": "NaN extension: ORT seeds ArgMin from the first reduced element, so a leading NaN remains the selected incumbent while later NaNs are ignored after a finite incumbent."
209
  },
210
  "attrs": { "axis": 1, "keepdims": 0 },
211
  "inputs": {
@@ -224,7 +221,7 @@
224
  "name": "ort_axis1_nan_select_last_index_gpu_gap",
225
  "skipGpu": {
226
  "category": "todo",
227
- "reason": "The parallel min/max reduction routes do not yet preserve the reference's first-element NaN incumbent semantics; explicit NaN and index tracking is implementable in WGSL."
228
  },
229
  "provenance": {
230
  "source": "onnxruntime/test/providers/cpu/reduction/reduction_ops_test.cc",
@@ -279,7 +276,7 @@
279
  "shape": [2, 4],
280
  "data": {
281
  "kind": "values",
282
- "values": ["Infinity", "Infinity", "Infinity", "Infinity", "Infinity", 5.0, "Infinity", 5.0]
283
  }
284
  }
285
  },
@@ -537,7 +534,7 @@
537
  "name": "onnx_backend_argmin_default_axis_example",
538
  "provenance": {
539
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_argmin_default_axis_example",
540
- "notes": "ONNX arg-reduction outputs are int64; this framework uses uint32 index storage."
541
  },
542
  "attrs": { "keepdims": 1, "axis": 0 },
543
  "inputs": {
@@ -549,7 +546,7 @@
549
  "name": "onnx_backend_argmin_default_axis_example_select_last_index",
550
  "provenance": {
551
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_argmin_default_axis_example_select_last_index",
552
- "notes": "ONNX arg-reduction outputs are int64; this framework uses uint32 index storage."
553
  },
554
  "attrs": { "keepdims": 1, "select_last_index": 1, "axis": 0 },
555
  "inputs": {
@@ -561,7 +558,7 @@
561
  "name": "onnx_backend_argmin_default_axis_random",
562
  "provenance": {
563
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_argmin_default_axis_random",
564
- "notes": "ONNX arg-reduction outputs are int64; this framework uses uint32 index storage."
565
  },
566
  "attrs": { "keepdims": 1, "axis": 0 },
567
  "inputs": {
@@ -577,7 +574,7 @@
577
  "name": "onnx_backend_argmin_default_axis_random_select_last_index",
578
  "provenance": {
579
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_argmin_default_axis_random_select_last_index",
580
- "notes": "ONNX arg-reduction outputs are int64; this framework uses uint32 index storage."
581
  },
582
  "attrs": { "keepdims": 1, "select_last_index": 1, "axis": 0 },
583
  "inputs": {
@@ -593,7 +590,7 @@
593
  "name": "onnx_backend_argmin_keepdims_example",
594
  "provenance": {
595
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_argmin_keepdims_example",
596
- "notes": "ONNX arg-reduction outputs are int64; this framework uses uint32 index storage."
597
  },
598
  "attrs": { "axis": 1, "keepdims": 1 },
599
  "inputs": {
@@ -605,7 +602,7 @@
605
  "name": "onnx_backend_argmin_keepdims_example_select_last_index",
606
  "provenance": {
607
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_argmin_keepdims_example_select_last_index",
608
- "notes": "ONNX arg-reduction outputs are int64; this framework uses uint32 index storage."
609
  },
610
  "attrs": { "axis": 1, "keepdims": 1, "select_last_index": 1 },
611
  "inputs": {
@@ -617,7 +614,7 @@
617
  "name": "onnx_backend_argmin_keepdims_random",
618
  "provenance": {
619
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_argmin_keepdims_random",
620
- "notes": "ONNX arg-reduction outputs are int64; this framework uses uint32 index storage."
621
  },
622
  "attrs": { "axis": 1, "keepdims": 1 },
623
  "inputs": {
@@ -633,7 +630,7 @@
633
  "name": "onnx_backend_argmin_keepdims_random_select_last_index",
634
  "provenance": {
635
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_argmin_keepdims_random_select_last_index",
636
- "notes": "ONNX arg-reduction outputs are int64; this framework uses uint32 index storage."
637
  },
638
  "attrs": { "axis": 1, "keepdims": 1, "select_last_index": 1 },
639
  "inputs": {
@@ -649,7 +646,7 @@
649
  "name": "onnx_backend_argmin_negative_axis_keepdims_example",
650
  "provenance": {
651
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_argmin_negative_axis_keepdims_example",
652
- "notes": "ONNX arg-reduction outputs are int64; this framework uses uint32 index storage."
653
  },
654
  "attrs": { "axis": -1, "keepdims": 1 },
655
  "inputs": {
@@ -661,7 +658,7 @@
661
  "name": "onnx_backend_argmin_negative_axis_keepdims_example_select_last_index",
662
  "provenance": {
663
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_argmin_negative_axis_keepdims_example_select_last_index",
664
- "notes": "ONNX arg-reduction outputs are int64; this framework uses uint32 index storage."
665
  },
666
  "attrs": { "axis": -1, "keepdims": 1, "select_last_index": 1 },
667
  "inputs": {
@@ -673,7 +670,7 @@
673
  "name": "onnx_backend_argmin_negative_axis_keepdims_random",
674
  "provenance": {
675
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_argmin_negative_axis_keepdims_random",
676
- "notes": "ONNX arg-reduction outputs are int64; this framework uses uint32 index storage."
677
  },
678
  "attrs": { "axis": -1, "keepdims": 1 },
679
  "inputs": {
@@ -689,7 +686,7 @@
689
  "name": "onnx_backend_argmin_negative_axis_keepdims_random_select_last_index",
690
  "provenance": {
691
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_argmin_negative_axis_keepdims_random_select_last_index",
692
- "notes": "ONNX arg-reduction outputs are int64; this framework uses uint32 index storage."
693
  },
694
  "attrs": { "axis": -1, "keepdims": 1, "select_last_index": 1 },
695
  "inputs": {
@@ -705,7 +702,7 @@
705
  "name": "onnx_backend_argmin_no_keepdims_example",
706
  "provenance": {
707
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_argmin_no_keepdims_example",
708
- "notes": "ONNX arg-reduction outputs are int64; this framework uses uint32 index storage."
709
  },
710
  "attrs": { "axis": 1, "keepdims": 0 },
711
  "inputs": {
@@ -717,7 +714,7 @@
717
  "name": "onnx_backend_argmin_no_keepdims_example_select_last_index",
718
  "provenance": {
719
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_argmin_no_keepdims_example_select_last_index",
720
- "notes": "ONNX arg-reduction outputs are int64; this framework uses uint32 index storage."
721
  },
722
  "attrs": { "axis": 1, "keepdims": 0, "select_last_index": 1 },
723
  "inputs": {
@@ -729,7 +726,7 @@
729
  "name": "onnx_backend_argmin_no_keepdims_random",
730
  "provenance": {
731
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_argmin_no_keepdims_random",
732
- "notes": "ONNX arg-reduction outputs are int64; this framework uses uint32 index storage."
733
  },
734
  "attrs": { "axis": 1, "keepdims": 0 },
735
  "inputs": {
@@ -745,7 +742,7 @@
745
  "name": "onnx_backend_argmin_no_keepdims_random_select_last_index",
746
  "provenance": {
747
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_argmin_no_keepdims_random_select_last_index",
748
- "notes": "ONNX arg-reduction outputs are int64; this framework uses uint32 index storage."
749
  },
750
  "attrs": { "axis": 1, "keepdims": 0, "select_last_index": 1 },
751
  "inputs": {
@@ -767,7 +764,7 @@
767
  "data": { "kind": "values", "values": { "$ref": "#/fixtureArrays/subgroup_vec4_ties_first_index_input_x" } }
768
  }
769
  },
770
- "outputs": { "y": { "dtype": "uint32", "shape": [2], "data": { "kind": "values", "values": [5, 0] } } }
771
  },
772
  {
773
  "name": "subgroup_vec4_ties_select_last_index",
@@ -779,7 +776,7 @@
779
  "data": { "kind": "values", "values": { "$ref": "#/fixtureArrays/subgroup_vec4_ties_first_index_input_x" } }
780
  }
781
  },
782
- "outputs": { "y": { "dtype": "uint32", "shape": [2], "data": { "kind": "values", "values": [7, 7] } } }
783
  },
784
  {
785
  "name": "subgroup_scalar_f16_2x5",
@@ -889,7 +886,7 @@
889
  "name": "rank4_axis0_batch_argmin_f32_keepdims",
890
  "provenance": {
891
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_argmin_keepdims_random (extended to rank4 batch axis)",
892
- "notes": "Same rank4 axis=0 selection hole on the default float32 dtype with keepdims=1, to prove the gap is dtype-independent (not an integer-only branch)."
893
  },
894
  "attrs": { "axis": 0, "keepdims": 1 },
895
  "inputs": {
@@ -909,7 +906,7 @@
909
  "provenance": {
910
  "source": "onnxruntime/test/providers/cpu/reduction/reduction_ops_test.cc",
911
  "test": "ReductionOpTest.ArgMin_int8 (rank4 NCHW channel argmin)",
912
- "notes": "int8 channel-axis argmin on a rank4 NCHW tensor. rank4_axis1 gates dtype only via f16Ok(T) (true for int8) so this is a WORKING-but-untested integer dtype on the rank4 selection path."
913
  },
914
  "attrs": { "axis": 1, "keepdims": 0 },
915
  "inputs": {
@@ -929,7 +926,7 @@
929
  "provenance": {
930
  "source": "onnxruntime/test/providers/cpu/reduction/reduction_ops_test.cc",
931
  "test": "ReductionOpTest.ArgMin (rank3 middle-axis f16 select_last)",
932
- "notes": "f16 middle-axis argmin on rank3 with keepdims=1 and select_last_index=1. rank3_axis1 admits f16 via f16Ok(T) on an f16 device; no existing rank3/rank4 case is f16."
933
  },
934
  "attrs": { "axis": 1, "keepdims": 1, "select_last_index": 1 },
935
  "inputs": {
@@ -1060,7 +1057,7 @@
1060
  {
1061
  "name": "axis0_narrow_splitk_16384x8_f32",
1062
  "provenance": {
1063
- "notes": "Tall narrow axis-0 (16384 rows, 8 cols) routes to the split-and-tiled kernel: all 256 lanes cooperate across eight columns instead of leaving 248 lanes idle. Cross-checks the tiled partials and combine pass against the reference."
1064
  },
1065
  "attrs": { "axis": 0, "keepdims": 0 },
1066
  "inputs": {
@@ -1088,7 +1085,7 @@
1088
  {
1089
  "name": "last_axis_split_scalar_logits_ties_last",
1090
  "provenance": {
1091
- "notes": "The unaligned 32769-element companion route-locks the scalar split-row reduction and verifies select_last_index across split boundaries."
1092
  },
1093
  "attrs": { "axis": 1, "keepdims": 0, "select_last_index": 1 },
1094
  "inputs": { "x": { "dtype": "float32", "shape": [1, 32769], "data": { "kind": "constant", "value": -5.0 } } },
@@ -1132,7 +1129,7 @@
1132
  {
1133
  "name": "last_axis_split_vec4_f16_rows",
1134
  "provenance": {
1135
- "notes": "float16 on the vec4 split-row reduction: each vec4 component must be widened to f32 before comparison, and the shader must enable f16. Rank 3 (1, 2, 32768) reduces the last axis exactly as the rank-2 logits case does while keeping the reference on its hoisted-decode path. All seven cycle values are exactly representable in float16, so the answer does not depend on rounding. ONNX ArgMin admits float16 via T; first index on ties."
1136
  },
1137
  "attrs": { "axis": 2, "keepdims": 0 },
1138
  "inputs": {
@@ -1149,7 +1146,7 @@
1149
  {
1150
  "name": "last_axis_split_scalar_f16_rows",
1151
  "provenance": {
1152
- "notes": "The unaligned (32769-column) companion: float16 on the scalar split-row reduction, where the widening happens on the plain load rather than on a vec4 component. Rank 3 keeps the reference on its hoisted-decode path; all cycle values are exact in float16. ONNX ArgMin admits float16 via T; first index on ties."
1153
  },
1154
  "attrs": { "axis": 2, "keepdims": 0 },
1155
  "inputs": {
@@ -1183,7 +1180,7 @@
1183
  {
1184
  "name": "rank5_middle_axis_generic_geometry",
1185
  "provenance": {
1186
- "notes": "Proves the rank-independent (outer, axis, inner) fallback covers ranks beyond the former rank-4 manifest specializations."
1187
  },
1188
  "attrs": { "axis": 2, "keepdims": 0, "select_last_index": 1 },
1189
  "inputs": {
@@ -1198,7 +1195,7 @@
1198
  {
1199
  "name": "axis0_splitk_select_last_index_ties_8192x16",
1200
  "provenance": {
1201
- "notes": "select_last_index=1 on the split-tiled route, which no case had ever rendered: every existing case for that geometry leaves the attribute at its default. The 17-value cycle is coprime with the 16 columns, so each column sees every value about 482 times and the minimum is a 482-way tie whose winning index differs per column -- exactly the tie-break the two arms disagree about, and one a per-lane fold can get wrong even when its lane-to-index map is monotone."
1202
  },
1203
  "attrs": { "axis": 0, "keepdims": 0, "select_last_index": 1 },
1204
  "inputs": {
 
1
  {
 
2
  "fixtureArrays": {
3
  "onnx_backend_argmin_input_x": [0.9762700796127319, 4.3037872314453125, 2.055267572402954, 0.8976636528968811, -1.5269039869308472, 2.917882204055786, -1.248255729675293, 7.835460186004639, 9.273255348205566, -2.331169605255127, 5.834500789642334, 0.577898383140564, 1.3608912229537964, 8.511932373046875, -8.579278945922852, -8.257413864135742, -9.59563159942627, 6.6523966789245605, 5.563135147094727, 7.400242805480957, 9.572366714477539, 5.983171463012695, -0.7704127430915833, 5.610583305358887],
4
  "axis0_splitk_8192x32_ties_first_index_input_x": [2, -3, -3, 1, 2, -3, -3, 1, 2, -3, -3, 1, 2, -3, -3, 1, 2, -3, -3, 1, 2, -3, -3, 1, 2, -3, -3, 1, 2, -3, -3, 1, 2],
5
+ "subgroup_vec4_ties_first_index_input_x": [1, 5, 0, 2, 5, 0, 1, 4, 7, 7, 7, 7, 7, 7, 7, 7],
6
  "axis0_splitk_i32_8192x16_precision_min_input_x": [16777217, 16777217, 16777217, 16777217, 16777217, 16777217, 16777217, 16777217, 16777216, 16777216, 16777216, 16777216, 16777216, 16777216, 16777216, 16777216, 16777216]
7
  },
8
  "cases": [
 
25
  "provenance": {
26
  "source": "https://onnx.ai/onnx/operators/onnx__ArgMin.html",
27
  "test": "ArgMin axis default",
28
+ "notes": "Omitting axis must reduce the leading dimension (ONNX default 0), not axis 1. The rank-2 output shape and per-column indices distinguish the two semantics. Representable ONNX int64 indices are stored in uint32 slots."
29
  },
30
  "inputs": {
31
  "x": {
 
83
  "provenance": {
84
  "source": "onnxruntime/test/providers/cpu/reduction/reduction_ops_test.cc",
85
  "test": "ArgMin",
86
+ "notes": "Along axis 0, a negative subnormal is strictly less than zero and must not be tie-broken as though it were flushed to zero."
87
  },
88
  "attrs": { "axis": 0, "keepdims": 0 },
89
  "inputs": {
 
94
  {
95
  "name": "dispatch_cliff_axis1_16777216x1",
96
  "attrs": { "axis": 1, "keepdims": 0 },
97
+ "inputs": { "x": { "dtype": "float32", "shape": [16777216, 1], "data": { "kind": "constant", "value": 1.0 } } },
 
 
98
  "outputs": { "y": { "dtype": "uint32", "shape": [16777216], "tolerance": 0 } }
99
  },
100
  {
 
197
  "name": "ort_axis1_nan_first_incumbent_gpu_gap",
198
  "skipGpu": {
199
  "category": "todo",
200
+ "reason": "The parallel reduction routes do not preserve first-element NaN incumbent semantics. Explicit NaN and index tracking is implementable in WGSL."
201
  },
202
  "provenance": {
203
  "source": "onnxruntime/test/providers/cpu/reduction/reduction_ops_test.cc",
204
  "test": "ReductionOpTest.ArgMin",
205
+ "notes": "NaN extension: ArgMin seeds the reduction from the first reduced element, so a leading NaN remains the selected incumbent while later NaNs are ignored after a finite incumbent."
206
  },
207
  "attrs": { "axis": 1, "keepdims": 0 },
208
  "inputs": {
 
221
  "name": "ort_axis1_nan_select_last_index_gpu_gap",
222
  "skipGpu": {
223
  "category": "todo",
224
+ "reason": "The parallel reduction routes do not preserve first-element NaN incumbent semantics. Explicit NaN and index tracking is implementable in WGSL."
225
  },
226
  "provenance": {
227
  "source": "onnxruntime/test/providers/cpu/reduction/reduction_ops_test.cc",
 
276
  "shape": [2, 4],
277
  "data": {
278
  "kind": "values",
279
+ "values": ["Infinity", "Infinity", "Infinity", "Infinity", 5.0, 5.0, "Infinity", "Infinity"]
280
  }
281
  }
282
  },
 
534
  "name": "onnx_backend_argmin_default_axis_example",
535
  "provenance": {
536
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_argmin_default_axis_example",
537
+ "notes": "ONNX arg-reduction outputs use int64 indices; this WebGPU package stores representable indices in uint32 slots."
538
  },
539
  "attrs": { "keepdims": 1, "axis": 0 },
540
  "inputs": {
 
546
  "name": "onnx_backend_argmin_default_axis_example_select_last_index",
547
  "provenance": {
548
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_argmin_default_axis_example_select_last_index",
549
+ "notes": "ONNX arg-reduction outputs use int64 indices; this WebGPU package stores representable indices in uint32 slots."
550
  },
551
  "attrs": { "keepdims": 1, "select_last_index": 1, "axis": 0 },
552
  "inputs": {
 
558
  "name": "onnx_backend_argmin_default_axis_random",
559
  "provenance": {
560
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_argmin_default_axis_random",
561
+ "notes": "ONNX arg-reduction outputs use int64 indices; this WebGPU package stores representable indices in uint32 slots."
562
  },
563
  "attrs": { "keepdims": 1, "axis": 0 },
564
  "inputs": {
 
574
  "name": "onnx_backend_argmin_default_axis_random_select_last_index",
575
  "provenance": {
576
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_argmin_default_axis_random_select_last_index",
577
+ "notes": "ONNX arg-reduction outputs use int64 indices; this WebGPU package stores representable indices in uint32 slots."
578
  },
579
  "attrs": { "keepdims": 1, "select_last_index": 1, "axis": 0 },
580
  "inputs": {
 
590
  "name": "onnx_backend_argmin_keepdims_example",
591
  "provenance": {
592
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_argmin_keepdims_example",
593
+ "notes": "ONNX arg-reduction outputs use int64 indices; this WebGPU package stores representable indices in uint32 slots."
594
  },
595
  "attrs": { "axis": 1, "keepdims": 1 },
596
  "inputs": {
 
602
  "name": "onnx_backend_argmin_keepdims_example_select_last_index",
603
  "provenance": {
604
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_argmin_keepdims_example_select_last_index",
605
+ "notes": "ONNX arg-reduction outputs use int64 indices; this WebGPU package stores representable indices in uint32 slots."
606
  },
607
  "attrs": { "axis": 1, "keepdims": 1, "select_last_index": 1 },
608
  "inputs": {
 
614
  "name": "onnx_backend_argmin_keepdims_random",
615
  "provenance": {
616
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_argmin_keepdims_random",
617
+ "notes": "ONNX arg-reduction outputs use int64 indices; this WebGPU package stores representable indices in uint32 slots."
618
  },
619
  "attrs": { "axis": 1, "keepdims": 1 },
620
  "inputs": {
 
630
  "name": "onnx_backend_argmin_keepdims_random_select_last_index",
631
  "provenance": {
632
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_argmin_keepdims_random_select_last_index",
633
+ "notes": "ONNX arg-reduction outputs use int64 indices; this WebGPU package stores representable indices in uint32 slots."
634
  },
635
  "attrs": { "axis": 1, "keepdims": 1, "select_last_index": 1 },
636
  "inputs": {
 
646
  "name": "onnx_backend_argmin_negative_axis_keepdims_example",
647
  "provenance": {
648
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_argmin_negative_axis_keepdims_example",
649
+ "notes": "ONNX arg-reduction outputs use int64 indices; this WebGPU package stores representable indices in uint32 slots."
650
  },
651
  "attrs": { "axis": -1, "keepdims": 1 },
652
  "inputs": {
 
658
  "name": "onnx_backend_argmin_negative_axis_keepdims_example_select_last_index",
659
  "provenance": {
660
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_argmin_negative_axis_keepdims_example_select_last_index",
661
+ "notes": "ONNX arg-reduction outputs use int64 indices; this WebGPU package stores representable indices in uint32 slots."
662
  },
663
  "attrs": { "axis": -1, "keepdims": 1, "select_last_index": 1 },
664
  "inputs": {
 
670
  "name": "onnx_backend_argmin_negative_axis_keepdims_random",
671
  "provenance": {
672
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_argmin_negative_axis_keepdims_random",
673
+ "notes": "ONNX arg-reduction outputs use int64 indices; this WebGPU package stores representable indices in uint32 slots."
674
  },
675
  "attrs": { "axis": -1, "keepdims": 1 },
676
  "inputs": {
 
686
  "name": "onnx_backend_argmin_negative_axis_keepdims_random_select_last_index",
687
  "provenance": {
688
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_argmin_negative_axis_keepdims_random_select_last_index",
689
+ "notes": "ONNX arg-reduction outputs use int64 indices; this WebGPU package stores representable indices in uint32 slots."
690
  },
691
  "attrs": { "axis": -1, "keepdims": 1, "select_last_index": 1 },
692
  "inputs": {
 
702
  "name": "onnx_backend_argmin_no_keepdims_example",
703
  "provenance": {
704
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_argmin_no_keepdims_example",
705
+ "notes": "ONNX arg-reduction outputs use int64 indices; this WebGPU package stores representable indices in uint32 slots."
706
  },
707
  "attrs": { "axis": 1, "keepdims": 0 },
708
  "inputs": {
 
714
  "name": "onnx_backend_argmin_no_keepdims_example_select_last_index",
715
  "provenance": {
716
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_argmin_no_keepdims_example_select_last_index",
717
+ "notes": "ONNX arg-reduction outputs use int64 indices; this WebGPU package stores representable indices in uint32 slots."
718
  },
719
  "attrs": { "axis": 1, "keepdims": 0, "select_last_index": 1 },
720
  "inputs": {
 
726
  "name": "onnx_backend_argmin_no_keepdims_random",
727
  "provenance": {
728
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_argmin_no_keepdims_random",
729
+ "notes": "ONNX arg-reduction outputs use int64 indices; this WebGPU package stores representable indices in uint32 slots."
730
  },
731
  "attrs": { "axis": 1, "keepdims": 0 },
732
  "inputs": {
 
742
  "name": "onnx_backend_argmin_no_keepdims_random_select_last_index",
743
  "provenance": {
744
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_argmin_no_keepdims_random_select_last_index",
745
+ "notes": "ONNX arg-reduction outputs use int64 indices; this WebGPU package stores representable indices in uint32 slots."
746
  },
747
  "attrs": { "axis": 1, "keepdims": 0, "select_last_index": 1 },
748
  "inputs": {
 
764
  "data": { "kind": "values", "values": { "$ref": "#/fixtureArrays/subgroup_vec4_ties_first_index_input_x" } }
765
  }
766
  },
767
+ "outputs": { "y": { "dtype": "uint32", "shape": [2], "data": { "kind": "values", "values": [2, 0] } } }
768
  },
769
  {
770
  "name": "subgroup_vec4_ties_select_last_index",
 
776
  "data": { "kind": "values", "values": { "$ref": "#/fixtureArrays/subgroup_vec4_ties_first_index_input_x" } }
777
  }
778
  },
779
+ "outputs": { "y": { "dtype": "uint32", "shape": [2], "data": { "kind": "values", "values": [5, 7] } } }
780
  },
781
  {
782
  "name": "subgroup_scalar_f16_2x5",
 
886
  "name": "rank4_axis0_batch_argmin_f32_keepdims",
887
  "provenance": {
888
  "source": "cmake/external/onnx/onnx/backend/test/data/node/test_argmin_keepdims_random (extended to rank4 batch axis)",
889
+ "notes": "A rank-4 float32 axis-0 reduction with keepdims=1 exercises the generic batch-axis selection path."
890
  },
891
  "attrs": { "axis": 0, "keepdims": 1 },
892
  "inputs": {
 
906
  "provenance": {
907
  "source": "onnxruntime/test/providers/cpu/reduction/reduction_ops_test.cc",
908
  "test": "ReductionOpTest.ArgMin_int8 (rank4 NCHW channel argmin)",
909
+ "notes": "An int8 channel-axis ArgMin over a rank-4 NCHW tensor exercises integer comparison on the rank-4 axis-1 path."
910
  },
911
  "attrs": { "axis": 1, "keepdims": 0 },
912
  "inputs": {
 
926
  "provenance": {
927
  "source": "onnxruntime/test/providers/cpu/reduction/reduction_ops_test.cc",
928
  "test": "ReductionOpTest.ArgMin (rank3 middle-axis f16 select_last)",
929
+ "notes": "A float16 rank-3 middle-axis reduction with keepdims=1 and select_last_index=1 exercises the float16 strided route and last-index tie breaking."
930
  },
931
  "attrs": { "axis": 1, "keepdims": 1, "select_last_index": 1 },
932
  "inputs": {
 
1057
  {
1058
  "name": "axis0_narrow_splitk_16384x8_f32",
1059
  "provenance": {
1060
+ "notes": "A tall, narrow axis-0 reduction with 16,384 rows and eight columns exercises split-and-tiled partial reductions and their combine pass across all columns."
1061
  },
1062
  "attrs": { "axis": 0, "keepdims": 0 },
1063
  "inputs": {
 
1085
  {
1086
  "name": "last_axis_split_scalar_logits_ties_last",
1087
  "provenance": {
1088
+ "notes": "An unaligned 32,769-element row exercises scalar split-row reduction and verifies select_last_index across split boundaries."
1089
  },
1090
  "attrs": { "axis": 1, "keepdims": 0, "select_last_index": 1 },
1091
  "inputs": { "x": { "dtype": "float32", "shape": [1, 32769], "data": { "kind": "constant", "value": -5.0 } } },
 
1129
  {
1130
  "name": "last_axis_split_vec4_f16_rows",
1131
  "provenance": {
1132
+ "notes": "A rank-3 float16 tensor with 32,768-element rows exercises vec4 split-row reduction. Each component is widened before comparison, all seven cycle values are exactly representable, and ties select the first index."
1133
  },
1134
  "attrs": { "axis": 2, "keepdims": 0 },
1135
  "inputs": {
 
1146
  {
1147
  "name": "last_axis_split_scalar_f16_rows",
1148
  "provenance": {
1149
+ "notes": "A rank-3 float16 tensor with 32,769-element rows exercises scalar split-row reduction. Loaded values are widened before comparison, all cycle values are exactly representable, and ties select the first index."
1150
  },
1151
  "attrs": { "axis": 2, "keepdims": 0 },
1152
  "inputs": {
 
1180
  {
1181
  "name": "rank5_middle_axis_generic_geometry",
1182
  "provenance": {
1183
+ "notes": "A rank-5 middle-axis reduction exercises the rank-independent outer-axis-inner geometry."
1184
  },
1185
  "attrs": { "axis": 2, "keepdims": 0, "select_last_index": 1 },
1186
  "inputs": {
 
1195
  {
1196
  "name": "axis0_splitk_select_last_index_ties_8192x16",
1197
  "provenance": {
1198
+ "notes": "On the split-tiled route, a 17-value cycle coprime with the 16 columns gives each column an approximately 482-way minimum tie. With select_last_index=1, each column must retain its final matching axis index across lane-local folds."
1199
  },
1200
  "attrs": { "axis": 0, "keepdims": 0, "select_last_index": 1 },
1201
  "inputs": {