Xenova HF Staff commited on
Commit
a622bea
·
verified ·
1 Parent(s): a3bef3a

sync 6fdf6301e2bb

Browse files
README.md CHANGED
@@ -45,6 +45,12 @@ Default values (overridable per request):
45
  | `T` | `float32`, `float16`, `int32`, `uint32`, `int16`, `int8`, `uint8` |
46
  | `I` | `int64` |
47
 
 
 
 
 
 
 
48
  ## Device requirements
49
 
50
  Some implementation variants require `subgroups`. These are route-specific capabilities, not package-wide requirements; availability also depends on the request shape and dtype.
@@ -54,7 +60,7 @@ Some implementation variants require `subgroups`. These are route-specific capab
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
58
  - [`reduce-arg-axis-split-combine.wgsl.jinja`](build/webgpu/reduce-arg-axis-split-combine.wgsl.jinja)
59
  - [`reduce-arg-axis-split-reduce.wgsl.jinja`](build/webgpu/reduce-arg-axis-split-reduce.wgsl.jinja)
60
  - [`reduce-arg-axis-split-tiled.wgsl.jinja`](build/webgpu/reduce-arg-axis-split-tiled.wgsl.jinja)
@@ -66,7 +72,7 @@ Some implementation variants require `subgroups`. These are route-specific capab
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.
 
45
  | `T` | `float32`, `float16`, `int32`, `uint32`, `int16`, `int8`, `uint8` |
46
  | `I` | `int64` |
47
 
48
+ ## Implementation variants
49
+
50
+ One implementation is selected per call from the device capabilities, the request shapes and the dtypes; these notes say what each one covers.
51
+
52
+ - `axis_split_packed` — Scans four adjacent float32 inner-axis columns per invocation while preserving each column’s axis order and the scalar partial/combine layout. Small output grids retain the scalar route.
53
+
54
  ## Device requirements
55
 
56
  Some implementation variants require `subgroups`. These are route-specific capabilities, not package-wide requirements; availability also depends on the request shape and dtype.
 
60
  - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, per-variant templates, provenance)
61
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
62
  - [`test.json`](build/webgpu/test.json) — correctness cases
63
+ - [`bench.json`](build/webgpu/bench.json) — benchmark cases
64
  - [`reduce-arg-axis-split-combine.wgsl.jinja`](build/webgpu/reduce-arg-axis-split-combine.wgsl.jinja)
65
  - [`reduce-arg-axis-split-reduce.wgsl.jinja`](build/webgpu/reduce-arg-axis-split-reduce.wgsl.jinja)
66
  - [`reduce-arg-axis-split-tiled.wgsl.jinja`](build/webgpu/reduce-arg-axis-split-tiled.wgsl.jinja)
 
72
  ## Use with `@huggingface/kernels`
73
 
74
  ```sh
75
+ npm install --save-exact @huggingface/kernels@0.0.1-preview.3
76
  ```
77
 
78
  Required output shapes and logical data types are inferred from the supplied inputs and attributes; result tensors are allocated automatically.
build/webgpu/bench.json CHANGED
@@ -137,7 +137,7 @@
137
  "name": "argmax-lastaxis-f32-64x50260-vocab-control",
138
  "preset": "stress",
139
  "provenance": {
140
- "notes": "Nearest multiple-of-4 vocabulary width for argmax-lastaxis-f32-64x50257-vocab-odd-pathology; same bytes within 0.01%, selects last_axis_vec4."
141
  },
142
  "vars": { "dtype": "float32", "rows": 64, "cols": 50260 },
143
  "attrs": { "axis": 1 },
 
137
  "name": "argmax-lastaxis-f32-64x50260-vocab-control",
138
  "preset": "stress",
139
  "provenance": {
140
+ "notes": "The next multiple-of-four vocabulary width above the paired 50,257-column case, with nearly the same input bytes."
141
  },
142
  "vars": { "dtype": "float32", "rows": 64, "cols": 50260 },
143
  "attrs": { "axis": 1 },
build/webgpu/manifest.json CHANGED
@@ -60,71 +60,9 @@
60
  "rowSplitScratchBytes": "rowSplitCount * outputCount * 8",
61
  "rowSplitScratchFits": "rowSplitScratchBytes <= device.limits.maxStorageBufferBindingSize and rowSplitScratchBytes <= device.limits.maxBufferSize",
62
  "rowSplitDispatchFits": "outputCount <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535) and rowSplitCount <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535)",
63
- "tileDispatchFits": "ceilDiv(outputCount, tunables.TILE_COLUMNS) <= foldedDispatchCapacity"
64
- },
65
- "bindings": {
66
- "x": { "buffer": "read-only-storage", "elementType": "$vectorScalar" },
67
- "partials_val": { "buffer": "storage", "elementType": "u32" },
68
- "partials_idx": { "buffer": "storage", "elementType": "u32" },
69
- "params": {
70
- "buffer": "uniform",
71
- "struct": [
72
- { "name": "rows", "type": "u32", "value": "outputCount" },
73
- { "name": "chunkCount", "type": "u32", "value": "axisDim / 4" }
74
- ]
75
- },
76
- "partials_val_2": { "name": "partials_val", "buffer": "read-only-storage", "elementType": "u32" },
77
- "partials_idx_2": { "name": "partials_idx", "buffer": "read-only-storage", "elementType": "u32" },
78
- "y": { "buffer": "storage", "elementType": "u32" },
79
- "params_2": {
80
- "name": "params",
81
- "buffer": "uniform",
82
- "struct": [{ "name": "outputCount", "type": "u32", "value": "outputCount" }]
83
- },
84
- "x_2": { "name": "x", "buffer": "read-only-storage", "elementType": "$scalar" },
85
- "params_3": {
86
- "name": "params",
87
- "buffer": "uniform",
88
- "struct": [
89
- { "name": "rows", "type": "u32", "value": "outputCount" },
90
- { "name": "chunkCount", "type": "u32", "value": "axisDim" }
91
- ]
92
- },
93
- "params_4": {
94
- "name": "params",
95
- "buffer": "uniform",
96
- "struct": [
97
- { "name": "axisDim", "type": "u32", "value": "axisDim" },
98
- { "name": "outputCount", "type": "u32", "value": "outputCount" },
99
- { "name": "innerSize", "type": "u32", "value": "axisInner" }
100
- ]
101
- },
102
- "params_5": {
103
- "name": "params",
104
- "buffer": "uniform",
105
- "struct": [
106
- { "name": "rows", "type": "u32", "value": "outputCount" },
107
- { "name": "chunkCount", "type": "u32", "value": "axisDim / 4" },
108
- {
109
- "name": "rowStride",
110
- "type": "u32",
111
- "value": "max(1, min(outputCount, min(device.limits.maxComputeWorkgroupsPerDimension, 65535)))"
112
- }
113
- ]
114
- },
115
- "params_6": {
116
- "name": "params",
117
- "buffer": "uniform",
118
- "struct": [
119
- { "name": "rows", "type": "u32", "value": "outputCount" },
120
- { "name": "chunkCount", "type": "u32", "value": "axisDim" },
121
- {
122
- "name": "rowStride",
123
- "type": "u32",
124
- "value": "max(1, min(outputCount, min(device.limits.maxComputeWorkgroupsPerDimension, 65535)))"
125
- }
126
- ]
127
- }
128
  },
129
  "variants": [
130
  {
@@ -134,8 +72,6 @@
134
  "derive": {
135
  "scalar": "dtypes.T",
136
  "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
137
- "usesF16": "dtypes.T == \"f16\"",
138
- "selectLastIndex": "attrs.select_last_index != 0",
139
  "workgroupSize": "workgroupSize",
140
  "split": "rowSplitCount"
141
  },
@@ -148,8 +84,7 @@
148
  "id": "split_reduce",
149
  "name": "ArgMax.LastAxisSplitVec4Reduce",
150
  "shader": "reduce-arg-row-split.wgsl.jinja",
151
- "subgroupCollectivesWidth": "portable",
152
- "derive": { "modeSpec": "\"max\"", "vec4": true, "useSubgroups": "device.features.has(\"subgroups\")" },
153
  "bindings": ["x", "partials_val", "partials_idx", "params"],
154
  "dispatch": { "x": "outputCount", "y": "rowSplitCount" }
155
  },
@@ -157,8 +92,7 @@
157
  "id": "combine",
158
  "name": "ArgMax.LastAxisSplitCombine",
159
  "shader": "reduce-arg-axis-split-combine.wgsl.jinja",
160
- "derive": { "modeSpec": "\"max\"" },
161
- "bindings": ["partials_val_2", "partials_idx_2", "y", "params_2"],
162
  "dispatch": {
163
  "x": "min(ceilDiv((outputCount), (workgroupSize)), 65535)",
164
  "y": "ceilDiv(ceilDiv((outputCount), (workgroupSize)), 65535)",
@@ -171,13 +105,7 @@
171
  "id": "last_axis_split",
172
  "priority": 41,
173
  "when": ["baseContract", "rowParallelType", "lastAxis", "axisDim >= tunables.ROW_SPLIT_MIN_AXIS", "axisDim % 4 != 0", "outputCount <= tunables.ROW_SPLIT_MAX_OUTPUTS", "rowSplitScratchFits", "rowSplitDispatchFits", "rowStorageFits"],
174
- "derive": {
175
- "scalar": "dtypes.T",
176
- "usesF16": "dtypes.T == \"f16\"",
177
- "selectLastIndex": "attrs.select_last_index != 0",
178
- "workgroupSize": "workgroupSize",
179
- "split": "rowSplitCount"
180
- },
181
  "intermediates": [
182
  { "id": "partials_val", "dtype": "uint32", "shape": "[rowSplitCount * outputCount]" },
183
  { "id": "partials_idx", "dtype": "uint32", "shape": "[rowSplitCount * outputCount]" }
@@ -187,17 +115,15 @@
187
  "id": "split_reduce",
188
  "name": "ArgMax.LastAxisSplitReduce",
189
  "shader": "reduce-arg-row-split.wgsl.jinja",
190
- "subgroupCollectivesWidth": "portable",
191
- "derive": { "modeSpec": "\"max\"", "vec4": false, "useSubgroups": "device.features.has(\"subgroups\")" },
192
- "bindings": ["x_2", "partials_val", "partials_idx", "params_3"],
193
  "dispatch": { "x": "outputCount", "y": "rowSplitCount" }
194
  },
195
  {
196
  "id": "combine",
197
  "name": "ArgMax.LastAxisSplitCombine",
198
  "shader": "reduce-arg-axis-split-combine.wgsl.jinja",
199
- "derive": { "modeSpec": "\"max\"" },
200
- "bindings": ["partials_val_2", "partials_idx_2", "y", "params_2"],
201
  "dispatch": {
202
  "x": "min(ceilDiv((outputCount), (workgroupSize)), 65535)",
203
  "y": "ceilDiv(ceilDiv((outputCount), (workgroupSize)), 65535)",
@@ -212,7 +138,6 @@
212
  "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"],
213
  "derive": {
214
  "scalar": "dtypes.T",
215
- "selectLastIndex": "attrs.select_last_index != 0",
216
  "workgroupSize": "workgroupSize",
217
  "split": "splitCount",
218
  "tileCols": "tunables.SPLIT_TILE_COLUMNS"
@@ -226,16 +151,14 @@
226
  "id": "split_reduce",
227
  "name": "ArgMax.AxisSplitTiledReduce",
228
  "shader": "reduce-arg-axis-split-tiled.wgsl.jinja",
229
- "derive": { "modeSpec": "\"max\"" },
230
- "bindings": ["x_2", "partials_val", "partials_idx", "params_4"],
231
  "dispatch": { "x": "ceilDiv(outputCount, tunables.SPLIT_TILE_COLUMNS)", "y": "splitCount" }
232
  },
233
  {
234
  "id": "combine",
235
  "name": "ArgMax.AxisSplitCombine",
236
  "shader": "reduce-arg-axis-split-combine.wgsl.jinja",
237
- "derive": { "modeSpec": "\"max\"" },
238
- "bindings": ["partials_val_2", "partials_idx_2", "y", "params_2"],
239
  "dispatch": {
240
  "x": "min(ceilDiv((outputCount), (workgroupSize)), 65535)",
241
  "y": "ceilDiv(ceilDiv((outputCount), (workgroupSize)), 65535)",
@@ -252,8 +175,6 @@
252
  "derive": {
253
  "scalar": "dtypes.T",
254
  "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
255
- "usesF16": "dtypes.T == \"f16\"",
256
- "selectLastIndex": "attrs.select_last_index != 0",
257
  "workgroupSize": "device.adapterInfo.subgroupMinSize"
258
  },
259
  "passes": [
@@ -261,9 +182,8 @@
261
  "id": "main",
262
  "name": "ArgMax.SubgroupMinLastAxisVec4",
263
  "shader": "reduce-arg-row-subgroup.wgsl.jinja",
264
- "subgroupCollectivesWidth": "portable",
265
- "derive": { "modeSpec": "\"max\"", "vec4": true, "singleSubgroup": true },
266
- "bindings": ["x", "y", "params_5"],
267
  "dispatch": { "x": "min(outputCount, 65535)", "y": "ceilDiv(outputCount, 65535)", "z": 1 }
268
  }
269
  ]
@@ -273,20 +193,14 @@
273
  "priority": 41,
274
  "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"],
275
  "requires": { "features": ["subgroups"] },
276
- "derive": {
277
- "scalar": "dtypes.T",
278
- "usesF16": "dtypes.T == \"f16\"",
279
- "selectLastIndex": "attrs.select_last_index != 0",
280
- "workgroupSize": "device.adapterInfo.subgroupMinSize"
281
- },
282
  "passes": [
283
  {
284
  "id": "main",
285
  "name": "ArgMax.SubgroupMinLastAxis",
286
  "shader": "reduce-arg-row-subgroup.wgsl.jinja",
287
- "subgroupCollectivesWidth": "portable",
288
- "derive": { "modeSpec": "\"max\"", "vec4": false, "singleSubgroup": true },
289
- "bindings": ["x_2", "y", "params_6"],
290
  "dispatch": { "x": "min(outputCount, 65535)", "y": "ceilDiv(outputCount, 65535)", "z": 1 }
291
  }
292
  ]
@@ -298,8 +212,6 @@
298
  "derive": {
299
  "scalar": "dtypes.T",
300
  "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
301
- "usesF16": "dtypes.T == \"f16\"",
302
- "selectLastIndex": "attrs.select_last_index != 0",
303
  "workgroupSize": "min(workgroupSize, max(32, pow2ceil(ceilDiv(axisDim, 4))))"
304
  },
305
  "passes": [
@@ -307,9 +219,8 @@
307
  "id": "main",
308
  "name": "ArgMax.LastAxisVec4",
309
  "shader": "reduce-arg-row-subgroup.wgsl.jinja",
310
- "subgroupCollectivesWidth": "portable",
311
- "derive": { "modeSpec": "\"max\"", "vec4": true, "useSubgroups": "device.features.has(\"subgroups\")" },
312
- "bindings": ["x", "y", "params_5"],
313
  "dispatch": { "x": "min(outputCount, 65535)", "y": "ceilDiv(outputCount, 65535)", "z": 1 }
314
  }
315
  ]
@@ -318,46 +229,79 @@
318
  "id": "last_axis",
319
  "priority": 39,
320
  "when": ["baseContract", "rowParallelType", "lastAxis", "axisDim % 4 != 0", "parallelRowWorthwhile", "rowDispatchFits", "rowStorageFits"],
321
- "derive": {
322
- "scalar": "dtypes.T",
323
- "usesF16": "dtypes.T == \"f16\"",
324
- "selectLastIndex": "attrs.select_last_index != 0",
325
- "workgroupSize": "min(workgroupSize, max(32, pow2ceil(axisDim)))"
326
- },
327
  "passes": [
328
  {
329
  "id": "main",
330
  "name": "ArgMax.LastAxis",
331
  "shader": "reduce-arg-row-subgroup.wgsl.jinja",
332
- "subgroupCollectivesWidth": "portable",
333
- "derive": { "modeSpec": "\"max\"", "vec4": false, "useSubgroups": "device.features.has(\"subgroups\")" },
334
- "bindings": ["x_2", "y", "params_6"],
 
 
 
335
  "dispatch": { "x": "min(outputCount, 65535)", "y": "ceilDiv(outputCount, 65535)", "z": 1 }
336
  }
337
  ]
338
  },
339
  {
340
- "id": "axis_split",
341
- "priority": 30,
342
- "when": ["baseContract", "nativeParallelType", "not lastAxis", "axisDim >= tunables.SPLIT_MIN_AXIS", "outputCount >= 1", "outputCount <= tunables.SPLIT_MAX_OUTPUTS", "splitScratchFits", "splitDispatchFits", "threadDispatchFits"],
343
- "demoteWhen": ["narrowLargeAxisTilePreferred"],
344
  "derive": {
345
  "scalar": "dtypes.T",
346
- "selectLastIndex": "attrs.select_last_index != 0",
347
  "workgroupSize": "workgroupSize",
348
- "split": "splitCount"
 
349
  },
350
  "intermediates": [
351
  { "id": "partials_val", "dtype": "uint32", "shape": "[splitCount * outputCount]" },
352
  { "id": "partials_idx", "dtype": "uint32", "shape": "[splitCount * outputCount]" }
353
  ],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354
  "passes": [
355
  {
356
  "id": "split_reduce",
357
  "name": "ArgMax.AxisSplitReduce",
358
  "shader": "reduce-arg-axis-split-reduce.wgsl.jinja",
359
- "derive": { "modeSpec": "\"max\"" },
360
- "bindings": ["x_2", "partials_val", "partials_idx", "params_4"],
361
  "dispatch": {
362
  "x": "min(ceilDiv((outputCount), (workgroupSize)), DISPATCH_FOLD_WIDTH)",
363
  "y": "splitCount",
@@ -368,8 +312,7 @@
368
  "id": "combine",
369
  "name": "ArgMax.AxisSplitCombine",
370
  "shader": "reduce-arg-axis-split-combine.wgsl.jinja",
371
- "derive": { "modeSpec": "\"max\"" },
372
- "bindings": ["partials_val_2", "partials_idx_2", "y", "params_2"],
373
  "dispatch": {
374
  "x": "min(ceilDiv((outputCount), (workgroupSize)), 65535)",
375
  "y": "ceilDiv(ceilDiv((outputCount), (workgroupSize)), 65535)",
@@ -382,19 +325,13 @@
382
  "id": "axis_tiled",
383
  "priority": 20,
384
  "when": ["baseContract", "nativeParallelType", "not lastAxis", "axisDim >= tunables.TILED_MIN_AXIS", "outputCount >= tunables.TILED_MIN_OUTPUTS", "workgroupSize % tunables.TILE_COLUMNS == 0", "rowStorageFits", "tileDispatchFits"],
385
- "derive": {
386
- "scalar": "dtypes.T",
387
- "selectLastIndex": "attrs.select_last_index != 0",
388
- "workgroupSize": "workgroupSize",
389
- "tileCols": "tunables.TILE_COLUMNS"
390
- },
391
  "passes": [
392
  {
393
  "id": "main",
394
  "name": "ArgMax.AxisTiled",
395
  "shader": "reduce-arg-axis-tiled.wgsl.jinja",
396
- "derive": { "modeSpec": "\"max\"" },
397
- "bindings": ["x_2", "y", "params_4"],
398
  "dispatch": {
399
  "x": "min(ceilDiv((outputCount), (tunables.TILE_COLUMNS)), 65535)",
400
  "y": "ceilDiv(ceilDiv((outputCount), (tunables.TILE_COLUMNS)), 65535)",
@@ -408,19 +345,13 @@
408
  "priority": 0,
409
  "when": ["baseContract", "threadDispatchFits"],
410
  "supersededBy": ["axis_tiled"],
411
- "derive": {
412
- "scalar": "dtypes.T",
413
- "usesF16": "dtypes.T == \"f16\"",
414
- "selectLastIndex": "attrs.select_last_index != 0",
415
- "workgroupSize": "workgroupSize"
416
- },
417
  "passes": [
418
  {
419
  "id": "main",
420
  "name": "ArgMax.AxisSerial",
421
  "shader": "reduce-arg-axis.wgsl.jinja",
422
- "derive": { "modeSpec": "\"max\"" },
423
- "bindings": ["x_2", "y", "params_4"],
424
  "dispatch": {
425
  "x": "min(ceilDiv((outputCount), (workgroupSize)), 65535)",
426
  "y": "ceilDiv(ceilDiv((outputCount), (workgroupSize)), 65535)",
@@ -429,5 +360,60 @@
429
  }
430
  ]
431
  }
432
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
  }
 
60
  "rowSplitScratchBytes": "rowSplitCount * outputCount * 8",
61
  "rowSplitScratchFits": "rowSplitScratchBytes <= device.limits.maxStorageBufferBindingSize and rowSplitScratchBytes <= device.limits.maxBufferSize",
62
  "rowSplitDispatchFits": "outputCount <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535) and rowSplitCount <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535)",
63
+ "tileDispatchFits": "ceilDiv(outputCount, tunables.TILE_COLUMNS) <= foldedDispatchCapacity",
64
+ "selectLastIndex": "attrs.select_last_index != 0",
65
+ "modeSpec": "\"max\""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  },
67
  "variants": [
68
  {
 
72
  "derive": {
73
  "scalar": "dtypes.T",
74
  "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
 
 
75
  "workgroupSize": "workgroupSize",
76
  "split": "rowSplitCount"
77
  },
 
84
  "id": "split_reduce",
85
  "name": "ArgMax.LastAxisSplitVec4Reduce",
86
  "shader": "reduce-arg-row-split.wgsl.jinja",
87
+ "derive": { "vec4": true, "useSubgroups": "device.features.has(\"subgroups\")" },
 
88
  "bindings": ["x", "partials_val", "partials_idx", "params"],
89
  "dispatch": { "x": "outputCount", "y": "rowSplitCount" }
90
  },
 
92
  "id": "combine",
93
  "name": "ArgMax.LastAxisSplitCombine",
94
  "shader": "reduce-arg-axis-split-combine.wgsl.jinja",
95
+ "bindings": ["partials_val_u32", "partials_idx_u32", "y", "params_combine"],
 
96
  "dispatch": {
97
  "x": "min(ceilDiv((outputCount), (workgroupSize)), 65535)",
98
  "y": "ceilDiv(ceilDiv((outputCount), (workgroupSize)), 65535)",
 
105
  "id": "last_axis_split",
106
  "priority": 41,
107
  "when": ["baseContract", "rowParallelType", "lastAxis", "axisDim >= tunables.ROW_SPLIT_MIN_AXIS", "axisDim % 4 != 0", "outputCount <= tunables.ROW_SPLIT_MAX_OUTPUTS", "rowSplitScratchFits", "rowSplitDispatchFits", "rowStorageFits"],
108
+ "derive": { "scalar": "dtypes.T", "workgroupSize": "workgroupSize", "split": "rowSplitCount" },
 
 
 
 
 
 
109
  "intermediates": [
110
  { "id": "partials_val", "dtype": "uint32", "shape": "[rowSplitCount * outputCount]" },
111
  { "id": "partials_idx", "dtype": "uint32", "shape": "[rowSplitCount * outputCount]" }
 
115
  "id": "split_reduce",
116
  "name": "ArgMax.LastAxisSplitReduce",
117
  "shader": "reduce-arg-row-split.wgsl.jinja",
118
+ "derive": { "vec4": false, "useSubgroups": "device.features.has(\"subgroups\")" },
119
+ "bindings": ["x_main", "partials_val", "partials_idx", "params__uniform"],
 
120
  "dispatch": { "x": "outputCount", "y": "rowSplitCount" }
121
  },
122
  {
123
  "id": "combine",
124
  "name": "ArgMax.LastAxisSplitCombine",
125
  "shader": "reduce-arg-axis-split-combine.wgsl.jinja",
126
+ "bindings": ["partials_val_u32", "partials_idx_u32", "y", "params_combine"],
 
127
  "dispatch": {
128
  "x": "min(ceilDiv((outputCount), (workgroupSize)), 65535)",
129
  "y": "ceilDiv(ceilDiv((outputCount), (workgroupSize)), 65535)",
 
138
  "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"],
139
  "derive": {
140
  "scalar": "dtypes.T",
 
141
  "workgroupSize": "workgroupSize",
142
  "split": "splitCount",
143
  "tileCols": "tunables.SPLIT_TILE_COLUMNS"
 
151
  "id": "split_reduce",
152
  "name": "ArgMax.AxisSplitTiledReduce",
153
  "shader": "reduce-arg-axis-split-tiled.wgsl.jinja",
154
+ "bindings": ["x_main", "partials_val", "partials_idx", "params_main"],
 
155
  "dispatch": { "x": "ceilDiv(outputCount, tunables.SPLIT_TILE_COLUMNS)", "y": "splitCount" }
156
  },
157
  {
158
  "id": "combine",
159
  "name": "ArgMax.AxisSplitCombine",
160
  "shader": "reduce-arg-axis-split-combine.wgsl.jinja",
161
+ "bindings": ["partials_val_u32", "partials_idx_u32", "y", "params_combine"],
 
162
  "dispatch": {
163
  "x": "min(ceilDiv((outputCount), (workgroupSize)), 65535)",
164
  "y": "ceilDiv(ceilDiv((outputCount), (workgroupSize)), 65535)",
 
175
  "derive": {
176
  "scalar": "dtypes.T",
177
  "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
 
 
178
  "workgroupSize": "device.adapterInfo.subgroupMinSize"
179
  },
180
  "passes": [
 
182
  "id": "main",
183
  "name": "ArgMax.SubgroupMinLastAxisVec4",
184
  "shader": "reduce-arg-row-subgroup.wgsl.jinja",
185
+ "derive": { "vec4": true, "singleSubgroup": true },
186
+ "bindings": ["x", "y", "params_last_axis_vec4"],
 
187
  "dispatch": { "x": "min(outputCount, 65535)", "y": "ceilDiv(outputCount, 65535)", "z": 1 }
188
  }
189
  ]
 
193
  "priority": 41,
194
  "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"],
195
  "requires": { "features": ["subgroups"] },
196
+ "derive": { "scalar": "dtypes.T", "workgroupSize": "device.adapterInfo.subgroupMinSize" },
 
 
 
 
 
197
  "passes": [
198
  {
199
  "id": "main",
200
  "name": "ArgMax.SubgroupMinLastAxis",
201
  "shader": "reduce-arg-row-subgroup.wgsl.jinja",
202
+ "derive": { "vec4": false, "singleSubgroup": true },
203
+ "bindings": ["x_main", "y", "params_last_axis"],
 
204
  "dispatch": { "x": "min(outputCount, 65535)", "y": "ceilDiv(outputCount, 65535)", "z": 1 }
205
  }
206
  ]
 
212
  "derive": {
213
  "scalar": "dtypes.T",
214
  "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
 
 
215
  "workgroupSize": "min(workgroupSize, max(32, pow2ceil(ceilDiv(axisDim, 4))))"
216
  },
217
  "passes": [
 
219
  "id": "main",
220
  "name": "ArgMax.LastAxisVec4",
221
  "shader": "reduce-arg-row-subgroup.wgsl.jinja",
222
+ "derive": { "vec4": true, "useSubgroups": "device.features.has(\"subgroups\")" },
223
+ "bindings": ["x", "y", "params_last_axis_vec4"],
 
224
  "dispatch": { "x": "min(outputCount, 65535)", "y": "ceilDiv(outputCount, 65535)", "z": 1 }
225
  }
226
  ]
 
229
  "id": "last_axis",
230
  "priority": 39,
231
  "when": ["baseContract", "rowParallelType", "lastAxis", "axisDim % 4 != 0", "parallelRowWorthwhile", "rowDispatchFits", "rowStorageFits"],
232
+ "derive": { "scalar": "dtypes.T", "workgroupSize": "min(workgroupSize, max(32, pow2ceil(axisDim)))" },
 
 
 
 
 
233
  "passes": [
234
  {
235
  "id": "main",
236
  "name": "ArgMax.LastAxis",
237
  "shader": "reduce-arg-row-subgroup.wgsl.jinja",
238
+ "derive": {
239
+ "vec4": false,
240
+ "useSubgroups": "device.features.has(\"subgroups\")",
241
+ "adjacentScalarLoads": "false"
242
+ },
243
+ "bindings": ["x_main", "y", "params_last_axis"],
244
  "dispatch": { "x": "min(outputCount, 65535)", "y": "ceilDiv(outputCount, 65535)", "z": 1 }
245
  }
246
  ]
247
  },
248
  {
249
+ "id": "axis_split_packed",
250
+ "priority": 32,
251
+ "when": ["baseContract", "nativeParallelType", "not lastAxis", "axisDim >= tunables.SPLIT_MIN_AXIS", "outputCount >= 1", "outputCount <= tunables.SPLIT_MAX_OUTPUTS", "splitScratchFits", "splitDispatchFits", "threadDispatchFits", "dtypes.T == \"f32\"", "axisInner % 4 == 0"],
252
+ "demoteWhen": ["narrowLargeAxisTilePreferred", "outputCount < workgroupSize * 2"],
253
  "derive": {
254
  "scalar": "dtypes.T",
 
255
  "workgroupSize": "workgroupSize",
256
+ "split": "splitCount",
257
+ "vectorScalar": "\"vec4<f32>\""
258
  },
259
  "intermediates": [
260
  { "id": "partials_val", "dtype": "uint32", "shape": "[splitCount * outputCount]" },
261
  { "id": "partials_idx", "dtype": "uint32", "shape": "[splitCount * outputCount]" }
262
  ],
263
+ "passes": [
264
+ {
265
+ "id": "split_reduce",
266
+ "name": "ArgMax.AxisSplitPackedReduce",
267
+ "shader": "reduce-arg-axis-split-reduce.wgsl.jinja",
268
+ "derive": { "columnVectorWidth": 4 },
269
+ "bindings": ["x", "partials_val", "partials_idx", "params_main"],
270
+ "dispatch": {
271
+ "x": "min(ceilDiv(outputCount, workgroupSize * 4), DISPATCH_FOLD_WIDTH)",
272
+ "y": "splitCount",
273
+ "z": "ceilDiv(ceilDiv(outputCount, workgroupSize * 4), DISPATCH_FOLD_WIDTH)"
274
+ }
275
+ },
276
+ {
277
+ "id": "combine",
278
+ "name": "ArgMax.AxisSplitCombine",
279
+ "shader": "reduce-arg-axis-split-combine.wgsl.jinja",
280
+ "bindings": ["partials_val_u32", "partials_idx_u32", "y", "params_combine"],
281
+ "dispatch": {
282
+ "x": "min(ceilDiv((outputCount), (workgroupSize)), 65535)",
283
+ "y": "ceilDiv(ceilDiv((outputCount), (workgroupSize)), 65535)",
284
+ "z": 1
285
+ }
286
+ }
287
+ ]
288
+ },
289
+ {
290
+ "id": "axis_split",
291
+ "priority": 30,
292
+ "when": ["baseContract", "nativeParallelType", "not lastAxis", "axisDim >= tunables.SPLIT_MIN_AXIS", "outputCount >= 1", "outputCount <= tunables.SPLIT_MAX_OUTPUTS", "splitScratchFits", "splitDispatchFits", "threadDispatchFits"],
293
+ "demoteWhen": ["narrowLargeAxisTilePreferred"],
294
+ "derive": { "scalar": "dtypes.T", "workgroupSize": "workgroupSize", "split": "splitCount" },
295
+ "intermediates": [
296
+ { "id": "partials_val", "dtype": "uint32", "shape": "[splitCount * outputCount]" },
297
+ { "id": "partials_idx", "dtype": "uint32", "shape": "[splitCount * outputCount]" }
298
+ ],
299
  "passes": [
300
  {
301
  "id": "split_reduce",
302
  "name": "ArgMax.AxisSplitReduce",
303
  "shader": "reduce-arg-axis-split-reduce.wgsl.jinja",
304
+ "bindings": ["x_main", "partials_val", "partials_idx", "params_main"],
 
305
  "dispatch": {
306
  "x": "min(ceilDiv((outputCount), (workgroupSize)), DISPATCH_FOLD_WIDTH)",
307
  "y": "splitCount",
 
312
  "id": "combine",
313
  "name": "ArgMax.AxisSplitCombine",
314
  "shader": "reduce-arg-axis-split-combine.wgsl.jinja",
315
+ "bindings": ["partials_val_u32", "partials_idx_u32", "y", "params_combine"],
 
316
  "dispatch": {
317
  "x": "min(ceilDiv((outputCount), (workgroupSize)), 65535)",
318
  "y": "ceilDiv(ceilDiv((outputCount), (workgroupSize)), 65535)",
 
325
  "id": "axis_tiled",
326
  "priority": 20,
327
  "when": ["baseContract", "nativeParallelType", "not lastAxis", "axisDim >= tunables.TILED_MIN_AXIS", "outputCount >= tunables.TILED_MIN_OUTPUTS", "workgroupSize % tunables.TILE_COLUMNS == 0", "rowStorageFits", "tileDispatchFits"],
328
+ "derive": { "scalar": "dtypes.T", "workgroupSize": "workgroupSize", "tileCols": "tunables.TILE_COLUMNS" },
 
 
 
 
 
329
  "passes": [
330
  {
331
  "id": "main",
332
  "name": "ArgMax.AxisTiled",
333
  "shader": "reduce-arg-axis-tiled.wgsl.jinja",
334
+ "bindings": ["x_main", "y", "params_main"],
 
335
  "dispatch": {
336
  "x": "min(ceilDiv((outputCount), (tunables.TILE_COLUMNS)), 65535)",
337
  "y": "ceilDiv(ceilDiv((outputCount), (tunables.TILE_COLUMNS)), 65535)",
 
345
  "priority": 0,
346
  "when": ["baseContract", "threadDispatchFits"],
347
  "supersededBy": ["axis_tiled"],
348
+ "derive": { "scalar": "dtypes.T", "workgroupSize": "workgroupSize" },
 
 
 
 
 
349
  "passes": [
350
  {
351
  "id": "main",
352
  "name": "ArgMax.AxisSerial",
353
  "shader": "reduce-arg-axis.wgsl.jinja",
354
+ "bindings": ["x_main", "y", "params_main"],
 
355
  "dispatch": {
356
  "x": "min(ceilDiv((outputCount), (workgroupSize)), 65535)",
357
  "y": "ceilDiv(ceilDiv((outputCount), (workgroupSize)), 65535)",
 
360
  }
361
  ]
362
  }
363
+ ],
364
+ "bindings": {
365
+ "x": { "elementType": "$vectorScalar" },
366
+ "partials_val": { "buffer": "storage", "elementType": "u32" },
367
+ "partials_idx": { "buffer": "storage", "elementType": "u32" },
368
+ "params": {
369
+ "struct": [
370
+ { "name": "rows", "type": "u32", "value": "outputCount" },
371
+ { "name": "chunkCount", "type": "u32", "value": "axisDim / 4" }
372
+ ]
373
+ },
374
+ "partials_val_u32": { "name": "partials_val", "buffer": "read-only-storage", "elementType": "u32" },
375
+ "partials_idx_u32": { "name": "partials_idx", "buffer": "read-only-storage", "elementType": "u32" },
376
+ "y": { "elementType": "u32" },
377
+ "params_combine": { "name": "params", "struct": [{ "name": "outputCount", "type": "u32", "value": "outputCount" }] },
378
+ "x_main": { "name": "x", "elementType": "$scalar" },
379
+ "params__uniform": {
380
+ "name": "params",
381
+ "struct": [
382
+ { "name": "rows", "type": "u32", "value": "outputCount" },
383
+ { "name": "chunkCount", "type": "u32", "value": "axisDim" }
384
+ ]
385
+ },
386
+ "params_main": {
387
+ "name": "params",
388
+ "struct": [
389
+ { "name": "axisDim", "type": "u32", "value": "axisDim" },
390
+ { "name": "outputCount", "type": "u32", "value": "outputCount" },
391
+ { "name": "innerSize", "type": "u32", "value": "axisInner" }
392
+ ]
393
+ },
394
+ "params_last_axis_vec4": {
395
+ "name": "params",
396
+ "struct": [
397
+ { "name": "rows", "type": "u32", "value": "outputCount" },
398
+ { "name": "chunkCount", "type": "u32", "value": "axisDim / 4" },
399
+ {
400
+ "name": "rowStride",
401
+ "type": "u32",
402
+ "value": "max(1, min(outputCount, min(device.limits.maxComputeWorkgroupsPerDimension, 65535)))"
403
+ }
404
+ ]
405
+ },
406
+ "params_last_axis": {
407
+ "name": "params",
408
+ "struct": [
409
+ { "name": "rows", "type": "u32", "value": "outputCount" },
410
+ { "name": "chunkCount", "type": "u32", "value": "axisDim" },
411
+ {
412
+ "name": "rowStride",
413
+ "type": "u32",
414
+ "value": "max(1, min(outputCount, min(device.limits.maxComputeWorkgroupsPerDimension, 65535)))"
415
+ }
416
+ ]
417
+ }
418
+ }
419
  }
build/webgpu/metadata.json CHANGED
@@ -1,27 +1,27 @@
1
  {
2
  "name": "ai.onnx.ArgMax",
3
- "id": "_ai_onnx_argmax_webgpu_7a0b0be",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
- "bench.json": "Pmvyno397Q/iYsA6Ig9/ltAyWvlxIqkGycwLnpwWjUg=",
11
- "manifest.json": "b/CUpSEy7gFwzPXKc2bK0XEQQDCO0MiKBs/Ynk6Jals=",
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": "U8wriN+k8hXANjPPj7ZRRD++kmWr1wh3oCfOcac3qVg=",
18
- "reduce-arg-row-subgroup.wgsl.jinja": "QVlX6iupq59hl4a3lLGuLsm62X4VGmwDVvNU+J9/Pwo=",
19
- "test.json": "bOm2DRCrjR5ovA0Q7f5/UeTwJ6e1dqlOwSwWilMPCJo="
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"],
@@ -30,6 +30,7 @@
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"]
 
1
  {
2
  "name": "ai.onnx.ArgMax",
3
+ "id": "_ai_onnx_argmax_webgpu_1e3a265",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
+ "bench.json": "iuzwJJYAdNsUw+DglCvHX2WAtQEjRzfBOZt6MvlMa1U=",
11
+ "manifest.json": "TiCizYs12kW8ZsFx/7wB+8OysF5fLvrk+gdZ0EoFqNk=",
12
+ "reduce-arg-axis-split-combine.wgsl.jinja": "Zgdr9BmWms05/uDEY+YMSJE5jj6soIVfmxLsIfzt6M4=",
13
+ "reduce-arg-axis-split-reduce.wgsl.jinja": "iYeNFJ7YX9FMCNdlFlhUIW21KpKmU4ouODT2u9MjjjE=",
14
+ "reduce-arg-axis-split-tiled.wgsl.jinja": "MvEKY6B3FsRB7SfFPVWK7rWWCUsdhTSYOUmbQxtzP2w=",
15
+ "reduce-arg-axis-tiled.wgsl.jinja": "sWYpTFWzc2ATUo2VGrKw+zLAyWFVD6O9og+WpY9f28g=",
16
+ "reduce-arg-axis.wgsl.jinja": "9wjP1QeFI3ydGt9T537YVC3O8tPYSK4qSvw+urw1ruY=",
17
+ "reduce-arg-row-split.wgsl.jinja": "yfXWnH5jaZHa5PW28zyTSF0O7A1yGKOIJUCZdZliFS0=",
18
+ "reduce-arg-row-subgroup.wgsl.jinja": "A1sX+NMUilPMxLDzHhdKFlwdHIDgFTe9a6igb7pOX7k=",
19
+ "test.json": "6RGFz3E0tdm0K3gRicwYZ3hU54MqthGV/tnBeAxFxHs="
20
  }
21
  },
22
+ "provenance": { "kernel": { "sha": "6fdf6301e2bbcc2f03bf1eaf493b7ad55ef33afc", "dirty": false } },
23
  "webgpu": {
24
+ "manifestSpec": "2.1",
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"],
 
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_packed": ["reduce-arg-axis-split-combine.wgsl.jinja", "reduce-arg-axis-split-reduce.wgsl.jinja"],
34
  "axis_split": ["reduce-arg-axis-split-combine.wgsl.jinja", "reduce-arg-axis-split-reduce.wgsl.jinja"],
35
  "axis_tiled": ["reduce-arg-axis-tiled.wgsl.jinja"],
36
  "axis_serial": ["reduce-arg-axis.wgsl.jinja"]
build/webgpu/reduce-arg-axis-split-combine.wgsl.jinja CHANGED
@@ -7,9 +7,17 @@
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 }}
14
 
15
  const WG: u32 = {{ workgroupSize }}u;
@@ -18,28 +26,14 @@ 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
- }
25
- {% if scalar == "u32" %}
26
- var out_val = partials_val[output_index];
27
- {% else %}
28
- var out_val = bitcast<{{ work }}>(partials_val[output_index]);
29
- {% endif %}
30
  var out_idx = partials_idx[output_index];
31
  for (var split_index = 1u; split_index < SPLIT; split_index++) {
32
- {% if scalar == "u32" %}
33
- let candidate_val = partials_val[split_index * params.outputCount + output_index];
34
- {% else %}
35
- let candidate_val = bitcast<{{ work }}>(partials_val[split_index * params.outputCount + output_index]);
36
- {% endif %}
37
  let candidate_idx = partials_idx[split_index * params.outputCount + output_index];
38
- {% if selectLastIndex %}
39
- if (candidate_idx != SENTINEL_IDX && (out_idx == SENTINEL_IDX || candidate_val {{ cmp }} out_val || (candidate_val == out_val && candidate_idx > out_idx))) {
40
- {% else %}
41
- if (candidate_idx != SENTINEL_IDX && (out_idx == SENTINEL_IDX || candidate_val {{ cmp }} out_val || (candidate_val == out_val && candidate_idx < out_idx))) {
42
- {% endif %}
43
  out_val = candidate_val;
44
  out_idx = candidate_idx;
45
  }
 
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 is_int = scalar == "i32" or scalar == "u32" %}
11
  {% set work = scalar if is_int else "f32" %}
12
+ {% macro arg_merge_wins(v, i, best, bestIdx) %}{{ i }} != SENTINEL_IDX && ({{ bestIdx }} == SENTINEL_IDX || {{ v }} {{ ">" if modeSpec == "max" else "<" }} {{ best }} || ({{ v }} == {{ best }} && {{ i }} {{ ">" if selectLastIndex else "<" }} {{ bestIdx }})){% endmacro %}
13
+ {% macro flat_index_2d(workgroupSize, name="i", bound="params.count", guardInline=false) %}
14
+ {% set wgTerm = workgroupSize ~ "u" if workgroupSize is number else workgroupSize %}
15
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
16
+ // per-axis workgroup fold width.
17
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ wgTerm }};
18
+ if ({{ name }} >= {{ bound }}) {
19
+ return;
20
+ }{% endmacro %}
21
  {{ env.wgsl.resourceDeclarations }}
22
 
23
  const WG: u32 = {{ workgroupSize }}u;
 
26
 
27
  @compute @workgroup_size(WG, 1, 1)
28
  fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
29
+ {{ flat_index_2d("WG", "output_index", "params.outputCount") }}
30
+ var out_val = {{ "partials_val[output_index]" if scalar == "u32" else "bitcast<" ~ work ~ ">(partials_val[output_index])" }};
 
 
 
 
 
 
 
31
  var out_idx = partials_idx[output_index];
32
  for (var split_index = 1u; split_index < SPLIT; split_index++) {
33
+ {% set candidate = "partials_val[split_index * params.outputCount + output_index]" %}
34
+ let candidate_val = {{ candidate if scalar == "u32" else "bitcast<" ~ work ~ ">(" ~ candidate ~ ")" }};
 
 
 
35
  let candidate_idx = partials_idx[split_index * params.outputCount + output_index];
36
+ if ({{ arg_merge_wins("candidate_val", "candidate_idx", "out_val", "out_idx") }}) {
 
 
 
 
37
  out_val = candidate_val;
38
  out_idx = candidate_idx;
39
  }
build/webgpu/reduce-arg-axis-split-reduce.wgsl.jinja CHANGED
@@ -6,9 +6,10 @@
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 }}
13
 
14
  const WG: u32 = {{ workgroupSize }}u;
@@ -34,35 +35,48 @@ 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;
41
  let inner_index = output_index % params.innerSize;
42
- let input_base = outer_index * params.axisDim * params.innerSize + inner_index;
 
 
 
43
 
44
  // Even split of [0, rows) across SPLIT segments (last segment may be shorter).
45
  let chunk = (params.axisDim + SPLIT - 1u) / SPLIT;
46
  let axis_begin = split_index * chunk;
47
  var axis_end = min(axis_begin + chunk, params.axisDim);
48
 
49
- var best_val = lane_identity();
50
- var best_idx = SENTINEL_IDX;
51
  for (var axis_index = axis_begin; axis_index < axis_end; axis_index++) {
52
- let v = x[input_base + axis_index * params.innerSize];
53
- {% if selectLastIndex %}
54
- if (v {{ cmp }} best_val || (v == best_val && (best_idx == SENTINEL_IDX || axis_index > best_idx))) {
 
 
 
 
 
 
55
  {% else %}
56
- if (v {{ cmp }} best_val || (v == best_val && axis_index < best_idx)) {
57
- {% endif %}
58
  best_val = v;
59
  best_idx = axis_index;
60
  }
 
61
  }
62
- {% if scalar == "u32" %}
63
- partials_val[split_index * params.outputCount + output_index] = best_val;
 
 
 
64
  {% else %}
65
- partials_val[split_index * params.outputCount + output_index] = bitcast<u32>(best_val);
66
- {% endif %}
67
  partials_idx[split_index * params.outputCount + output_index] = best_idx;
 
68
  }
 
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 is_int = scalar == "i32" or scalar == "u32" %}
10
  {% set work = scalar if is_int else "f32" %}
11
+ {% set columnVectorWidth = columnVectorWidth | default(1) %}
12
+ {% macro arg_wins(v, i, best, bestIdx) %}{{ v }} {{ ">" if modeSpec == "max" else "<" }} {{ best }} || ({{ v }} == {{ best }} && {{ "(" ~ bestIdx ~ " == SENTINEL_IDX || " ~ i ~ " > " ~ bestIdx ~ ")" if selectLastIndex else i ~ " < " ~ bestIdx }}){% endmacro %}
13
  {{ env.wgsl.resourceDeclarations }}
14
 
15
  const WG: u32 = {{ workgroupSize }}u;
 
35
  @builtin(workgroup_id) wg: vec3<u32>) {
36
  // 2D-folded column index: wg.z carries the high bits past the per-axis dispatch
37
  // fold width on the x dimension.
38
+ let output_index = {% if columnVectorWidth > 1 %}({% endif %}(wg.x + wg.z * {{ DISPATCH_FOLD_WIDTH }}u) * WG + (gid.x % WG){% if columnVectorWidth > 1 %}) * {{ columnVectorWidth }}u{% endif %};
39
  let split_index = wg.y;
40
  if (output_index >= params.outputCount) { return; }
41
  let outer_index = output_index / params.innerSize;
42
  let inner_index = output_index % params.innerSize;
43
+ let input_base = {% if columnVectorWidth > 1 %}({% endif %}outer_index * params.axisDim * params.innerSize + inner_index{% if columnVectorWidth > 1 %}) / {{ columnVectorWidth }}u{% endif %};
44
+ {% if columnVectorWidth > 1 %}
45
+ let inner_vectors = params.innerSize / {{ columnVectorWidth }}u;
46
+ {% endif %}
47
 
48
  // Even split of [0, rows) across SPLIT segments (last segment may be shorter).
49
  let chunk = (params.axisDim + SPLIT - 1u) / SPLIT;
50
  let axis_begin = split_index * chunk;
51
  var axis_end = min(axis_begin + chunk, params.axisDim);
52
 
53
+ var best_val = {% if columnVectorWidth > 1 %}vec{{ columnVectorWidth }}<{{ work }}>({% endif %}lane_identity(){% if columnVectorWidth > 1 %}){% endif %};
54
+ var best_idx = {% if columnVectorWidth > 1 %}vec{{ columnVectorWidth }}<u32>({% endif %}SENTINEL_IDX{% if columnVectorWidth > 1 %}){% endif %};
55
  for (var axis_index = axis_begin; axis_index < axis_end; axis_index++) {
56
+ {% if columnVectorWidth > 1 %}
57
+ let v = x[input_base + axis_index * inner_vectors];
58
+ let take = vec{{ columnVectorWidth }}<bool>(
59
+ {% for c in ['x', 'y', 'z', 'w'][:columnVectorWidth] %}
60
+ {{ arg_wins("v." ~ c, "axis_index", "best_val." ~ c, "best_idx." ~ c) }}{{ ',' if not loop.last else '' }}
61
+ {% endfor %}
62
+ );
63
+ best_val = select(best_val, v, take);
64
+ best_idx = select(best_idx, vec{{ columnVectorWidth }}<u32>(axis_index), take);
65
  {% else %}
66
+ let v = x[input_base + axis_index * params.innerSize];
67
+ if ({{ arg_wins("v", "axis_index", "best_val", "best_idx") }}) {
68
  best_val = v;
69
  best_idx = axis_index;
70
  }
71
+ {% endif %}
72
  }
73
+ {% if columnVectorWidth > 1 %}
74
+ {% for component in ['x', 'y', 'z', 'w'][:columnVectorWidth] %}
75
+ partials_val[split_index * params.outputCount + output_index + {{ loop.index0 }}u] = bitcast<u32>(best_val.{{ component }});
76
+ partials_idx[split_index * params.outputCount + output_index + {{ loop.index0 }}u] = best_idx.{{ component }};
77
+ {% endfor %}
78
  {% else %}
79
+ partials_val[split_index * params.outputCount + output_index] = {{ "best_val" if scalar == "u32" else "bitcast<u32>(best_val)" }};
 
80
  partials_idx[split_index * params.outputCount + output_index] = best_idx;
81
+ {% endif %}
82
  }
build/webgpu/reduce-arg-axis-split-tiled.wgsl.jinja CHANGED
@@ -7,9 +7,10 @@
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 }}
14
 
15
  const WG: u32 = {{ workgroupSize }}u;
@@ -56,11 +57,7 @@ fn main(
56
  if (in_bounds) {
57
  for (var axis_index = axis_begin + row_lane; axis_index < axis_end; axis_index += ROW_LANES) {
58
  let value = x[input_base + axis_index * params.innerSize];
59
- {% if selectLastIndex %}
60
- if (value {{ cmp }} best_val || (value == best_val && (best_idx == SENTINEL_IDX || axis_index > best_idx))) {
61
- {% else %}
62
- if (value {{ cmp }} best_val || (value == best_val && axis_index < best_idx)) {
63
- {% endif %}
64
  best_val = value;
65
  best_idx = axis_index;
66
  }
@@ -76,20 +73,12 @@ fn main(
76
  for (var lane = 1u; lane < ROW_LANES; lane++) {
77
  let candidate_val = partial_val[lane * TILE_COLS + col_lane];
78
  let candidate_idx = partial_idx[lane * TILE_COLS + col_lane];
79
- {% if selectLastIndex %}
80
- if (candidate_idx != SENTINEL_IDX && (out_idx == SENTINEL_IDX || candidate_val {{ cmp }} out_val || (candidate_val == out_val && candidate_idx > out_idx))) {
81
- {% else %}
82
- if (candidate_idx != SENTINEL_IDX && (out_idx == SENTINEL_IDX || candidate_val {{ cmp }} out_val || (candidate_val == out_val && candidate_idx < out_idx))) {
83
- {% endif %}
84
  out_val = candidate_val;
85
  out_idx = candidate_idx;
86
  }
87
  }
88
- {% if scalar == "u32" %}
89
- partials_val[split_index * params.outputCount + output_index] = out_val;
90
- {% else %}
91
- partials_val[split_index * params.outputCount + output_index] = bitcast<u32>(out_val);
92
- {% endif %}
93
  partials_idx[split_index * params.outputCount + output_index] = out_idx;
94
  }
95
  }
 
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 is_int = scalar == "i32" or scalar == "u32" %}
11
  {% set work = scalar if is_int else "f32" %}
12
+ {% macro arg_wins(v, i, best, bestIdx) %}{{ v }} {{ ">" if modeSpec == "max" else "<" }} {{ best }} || ({{ v }} == {{ best }} && {{ "(" ~ bestIdx ~ " == SENTINEL_IDX || " ~ i ~ " > " ~ bestIdx ~ ")" if selectLastIndex else i ~ " < " ~ bestIdx }}){% endmacro %}
13
+ {% macro arg_merge_wins(v, i, best, bestIdx) %}{{ i }} != SENTINEL_IDX && ({{ bestIdx }} == SENTINEL_IDX || {{ v }} {{ ">" if modeSpec == "max" else "<" }} {{ best }} || ({{ v }} == {{ best }} && {{ i }} {{ ">" if selectLastIndex else "<" }} {{ bestIdx }})){% endmacro %}
14
  {{ env.wgsl.resourceDeclarations }}
15
 
16
  const WG: u32 = {{ workgroupSize }}u;
 
57
  if (in_bounds) {
58
  for (var axis_index = axis_begin + row_lane; axis_index < axis_end; axis_index += ROW_LANES) {
59
  let value = x[input_base + axis_index * params.innerSize];
60
+ if ({{ arg_wins("value", "axis_index", "best_val", "best_idx") }}) {
 
 
 
 
61
  best_val = value;
62
  best_idx = axis_index;
63
  }
 
73
  for (var lane = 1u; lane < ROW_LANES; lane++) {
74
  let candidate_val = partial_val[lane * TILE_COLS + col_lane];
75
  let candidate_idx = partial_idx[lane * TILE_COLS + col_lane];
76
+ if ({{ arg_merge_wins("candidate_val", "candidate_idx", "out_val", "out_idx") }}) {
 
 
 
 
77
  out_val = candidate_val;
78
  out_idx = candidate_idx;
79
  }
80
  }
81
+ partials_val[split_index * params.outputCount + output_index] = {{ "out_val" if scalar == "u32" else "bitcast<u32>(out_val)" }};
 
 
 
 
82
  partials_idx[split_index * params.outputCount + output_index] = out_idx;
83
  }
84
  }
build/webgpu/reduce-arg-axis-tiled.wgsl.jinja CHANGED
@@ -4,9 +4,10 @@
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 }}
11
 
12
  const WG: u32 = {{ workgroupSize }}u;
@@ -50,11 +51,7 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid:
50
  if (in_bounds) {
51
  for (var axis_index = row_lane; axis_index < params.axisDim; axis_index = axis_index + ROW_LANES) {
52
  let v = x[input_base + axis_index * params.innerSize];
53
- {% if selectLastIndex %}
54
- if (v {{ cmp }} best_val || (v == best_val && (best_idx == SENTINEL_IDX || axis_index > best_idx))) {
55
- {% else %}
56
- if (v {{ cmp }} best_val || (v == best_val && axis_index < best_idx)) {
57
- {% endif %}
58
  best_val = v;
59
  best_idx = axis_index;
60
  }
@@ -70,11 +67,7 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid:
70
  for (var lane = 1u; lane < ROW_LANES; lane = lane + 1u) {
71
  let candidate_val = partial_val[lane * TILE_COLS + col_lane];
72
  let candidate_idx = partial_idx[lane * TILE_COLS + col_lane];
73
- {% if selectLastIndex %}
74
- if (candidate_idx != SENTINEL_IDX && (out_idx == SENTINEL_IDX || candidate_val {{ cmp }} out_val || (candidate_val == out_val && candidate_idx > out_idx))) {
75
- {% else %}
76
- if (candidate_idx != SENTINEL_IDX && (out_idx == SENTINEL_IDX || candidate_val {{ cmp }} out_val || (candidate_val == out_val && candidate_idx < out_idx))) {
77
- {% endif %}
78
  out_val = candidate_val;
79
  out_idx = candidate_idx;
80
  }
 
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 is_int = scalar == "i32" or scalar == "u32" %}
8
  {% set work = scalar if is_int else "f32" %}
9
+ {% macro arg_wins(v, i, best, bestIdx) %}{{ v }} {{ ">" if modeSpec == "max" else "<" }} {{ best }} || ({{ v }} == {{ best }} && {{ "(" ~ bestIdx ~ " == SENTINEL_IDX || " ~ i ~ " > " ~ bestIdx ~ ")" if selectLastIndex else i ~ " < " ~ bestIdx }}){% endmacro %}
10
+ {% macro arg_merge_wins(v, i, best, bestIdx) %}{{ i }} != SENTINEL_IDX && ({{ bestIdx }} == SENTINEL_IDX || {{ v }} {{ ">" if modeSpec == "max" else "<" }} {{ best }} || ({{ v }} == {{ best }} && {{ i }} {{ ">" if selectLastIndex else "<" }} {{ bestIdx }})){% endmacro %}
11
  {{ env.wgsl.resourceDeclarations }}
12
 
13
  const WG: u32 = {{ workgroupSize }}u;
 
51
  if (in_bounds) {
52
  for (var axis_index = row_lane; axis_index < params.axisDim; axis_index = axis_index + ROW_LANES) {
53
  let v = x[input_base + axis_index * params.innerSize];
54
+ if ({{ arg_wins("v", "axis_index", "best_val", "best_idx") }}) {
 
 
 
 
55
  best_val = v;
56
  best_idx = axis_index;
57
  }
 
67
  for (var lane = 1u; lane < ROW_LANES; lane = lane + 1u) {
68
  let candidate_val = partial_val[lane * TILE_COLS + col_lane];
69
  let candidate_idx = partial_idx[lane * TILE_COLS + col_lane];
70
+ if ({{ arg_merge_wins("candidate_val", "candidate_idx", "out_val", "out_idx") }}) {
 
 
 
 
71
  out_val = candidate_val;
72
  out_idx = candidate_idx;
73
  }
build/webgpu/reduce-arg-axis.wgsl.jinja CHANGED
@@ -2,43 +2,34 @@
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
- }
18
 
19
  let outer_index = output_index / params.innerSize;
20
  let inner_index = output_index % params.innerSize;
21
  let input_base = outer_index * params.axisDim * params.innerSize + inner_index;
22
 
23
  var best_index = 0u;
24
- {%- if scalar == "i32" or scalar == "u32" %}
25
- var best = x[input_base];
26
- for (var axis_index = 1u; axis_index < params.axisDim; axis_index++) {
27
- let candidate = x[input_base + axis_index * params.innerSize];
28
- if (candidate {{ cmp }} best{% if selectLastIndex %} || candidate == best{% endif %}) {
29
- best = candidate;
30
- best_index = axis_index;
31
- }
32
- }
33
- {%- else %}
34
- var best = f32(x[input_base]);
35
  for (var axis_index = 1u; axis_index < params.axisDim; axis_index++) {
36
- let candidate = f32(x[input_base + axis_index * params.innerSize]);
37
  if (candidate {{ cmp }} best{% if selectLastIndex %} || candidate == best{% endif %}) {
38
  best = candidate;
39
  best_index = axis_index;
40
  }
41
  }
42
- {%- endif %}
43
  y[output_index] = best_index;
44
  }
 
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 = ">" %}
6
+ {% set widen = scalar != "i32" and scalar != "u32" %}
7
+ {% macro flat_index_2d(workgroupSize, name="i", bound="params.count", guardInline=false) %}
8
+ {% set wgTerm = workgroupSize ~ "u" if workgroupSize is number else workgroupSize %}
9
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
10
+ // per-axis workgroup fold width.
11
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ wgTerm }};
12
+ if ({{ name }} >= {{ bound }}) {
13
+ return;
14
+ }{% endmacro %}
15
  {{ env.wgsl.resourceDeclarations }}
16
 
17
  @compute @workgroup_size({{ workgroupSize }})
18
  fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
19
+ {{ flat_index_2d(workgroupSize, "output_index", "params.outputCount") }}
 
 
 
 
20
 
21
  let outer_index = output_index / params.innerSize;
22
  let inner_index = output_index % params.innerSize;
23
  let input_base = outer_index * params.axisDim * params.innerSize + inner_index;
24
 
25
  var best_index = 0u;
26
+ var best = {{ "f32(x[input_base])" if widen else "x[input_base]" }};
 
 
 
 
 
 
 
 
 
 
27
  for (var axis_index = 1u; axis_index < params.axisDim; axis_index++) {
28
+ let candidate = {{ "f32(x[input_base + axis_index * params.innerSize])" if widen else "x[input_base + axis_index * params.innerSize]" }};
29
  if (candidate {{ cmp }} best{% if selectLastIndex %} || candidate == best{% endif %}) {
30
  best = candidate;
31
  best_index = axis_index;
32
  }
33
  }
 
34
  y[output_index] = best_index;
35
  }
build/webgpu/reduce-arg-row-split.wgsl.jinja CHANGED
@@ -1,33 +1,26 @@
1
- {% set useSubgroups = useSubgroups %}
2
- {% if usesF16 %}
3
- enable f16;
4
- {%- endif %}
 
5
  {% if useSubgroups %}
6
  enable subgroups;
7
- {%- endif %}
8
  {{ env.wgsl.resourceDeclarations }}
9
 
10
  const WG: u32 = {{ workgroupSize }}u;
11
  const SPLIT: u32 = {{ split }}u;
12
  const SENTINEL_IDX: u32 = 4294967295u;
13
- {%- if scalar == "i32" %}
14
- {% set work = "i32" %}
15
- {% elif scalar == "u32" %}
16
- {% set work = "u32" %}
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
  }
32
 
33
  var<workgroup> wg_val: array<{{ work }}, WG>;
@@ -36,6 +29,7 @@ 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_size) subgroup_size: u32{% endif %}) {
40
  let row = wg.x;
41
  let split_index = wg.y;
@@ -48,68 +42,59 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>,
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
  {
55
- {%- if scalar == "f16" %}
56
- let value = f32(value4.{{ component }});
57
- {%- else %}
58
- let value = value4.{{ component }};
59
- {%- endif %}
60
  let index = chunk * 4u + {{ loop.index0 }}u;
61
- {%- if selectLastIndex %}
62
- if (value {{ cmp }} best_val || (value == best_val && (best_idx == SENTINEL_IDX || index > best_idx))) {
63
- {%- else %}
64
- if (value {{ cmp }} best_val || (value == best_val && index < best_idx)) {
65
- {%- endif %}
66
  best_val = value;
67
  best_idx = index;
68
  }
69
  }
70
- {%- endfor %}
71
  {% else %}
72
- {% if scalar == "f16" %}
73
- let value = f32(x[base + chunk]);
74
- {%- else %}
75
- let value = x[base + chunk];
76
- {%- endif %}
77
- {% if selectLastIndex %}
78
- if (value {{ cmp }} best_val || (value == best_val && (best_idx == SENTINEL_IDX || chunk > best_idx))) {
79
- {%- else %}
80
- if (value {{ cmp }} best_val || (value == best_val && chunk < best_idx)) {
81
- {%- endif %}
82
  best_val = value;
83
  best_idx = chunk;
84
  }
85
- {%- endif %}
86
  }
87
- {%- if useSubgroups %}
88
- let subgroup_val = subgroupMax(best_val);{% if selectLastIndex %}
 
89
  let subgroup_candidate = select(0u, best_idx, best_val == subgroup_val && best_idx != SENTINEL_IDX);
90
  let subgroup_idx = subgroupMax(subgroup_candidate);
91
- {%- else %}
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);
@@ -119,33 +104,26 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>,
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 = subgroupMax(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;
143
- {%- else %}
144
- partials_val[scratch_index] = bitcast<u32>(output_val);
145
- {%- endif %}
146
  partials_idx[scratch_index] = output_idx;
147
  }
148
- {%- else %}
149
  wg_val[tid] = best_val;
150
  wg_idx[tid] = best_idx;
151
  workgroupBarrier();
@@ -154,11 +132,7 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>,
154
  let candidate_val = wg_val[tid + stride];
155
  let candidate_idx = wg_idx[tid + stride];
156
  let current_idx = wg_idx[tid];
157
- {%- if selectLastIndex %}
158
- if (candidate_idx != SENTINEL_IDX && (current_idx == SENTINEL_IDX || candidate_val {{ cmp }} wg_val[tid] || (candidate_val == wg_val[tid] && candidate_idx > current_idx))) {
159
- {%- else %}
160
- if (candidate_idx != SENTINEL_IDX && (current_idx == SENTINEL_IDX || candidate_val {{ cmp }} wg_val[tid] || (candidate_val == wg_val[tid] && candidate_idx < current_idx))) {
161
- {%- endif %}
162
  wg_val[tid] = candidate_val;
163
  wg_idx[tid] = candidate_idx;
164
  }
@@ -167,12 +141,8 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>,
167
  }
168
  if (tid == 0u) {
169
  let scratch_index = split_index * params.rows + row;
170
- {%- if scalar == "u32" %}
171
- partials_val[scratch_index] = wg_val[0];
172
- {%- else %}
173
- partials_val[scratch_index] = bitcast<u32>(wg_val[0]);
174
- {%- endif %}
175
  partials_idx[scratch_index] = wg_idx[0];
176
  }
177
- {%- endif %}
178
  }
 
1
+ {% set is_int = scalar == "i32" or scalar == "u32" %}
2
+ {% set work = scalar if is_int else "f32" %}
3
+ {% set sgReduce = "subgroupMax" %}
4
+ {% macro arg_wins(v, i, best, bestIdx) %}{{ v }} {{ ">" if modeSpec == "max" else "<" }} {{ best }} || ({{ v }} == {{ best }} && {{ "(" ~ bestIdx ~ " == SENTINEL_IDX || " ~ i ~ " > " ~ bestIdx ~ ")" if selectLastIndex else i ~ " < " ~ bestIdx }}){% endmacro %}
5
+ {% macro arg_merge_wins(v, i, best, bestIdx) %}{{ i }} != SENTINEL_IDX && ({{ bestIdx }} == SENTINEL_IDX || {{ v }} {{ ">" if modeSpec == "max" else "<" }} {{ best }} || ({{ v }} == {{ best }} && {{ i }} {{ ">" if selectLastIndex else "<" }} {{ bestIdx }})){% endmacro %}
6
  {% if useSubgroups %}
7
  enable subgroups;
8
+ {% endif %}
9
  {{ env.wgsl.resourceDeclarations }}
10
 
11
  const WG: u32 = {{ workgroupSize }}u;
12
  const SPLIT: u32 = {{ split }}u;
13
  const SENTINEL_IDX: u32 = 4294967295u;
 
 
 
 
 
 
 
 
14
 
15
  fn lane_identity() -> {{ work }} {
16
+ {% if scalar == "i32" %}
17
  return bitcast<i32>({{ "0x80000000u" if modeSpec == "max" else "0x7fffffffu" }});
18
+ {% elif scalar == "u32" %}
19
  return {{ "0u" if modeSpec == "max" else "0xffffffffu" }};
20
+ {% else %}
21
  var bits = {{ "0xff800000u" if modeSpec == "max" else "0x7f800000u" }};
22
  return bitcast<f32>(bits);
23
+ {% endif %}
24
  }
25
 
26
  var<workgroup> wg_val: array<{{ work }}, WG>;
 
29
  @compute @workgroup_size(WG, 1, 1)
30
  fn main(@builtin(workgroup_id) wg: vec3<u32>,
31
  @builtin(local_invocation_id) lid: vec3<u32>{% if useSubgroups %},
32
+ @builtin(subgroup_invocation_id) subgroup_lane: u32,
33
  @builtin(subgroup_size) subgroup_size: u32{% endif %}) {
34
  let row = wg.x;
35
  let split_index = wg.y;
 
42
  var best_val = lane_identity();
43
  var best_idx = SENTINEL_IDX;
44
  for (var chunk = segment_begin + tid; chunk < segment_end; chunk += WG) {
45
+ {% if vec4 %}
46
  let value4 = x[base + chunk];
47
+ {% for component in ["x", "y", "z", "w"] %}
48
  {
49
+ let value = {{ "f32(value4." ~ component ~ ")" if scalar == "f16" else "value4." ~ component }};
 
 
 
 
50
  let index = chunk * 4u + {{ loop.index0 }}u;
51
+ if ({{ arg_wins("value", "index", "best_val", "best_idx") }}) {
 
 
 
 
52
  best_val = value;
53
  best_idx = index;
54
  }
55
  }
56
+ {% endfor %}
57
  {% else %}
58
+ let value = {{ "f32(x[base + chunk])" if scalar == "f16" else "x[base + chunk]" }};
59
+ if ({{ arg_wins("value", "chunk", "best_val", "best_idx") }}) {
 
 
 
 
 
 
 
 
60
  best_val = value;
61
  best_idx = chunk;
62
  }
63
+ {% endif %}
64
  }
65
+ {% if useSubgroups %}
66
+ let subgroup_val = {{ sgReduce }}(best_val);
67
+ {% if selectLastIndex %}
68
  let subgroup_candidate = select(0u, best_idx, best_val == subgroup_val && best_idx != SENTINEL_IDX);
69
  let subgroup_idx = subgroupMax(subgroup_candidate);
70
+ {% else %}
71
  let subgroup_candidate = select(SENTINEL_IDX, best_idx, best_val == subgroup_val);
72
  let subgroup_idx = subgroupMin(subgroup_candidate);
73
+ {% endif %}
74
  // The cross-subgroup fold assumes nothing about which invocations share a
75
+ // subgroup or how many subgroups there are (it does require the uniform
76
+ // control flow this entry point already has): every invocation owns the slot
77
+ // at its own index, the elected lane
78
  // publishes its subgroup pair there and every other lane publishes the
79
  // identity pair (the sentinel index, which the merge skips). Each subgroup
80
+ // then folds all WG slots — lane `subgroup_lane` walks slots subgroup_lane,
81
+ // subgroup_lane + subgroup_size, ... — and one more collective merges the lane
82
+ // partials, so every slot is merged exactly once at any legal width. The merge
83
+ // (best value, then the tie-break index) is commutative and associative, so
84
+ // the fold order does not change the result.
85
  var output_val = subgroup_val;
86
  var output_idx = subgroup_idx;
87
  // A one-subgroup workgroup is already fully reduced by the collectives above.
88
  // The test reads the `subgroup_size` builtin, which is uniform; a collective's
89
  // result is not uniform to WGSL's analysis and may not guard a barrier.
90
  if (subgroup_size != WG) {
91
+ // The coordinates are the BUILTINS, never `subgroupExclusiveAdd(1u)` /
92
+ // `subgroupAdd(1u)`: those agree with them under this contract, but a driver
93
+ // in the wild answers a claim derived from them with zero and leaves most of
94
+ // the workgroup's slots unclaimed. Every lane of this entry point is active
95
+ // here, so the full subgroup width is the right stride.
96
+ let rank = subgroup_lane;
97
+ let count = subgroup_size;
98
  let leader = rank == 0u;
99
  wg_val[tid] = select(lane_identity(), subgroup_val, leader);
100
  wg_idx[tid] = select(SENTINEL_IDX, subgroup_idx, leader);
 
104
  for (var slot = rank; slot < WG; slot = slot + count) {
105
  let candidate_val = wg_val[slot];
106
  let candidate_idx = wg_idx[slot];
107
+ if ({{ arg_merge_wins("candidate_val", "candidate_idx", "fold_val", "fold_idx") }}) {
 
 
 
 
108
  fold_val = candidate_val;
109
  fold_idx = candidate_idx;
110
  }
111
  }
112
+ output_val = {{ sgReduce }}(fold_val);
113
+ {% if selectLastIndex %}
114
  let fold_candidate = select(0u, fold_idx, fold_val == output_val && fold_idx != SENTINEL_IDX);
115
  output_idx = subgroupMax(fold_candidate);
116
+ {% else %}
117
  let fold_candidate = select(SENTINEL_IDX, fold_idx, fold_val == output_val);
118
  output_idx = subgroupMin(fold_candidate);
119
+ {% endif %}
120
  }
121
  if (tid == 0u) {
122
  let scratch_index = split_index * params.rows + row;
123
+ partials_val[scratch_index] = {{ "output_val" if scalar == "u32" else "bitcast<u32>(output_val)" }};
 
 
 
 
124
  partials_idx[scratch_index] = output_idx;
125
  }
126
+ {% else %}
127
  wg_val[tid] = best_val;
128
  wg_idx[tid] = best_idx;
129
  workgroupBarrier();
 
132
  let candidate_val = wg_val[tid + stride];
133
  let candidate_idx = wg_idx[tid + stride];
134
  let current_idx = wg_idx[tid];
135
+ if ({{ arg_merge_wins("candidate_val", "candidate_idx", "wg_val[tid]", "current_idx") }}) {
 
 
 
 
136
  wg_val[tid] = candidate_val;
137
  wg_idx[tid] = candidate_idx;
138
  }
 
141
  }
142
  if (tid == 0u) {
143
  let scratch_index = split_index * params.rows + row;
144
+ partials_val[scratch_index] = {{ "wg_val[0]" if scalar == "u32" else "bitcast<u32>(wg_val[0])" }};
 
 
 
 
145
  partials_idx[scratch_index] = wg_idx[0];
146
  }
147
+ {% endif %}
148
  }
build/webgpu/reduce-arg-row-subgroup.wgsl.jinja CHANGED
@@ -11,148 +11,135 @@
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 %}
 
 
 
 
 
 
 
 
 
 
18
  {% if useSubgroups %}
19
  enable subgroups;
20
- {%- endif %}
21
  {{ env.wgsl.resourceDeclarations }}
22
 
23
  const WG: u32 = {{ workgroupSize }}u;
24
  const SENTINEL_IDX: u32 = 4294967295u;
25
- {% if modeSpec == "max" and scalar != "i32" and scalar != "u32" %}
26
 
27
- fn neg_inf_f32() -> f32 {
28
  var bits = 0xff800000u;
29
  return bitcast<f32>(bits);
30
  }
 
31
 
 
 
 
 
32
  {% endif %}
33
- {%- if scalar == "i32" %}
34
- {% set cmpType = "i32" %}
35
- {% elif scalar == "u32" %}
36
- {% set cmpType = "u32" %}
37
- {% else %}
38
- {% set cmpType = "f32" %}
39
- {% endif %}
40
- {% set cmp = ">" %}
41
- {%- if cmpType == "i32" %}
42
- {% set identityVal = "-2147483647i - 1i" %}
43
- {%- elif cmpType == "u32" %}
44
- {% set identityVal = "0u" %}
45
- {%- else %}
46
- {% set identityVal = "neg_inf_f32()" %}
47
- {%- endif %}
48
- {%- if not (useSubgroups and singleSubgroup is defined and singleSubgroup) %}
49
  var<workgroup> wgVal: array<{{ cmpType }}, WG>;
50
  var<workgroup> wgIdx: array<u32, WG>;
51
- {%- endif %}
52
 
53
  @compute @workgroup_size(WG, 1, 1)
54
  fn main(@builtin(workgroup_id) wg: vec3<u32>,
55
- @builtin(local_invocation_id) lid: vec3<u32>{% if useSubgroups and not (singleSubgroup is defined and singleSubgroup) %},
 
56
  @builtin(subgroup_size) sgSize: u32{% endif %}) {
57
  let row = wg.x + wg.y * params.rowStride;
58
  if (row >= params.rows) {
59
  return;
60
  }
61
  let tid = lid.x;
62
- let base = row * params.chunkCount;{% if cmpType == "i32" %}
63
- var bestVal: i32 = -2147483647i - 1i;
64
- {%- elif cmpType == "u32" %}
65
- var bestVal: u32 = 0u;
66
- {%- else %}
67
- var bestVal: f32 = neg_inf_f32();
68
- {%- endif %}
69
  var bestIdx: u32 = SENTINEL_IDX;
70
- {%- if vec4 %}
71
  for (var c = tid; c < params.chunkCount; c = c + WG) {
72
  let v4 = x[base + c];
73
- {%- for comp in ["x", "y", "z", "w"] %}
74
  {
75
- {%- if scalar == "f16" %}
76
- let v = f32(v4.{{ comp }});
77
- {%- else %}
78
- let v = v4.{{ comp }};
79
- {%- endif %}
80
  let idx = c * 4u + {{ loop.index0 }}u;
81
- {%- if selectLastIndex %}
82
- if (v {{ cmp }} bestVal || (v == bestVal && (bestIdx == SENTINEL_IDX || idx > bestIdx))) {
83
- {%- else %}
84
- if (v {{ cmp }} bestVal || (v == bestVal && idx < bestIdx)) {
85
- {%- endif %}
86
  bestVal = v;
87
  bestIdx = idx;
88
  }
89
  }
90
- {%- endfor %}
91
  }
92
- {%- else %}
93
- // Four independent, lane-coalesced loads per loop iteration. Keeping each
94
- // unrolled plane WG elements apart preserves contiguous accesses across the
95
- // workgroup while exposing enough ILP for long odd-width rows (where a vec4
96
- // storage binding cannot represent row boundaries).
97
- for (var c0 = tid; c0 < params.chunkCount; c0 = c0 + 4u * WG) {
98
- {%- for plane in [0, 1, 2, 3] %}
99
  {
100
- let c = c0 + {{ plane }}u * WG;
101
  if (c < params.chunkCount) {
102
- {%- if scalar == "f16" %}
103
- let v = f32(x[base + c]);
104
- {%- else %}
105
- let v = x[base + c];
106
- {%- endif %}
107
- {% if selectLastIndex %}
108
- if (v {{ cmp }} bestVal || (v == bestVal && (bestIdx == SENTINEL_IDX || c > bestIdx))) {
109
- {%- else %}
110
- if (v {{ cmp }} bestVal || (v == bestVal && c < bestIdx)) {
111
- {%- endif %}
112
  bestVal = v;
113
  bestIdx = c;
114
  }
115
  }
116
  }
117
- {%- endfor %}
118
  }
119
- {%- endif %}
120
- {%- if useSubgroups %}
121
- let m = subgroupMax(bestVal);{% if selectLastIndex %}
 
122
  // Lanes that match the subgroup best contribute their (real) index; the
123
  // rest contribute 0 so subgroupMax picks the highest matching index.
124
  let cand = select(0u, bestIdx, bestVal == m && bestIdx != SENTINEL_IDX);
125
  let sgIdx = subgroupMax(cand);
126
- {%- else %}
127
  // Lanes that match the subgroup best contribute their index; the rest
128
  // contribute the sentinel so subgroupMin picks the lowest matching index.
129
  let cand = select(SENTINEL_IDX, bestIdx, bestVal == m);
130
  let sgIdx = subgroupMin(cand);
131
- {%- endif %}
132
- {% if singleSubgroup is defined and singleSubgroup %}
133
  // The workgroup is one (possibly partial) subgroup: elect the writer rather
134
  // than assuming lane 0 is active.
135
  if (subgroupElect()) {
136
  y[row] = select(sgIdx, 0u, sgIdx == SENTINEL_IDX);
137
  }
138
- {%- else %}
139
  // Cross-subgroup fold that assumes nothing about which invocations share a
140
- // subgroup, how many subgroups there are, or which of a subgroup's lanes are
141
- // active: every invocation owns the slot at its own index, the elected lane
 
142
  // publishes its subgroup pair there and every other lane publishes the
143
  // identity pair (the sentinel index, which the merge skips). Each subgroup
144
- // then folds all WG slots — lane `rank`, its dense position among the active
145
- // lanes, walks slots rank, rank + count, ... — and one more collective merges
146
- // the lane partials, so every slot is merged exactly once at any legal width
147
- // and partition. The merge is commutative and associative (best value, then
148
- // the tie-break index), so the fold order does not change the result.
149
  var winnerIdx = sgIdx;
150
  // A one-subgroup workgroup is already fully reduced by the collectives above.
151
  // The test reads the `subgroup_size` builtin, which is uniform; a collective's
152
  // result is not uniform to WGSL's analysis and may not guard a barrier.
153
  if (sgSize != WG) {
154
- let rank = subgroupExclusiveAdd(1u);
155
- let count = subgroupAdd(1u);
 
 
 
 
 
156
  let leader = rank == 0u;
157
  wgVal[tid] = select({{ identityVal }}, m, leader);
158
  wgIdx[tid] = select(SENTINEL_IDX, sgIdx, leader);
@@ -162,25 +149,22 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>,
162
  for (var i = rank; i < WG; i = i + count) {
163
  let v = wgVal[i];
164
  let vi = wgIdx[i];
165
- {%- if selectLastIndex %}
166
- if (vi != SENTINEL_IDX && (v {{ cmp }} outVal || (v == outVal && (outIdx == SENTINEL_IDX || vi > outIdx)))) {
167
- {%- else %}
168
- if (vi != SENTINEL_IDX && (v {{ cmp }} outVal || (v == outVal && vi < outIdx))) {
169
- {%- endif %}
170
  outVal = v;
171
  outIdx = vi;
172
  }
173
  }
174
- let foldVal = subgroupMax(outVal);{% if selectLastIndex %}
 
175
  winnerIdx = subgroupMax(select(0u, outIdx, outVal == foldVal && outIdx != SENTINEL_IDX));
176
- {%- else %}
177
  winnerIdx = subgroupMin(select(SENTINEL_IDX, outIdx, outVal == foldVal));
178
- {%- endif %}
179
  }
180
  if (tid == 0u) {
181
  y[row] = select(winnerIdx, 0u, winnerIdx == SENTINEL_IDX);
182
  }
183
- {%- endif %}
184
  {% else %}
185
  // No-subgroup tier: workgroup barrier tree-reduction over the per-thread
186
  // (bestVal, bestIdx) pairs. Same ONNX tie rule (lowest index by default,
@@ -195,11 +179,7 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>,
195
  let v = wgVal[tid + stride];
196
  let vi = wgIdx[tid + stride];
197
  let curIdx = wgIdx[tid];
198
- {%- if selectLastIndex %}
199
- if (vi != SENTINEL_IDX && (curIdx == SENTINEL_IDX || v {{ cmp }} wgVal[tid] || (v == wgVal[tid] && vi > curIdx))) {
200
- {%- else %}
201
- if (vi != SENTINEL_IDX && (curIdx == SENTINEL_IDX || v {{ cmp }} wgVal[tid] || (v == wgVal[tid] && vi < curIdx))) {
202
- {%- endif %}
203
  wgVal[tid] = v;
204
  wgIdx[tid] = vi;
205
  }
@@ -209,5 +189,5 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>,
209
  if (tid == 0u) {
210
  y[row] = select(wgIdx[0], 0u, wgIdx[0] == SENTINEL_IDX);
211
  }
212
- {%- endif %}
213
  }
 
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
+ {% set useSubgroups = useSubgroups | default(true) %}
15
+ {% set singleSubgroup = useSubgroups and singleSubgroup | default(false) %}
16
+ {% set adjacent = adjacentScalarLoads | default(false) %}
17
+ {% set cmpType = scalar if scalar == "i32" or scalar == "u32" else "f32" %}
18
+ {% set sgReduce = "subgroupMax" %}
19
+ {% if cmpType == "i32" %}
20
+ {% set identityVal = "-2147483647i - 1i" %}
21
+ {% elif cmpType == "u32" %}
22
+ {% set identityVal = "0u" %}
23
+ {% else %}
24
+ {% set identityVal = "negative_infinity()" %}
25
+ {% endif %}
26
+ {% macro arg_wins(v, i, best, bestIdx) %}{{ v }} {{ ">" if modeSpec == "max" else "<" }} {{ best }} || ({{ v }} == {{ best }} && {{ "(" ~ bestIdx ~ " == SENTINEL_IDX || " ~ i ~ " > " ~ bestIdx ~ ")" if selectLastIndex else i ~ " < " ~ bestIdx }}){% endmacro %}
27
+ {% macro arg_merge_wins(v, i, best, bestIdx) %}{{ i }} != SENTINEL_IDX && ({{ bestIdx }} == SENTINEL_IDX || {{ v }} {{ ">" if modeSpec == "max" else "<" }} {{ best }} || ({{ v }} == {{ best }} && {{ i }} {{ ">" if selectLastIndex else "<" }} {{ bestIdx }})){% endmacro %}
28
  {% if useSubgroups %}
29
  enable subgroups;
30
+ {% endif %}
31
  {{ env.wgsl.resourceDeclarations }}
32
 
33
  const WG: u32 = {{ workgroupSize }}u;
34
  const SENTINEL_IDX: u32 = 4294967295u;
35
+ {% if cmpType == "f32" and modeSpec == "max" %}
36
 
37
+ fn negative_infinity() -> f32 {
38
  var bits = 0xff800000u;
39
  return bitcast<f32>(bits);
40
  }
41
+ {% elif cmpType == "f32" %}
42
 
43
+ fn pos_inf_f32() -> f32 {
44
+ var bits = 0x7f800000u;
45
+ return bitcast<f32>(bits);
46
+ }
47
  {% endif %}
48
+ {% if not singleSubgroup %}
49
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  var<workgroup> wgVal: array<{{ cmpType }}, WG>;
51
  var<workgroup> wgIdx: array<u32, WG>;
52
+ {% endif %}
53
 
54
  @compute @workgroup_size(WG, 1, 1)
55
  fn main(@builtin(workgroup_id) wg: vec3<u32>,
56
+ @builtin(local_invocation_id) lid: vec3<u32>{% if useSubgroups and not singleSubgroup %},
57
+ @builtin(subgroup_invocation_id) sgLane: u32,
58
  @builtin(subgroup_size) sgSize: u32{% endif %}) {
59
  let row = wg.x + wg.y * params.rowStride;
60
  if (row >= params.rows) {
61
  return;
62
  }
63
  let tid = lid.x;
64
+ let base = row * params.chunkCount;
65
+ var bestVal: {{ cmpType }} = {{ identityVal }};
 
 
 
 
 
66
  var bestIdx: u32 = SENTINEL_IDX;
67
+ {% if vec4 %}
68
  for (var c = tid; c < params.chunkCount; c = c + WG) {
69
  let v4 = x[base + c];
70
+ {% for comp in ["x", "y", "z", "w"] %}
71
  {
72
+ let v = {{ "f32(v4." ~ comp ~ ")" if scalar == "f16" else "v4." ~ comp }};
 
 
 
 
73
  let idx = c * 4u + {{ loop.index0 }}u;
74
+ if ({{ arg_wins("v", "idx", "bestVal", "bestIdx") }}) {
 
 
 
 
75
  bestVal = v;
76
  bestIdx = idx;
77
  }
78
  }
79
+ {% endfor %}
80
  }
81
+ {% else %}
82
+ // Scan four bounded scalar values per lane. For long rows the manifest may
83
+ // assign adjacent values to a lane; otherwise the four planes stay WG apart.
84
+ // Both layouts cover each input exactly once and use the same tie comparison.
85
+ for (var c0 = {{ "4u * tid" if adjacent else "tid" }}; c0 < params.chunkCount; c0 = c0 + 4u * WG) {
86
+ {% for plane in [0, 1, 2, 3] %}
 
87
  {
88
+ let c = c0 + {{ plane }}u{{ "" if adjacent else " * WG" }};
89
  if (c < params.chunkCount) {
90
+ let v = {{ "f32(x[base + c])" if scalar == "f16" else "x[base + c]" }};
91
+ if ({{ arg_wins("v", "c", "bestVal", "bestIdx") }}) {
 
 
 
 
 
 
 
 
92
  bestVal = v;
93
  bestIdx = c;
94
  }
95
  }
96
  }
97
+ {% endfor %}
98
  }
99
+ {% endif %}
100
+ {% if useSubgroups %}
101
+ let m = {{ sgReduce }}(bestVal);
102
+ {% if selectLastIndex %}
103
  // Lanes that match the subgroup best contribute their (real) index; the
104
  // rest contribute 0 so subgroupMax picks the highest matching index.
105
  let cand = select(0u, bestIdx, bestVal == m && bestIdx != SENTINEL_IDX);
106
  let sgIdx = subgroupMax(cand);
107
+ {% else %}
108
  // Lanes that match the subgroup best contribute their index; the rest
109
  // contribute the sentinel so subgroupMin picks the lowest matching index.
110
  let cand = select(SENTINEL_IDX, bestIdx, bestVal == m);
111
  let sgIdx = subgroupMin(cand);
112
+ {% endif %}
113
+ {% if singleSubgroup %}
114
  // The workgroup is one (possibly partial) subgroup: elect the writer rather
115
  // than assuming lane 0 is active.
116
  if (subgroupElect()) {
117
  y[row] = select(sgIdx, 0u, sgIdx == SENTINEL_IDX);
118
  }
119
+ {% else %}
120
  // Cross-subgroup fold that assumes nothing about which invocations share a
121
+ // subgroup or how many subgroups there are (it does require the uniform
122
+ // control flow this entry point already has): every invocation owns the slot
123
+ // at its own index, the elected lane
124
  // publishes its subgroup pair there and every other lane publishes the
125
  // identity pair (the sentinel index, which the merge skips). Each subgroup
126
+ // then folds all WG slots — lane `sgLane` walks slots sgLane, sgLane + sgSize,
127
+ // ... — and one more collective merges the lane partials, so every slot is
128
+ // merged exactly once at any legal width. The merge is commutative and
129
+ // associative (best value, then the tie-break index), so the fold order does
130
+ // not change the result.
131
  var winnerIdx = sgIdx;
132
  // A one-subgroup workgroup is already fully reduced by the collectives above.
133
  // The test reads the `subgroup_size` builtin, which is uniform; a collective's
134
  // result is not uniform to WGSL's analysis and may not guard a barrier.
135
  if (sgSize != WG) {
136
+ // The coordinates are the BUILTINS, never `subgroupExclusiveAdd(1u)` /
137
+ // `subgroupAdd(1u)`: those agree with them under this contract, but a driver
138
+ // in the wild answers a claim derived from them with zero and leaves most of
139
+ // the workgroup's slots unclaimed. Every lane of this entry point is active
140
+ // here, so the full subgroup width is the right stride.
141
+ let rank = sgLane;
142
+ let count = sgSize;
143
  let leader = rank == 0u;
144
  wgVal[tid] = select({{ identityVal }}, m, leader);
145
  wgIdx[tid] = select(SENTINEL_IDX, sgIdx, leader);
 
149
  for (var i = rank; i < WG; i = i + count) {
150
  let v = wgVal[i];
151
  let vi = wgIdx[i];
152
+ if (vi != SENTINEL_IDX && ({{ arg_wins("v", "vi", "outVal", "outIdx") }})) {
 
 
 
 
153
  outVal = v;
154
  outIdx = vi;
155
  }
156
  }
157
+ let foldVal = {{ sgReduce }}(outVal);
158
+ {% if selectLastIndex %}
159
  winnerIdx = subgroupMax(select(0u, outIdx, outVal == foldVal && outIdx != SENTINEL_IDX));
160
+ {% else %}
161
  winnerIdx = subgroupMin(select(SENTINEL_IDX, outIdx, outVal == foldVal));
162
+ {% endif %}
163
  }
164
  if (tid == 0u) {
165
  y[row] = select(winnerIdx, 0u, winnerIdx == SENTINEL_IDX);
166
  }
167
+ {% endif %}
168
  {% else %}
169
  // No-subgroup tier: workgroup barrier tree-reduction over the per-thread
170
  // (bestVal, bestIdx) pairs. Same ONNX tie rule (lowest index by default,
 
179
  let v = wgVal[tid + stride];
180
  let vi = wgIdx[tid + stride];
181
  let curIdx = wgIdx[tid];
182
+ if ({{ arg_merge_wins("v", "vi", "wgVal[tid]", "curIdx") }}) {
 
 
 
 
183
  wgVal[tid] = v;
184
  wgIdx[tid] = vi;
185
  }
 
189
  if (tid == 0u) {
190
  y[row] = select(wgIdx[0], 0u, wgIdx[0] == SENTINEL_IDX);
191
  }
192
+ {% endif %}
193
  }
build/webgpu/test.json CHANGED
@@ -864,9 +864,7 @@
864
  },
865
  {
866
  "name": "subgroup_min_vec4_rows128x256_ties_first",
867
- "provenance": {
868
- "notes": "Route-locks the one-minimum-subgroup vec4 path on variable-width adapters and verifies first-index tie ordering without a workgroup-memory combine."
869
- },
870
  "attrs": { "axis": 1, "keepdims": 0 },
871
  "inputs": { "x": { "dtype": "float32", "shape": [128, 256], "data": { "kind": "constant", "value": 7.0 } } },
872
  "outputs": {
@@ -875,9 +873,7 @@
875
  },
876
  {
877
  "name": "subgroup_min_scalar_rows128x257_ties_last",
878
- "provenance": {
879
- "notes": "Route-locks the unaligned one-minimum-subgroup path and verifies select_last_index ordering across all lane-local chunks."
880
- },
881
  "attrs": { "axis": 1, "keepdims": 0, "select_last_index": 1 },
882
  "inputs": { "x": { "dtype": "float32", "shape": [128, 257], "data": { "kind": "constant", "value": 7.0 } } },
883
  "outputs": {
@@ -1122,7 +1118,7 @@
1122
  {
1123
  "name": "last_axis_split_vec4_logits_ties_first",
1124
  "provenance": {
1125
- "notes": "A single 32768-element row route-locks the cooperative split-row vec4 reduction. Equal maxima span many split segments and must merge to the first global index."
1126
  },
1127
  "attrs": { "axis": 1, "keepdims": 0 },
1128
  "inputs": {
@@ -1146,7 +1142,7 @@
1146
  {
1147
  "name": "last_axis_split_vec4_i32_negative_rows",
1148
  "provenance": {
1149
- "notes": "int32 on the cooperative split-row vec4 reduction (32768 columns, two rows). Every value in the row is negative, so a lane identity of 0 instead of INT_MIN would beat the whole row and the merge would return the sentinel; this directly exercises the i32 identity and native signed compare. The period-7 cycle does not divide 32768, so the two rows are phase-shifted and their answers differ — a kernel that reduced row 0 for both rows cannot pass. ONNX ArgMax admits int32 via T and breaks ties on the first index."
1150
  },
1151
  "attrs": { "axis": 1, "keepdims": 0 },
1152
  "inputs": {
@@ -1163,7 +1159,7 @@
1163
  {
1164
  "name": "last_axis_split_scalar_u32_above_int32_max",
1165
  "provenance": {
1166
- "notes": "uint32 on the unaligned (32769-column) scalar split-row reduction. Six of the seven cycle values exceed 2^31, so a signed compare would rank them negative and answer [0, 5] instead of [3, 1]. This directly exercises the unsigned identity 0u and the partial store that must write the value through without bitcasting it. ONNX ArgMax admits uint32 via T; first index on ties."
1167
  },
1168
  "attrs": { "axis": 1, "keepdims": 0 },
1169
  "inputs": {
@@ -1264,7 +1260,7 @@
1264
  {
1265
  "name": "subgroup_min_scalar_keepdims_128x197_vit_tokens",
1266
  "provenance": {
1267
- "notes": "With 128 rows, the 197-element last axis satisfies the subgroup row floor but is not vec4-aligned, selecting the scalar subgroup-minimum reduction with default keepdims."
1268
  },
1269
  "attrs": { "axis": -1 },
1270
  "inputs": {
@@ -1275,6 +1271,122 @@
1275
  }
1276
  },
1277
  "outputs": { "y": { "dtype": "uint32", "shape": [128, 1], "tolerance": 0 } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1278
  }
1279
  ]
1280
  }
 
864
  },
865
  {
866
  "name": "subgroup_min_vec4_rows128x256_ties_first",
867
+ "provenance": { "notes": "Aligned 256-element rows with equal maxima verify first-index tie ordering." },
 
 
868
  "attrs": { "axis": 1, "keepdims": 0 },
869
  "inputs": { "x": { "dtype": "float32", "shape": [128, 256], "data": { "kind": "constant", "value": 7.0 } } },
870
  "outputs": {
 
873
  },
874
  {
875
  "name": "subgroup_min_scalar_rows128x257_ties_last",
876
+ "provenance": { "notes": "Unaligned 257-element rows with equal maxima verify last-index tie ordering." },
 
 
877
  "attrs": { "axis": 1, "keepdims": 0, "select_last_index": 1 },
878
  "inputs": { "x": { "dtype": "float32", "shape": [128, 257], "data": { "kind": "constant", "value": 7.0 } } },
879
  "outputs": {
 
1118
  {
1119
  "name": "last_axis_split_vec4_logits_ties_first",
1120
  "provenance": {
1121
+ "notes": "A 32,768-element row has equal maxima across distant segments; the result must be the first global index."
1122
  },
1123
  "attrs": { "axis": 1, "keepdims": 0 },
1124
  "inputs": {
 
1142
  {
1143
  "name": "last_axis_split_vec4_i32_negative_rows",
1144
  "provenance": {
1145
+ "notes": "A [2, 32768] int32 tensor's last axis cycles seven negative values (-9, -5, -3, -1, -2, -4, -6), so a zero-initialized accumulator would misreport the maximum. Since 32768 is not a multiple of 7, row 1 begins one step into the cycle, putting its first-occurrence maximum at column 2 versus row 0's column 3."
1146
  },
1147
  "attrs": { "axis": 1, "keepdims": 0 },
1148
  "inputs": {
 
1159
  {
1160
  "name": "last_axis_split_scalar_u32_above_int32_max",
1161
  "provenance": {
1162
+ "notes": "A [2, 32769] uint32 tensor's last axis cycles seven values; six of seven exceed 2^31 (up to 4,294,967,290, near UINT32_MAX), so a signed comparison would misorder them. Since 32769 is not a multiple of 7, row 1 begins two steps into the cycle, moving its maximum's column from 3 (row 0) to 1 (row 1)."
1163
  },
1164
  "attrs": { "axis": 1, "keepdims": 0 },
1165
  "inputs": {
 
1260
  {
1261
  "name": "subgroup_min_scalar_keepdims_128x197_vit_tokens",
1262
  "provenance": {
1263
+ "notes": "A 197-element last axis across 128 rows checks an unaligned reduction and default keepdims."
1264
  },
1265
  "attrs": { "axis": -1 },
1266
  "inputs": {
 
1271
  }
1272
  },
1273
  "outputs": { "y": { "dtype": "uint32", "shape": [128, 1], "tolerance": 0 } }
1274
+ },
1275
+ {
1276
+ "name": "ort_wide_last_axis_2x202048_keepdims_negative_axis",
1277
+ "provenance": {
1278
+ "source": "onnxruntime/test/providers/cpu/reduction/reduction_ops_test.cc",
1279
+ "test": "ReductionOpTest.ArgMax_float_wide_last_axis_keepdims",
1280
+ "notes": "Vocabulary-scale last axis with keepdims and a negative axis; upstream added it so the CUDA multi-block scratch path is covered end to end. 2 x 202048 float32 = 1.6 MB."
1281
+ },
1282
+ "attrs": { "axis": -1, "keepdims": 1 },
1283
+ "inputs": {
1284
+ "x": {
1285
+ "dtype": "float32",
1286
+ "shape": [2, 202048],
1287
+ "data": {
1288
+ "kind": "cycle",
1289
+ "values": [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5, 6.0, 6.5, 7.0, 7.5, 8.0, 8.5, 9.0, 9.5, 10.0, 10.5, 11.0, 11.5, 12.0, 12.5, 13.0, 13.5, 14.0, 14.5, 15.0, 15.5, 16.0, 16.5, 17.0, 17.5, 18.0, 18.5, 19.0, 19.5, 20.0, 20.5, 21.0, 21.5, 22.0, 22.5, 23.0, 23.5, 24.0, 24.5, 25.0, 25.5, 26.0, 26.5, 27.0, 27.5, 28.0, 28.5, 29.0, 29.5, 30.0, 30.5, 31.0, 31.5, 32.0, 32.5, 33.0, 33.5, 34.0, 34.5, 35.0, 35.5, 36.0, 36.5, 37.0, 37.5, 38.0, 38.5, 39.0, 39.5, 40.0, 40.5, 41.0, 41.5, 42.0, 42.5, 43.0, 43.5, 44.0, 44.5, 45.0, 45.5, 46.0, 46.5, 47.0, 47.5, 48.0]
1290
+ }
1291
+ }
1292
+ },
1293
+ "outputs": { "y": { "dtype": "uint32", "shape": [2, 1], "tolerance": 0 } }
1294
+ },
1295
+ {
1296
+ "name": "packed_axis_columns_8192x32_ties_first",
1297
+ "provenance": {
1298
+ "notes": "A [8192, 32] float32 tensor's axis=-2 cycles a 13-value pattern whose maximum, 8.0, repeats twice per period; every column sees that tie across its 8192 rows, and select_last_index=0 resolves each to the earliest matching row."
1299
+ },
1300
+ "tunables": { "WORKGROUP_SIZE": 16 },
1301
+ "attrs": { "axis": -2, "keepdims": 0, "select_last_index": 0 },
1302
+ "inputs": {
1303
+ "x": {
1304
+ "dtype": "float32",
1305
+ "shape": [8192, 32],
1306
+ "data": { "kind": "cycle", "values": [-3.0, 7.0, 7.0, -9.0, 2.0, 8.0, -9.0, 8.0, -1.0, 0.0, 2.0, 7.0, -9.0] }
1307
+ }
1308
+ },
1309
+ "outputs": { "y": { "dtype": "uint32", "shape": [32], "tolerance": 0 } }
1310
+ },
1311
+ {
1312
+ "name": "packed_axis_columns_8192x32_ties_last",
1313
+ "provenance": {
1314
+ "notes": "A [8192, 32] float32 tensor's axis=-2 cycles a 13-value pattern whose maximum, 8.0, repeats twice per period; every column sees that tie across its 8192 rows, and select_last_index=1 resolves each to the latest matching row."
1315
+ },
1316
+ "tunables": { "WORKGROUP_SIZE": 16 },
1317
+ "attrs": { "axis": -2, "keepdims": 0, "select_last_index": 1 },
1318
+ "inputs": {
1319
+ "x": {
1320
+ "dtype": "float32",
1321
+ "shape": [8192, 32],
1322
+ "data": { "kind": "cycle", "values": [-3.0, 7.0, 7.0, -9.0, 2.0, 8.0, -9.0, 8.0, -1.0, 0.0, 2.0, 7.0, -9.0] }
1323
+ }
1324
+ },
1325
+ "outputs": { "y": { "dtype": "uint32", "shape": [32], "tolerance": 0 } }
1326
+ },
1327
+ {
1328
+ "name": "packed_axis_columns_8193x28_ties_first",
1329
+ "provenance": {
1330
+ "notes": "A [8193, 28] float32 tensor's axis=-2 (one row more than the 8192-row case) cycles the same 13-value pattern, whose maximum 8.0 repeats twice per period; keepdims=1 keeps that axis as size 1 in a [1, 28] output, and select_last_index=0 resolves each column's tie to its earliest row."
1331
+ },
1332
+ "tunables": { "WORKGROUP_SIZE": 8 },
1333
+ "attrs": { "axis": -2, "keepdims": 1, "select_last_index": 0 },
1334
+ "inputs": {
1335
+ "x": {
1336
+ "dtype": "float32",
1337
+ "shape": [8193, 28],
1338
+ "data": { "kind": "cycle", "values": [-3.0, 7.0, 7.0, -9.0, 2.0, 8.0, -9.0, 8.0, -1.0, 0.0, 2.0, 7.0, -9.0] }
1339
+ }
1340
+ },
1341
+ "outputs": { "y": { "dtype": "uint32", "shape": [1, 28], "tolerance": 0 } }
1342
+ },
1343
+ {
1344
+ "name": "packed_axis_columns_8193x28_ties_last",
1345
+ "provenance": {
1346
+ "notes": "A [8193, 28] float32 tensor's axis=-2 (one row more than the 8192-row case) cycles the same 13-value pattern, whose maximum 8.0 repeats twice per period; keepdims=1 keeps that axis as size 1 in a [1, 28] output, and select_last_index=1 resolves each column's tie to its latest row."
1347
+ },
1348
+ "tunables": { "WORKGROUP_SIZE": 8 },
1349
+ "attrs": { "axis": -2, "keepdims": 1, "select_last_index": 1 },
1350
+ "inputs": {
1351
+ "x": {
1352
+ "dtype": "float32",
1353
+ "shape": [8193, 28],
1354
+ "data": { "kind": "cycle", "values": [-3.0, 7.0, 7.0, -9.0, 2.0, 8.0, -9.0, 8.0, -1.0, 0.0, 2.0, 7.0, -9.0] }
1355
+ }
1356
+ },
1357
+ "outputs": { "y": { "dtype": "uint32", "shape": [1, 28], "tolerance": 0 } }
1358
+ },
1359
+ {
1360
+ "name": "packed_axis_columns_2x8193x12_ties_first",
1361
+ "provenance": {
1362
+ "notes": "A [2, 8193, 12] float32 tensor's axis=-2 (the 8193-length middle axis) cycles the same 13-value pattern for each of the 2 batches' 12 columns; the maximum 8.0 repeats twice per period, and select_last_index=0 resolves each tie to its earliest row, giving a [2, 12] output."
1363
+ },
1364
+ "tunables": { "WORKGROUP_SIZE": 8 },
1365
+ "attrs": { "axis": -2, "keepdims": 0, "select_last_index": 0 },
1366
+ "inputs": {
1367
+ "x": {
1368
+ "dtype": "float32",
1369
+ "shape": [2, 8193, 12],
1370
+ "data": { "kind": "cycle", "values": [-3.0, 7.0, 7.0, -9.0, 2.0, 8.0, -9.0, 8.0, -1.0, 0.0, 2.0, 7.0, -9.0] }
1371
+ }
1372
+ },
1373
+ "outputs": { "y": { "dtype": "uint32", "shape": [2, 12], "tolerance": 0 } }
1374
+ },
1375
+ {
1376
+ "name": "packed_axis_columns_2x8193x12_ties_last",
1377
+ "provenance": {
1378
+ "notes": "A [2, 8193, 12] float32 tensor's axis=-2 (the 8193-length middle axis) cycles the same 13-value pattern for each of the 2 batches' 12 columns; the maximum 8.0 repeats twice per period, and select_last_index=1 resolves each tie to its latest row, giving a [2, 12] output."
1379
+ },
1380
+ "tunables": { "WORKGROUP_SIZE": 8 },
1381
+ "attrs": { "axis": -2, "keepdims": 0, "select_last_index": 1 },
1382
+ "inputs": {
1383
+ "x": {
1384
+ "dtype": "float32",
1385
+ "shape": [2, 8193, 12],
1386
+ "data": { "kind": "cycle", "values": [-3.0, 7.0, 7.0, -9.0, 2.0, 8.0, -9.0, 8.0, -1.0, 0.0, 2.0, 7.0, -9.0] }
1387
+ }
1388
+ },
1389
+ "outputs": { "y": { "dtype": "uint32", "shape": [2, 12], "tolerance": 0 } }
1390
  }
1391
  ]
1392
  }