Xenova HF Staff commited on
Commit
e93cfe0
·
verified ·
1 Parent(s): 18b1ad3

sync 91d990483a17

Browse files
README.md CHANGED
@@ -18,15 +18,15 @@ See the [ONNX `ReduceSum` spec](https://onnx.ai/onnx/operators/onnx__ReduceSum.h
18
 
19
  ## Inputs
20
 
21
- | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
22
  | --- | --- | --- | --- | --- | --- | --- |
23
- | `data` | `x` | `T` | — | — | The input tensor to reduce. | required |
24
 
25
  ## Outputs
26
 
27
- | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
28
  | --- | --- | --- | --- | --- | --- | --- |
29
- | `reduced` | `y` | `T` | derived | — | The summed output tensor, with reduced dimensions either kept as size 1 or removed. | required |
30
 
31
  ## Attributes
32
 
@@ -34,9 +34,9 @@ Default values (overridable per request):
34
 
35
  | Attribute | Default | Description |
36
  | --- | --- | --- |
 
37
  | `keepdims` | `1` | If 1 (default), retains reduced dimensions with size 1; if 0, removes them from the output shape. |
38
  | `noop_with_empty_axes` | `0` | When axes is empty, if 0 (default) reduce over all axes; if 1, treat as a no-op identity and return the input unchanged. |
39
- | `axes` | `[]` | Values of the optional ONNX `axes` tensor input, supplied through this request attribute; an empty list follows `noop_with_empty_axes`. |
40
 
41
  ## Type constraints
42
 
@@ -44,13 +44,21 @@ Default values (overridable per request):
44
  | --- | --- |
45
  | `T` | `float32`, `float16`, `int32` |
46
 
 
 
 
 
 
 
 
 
47
  ## Device requirements
48
 
49
  Some implementation variants require `subgroups`. These are route-specific capabilities, not package-wide requirements; availability also depends on the request shape and dtype.
50
 
51
  ## Files
52
 
53
- - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, provenance)
54
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
55
  - [`test.json`](build/webgpu/test.json) — correctness cases
56
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
@@ -60,21 +68,26 @@ Some implementation variants require `subgroups`. These are route-specific capab
60
  - [`reduce-axis0-splitk-reduce.wgsl.jinja`](build/webgpu/reduce-axis0-splitk-reduce.wgsl.jinja)
61
  - [`reduce-axis0-tilecols.wgsl.jinja`](build/webgpu/reduce-axis0-tilecols.wgsl.jinja)
62
  - [`reduce-flat-partial.wgsl.jinja`](build/webgpu/reduce-flat-partial.wgsl.jinja)
 
 
63
  - [`reduce-row-subgroup.wgsl.jinja`](build/webgpu/reduce-row-subgroup.wgsl.jinja)
64
  - [`reduce-row-tree.wgsl.jinja`](build/webgpu/reduce-row-tree.wgsl.jinja)
65
  - [`reduce-serial-axis.wgsl.jinja`](build/webgpu/reduce-serial-axis.wgsl.jinja)
66
 
67
  ## Use with `@huggingface/kernels`
68
 
69
- The loader automatically allocates outputs whose metadata it can derive from the manifest contract and this call.
 
 
 
 
70
 
71
- The explicit `outputs` entries provide shape and logical dtype metadata for the results listed below:
72
 
73
  - `y`
74
 
75
- Each entry either requests an optional result or supplies metadata that cannot be inferred from the inputs.
76
-
77
  The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
 
78
 
79
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
80
 
 
18
 
19
  ## Inputs
20
 
21
+ | Name | Upstream name | Logical dtype | Rank | Shape | Description | Presence |
22
  | --- | --- | --- | --- | --- | --- | --- |
23
+ | `x` | `data` | `T` | — | — | The input tensor to reduce. | required |
24
 
25
  ## Outputs
26
 
27
+ | Name | Upstream name | Logical dtype | Rank | Shape | Description | Presence |
28
  | --- | --- | --- | --- | --- | --- | --- |
29
+ | `y` | `reduced` | `T` | derived | — | The summed output tensor, with reduced dimensions either kept as size 1 or removed. | required |
30
 
31
  ## Attributes
32
 
 
34
 
35
  | Attribute | Default | Description |
36
  | --- | --- | --- |
37
+ | `axes` | `[]` | Values of the optional ONNX `axes` tensor input, supplied through this request attribute; an empty list follows `noop_with_empty_axes`. |
38
  | `keepdims` | `1` | If 1 (default), retains reduced dimensions with size 1; if 0, removes them from the output shape. |
39
  | `noop_with_empty_axes` | `0` | When axes is empty, if 0 (default) reduce over all axes; if 1, treat as a no-op identity and return the input unchanged. |
 
40
 
41
  ## Type constraints
42
 
 
44
  | --- | --- |
45
  | `T` | `float32`, `float16`, `int32` |
46
 
47
+ ## Implementation variants
48
+
49
+ One implementation is selected per call from the device capabilities, the request shapes and the dtypes; these notes say what each one covers.
50
+
51
+ - `axis0_splitk_i32` — Partitions a long rank-two axis-zero integer reduction across workgroups and combines exact int32 partials. It applies when the reduced row dimension is too large for one pass to expose enough parallelism.
52
+ - `subgroup_last_axis_vec4` — Reduces each contiguous last-axis row with subgroup collectives and vec4-packed reads.
53
+ - `subgroup_last_axis` — Reduces each contiguous last-axis row with subgroup collectives and scalar reads for an unaligned row width.
54
+
55
  ## Device requirements
56
 
57
  Some implementation variants require `subgroups`. These are route-specific capabilities, not package-wide requirements; availability also depends on the request shape and dtype.
58
 
59
  ## Files
60
 
61
+ - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, per-variant templates, provenance)
62
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
63
  - [`test.json`](build/webgpu/test.json) — correctness cases
64
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
 
68
  - [`reduce-axis0-splitk-reduce.wgsl.jinja`](build/webgpu/reduce-axis0-splitk-reduce.wgsl.jinja)
69
  - [`reduce-axis0-tilecols.wgsl.jinja`](build/webgpu/reduce-axis0-tilecols.wgsl.jinja)
70
  - [`reduce-flat-partial.wgsl.jinja`](build/webgpu/reduce-flat-partial.wgsl.jinja)
71
+ - [`reduce-multi-axis-coop.wgsl.jinja`](build/webgpu/reduce-multi-axis-coop.wgsl.jinja)
72
+ - [`reduce-row-subgroup-rows.wgsl.jinja`](build/webgpu/reduce-row-subgroup-rows.wgsl.jinja)
73
  - [`reduce-row-subgroup.wgsl.jinja`](build/webgpu/reduce-row-subgroup.wgsl.jinja)
74
  - [`reduce-row-tree.wgsl.jinja`](build/webgpu/reduce-row-tree.wgsl.jinja)
75
  - [`reduce-serial-axis.wgsl.jinja`](build/webgpu/reduce-serial-axis.wgsl.jinja)
76
 
77
  ## Use with `@huggingface/kernels`
78
 
79
+ ```sh
80
+ npm install --save-exact @huggingface/kernels@0.0.1-preview.2
81
+ ```
82
+
83
+ Outputs with inferable metadata are allocated automatically. Explicit `outputs` entries request optional results or provide metadata that cannot be inferred from the supplied inputs and attributes.
84
 
85
+ This example supplies explicit metadata for:
86
 
87
  - `y`
88
 
 
 
89
  The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
90
+ It follows the `v1` branch as fixes land. To pin exact artifact bytes, pass a 40-character commit `revision` instead of `version`.
91
 
92
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
93
 
build/webgpu/bench.json CHANGED
@@ -1,5 +1,4 @@
1
  {
2
- "op": "ai.onnx.ReduceSum",
3
  "tunableSpace": { "WORKGROUP_SIZE": [64, 128, 256] },
4
  "cases": [
5
  {
@@ -143,7 +142,7 @@
143
  "name": "reducesum-rank3-spatial-axes12-f32-128x256x256-pathology",
144
  "preset": "stress",
145
  "provenance": {
146
- "source": "authored for branch coverage",
147
  "notes": "Rank-3 multi-axis f32 reduction with 128 output lanes, each serially scanning a 256x256 plane."
148
  },
149
  "attrs": { "axes": [1, 2], "keepdims": 1 },
@@ -155,7 +154,7 @@
155
  "name": "reducesum-rank3-spatial-axes12-f16-128x256x256-pathology",
156
  "preset": "stress",
157
  "provenance": {
158
- "source": "authored for branch coverage",
159
  "notes": "Rank-3 multi-axis f16 reduction with 128 output lanes, each serially scanning a 256x256 plane."
160
  },
161
  "attrs": { "axes": [1, 2], "keepdims": 1 },
@@ -167,7 +166,7 @@
167
  "name": "reducesum-rank4-spatial-axes23-f32-2x64x256x256-pathology",
168
  "preset": "stress",
169
  "provenance": {
170
- "source": "authored for branch coverage",
171
  "notes": "Rank-4 multi-axis f32 reduction with 128 output lanes, each serially scanning a 256x256 plane."
172
  },
173
  "attrs": { "axes": [2, 3], "keepdims": 1 },
@@ -201,6 +200,44 @@
201
  "inputs": { "x": { "shape": [8, 65536, 64], "dtype": "float32", "dist": "normal", "seed": 118, "scale": 0.2 } },
202
  "outputs": { "y": { "shape": [8, 64], "dtype": "float32" } },
203
  "bench": { "metrics": [{ "type": "bandwidth", "value": "args.numel * 4" }] }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  }
205
  ]
206
  }
 
1
  {
 
2
  "tunableSpace": { "WORKGROUP_SIZE": [64, 128, 256] },
3
  "cases": [
4
  {
 
142
  "name": "reducesum-rank3-spatial-axes12-f32-128x256x256-pathology",
143
  "preset": "stress",
144
  "provenance": {
145
+ "source": "repository-authored",
146
  "notes": "Rank-3 multi-axis f32 reduction with 128 output lanes, each serially scanning a 256x256 plane."
147
  },
148
  "attrs": { "axes": [1, 2], "keepdims": 1 },
 
154
  "name": "reducesum-rank3-spatial-axes12-f16-128x256x256-pathology",
155
  "preset": "stress",
156
  "provenance": {
157
+ "source": "repository-authored",
158
  "notes": "Rank-3 multi-axis f16 reduction with 128 output lanes, each serially scanning a 256x256 plane."
159
  },
160
  "attrs": { "axes": [1, 2], "keepdims": 1 },
 
166
  "name": "reducesum-rank4-spatial-axes23-f32-2x64x256x256-pathology",
167
  "preset": "stress",
168
  "provenance": {
169
+ "source": "repository-authored",
170
  "notes": "Rank-4 multi-axis f32 reduction with 128 output lanes, each serially scanning a 256x256 plane."
171
  },
172
  "attrs": { "axes": [2, 3], "keepdims": 1 },
 
200
  "inputs": { "x": { "shape": [8, 65536, 64], "dtype": "float32", "dist": "normal", "seed": 118, "scale": 0.2 } },
201
  "outputs": { "y": { "shape": [8, 64], "dtype": "float32" } },
202
  "bench": { "metrics": [{ "type": "bandwidth", "value": "args.numel * 4" }] }
203
+ },
204
+ {
205
+ "name": "reducesum-rank4-axes023-f32-8x256x56x56-noncontiguous-pathology",
206
+ "preset": "stress",
207
+ "provenance": {
208
+ "notes": "Rank-4 reduction over axes {0,2,3} (per-channel statistics over batch and space with the channel axis second): the kept axis sits in the middle, so the reduced elements are not a contiguous suffix and the multi-axis rank-4 kernel launches one serial lane per output, 256 lanes each walking 25088 strided inputs. Its control reduces the same values with the channel axis first."
209
+ },
210
+ "vars": { "dtype": "float32", "batch": 8, "channels": 256, "height": 56, "width": 56 },
211
+ "attrs": { "axes": [0, 2, 3], "keepdims": 0 },
212
+ "inputs": { "x": { "shape": [8, 256, 56, 56], "dtype": "float32", "dist": "normal", "seed": 930, "scale": 0.2 } },
213
+ "outputs": { "y": { "shape": [256], "dtype": "float32", "dist": "empty" } },
214
+ "bench": {
215
+ "metrics": [
216
+ {
217
+ "type": "bandwidth",
218
+ "value": "args.batch * args.channels * args.height * args.width * dtypeBytes(args.dtype)"
219
+ }
220
+ ]
221
+ }
222
+ },
223
+ {
224
+ "name": "reducesum-rank4-axes123-f32-256x8x56x56-contiguous-suffix-control",
225
+ "preset": "stress",
226
+ "provenance": {
227
+ "notes": "Contiguous-suffix control for the axes {0,2,3} reduction: the same 256 reductions of 25088 elements with the kept axis first, which the cooperative suffix reduction covers with one workgroup per output."
228
+ },
229
+ "vars": { "dtype": "float32", "batch": 256, "channels": 8, "height": 56, "width": 56 },
230
+ "attrs": { "axes": [1, 2, 3], "keepdims": 0 },
231
+ "inputs": { "x": { "shape": [256, 8, 56, 56], "dtype": "float32", "dist": "normal", "seed": 931, "scale": 0.2 } },
232
+ "outputs": { "y": { "shape": [256], "dtype": "float32", "dist": "empty" } },
233
+ "bench": {
234
+ "metrics": [
235
+ {
236
+ "type": "bandwidth",
237
+ "value": "args.batch * args.channels * args.height * args.width * dtypeBytes(args.dtype)"
238
+ }
239
+ ]
240
+ }
241
  }
242
  ]
243
  }
build/webgpu/manifest.json CHANGED
@@ -2,469 +2,246 @@
2
  "domain": "ai.onnx",
3
  "name": "ReduceSum",
4
  "sinceVersion": 13,
5
- "description": "Computes the sum of elements along specified axes of the input tensor. The output rank matches the input rank when `keepdims` is 1; otherwise reduced dimensions are pruned. When no axes are provided, behavior is controlled by `noop_with_empty_axes`: reduce over all axes (default) or act as identity.",
6
- "inputs": [{ "role": "data", "dtype": "T", "description": "The input tensor to reduce." }],
7
- "outputs": [
8
- {
9
- "role": "reduced",
10
  "dtype": "T",
11
- "rank": "ranks.data if attrs.keepdims == 1 or ((attrs.axes | length) == 0 and attrs.noop_with_empty_axes == 1) else (ranks.data - (attrs.axes | length) if (attrs.axes | length) > 0 else 0)",
12
- "description": "The summed output tensor, with reduced dimensions either kept as size 1 or removed."
13
  }
14
- ],
15
- "attributes": { "keepdims": 1, "noop_with_empty_axes": 0, "axes": [] },
16
- "attributeDescriptions": {
17
- "keepdims": "If 1 (default), retains reduced dimensions with size 1; if 0, removes them from the output shape.",
18
- "noop_with_empty_axes": "When axes is empty, if 0 (default) reduce over all axes; if 1, treat as a no-op identity and return the input unchanged.",
19
- "axes": "Values of the optional ONNX `axes` tensor input, supplied through this request attribute; an empty list follows `noop_with_empty_axes`."
20
  },
 
21
  "attributeConstraints": { "keepdims": { "values": [0, 1] }, "noop_with_empty_axes": { "values": [0, 1] } },
22
  "typeConstraints": { "T": ["float32", "float16", "int32"] },
23
- "args": {
24
- "x": { "kind": "tensor", "semantic": "data", "role": "input" },
25
- "y": { "kind": "tensor", "semantic": "reduced", "role": "output" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  },
27
  "derive": {
28
  "deviceWorkgroupCap": "min(device.limits.maxComputeInvocationsPerWorkgroup, device.limits.maxComputeWorkgroupSizeX)",
29
  "reduceWorkgroupSize": "min(tunables.WORKGROUP_SIZE, deviceWorkgroupCap)",
30
  "treeWorkgroupOk": "reduceWorkgroupSize > 0 and pow2ceil(reduceWorkgroupSize) == reduceWorkgroupSize and reduceWorkgroupSize * dtypeBytes(\"float32\") <= device.limits.maxComputeWorkgroupStorageSize",
31
  "subgroupWorkgroupFloor": "min(reduceWorkgroupSize, max(1, device.adapterInfo.subgroupMaxSize))",
32
- "lastAxisRows": "rows(shapes.data, ranks.data - 1) if ranks.data > 0 else 1",
33
- "lastAxisCols": "dim(shapes.data, ranks.data - 1) if ranks.data > 0 else 1",
34
  "rowSerialPreferred": "lastAxisRows >= tunables.ROW_SERIAL_MIN_ROWS and lastAxisCols <= tunables.ROW_SERIAL_MAX_COLS",
35
- "axis0Rows": "dim(shapes.data, 0) if ranks.data >= 2 else 0",
36
- "axis0Cols": "dim(shapes.data, 1) if ranks.data >= 2 else 0",
37
  "axis0SplitCount": "min(tunables.AXIS0_MAX_SPLITS, pow2ceil(ceilDiv(axis0Rows, tunables.AXIS0_SPLIT_TARGET_ROWS)))",
38
  "axis0SplitScratchBytes": "axis0SplitCount * axis0Cols * dtypeBytes(\"float32\")",
39
- "axis0SplitPathFits": "axis0SplitCount <= device.limits.maxComputeWorkgroupsPerDimension and ceilDiv(ceilDiv(axis0Cols, reduceWorkgroupSize), device.limits.maxComputeWorkgroupsPerDimension) <= device.limits.maxComputeWorkgroupsPerDimension and axis0SplitScratchBytes <= device.limits.maxStorageBufferBindingSize and axis0SplitScratchBytes <= device.limits.maxBufferSize",
40
- "reduceAxis": "(attrs.axes[0] + ranks.data if attrs.axes[0] < 0 else attrs.axes[0]) if ((attrs.axes | length) == 1 and isUniqueIntList(attrs.axes, 0 - ranks.data, ranks.data, 1)) else ranks.data",
41
- "axisSplitDim": "dim(shapes.data, reduceAxis) if ranks.data >= 2 and reduceAxis < ranks.data else 0",
42
- "axisSplitInner": "inner(shapes.data, reduceAxis) if ranks.data >= 2 and reduceAxis < ranks.data else 1",
43
- "axisSplitOutputs": "numel(shapes.reduced)",
44
  "axisSplitCount": "min(tunables.AXIS0_MAX_SPLITS, pow2ceil(ceilDiv(axisSplitDim, tunables.AXIS0_SPLIT_TARGET_ROWS)))",
45
  "axisSplitScratchBytes": "axisSplitCount * axisSplitOutputs * 4",
46
- "axisSplitPathFits": "axisSplitCount <= device.limits.maxComputeWorkgroupsPerDimension and ceilDiv(ceilDiv(axisSplitOutputs, reduceWorkgroupSize), device.limits.maxComputeWorkgroupsPerDimension) <= device.limits.maxComputeWorkgroupsPerDimension and axisSplitScratchBytes <= device.limits.maxStorageBufferBindingSize and axisSplitScratchBytes <= device.limits.maxBufferSize",
47
  "axis0TilePathFits": "treeWorkgroupOk and tunables.AXIS0_TILE_COLS > 0 and tunables.AXIS0_TILE_COLS <= reduceWorkgroupSize and reduceWorkgroupSize % tunables.AXIS0_TILE_COLS == 0",
48
- "flatItems": "floor(numel(shapes.data) / tunables.VECTOR_WIDTH)",
49
  "flatSplitCount": "max(1, min(tunables.FULL_REDUCE_MAX_SPLITS, ceilDiv(flatItems, reduceWorkgroupSize)))",
50
  "flatScratchBytes": "flatSplitCount * dtypeBytes(\"float32\")",
51
- "flatPathFits": "treeWorkgroupOk and flatSplitCount <= device.limits.maxComputeWorkgroupsPerDimension and flatScratchBytes <= device.limits.maxStorageBufferBindingSize and flatScratchBytes <= device.limits.maxBufferSize",
52
- "flatParallelCovered": "(dtypes.T == \"f32\" or dtypes.T == \"f16\") and f16Ok(dtypes.T) and numel(shapes.reduced) == 1 and numel(shapes.data) >= tunables.FULL_REDUCE_MIN_ELEMENTS and flatPathFits",
53
- "contiguousSuffixParallelCovered": "(dtypes.T == \"f32\" or dtypes.T == \"f16\") and f16Ok(dtypes.T) and numel(shapes.reduced) > 0 and numel(shapes.data) % numel(shapes.reduced) == 0 and numel(shapes.data) / numel(shapes.reduced) >= tunables.CONTIGUOUS_SUFFIX_MIN_COLS and ((ranks.data == 3 and hasAxis(attrs.axes, 0, 3) == false and hasAxis(attrs.axes, 1, 3) and hasAxis(attrs.axes, 2, 3) and numel(shapes.reduced) == dim(shapes.data, 0)) or (ranks.data == 4 and hasAxis(attrs.axes, 0, 4) == false and hasAxis(attrs.axes, 1, 4) == false and hasAxis(attrs.axes, 2, 4) and hasAxis(attrs.axes, 3, 4) and numel(shapes.reduced) == dim(shapes.data, 0) * dim(shapes.data, 1)) or (ranks.data == 4 and hasAxis(attrs.axes, 0, 4) == false and hasAxis(attrs.axes, 1, 4) and hasAxis(attrs.axes, 2, 4) and hasAxis(attrs.axes, 3, 4) and numel(shapes.reduced) == dim(shapes.data, 0)))"
54
- },
55
- "tunables": {
56
- "WORKGROUP_SIZE": 256,
57
- "VECTOR_WIDTH": 4,
58
- "ROW_PARALLEL_MIN_COLS": 64,
59
- "SUBGROUP_MIN_COLS": 256,
60
- "SUBGROUP_SMALL_ROW_LIMIT": 32768,
61
- "AXIS0_SPLIT_MIN_ROWS": 8192,
62
- "AXIS0_SPLIT_TARGET_ROWS": 256,
63
- "AXIS0_MAX_SPLITS": 128,
64
- "AXIS0_TILE_MIN_ROWS": 64,
65
- "AXIS0_TILE_MIN_COLS": 16,
66
- "AXIS0_TILE_COLS": 16,
67
- "AXIS_SPLIT_TILE_COLS": 8,
68
- "FULL_REDUCE_MIN_ELEMENTS": 8192,
69
- "FULL_REDUCE_MAX_SPLITS": 256,
70
- "CONTIGUOUS_SUFFIX_MIN_COLS": 256,
71
- "ROW_SERIAL_MIN_ROWS": 8192,
72
- "ROW_SERIAL_MAX_COLS": 1024
73
- },
74
- "tunableDescriptions": {
75
- "SUBGROUP_SMALL_ROW_LIMIT": "Sets the row-count threshold that demotes subgroup last-axis reduction when each row is shorter than `SUBGROUP_MIN_COLS`.",
76
- "AXIS0_SPLIT_MIN_ROWS": "Sets the minimum reduced-row count admitted to axis-zero split reduction.",
77
- "AXIS0_SPLIT_TARGET_ROWS": "Sets the target reduced-row count per partial; the selector rounds the resulting split count up to a power of two.",
78
- "AXIS0_MAX_SPLITS": "Caps the partial count for axis-zero and general axis-split reductions, bounding dispatch and scratch.",
79
- "FULL_REDUCE_MAX_SPLITS": "Caps the partial count emitted by the flat all-axes reduction.",
80
- "ROW_SERIAL_MIN_ROWS": "Sets the minimum row count for the many-short-rows region that prefers one serial reducer per row.",
81
- "ROW_SERIAL_MAX_COLS": "Sets the maximum row width for the many-short-rows region that prefers one serial reducer per row."
82
  },
83
- "bindingSets": {
84
- "lastAxisVec4": [
85
- {
86
- "name": "x",
87
- "arg": "x",
88
- "semantic": "data",
89
- "buffer": { "type": "read-only-storage" },
90
- "elementType": "$vectorScalar"
91
- },
92
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
93
- {
94
- "name": "params",
95
- "semantic": "kernel.params",
96
- "buffer": { "type": "uniform" },
97
- "struct": {
98
- "name": "Params",
99
- "fields": [
100
- { "name": "rows", "type": "u32", "value": "rows(shapes.data, ranks.data - 1)" },
101
- { "name": "chunkCount", "type": "u32", "value": "dim(shapes.data, ranks.data - 1) / tunables.VECTOR_WIDTH" }
102
- ]
103
- }
104
- }
105
- ],
106
- "lastAxisScalar": [
107
- { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
108
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
109
- {
110
- "name": "params",
111
- "semantic": "kernel.params",
112
- "buffer": { "type": "uniform" },
113
- "struct": {
114
- "name": "Params",
115
- "fields": [
116
- { "name": "rows", "type": "u32", "value": "rows(shapes.data, ranks.data - 1)" },
117
- { "name": "cols", "type": "u32", "value": "dim(shapes.data, ranks.data - 1)" }
118
- ]
119
- }
120
- }
121
- ],
122
- "lastAxisScalarSubgroup": [
123
- { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
124
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
125
- {
126
- "name": "params",
127
- "semantic": "kernel.params",
128
- "buffer": { "type": "uniform" },
129
- "struct": {
130
- "name": "Params",
131
- "fields": [
132
- { "name": "rows", "type": "u32", "value": "rows(shapes.data, ranks.data - 1)" },
133
- { "name": "chunkCount", "type": "u32", "value": "dim(shapes.data, ranks.data - 1)" }
134
- ]
135
- }
136
- }
137
- ],
138
- "scalar": [
139
- { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
140
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
141
- {
142
- "name": "params",
143
- "semantic": "kernel.params",
144
- "buffer": { "type": "uniform" },
145
- "struct": {
146
- "name": "Params",
147
- "fields": [
148
- { "name": "rows", "type": "u32", "value": "1" },
149
- { "name": "cols", "type": "u32", "value": "1" },
150
- { "name": "outCount", "type": "u32", "value": "1" }
151
- ]
152
- }
153
- }
154
- ],
155
- "rank1Axis0": [
156
- { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
157
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
158
- {
159
- "name": "params",
160
- "semantic": "kernel.params",
161
- "buffer": { "type": "uniform" },
162
- "struct": {
163
- "name": "Params",
164
- "fields": [
165
- { "name": "rows", "type": "u32", "value": "dim(shapes.data, 0)" },
166
- { "name": "cols", "type": "u32", "value": "1" },
167
- { "name": "outCount", "type": "u32", "value": "numel(shapes.reduced)" }
168
- ]
169
- }
170
- }
171
- ],
172
- "rank2Serial": [
173
- { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
174
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
175
- {
176
- "name": "params",
177
- "semantic": "kernel.params",
178
- "buffer": { "type": "uniform" },
179
- "struct": {
180
- "name": "Params",
181
- "fields": [
182
- { "name": "rows", "type": "u32", "value": "dim(shapes.data, 0)" },
183
- { "name": "cols", "type": "u32", "value": "dim(shapes.data, 1)" },
184
- { "name": "outCount", "type": "u32", "value": "numel(shapes.reduced)" }
185
- ]
186
- }
187
- }
188
- ],
189
- "rank2SerialAxis1": [
190
- { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
191
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
192
- {
193
- "name": "params",
194
- "semantic": "kernel.params",
195
- "buffer": { "type": "uniform" },
196
- "struct": {
197
- "name": "Params",
198
- "fields": [
199
- { "name": "cols", "type": "u32", "value": "dim(shapes.data, 1)" },
200
- { "name": "outCount", "type": "u32", "value": "numel(shapes.reduced)" }
201
- ]
202
- }
203
- }
204
- ],
205
- "axis0Parallel": [
206
- { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
207
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
208
- {
209
- "name": "params",
210
- "semantic": "kernel.params",
211
- "buffer": { "type": "uniform" },
212
- "struct": {
213
- "name": "Params",
214
- "fields": [
215
- { "name": "rows", "type": "u32", "value": "dim(shapes.data, 0)" },
216
- { "name": "cols", "type": "u32", "value": "dim(shapes.data, 1)" }
217
- ]
218
- }
219
- }
220
- ],
221
- "axisSplitReduce": [
222
- { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
223
- { "name": "partials", "semantic": "partials", "buffer": { "type": "storage" }, "elementType": "$partialElement" },
224
- {
225
- "name": "params",
226
- "semantic": "kernel.params",
227
- "buffer": { "type": "uniform" },
228
- "struct": {
229
- "name": "Params",
230
- "fields": [
231
- { "name": "axisDim", "type": "u32", "value": "axisSplitDim" },
232
- { "name": "inner", "type": "u32", "value": "axisSplitInner" },
233
- { "name": "outputs", "type": "u32", "value": "axisSplitOutputs" }
234
- ]
235
- }
236
- }
237
- ],
238
- "axisSplitCombine": [
239
- {
240
- "name": "partials",
241
- "semantic": "partials",
242
- "buffer": { "type": "read-only-storage" },
243
- "elementType": "$partialElement"
244
- },
245
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
246
- {
247
- "name": "params",
248
- "semantic": "kernel.params",
249
- "buffer": { "type": "uniform" },
250
- "struct": { "name": "Params", "fields": [{ "name": "cols", "type": "u32", "value": "axisSplitOutputs" }] }
251
- }
252
- ],
253
- "axis0SplitReduce": [
254
- { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
255
- { "name": "partials", "semantic": "partials", "buffer": { "type": "storage" }, "elementType": "$partialElement" },
256
- {
257
- "name": "params",
258
- "semantic": "kernel.params",
259
- "buffer": { "type": "uniform" },
260
- "struct": {
261
- "name": "Params",
262
- "fields": [
263
- { "name": "rows", "type": "u32", "value": "dim(shapes.data, 0)" },
264
- { "name": "cols", "type": "u32", "value": "dim(shapes.data, 1)" }
265
- ]
266
- }
267
- }
268
- ],
269
- "axis0SplitCombine": [
270
- {
271
- "name": "partials",
272
- "semantic": "partials",
273
- "buffer": { "type": "read-only-storage" },
274
- "elementType": "$partialElement"
275
- },
276
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
277
- {
278
- "name": "params",
279
- "semantic": "kernel.params",
280
- "buffer": { "type": "uniform" },
281
- "struct": { "name": "Params", "fields": [{ "name": "cols", "type": "u32", "value": "dim(shapes.data, 1)" }] }
282
- }
283
- ],
284
- "elementwise": [
285
- { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
286
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
287
- {
288
- "name": "params",
289
- "semantic": "kernel.params",
290
- "buffer": { "type": "uniform" },
291
- "struct": { "name": "Params", "fields": [{ "name": "count", "type": "u32", "value": "numel(shapes.reduced)" }] }
292
- }
293
- ],
294
- "fullReduceSerial": [
295
- { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
296
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
297
- {
298
- "name": "params",
299
- "semantic": "kernel.params",
300
- "buffer": { "type": "uniform" },
301
- "struct": {
302
- "name": "Params",
303
- "fields": [
304
- { "name": "rows", "type": "u32", "value": "numel(shapes.data)" },
305
- { "name": "cols", "type": "u32", "value": "1" },
306
- { "name": "outCount", "type": "u32", "value": "numel(shapes.reduced)" }
307
- ]
308
- }
309
- }
310
- ],
311
- "rankNAxis": [
312
- { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
313
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
314
- {
315
- "name": "params",
316
- "semantic": "kernel.params",
317
- "buffer": { "type": "uniform" },
318
- "struct": {
319
- "name": "Params",
320
- "fields": [
321
- { "name": "axisDim", "type": "u32", "value": "axisSplitDim" },
322
- { "name": "outCount", "type": "u32", "value": "numel(shapes.reduced)" }
323
- ]
324
- }
325
- }
326
- ],
327
- "flatPartial": [
328
- { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
329
- { "name": "partials", "semantic": "partials", "buffer": { "type": "storage" }, "elementType": "$partialElement" },
330
- {
331
- "name": "params",
332
- "semantic": "kernel.params",
333
- "buffer": { "type": "uniform" },
334
- "struct": {
335
- "name": "Params",
336
- "fields": [
337
- { "name": "count4", "type": "u32", "value": "floor(numel(shapes.data) / tunables.VECTOR_WIDTH)" },
338
- { "name": "numel", "type": "u32", "value": "numel(shapes.data)" }
339
- ]
340
- }
341
- }
342
- ],
343
- "flatCombine": [
344
- {
345
- "name": "partials",
346
- "semantic": "partials",
347
- "buffer": { "type": "read-only-storage" },
348
- "elementType": "$partialElement"
349
- },
350
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
351
- {
352
- "name": "params",
353
- "semantic": "kernel.params",
354
- "buffer": { "type": "uniform" },
355
- "struct": { "name": "Params", "fields": [{ "name": "cols", "type": "u32", "value": "1" }] }
356
- }
357
- ],
358
- "suffixVec4": [
359
- {
360
- "name": "x",
361
- "arg": "x",
362
- "semantic": "data",
363
- "buffer": { "type": "read-only-storage" },
364
- "elementType": "$vectorScalar"
365
- },
366
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
367
- {
368
- "name": "params",
369
- "semantic": "kernel.params",
370
- "buffer": { "type": "uniform" },
371
- "struct": {
372
- "name": "Params",
373
- "fields": [
374
- { "name": "rows", "type": "u32", "value": "numel(shapes.reduced)" },
375
- {
376
- "name": "chunkCount",
377
- "type": "u32",
378
- "value": "numel(shapes.data) / numel(shapes.reduced) / tunables.VECTOR_WIDTH"
379
- }
380
- ]
381
- }
382
- }
383
- ],
384
- "suffixScalar": [
385
- { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
386
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
387
- {
388
- "name": "params",
389
- "semantic": "kernel.params",
390
- "buffer": { "type": "uniform" },
391
- "struct": {
392
- "name": "Params",
393
- "fields": [
394
- { "name": "rows", "type": "u32", "value": "numel(shapes.reduced)" },
395
- { "name": "cols", "type": "u32", "value": "numel(shapes.data) / numel(shapes.reduced)" }
396
- ]
397
- }
398
- }
399
- ],
400
- "multiAxis": [
401
- { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
402
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
403
- {
404
- "name": "params",
405
- "semantic": "kernel.params",
406
- "buffer": { "type": "uniform" },
407
- "struct": {
408
- "name": "Params",
409
- "fields": [{ "name": "outCount", "type": "u32", "value": "numel(shapes.reduced)" }]
410
- }
411
- }
412
- ]
413
  },
414
  "variants": [
415
  {
416
  "id": "contiguous_suffix_subgroup_vec4",
417
  "priority": 30,
 
418
  "requires": { "features": ["subgroups"] },
419
- "when": ["device.wgslLanguageFeatures.has(\"subgroup_id\")", "not flatParallelCovered", "contiguousSuffixParallelCovered", "(numel(shapes.data) / numel(shapes.reduced)) % tunables.VECTOR_WIDTH == 0"],
420
- "constants": {
421
  "scalar": "dtypes.T",
422
  "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
423
- "workgroupSize": "min(reduceWorkgroupSize, max(subgroupWorkgroupFloor, pow2ceil(ceilDiv(numel(shapes.data) / numel(shapes.reduced), tunables.VECTOR_WIDTH))))"
424
  },
425
  "passes": [
426
  {
427
  "id": "main",
428
  "name": "ReduceSum.ContiguousSuffixSubgroupVec4",
429
- "source": {
430
- "shader": "reduce-row-subgroup.wgsl.jinja",
431
- "inputs": {
432
- "op": "\"sum\"",
433
- "vec4": true,
434
- "castF32": "dtypes.T == \"f16\"",
435
- "usesF16": "dtypes.T == \"f16\""
436
- }
437
  },
438
- "subgroupCollectivesWidth": "portable",
439
- "bindings": "suffixVec4",
440
- "dispatch": { "workgroups": "numel(shapes.reduced)" }
441
  }
442
  ]
443
  },
444
  {
445
  "id": "contiguous_suffix_tree_vec4",
446
  "priority": 22,
447
- "when": ["not flatParallelCovered", "contiguousSuffixParallelCovered", "(numel(shapes.data) / numel(shapes.reduced)) % tunables.VECTOR_WIDTH == 0", "treeWorkgroupOk"],
448
- "constants": {
449
  "scalar": "dtypes.T",
450
  "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
451
- "workgroupSize": "min(reduceWorkgroupSize, pow2ceil(ceilDiv(numel(shapes.data) / numel(shapes.reduced), tunables.VECTOR_WIDTH)))"
452
  },
453
  "passes": [
454
  {
455
  "id": "main",
456
  "name": "ReduceSum.ContiguousSuffixTreeVec4",
457
- "source": {
458
- "shader": "reduce-row-tree.wgsl.jinja",
459
- "inputs": {
460
- "op": "\"sum\"",
461
- "vec4": true,
462
- "castF32": "dtypes.T == \"f16\"",
463
- "usesF16": "dtypes.T == \"f16\""
464
- }
465
  },
466
- "bindings": "suffixVec4",
467
- "dispatch": { "workgroups": "numel(shapes.reduced)" }
468
  }
469
  ]
470
  },
@@ -472,89 +249,174 @@
472
  "id": "contiguous_suffix_tree",
473
  "priority": 21,
474
  "when": ["not flatParallelCovered", "contiguousSuffixParallelCovered", "treeWorkgroupOk"],
475
- "constants": {
476
- "workgroupSize": "min(reduceWorkgroupSize, pow2ceil(numel(shapes.data) / numel(shapes.reduced)))",
477
  "scalar": "dtypes.T"
478
  },
479
  "passes": [
480
  {
481
  "id": "main",
482
  "name": "ReduceSum.ContiguousSuffixTree",
483
- "source": {
484
- "shader": "reduce-row-tree.wgsl.jinja",
485
- "inputs": { "op": "\"sum\"", "castF32": "dtypes.T == \"f16\"", "usesF16": "dtypes.T == \"f16\"" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
486
  },
487
- "bindings": "suffixScalar",
488
- "dispatch": { "workgroups": "numel(shapes.reduced)" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
489
  }
490
  ]
491
  },
492
  {
493
  "id": "multi_axis_rank3",
494
  "priority": 8,
495
- "when": ["not flatParallelCovered", "not contiguousSuffixParallelCovered", "f16Ok(dtypes.T)", "ranks.data == 3", "(attrs.keepdims == 1 and ranks.reduced == 3 and (dim(shapes.reduced, 0) == 1 if hasAxis(attrs.axes, 0, 3) else dim(shapes.reduced, 0) == dim(shapes.data, 0)) and (dim(shapes.reduced, 1) == 1 if hasAxis(attrs.axes, 1, 3) else dim(shapes.reduced, 1) == dim(shapes.data, 1)) and (dim(shapes.reduced, 2) == 1 if hasAxis(attrs.axes, 2, 3) else dim(shapes.reduced, 2) == dim(shapes.data, 2))) or (attrs.keepdims == 0 and ranks.reduced == 1)"],
 
496
  "passes": [
497
  {
498
  "id": "main",
499
  "name": "ReduceSum.MultiAxisRank3",
500
- "source": {
501
- "shader": "reduce-serial-axis.wgsl.jinja",
502
- "inputs": {
503
- "op": "\"sum\"",
504
- "indexing": "\"multiaxis\"",
505
- "rank": 3,
506
- "reduce": ["hasAxis(attrs.axes, 0, 3)", "hasAxis(attrs.axes, 1, 3)", "hasAxis(attrs.axes, 2, 3)"],
507
- "dataShape": "shapes.data",
508
- "outputShape": "shapes.reduced",
509
- "outputRank": "ranks.reduced",
510
- "keepDims": "attrs.keepdims != 0",
511
- "intMode": "dtypes.T == \"i32\"",
512
- "castF32": "dtypes.T == \"f16\"",
513
- "usesF16": "dtypes.T == \"f16\""
514
- }
515
  },
516
- "bindings": "multiAxis",
517
- "dispatch": { "threads": "numel(shapes.reduced)", "workgroupSize": "reduceWorkgroupSize" }
 
 
 
 
518
  }
519
- ],
520
- "constants": { "scalar": "dtypes.T" }
521
  },
522
  {
523
  "id": "multi_axis_rank4",
524
  "priority": 8,
525
- "when": ["not flatParallelCovered", "not contiguousSuffixParallelCovered", "f16Ok(dtypes.T)", "ranks.data == 4", "attrs.noop_with_empty_axes == 0", "numel(shapes.reduced) == (1 if hasAxis(attrs.axes, 0, 4) else dim(shapes.data, 0)) * (1 if hasAxis(attrs.axes, 1, 4) else dim(shapes.data, 1)) * (1 if hasAxis(attrs.axes, 2, 4) else dim(shapes.data, 2)) * (1 if hasAxis(attrs.axes, 3, 4) else dim(shapes.data, 3))", "((attrs.keepdims == 1 and ranks.reduced == 4) or (attrs.keepdims == 0 and ranks.reduced < 4))"],
 
526
  "passes": [
527
  {
528
  "id": "main",
529
  "name": "ReduceSum.MultiAxisRank4",
530
- "source": {
531
- "shader": "reduce-serial-axis.wgsl.jinja",
532
- "inputs": {
533
- "op": "\"sum\"",
534
- "indexing": "\"multiaxis\"",
535
- "rank": 4,
536
- "reduce": ["hasAxis(attrs.axes, 0, 4)", "hasAxis(attrs.axes, 1, 4)", "hasAxis(attrs.axes, 2, 4)", "hasAxis(attrs.axes, 3, 4)"],
537
- "dataShape": "shapes.data",
538
- "outputShape": "shapes.reduced",
539
- "outputRank": "ranks.reduced",
540
- "keepDims": "attrs.keepdims != 0",
541
- "intMode": "dtypes.T == \"i32\"",
542
- "castF32": "dtypes.T == \"f16\"",
543
- "usesF16": "dtypes.T == \"f16\""
544
- }
545
  },
546
- "bindings": "multiAxis",
547
- "dispatch": { "threads": "numel(shapes.reduced)", "workgroupSize": "reduceWorkgroupSize" }
 
 
 
 
548
  }
549
- ],
550
- "constants": { "scalar": "dtypes.T" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
551
  },
552
  {
553
  "id": "tree_last_axis_vec4",
554
  "priority": 23,
 
555
  "demoteWhen": ["rowSerialPreferred"],
556
- "when": ["not flatParallelCovered", "(dtypes.T == \"f32\" or dtypes.T == \"f16\" or dtypes.T == \"i32\")", "f16Ok(dtypes.T)", "ranks.data >= 1", "reduceAxis == ranks.data - 1", "numel(shapes.reduced) == rows(shapes.data, ranks.data - 1)", "attrs.noop_with_empty_axes == 0", "lastAxisCols >= tunables.ROW_PARALLEL_MIN_COLS", "lastAxisCols % tunables.VECTOR_WIDTH == 0", "treeWorkgroupOk"],
557
- "constants": {
558
  "scalar": "dtypes.T",
559
  "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
560
  "workgroupSize": "min(reduceWorkgroupSize, pow2ceil(ceilDiv(lastAxisCols, tunables.VECTOR_WIDTH)))"
@@ -563,138 +425,140 @@
563
  {
564
  "id": "main",
565
  "name": "ReduceSum.TreeRowVec4",
566
- "source": {
567
- "shader": "reduce-row-tree.wgsl.jinja",
568
- "inputs": {
569
- "op": "\"sum\"",
570
- "vec4": true,
571
- "castF32": "dtypes.T == \"f16\"",
572
- "usesF16": "dtypes.T == \"f16\""
573
- }
574
  },
575
- "bindings": "lastAxisVec4",
576
- "dispatch": { "workgroups": "lastAxisRows" }
577
  }
578
  ]
579
  },
580
  {
581
  "id": "rank0_scalar",
582
  "priority": 40,
583
- "constants": { "axis": 0, "scalar": "dtypes.T" },
584
- "when": ["f16Ok(dtypes.T)", "ranks.data == 0", "ranks.reduced == 0"],
585
  "passes": [
586
  {
587
  "id": "main",
588
  "name": "ReduceSum.Rank0Scalar",
589
- "source": {
590
- "shader": "reduce-serial-axis.wgsl.jinja",
591
- "inputs": {
592
- "op": "\"sum\"",
593
- "indexing": "\"axis2d\"",
594
- "intMode": "dtypes.T == \"i32\"",
595
- "castF32": "dtypes.T == \"f16\"",
596
- "usesF16": "dtypes.T == \"f16\"",
597
- "logicalBool": "tensorDtypes.data == \"bool\""
598
- }
599
  },
600
- "bindings": "scalar",
601
  "dispatch": { "x": 1 }
602
  }
603
  ]
604
  },
605
  {
606
  "id": "rank1_axis0",
607
- "constants": { "axis": 0, "scalar": "dtypes.T" },
608
- "when": ["not flatParallelCovered", "f16Ok(dtypes.T)", "ranks.data == 1", "reduceAxis == 0", "((attrs.keepdims == 0 and ranks.reduced == 0) or (attrs.keepdims == 1 and ranks.reduced == 1 and dim(shapes.reduced, 0) == 1))"],
609
  "passes": [
610
  {
611
  "id": "main",
612
  "name": "ReduceSum.Rank1Axis0",
613
- "source": {
614
- "shader": "reduce-serial-axis.wgsl.jinja",
615
- "inputs": {
616
- "op": "\"sum\"",
617
- "indexing": "\"axis2d\"",
618
- "intMode": "dtypes.T == \"i32\"",
619
- "castF32": "dtypes.T == \"f16\"",
620
- "usesF16": "dtypes.T == \"f16\"",
621
- "logicalBool": "tensorDtypes.data == \"bool\""
622
- }
623
  },
624
- "bindings": "rank1Axis0",
625
- "dispatch": { "threads": "numel(shapes.reduced)", "workgroupSize": "reduceWorkgroupSize" }
 
 
 
 
626
  }
627
  ]
628
  },
629
  {
630
  "id": "axis1_parallel",
631
  "priority": 20,
 
632
  "demoteWhen": ["rowSerialPreferred"],
633
- "when": ["not flatParallelCovered", "f16Ok(dtypes.T)", "ranks.data >= 2", "reduceAxis == ranks.data - 1", "numel(shapes.reduced) == rows(shapes.data, ranks.data - 1)", "lastAxisCols >= tunables.ROW_PARALLEL_MIN_COLS", "treeWorkgroupOk"],
634
- "constants": {
635
  "scalar": "dtypes.T",
636
- "workgroupSize": "min(reduceWorkgroupSize, pow2ceil(dim(shapes.data, ranks.data - 1)))"
637
  },
638
  "passes": [
639
  {
640
  "id": "main",
641
  "name": "ReduceSum.Axis1Parallel",
642
- "source": {
643
- "shader": "reduce-row-tree.wgsl.jinja",
644
- "inputs": { "op": "\"sum\"", "castF32": "dtypes.T == \"f16\"", "usesF16": "dtypes.T == \"f16\"" }
645
- },
646
- "bindings": "lastAxisScalar",
647
- "dispatch": { "workgroups": "rows(shapes.data, ranks.data - 1)" }
 
 
648
  }
649
  ]
650
  },
651
  {
652
  "id": "axis_split",
653
  "priority": 24,
654
- "when": ["not flatParallelCovered", "(dtypes.T == \"f32\" or dtypes.T == \"f16\")", "f16Ok(dtypes.T)", "attrs.noop_with_empty_axes == 0", "ranks.data >= 2", "reduceAxis < ranks.data - 1", "not (ranks.data == 2 and reduceAxis == 0)", "axisSplitDim >= tunables.AXIS0_SPLIT_MIN_ROWS", "axisSplitOutputs >= 1", "axisSplitOutputs <= 4096", "axisSplitOutputs == rows(shapes.data, reduceAxis)", "axisSplitPathFits"],
655
- "derive": { "splitCount": "axisSplitCount" },
656
- "constants": { "partialElement": "\"f32\"", "workgroupSize": "reduceWorkgroupSize", "split": "splitCount" },
 
 
 
 
657
  "intermediates": [{ "id": "partials", "dtype": "float32", "shape": "[splitCount * axisSplitOutputs]" }],
658
  "passes": [
659
  {
660
  "id": "split_reduce",
661
  "name": "ReduceSum.AxisSplitReduce",
662
- "source": {
663
- "shader": "reduce-axis-split-reduce.wgsl.jinja",
664
- "inputs": {
665
- "op": "\"sum\"",
666
- "workgroupSize": "reduceWorkgroupSize",
667
- "split": "splitCount",
668
- "castF32": "dtypes.T == \"f16\"",
669
- "usesF16": "dtypes.T == \"f16\""
670
- }
671
  },
672
- "bindings": "axisSplitReduce",
673
- "dispatch": { "threads": "axisSplitOutputs", "workgroupSize": "reduceWorkgroupSize", "y": "splitCount" }
 
 
 
 
674
  },
675
  {
676
  "id": "combine",
677
  "name": "ReduceSum.AxisSplitCombine",
678
- "source": {
679
- "shader": "reduce-axis0-splitk-combine.wgsl.jinja",
680
- "inputs": {
681
- "op": "\"sum\"",
682
- "workgroupSize": "reduceWorkgroupSize",
683
- "split": "splitCount",
684
- "outputF16": "dtypes.T == \"f16\""
685
- }
686
- },
687
- "bindings": "axisSplitCombine",
688
- "dispatch": { "threads": "axisSplitOutputs", "workgroupSize": "reduceWorkgroupSize" }
689
  }
690
  ]
691
  },
692
  {
693
  "id": "axis_split_tiled_narrow",
694
  "priority": 25,
695
- "when": ["not flatParallelCovered", "(dtypes.T == \"f32\" or dtypes.T == \"f16\")", "f16Ok(dtypes.T)", "attrs.noop_with_empty_axes == 0", "ranks.data >= 2", "reduceAxis < ranks.data - 1", "axisSplitDim >= tunables.AXIS0_SPLIT_MIN_ROWS", "axisSplitOutputs >= 1", "axisSplitOutputs <= 2 * tunables.AXIS_SPLIT_TILE_COLS", "reduceWorkgroupSize % tunables.AXIS_SPLIT_TILE_COLS == 0", "axisSplitOutputs == rows(shapes.data, reduceAxis)", "axisSplitPathFits"],
696
- "derive": { "splitCount": "axisSplitCount" },
697
- "constants": {
698
  "partialElement": "\"f32\"",
699
  "scalar": "dtypes.T",
700
  "workgroupSize": "reduceWorkgroupSize",
@@ -706,190 +570,189 @@
706
  {
707
  "id": "split_reduce",
708
  "name": "ReduceSum.AxisSplitTiledReduce",
709
- "source": {
710
- "shader": "reduce-axis0-tilecols.wgsl.jinja",
711
- "inputs": {
712
- "op": "\"sum\"",
713
- "workgroupSize": "reduceWorkgroupSize",
714
- "split": "splitCount",
715
- "tileCols": "tunables.AXIS_SPLIT_TILE_COLS",
716
- "castF32": "dtypes.T == \"f16\"",
717
- "usesF16": "dtypes.T == \"f16\""
718
- }
719
  },
720
- "bindings": "axisSplitReduce",
721
- "dispatch": { "workgroups": "ceilDiv((axisSplitOutputs), (constants.tileCols))", "y": "splitCount" }
 
 
 
 
722
  },
723
  {
724
  "id": "combine",
725
  "name": "ReduceSum.AxisSplitCombine",
726
- "source": {
727
- "shader": "reduce-axis0-splitk-combine.wgsl.jinja",
728
- "inputs": {
729
- "op": "\"sum\"",
730
- "workgroupSize": "reduceWorkgroupSize",
731
- "split": "splitCount",
732
- "outputF16": "dtypes.T == \"f16\""
733
- }
734
- },
735
- "bindings": "axisSplitCombine",
736
- "dispatch": { "threads": "axisSplitOutputs", "workgroupSize": "reduceWorkgroupSize" }
737
  }
738
  ]
739
  },
740
  {
741
  "id": "axis0_splitk",
742
  "priority": 22,
743
- "when": ["not flatParallelCovered", "(dtypes.T == \"f32\" or dtypes.T == \"f16\")", "f16Ok(dtypes.T)", "ranks.data == 2", "reduceAxis == 0", "axis0Rows >= tunables.AXIS0_SPLIT_MIN_ROWS", "dim(shapes.data, 1) > 0", "((attrs.keepdims == 0 and ranks.reduced == 1 and dim(shapes.reduced, 0) == dim(shapes.data, 1)) or (attrs.keepdims == 1 and ranks.reduced == 2 and dim(shapes.reduced, 0) == 1 and dim(shapes.reduced, 1) == dim(shapes.data, 1)))", "axis0SplitPathFits"],
744
- "derive": { "splitCount": "axis0SplitCount" },
745
- "constants": { "partialElement": "\"f32\"", "workgroupSize": "reduceWorkgroupSize", "split": "splitCount" },
746
- "intermediates": [{ "id": "partials", "dtype": "float32", "shape": "[splitCount * dim(shapes.data, 1)]" }],
 
 
 
 
747
  "passes": [
748
  {
749
  "id": "split_reduce",
750
  "name": "ReduceSum.Axis0SplitKReduce",
751
- "source": {
752
- "shader": "reduce-axis0-splitk-reduce.wgsl.jinja",
753
- "inputs": {
754
- "op": "\"sum\"",
755
- "workgroupSize": "reduceWorkgroupSize",
756
- "split": "splitCount",
757
- "castF32": "dtypes.T == \"f16\"",
758
- "usesF16": "dtypes.T == \"f16\""
759
- }
760
  },
761
- "bindings": "axis0SplitReduce",
762
- "dispatch": { "threads": "dim(shapes.data, 1)", "workgroupSize": "reduceWorkgroupSize", "y": "splitCount" }
 
 
 
 
763
  },
764
  {
765
  "id": "combine",
766
  "name": "ReduceSum.Axis0SplitKCombine",
767
- "source": {
768
- "shader": "reduce-axis0-splitk-combine.wgsl.jinja",
769
- "inputs": {
770
- "op": "\"sum\"",
771
- "workgroupSize": "reduceWorkgroupSize",
772
- "split": "splitCount",
773
- "outputF16": "dtypes.T == \"f16\""
774
- }
775
- },
776
- "bindings": "axis0SplitCombine",
777
- "dispatch": { "threads": "dim(shapes.data, 1)", "workgroupSize": "reduceWorkgroupSize" }
778
  }
779
  ]
780
  },
781
  {
782
  "id": "axis0_splitk_i32",
783
- "description": "Partitions a long rank-two axis-zero integer reduction across workgroups and combines exact int32 partials. It applies when the reduced row dimension is too large for one pass to expose enough parallelism.",
784
  "priority": 23,
785
- "when": ["dtypes.T == \"i32\"", "ranks.data == 2", "reduceAxis == 0", "axis0Rows >= tunables.AXIS0_SPLIT_MIN_ROWS", "dim(shapes.data, 1) > 0", "((attrs.keepdims == 0 and ranks.reduced == 1 and dim(shapes.reduced, 0) == dim(shapes.data, 1)) or (attrs.keepdims == 1 and ranks.reduced == 2 and dim(shapes.reduced, 0) == 1 and dim(shapes.reduced, 1) == dim(shapes.data, 1)))", "axis0SplitPathFits"],
786
- "derive": { "splitCount": "axis0SplitCount" },
787
- "constants": { "partialElement": "dtypes.T", "workgroupSize": "reduceWorkgroupSize", "split": "splitCount" },
788
- "intermediates": [{ "id": "partials", "dtype": "int32", "shape": "[splitCount * dim(shapes.data, 1)]" }],
 
 
 
 
789
  "passes": [
790
  {
791
  "id": "split_reduce",
792
  "name": "ReduceSum.Axis0SplitKReduceI32",
793
- "source": {
794
- "shader": "reduce-axis0-splitk-reduce.wgsl.jinja",
795
- "inputs": {
796
- "op": "\"sum\"",
797
- "intMode": true,
798
- "workgroupSize": "reduceWorkgroupSize",
799
- "split": "splitCount"
800
- }
801
- },
802
- "bindings": "axis0SplitReduce",
803
- "dispatch": { "threads": "dim(shapes.data, 1)", "workgroupSize": "reduceWorkgroupSize", "y": "splitCount" }
804
  },
805
  {
806
  "id": "combine",
807
  "name": "ReduceSum.Axis0SplitKCombineI32",
808
- "source": {
809
- "shader": "reduce-axis0-splitk-combine.wgsl.jinja",
810
- "inputs": {
811
- "op": "\"sum\"",
812
- "intMode": true,
813
- "workgroupSize": "reduceWorkgroupSize",
814
- "split": "splitCount"
815
- }
816
- },
817
- "bindings": "axis0SplitCombine",
818
- "dispatch": { "threads": "dim(shapes.data, 1)", "workgroupSize": "reduceWorkgroupSize" }
819
  }
820
  ]
821
  },
822
  {
823
  "id": "axis0_tilecols",
824
  "priority": 20,
825
- "when": ["not flatParallelCovered", "f16Ok(dtypes.T)", "ranks.data == 2", "reduceAxis == 0", "axis0Rows >= tunables.AXIS0_TILE_MIN_ROWS", "axis0Cols >= tunables.AXIS0_TILE_MIN_COLS", "((attrs.keepdims == 0 and ranks.reduced == 1 and dim(shapes.reduced, 0) == dim(shapes.data, 1)) or (attrs.keepdims == 1 and ranks.reduced == 2 and dim(shapes.reduced, 0) == 1 and dim(shapes.reduced, 1) == dim(shapes.data, 1)))", "axis0TilePathFits"],
826
- "constants": {
827
- "scalar": "dtypes.T",
828
- "workgroupSize": "reduceWorkgroupSize",
829
- "tileCols": "tunables.AXIS0_TILE_COLS"
830
- },
831
  "passes": [
832
  {
833
  "id": "main",
834
  "name": "ReduceSum.Axis0TileCols",
835
- "source": {
836
- "shader": "reduce-axis0-tilecols.wgsl.jinja",
837
- "inputs": {
838
- "op": "\"sum\"",
839
- "intMode": "dtypes.T == \"i32\"",
840
- "castF32": "dtypes.T == \"f16\"",
841
- "usesF16": "dtypes.T == \"f16\""
842
- }
843
  },
844
- "bindings": "axis0Parallel",
845
- "dispatch": { "workgroups": "ceilDiv((dim(shapes.data, 1)), (constants.tileCols))" }
 
 
 
 
846
  }
847
  ]
848
  },
849
  {
850
  "id": "noop_empty_axes",
851
  "priority": 40,
852
- "when": ["dtypes.T == \"f32\"", "attrs.noop_with_empty_axes == 1", "(attrs.axes | length) == 0", "sameShape(shapes.data, shapes.reduced)"],
853
  "passes": [
854
  {
855
  "id": "main",
856
  "name": "ReduceSum.NoopEmptyAxes",
857
  "shader": "datamove-elementwise-copy.wgsl.jinja",
858
- "bindings": "elementwise",
859
- "dispatch": { "gridStride": "numel(shapes.reduced)", "workgroupSize": "reduceWorkgroupSize" }
 
 
 
 
 
 
 
 
860
  }
861
  ]
862
  },
863
  {
864
  "id": "all_axes_flat",
865
  "priority": 31,
866
- "constants": {
 
867
  "partialElement": "\"f32\"",
868
  "scalar": "dtypes.T",
869
  "workgroupSize": "reduceWorkgroupSize",
870
  "split": "flatSplitCount"
871
  },
872
- "when": ["flatParallelCovered"],
873
  "intermediates": [{ "id": "partials", "dtype": "float32", "shape": "[flatSplitCount]" }],
874
  "passes": [
875
  {
876
  "id": "flat_partial",
877
  "name": "ReduceSum.AllAxesFlatPartial",
878
- "source": {
879
- "shader": "reduce-flat-partial.wgsl.jinja",
880
- "inputs": { "op": "\"sum\"", "castF32": "dtypes.T == \"f16\"", "usesF16": "dtypes.T == \"f16\"" }
881
- },
882
- "bindings": "flatPartial",
883
  "dispatch": { "x": "flatSplitCount" }
884
  },
885
  {
886
  "id": "combine",
887
  "name": "ReduceSum.AllAxesFlatCombine",
888
- "source": {
889
- "shader": "reduce-axis0-splitk-combine.wgsl.jinja",
890
- "inputs": { "op": "\"sum\"", "outputF16": "dtypes.T == \"f16\"" }
891
- },
892
- "bindings": "flatCombine",
893
  "dispatch": { "x": 1 }
894
  }
895
  ]
@@ -897,25 +760,24 @@
897
  {
898
  "id": "all_axes_flat_i32",
899
  "priority": 32,
900
- "constants": { "partialElement": "dtypes.T", "workgroupSize": "reduceWorkgroupSize", "split": "flatSplitCount" },
901
- "when": ["dtypes.T == \"i32\"", "numel(shapes.reduced) == 1", "numel(shapes.data) >= tunables.FULL_REDUCE_MIN_ELEMENTS", "flatPathFits"],
902
  "intermediates": [{ "id": "partials", "dtype": "int32", "shape": "[flatSplitCount]" }],
903
  "passes": [
904
  {
905
  "id": "flat_partial",
906
  "name": "ReduceSum.AllAxesFlatPartialI32",
907
- "source": { "shader": "reduce-flat-partial.wgsl.jinja", "inputs": { "op": "\"sum\"", "intMode": true } },
908
- "bindings": "flatPartial",
 
909
  "dispatch": { "x": "flatSplitCount" }
910
  },
911
  {
912
  "id": "combine",
913
  "name": "ReduceSum.AllAxesFlatCombineI32",
914
- "source": {
915
- "shader": "reduce-axis0-splitk-combine.wgsl.jinja",
916
- "inputs": { "op": "\"sum\"", "intMode": true, "workgroupSize": 1, "split": "flatSplitCount" }
917
- },
918
- "bindings": "flatCombine",
919
  "dispatch": { "x": 1 }
920
  }
921
  ]
@@ -923,42 +785,43 @@
923
  {
924
  "id": "rankn_single_axis_generic",
925
  "priority": 12,
 
926
  "supersededBy": ["axis_split_tiled_narrow", "axis_split", "subgroup_last_axis_vec4", "subgroup_last_axis", "tree_last_axis_vec4", "rank3_axis1", "rank3_axis2", "rank4_axis1"],
927
- "when": ["not flatParallelCovered", "f16Ok(dtypes.T)", "ranks.data >= 3", "attrs.noop_with_empty_axes == 0", "reduceAxis < ranks.data", "numel(shapes.reduced) == rows(shapes.data, reduceAxis)", "((attrs.keepdims == 0 and ranks.reduced == ranks.data - 1) or (attrs.keepdims == 1 and ranks.reduced == ranks.data and dim(shapes.reduced, reduceAxis) == 1))"],
928
  "passes": [
929
  {
930
  "id": "main",
931
  "name": "ReduceSum.RankNSingleAxisGeneric",
932
- "source": {
933
- "shader": "reduce-serial-axis.wgsl.jinja",
934
- "inputs": {
935
- "op": "\"sum\"",
936
- "indexing": "\"rankn\"",
937
- "rank": "ranks.data",
938
- "axis": "reduceAxis",
939
- "dataShape": "shapes.data",
940
- "outputShape": "shapes.reduced",
941
- "outputRank": "ranks.reduced",
942
- "keepDims": "attrs.keepdims != 0",
943
- "intMode": "dtypes.T == \"i32\"",
944
- "castF32": "dtypes.T == \"f16\"",
945
- "usesF16": "dtypes.T == \"f16\""
946
- }
947
  },
948
- "bindings": "rankNAxis",
949
- "dispatch": { "threads": "numel(shapes.reduced)", "workgroupSize": "reduceWorkgroupSize" }
 
 
 
 
950
  }
951
- ],
952
- "constants": { "scalar": "dtypes.T" }
953
  },
954
  {
955
  "id": "subgroup_last_axis_vec4",
956
- "description": "Reduces each contiguous last-axis row with subgroup collectives, using packed reads when the row width is vec4-aligned. It yields to the serial-row route when there are many short rows.",
957
  "priority": 25,
958
- "requires": { "features": ["subgroups"] },
959
  "demoteWhen": ["rowSerialPreferred", "lastAxisCols < tunables.SUBGROUP_MIN_COLS and lastAxisRows >= tunables.SUBGROUP_SMALL_ROW_LIMIT"],
960
- "when": ["device.wgslLanguageFeatures.has(\"subgroup_id\")", "not flatParallelCovered", "(dtypes.T == \"f32\" or dtypes.T == \"f16\")", "f16Ok(dtypes.T)", "ranks.data >= 1", "reduceAxis == ranks.data - 1", "numel(shapes.reduced) == rows(shapes.data, ranks.data - 1)", "attrs.noop_with_empty_axes == 0", "dim(shapes.data, ranks.data - 1) >= 4", "dim(shapes.data, ranks.data - 1) % tunables.VECTOR_WIDTH == 0"],
961
- "constants": {
962
  "scalar": "dtypes.T",
963
  "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
964
  "workgroupSize": "min(reduceWorkgroupSize, max(subgroupWorkgroupFloor, pow2ceil(ceilDiv(lastAxisCols, tunables.VECTOR_WIDTH))))"
@@ -967,29 +830,30 @@
967
  {
968
  "id": "main",
969
  "name": "ReduceSum.SubgroupRowVec4",
970
- "source": {
971
- "shader": "reduce-row-subgroup.wgsl.jinja",
972
- "inputs": {
973
- "op": "\"sum\"",
974
- "vec4": true,
975
- "castF32": "dtypes.T == \"f16\"",
976
- "usesF16": "dtypes.T == \"f16\""
977
- }
 
 
 
 
978
  },
979
- "subgroupCollectivesWidth": "portable",
980
- "bindings": "lastAxisVec4",
981
- "dispatch": { "workgroups": "rows(shapes.data, ranks.data - 1)" }
982
  }
983
  ]
984
  },
985
  {
986
  "id": "subgroup_last_axis",
987
- "description": "Reduces each contiguous last-axis row with subgroup collectives, using packed reads when the row width is vec4-aligned. It yields to the serial-row route when there are many short rows.",
988
  "priority": 24,
989
- "requires": { "features": ["subgroups"] },
990
  "demoteWhen": ["rowSerialPreferred", "lastAxisCols < tunables.SUBGROUP_MIN_COLS and lastAxisRows >= tunables.SUBGROUP_SMALL_ROW_LIMIT"],
991
- "when": ["device.wgslLanguageFeatures.has(\"subgroup_id\")", "not flatParallelCovered", "(dtypes.T == \"f32\" or dtypes.T == \"f16\")", "f16Ok(dtypes.T)", "ranks.data >= 1", "reduceAxis == ranks.data - 1", "numel(shapes.reduced) == rows(shapes.data, ranks.data - 1)", "attrs.noop_with_empty_axes == 0", "dim(shapes.data, ranks.data - 1) > 0", "dim(shapes.data, ranks.data - 1) % tunables.VECTOR_WIDTH != 0"],
992
- "constants": {
993
  "scalar": "dtypes.T",
994
  "workgroupSize": "min(reduceWorkgroupSize, max(subgroupWorkgroupFloor, pow2ceil(lastAxisCols)))"
995
  },
@@ -997,210 +861,248 @@
997
  {
998
  "id": "main",
999
  "name": "ReduceSum.SubgroupRow",
1000
- "source": {
1001
- "shader": "reduce-row-subgroup.wgsl.jinja",
1002
- "inputs": {
1003
- "op": "\"sum\"",
1004
- "vec4": false,
1005
- "castF32": "dtypes.T == \"f16\"",
1006
- "usesF16": "dtypes.T == \"f16\""
1007
- }
1008
  },
1009
- "subgroupCollectivesWidth": "portable",
1010
- "bindings": "lastAxisScalarSubgroup",
1011
- "dispatch": { "workgroups": "rows(shapes.data, ranks.data - 1)" }
 
 
 
 
1012
  }
1013
  ]
1014
  },
1015
  {
1016
  "id": "rank3_axis1",
1017
  "priority": 15,
 
1018
  "supersededBy": ["axis_split"],
1019
- "when": ["not flatParallelCovered", "f16Ok(dtypes.T)", "ranks.data == 3", "reduceAxis == 1", "((attrs.keepdims == 0 and ranks.reduced == 2 and dim(shapes.reduced, 0) == dim(shapes.data, 0) and dim(shapes.reduced, 1) == dim(shapes.data, 2)) or (attrs.keepdims == 1 and ranks.reduced == 3 and dim(shapes.reduced, 0) == dim(shapes.data, 0) and dim(shapes.reduced, 1) == 1 and dim(shapes.reduced, 2) == dim(shapes.data, 2)))"],
1020
  "passes": [
1021
  {
1022
  "id": "main",
1023
  "name": "ReduceSum.Rank3Axis1",
1024
- "source": {
1025
- "shader": "reduce-serial-axis.wgsl.jinja",
1026
- "inputs": {
1027
- "op": "\"sum\"",
1028
- "indexing": "\"rankn\"",
1029
- "rank": 3,
1030
- "axis": 1,
1031
- "dataShape": "shapes.data",
1032
- "outputShape": "shapes.reduced",
1033
- "outputRank": "ranks.reduced",
1034
- "keepDims": "attrs.keepdims != 0",
1035
- "intMode": "dtypes.T == \"i32\"",
1036
- "castF32": "dtypes.T == \"f16\"",
1037
- "usesF16": "dtypes.T == \"f16\""
1038
- }
1039
  },
1040
- "bindings": "rankNAxis",
1041
- "dispatch": { "threads": "numel(shapes.reduced)", "workgroupSize": "reduceWorkgroupSize" }
 
 
 
 
1042
  }
1043
- ],
1044
- "constants": { "scalar": "dtypes.T" }
1045
  },
1046
  {
1047
  "id": "rank3_axis2",
1048
  "priority": 15,
 
1049
  "supersededBy": ["axis_split"],
1050
- "when": ["not flatParallelCovered", "f16Ok(dtypes.T)", "ranks.data == 3", "reduceAxis == 2", "((attrs.keepdims == 0 and ranks.reduced == 2 and dim(shapes.reduced, 0) == dim(shapes.data, 0) and dim(shapes.reduced, 1) == dim(shapes.data, 1)) or (attrs.keepdims == 1 and ranks.reduced == 3 and dim(shapes.reduced, 0) == dim(shapes.data, 0) and dim(shapes.reduced, 1) == dim(shapes.data, 1) and dim(shapes.reduced, 2) == 1))"],
1051
  "passes": [
1052
  {
1053
  "id": "main",
1054
  "name": "ReduceSum.Rank3Axis2",
1055
- "source": {
1056
- "shader": "reduce-serial-axis.wgsl.jinja",
1057
- "inputs": {
1058
- "op": "\"sum\"",
1059
- "indexing": "\"rankn\"",
1060
- "rank": 3,
1061
- "axis": 2,
1062
- "dataShape": "shapes.data",
1063
- "outputShape": "shapes.reduced",
1064
- "outputRank": "ranks.reduced",
1065
- "keepDims": "attrs.keepdims != 0",
1066
- "intMode": "dtypes.T == \"i32\"",
1067
- "castF32": "dtypes.T == \"f16\"",
1068
- "usesF16": "dtypes.T == \"f16\""
1069
- }
1070
  },
1071
- "bindings": "rankNAxis",
1072
- "dispatch": { "threads": "numel(shapes.reduced)", "workgroupSize": "reduceWorkgroupSize" }
 
 
 
 
1073
  }
1074
- ],
1075
- "constants": { "scalar": "dtypes.T" }
1076
  },
1077
  {
1078
  "id": "rank4_axis1",
1079
  "priority": 15,
 
1080
  "supersededBy": ["axis_split"],
1081
- "when": ["not flatParallelCovered", "f16Ok(dtypes.T)", "ranks.data == 4", "reduceAxis == 1", "((attrs.keepdims == 0 and ranks.reduced == 3 and dim(shapes.reduced, 0) == dim(shapes.data, 0) and dim(shapes.reduced, 1) == dim(shapes.data, 2) and dim(shapes.reduced, 2) == dim(shapes.data, 3)) or (attrs.keepdims == 1 and ranks.reduced == 4 and dim(shapes.reduced, 0) == dim(shapes.data, 0) and dim(shapes.reduced, 1) == 1 and dim(shapes.reduced, 2) == dim(shapes.data, 2) and dim(shapes.reduced, 3) == dim(shapes.data, 3)))"],
1082
  "passes": [
1083
  {
1084
  "id": "main",
1085
  "name": "ReduceSum.Rank4Axis1",
1086
- "source": {
1087
- "shader": "reduce-serial-axis.wgsl.jinja",
1088
- "inputs": {
1089
- "op": "\"sum\"",
1090
- "indexing": "\"rankn\"",
1091
- "rank": 4,
1092
- "axis": 1,
1093
- "dataShape": "shapes.data",
1094
- "outputShape": "shapes.reduced",
1095
- "outputRank": "ranks.reduced",
1096
- "keepDims": "attrs.keepdims != 0",
1097
- "intMode": "dtypes.T == \"i32\"",
1098
- "castF32": "dtypes.T == \"f16\"",
1099
- "usesF16": "dtypes.T == \"f16\""
1100
- }
1101
  },
1102
- "bindings": "rankNAxis",
1103
- "dispatch": { "threads": "numel(shapes.reduced)", "workgroupSize": "reduceWorkgroupSize" }
 
 
 
 
1104
  }
1105
- ],
1106
- "constants": { "scalar": "dtypes.T" }
1107
  },
1108
  {
1109
  "id": "axis0",
1110
  "priority": 0,
 
1111
  "supersededBy": ["axis_split_tiled_narrow", "axis0_splitk_i32", "axis0_splitk", "axis0_tilecols"],
1112
- "constants": { "axis": 0, "scalar": "dtypes.T" },
1113
- "when": ["not flatParallelCovered", "f16Ok(dtypes.T)", "ranks.data == 2", "reduceAxis == 0", "((attrs.keepdims == 0 and ranks.reduced == 1 and dim(shapes.reduced, 0) == dim(shapes.data, 1)) or (attrs.keepdims == 1 and ranks.reduced == 2 and dim(shapes.reduced, 0) == 1 and dim(shapes.reduced, 1) == dim(shapes.data, 1)))"],
1114
  "passes": [
1115
  {
1116
  "id": "main",
1117
  "name": "axis0",
1118
- "source": {
1119
- "shader": "reduce-serial-axis.wgsl.jinja",
1120
- "inputs": {
1121
- "op": "\"sum\"",
1122
- "indexing": "\"axis2d\"",
1123
- "intMode": "dtypes.T == \"i32\"",
1124
- "castF32": "dtypes.T == \"f16\"",
1125
- "usesF16": "dtypes.T == \"f16\""
1126
- }
1127
  },
1128
- "bindings": "rank2Serial",
1129
- "constants": { "axis": 0 },
1130
- "dispatch": { "threads": "numel(shapes.reduced)", "workgroupSize": "reduceWorkgroupSize" }
 
 
 
1131
  }
1132
  ]
1133
  },
1134
  {
1135
  "id": "axis1",
1136
  "priority": 0,
1137
- "constants": { "axis": 1, "scalar": "dtypes.T" },
1138
- "when": ["not flatParallelCovered", "f16Ok(dtypes.T)", "ranks.data == 2", "reduceAxis == 1", "((attrs.keepdims == 0 and ranks.reduced == 1 and dim(shapes.reduced, 0) == dim(shapes.data, 0)) or (attrs.keepdims == 1 and ranks.reduced == 2 and dim(shapes.reduced, 0) == dim(shapes.data, 0) and dim(shapes.reduced, 1) == 1))"],
1139
  "passes": [
1140
  {
1141
  "id": "main",
1142
  "name": "axis1",
1143
- "source": {
1144
- "shader": "reduce-serial-axis.wgsl.jinja",
1145
- "inputs": {
1146
- "op": "\"sum\"",
1147
- "indexing": "\"axis2d\"",
1148
- "intMode": "dtypes.T == \"i32\"",
1149
- "castF32": "dtypes.T == \"f16\"",
1150
- "usesF16": "dtypes.T == \"f16\""
1151
- }
1152
  },
1153
- "bindings": "rank2SerialAxis1",
1154
- "constants": { "axis": 1 },
1155
- "dispatch": { "threads": "numel(shapes.reduced)", "workgroupSize": "reduceWorkgroupSize" }
 
 
 
1156
  }
1157
  ]
1158
  },
1159
  {
1160
  "id": "all_axes_no_keepdims",
1161
  "priority": 30,
1162
- "constants": { "axis": 0, "scalar": "dtypes.T" },
1163
- "when": ["not flatParallelCovered", "f16Ok(dtypes.T)", "ranks.data >= 3", "attrs.keepdims == 0", "attrs.noop_with_empty_axes == 0", "ranks.reduced == 0"],
1164
  "passes": [
1165
  {
1166
  "id": "main",
1167
  "name": "ReduceSum.Rank3AllAxesNoKeepdims",
1168
- "source": {
1169
- "shader": "reduce-serial-axis.wgsl.jinja",
1170
- "inputs": {
1171
- "op": "\"sum\"",
1172
- "indexing": "\"axis2d\"",
1173
- "intMode": "dtypes.T == \"i32\"",
1174
- "castF32": "dtypes.T == \"f16\"",
1175
- "usesF16": "dtypes.T == \"f16\""
1176
- }
1177
  },
1178
- "bindings": "fullReduceSerial",
1179
- "dispatch": { "threads": "numel(shapes.reduced)", "workgroupSize": "reduceWorkgroupSize" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1180
  }
1181
  ]
1182
  },
1183
  {
1184
  "id": "all_axes_keepdims",
1185
  "priority": 30,
1186
- "constants": { "axis": 0, "scalar": "dtypes.T" },
1187
- "when": ["not flatParallelCovered", "f16Ok(dtypes.T)", "ranks.data >= 3", "attrs.keepdims == 1", "ranks.reduced == ranks.data", "numel(shapes.reduced) == 1"],
1188
  "passes": [
1189
  {
1190
  "id": "main",
1191
  "name": "ReduceSum.Rank3AllAxesKeepdims",
1192
- "source": {
1193
- "shader": "reduce-serial-axis.wgsl.jinja",
1194
- "inputs": {
1195
- "op": "\"sum\"",
1196
- "indexing": "\"axis2d\"",
1197
- "intMode": "dtypes.T == \"i32\"",
1198
- "castF32": "dtypes.T == \"f16\"",
1199
- "usesF16": "dtypes.T == \"f16\""
1200
- }
1201
  },
1202
- "bindings": "fullReduceSerial",
1203
- "dispatch": { "threads": "numel(shapes.reduced)", "workgroupSize": "reduceWorkgroupSize" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1204
  }
1205
  ]
1206
  }
 
2
  "domain": "ai.onnx",
3
  "name": "ReduceSum",
4
  "sinceVersion": 13,
5
+ "inputs": { "x": { "onnx": "data", "dtype": "T" } },
6
+ "outputs": {
7
+ "y": {
8
+ "onnx": "reduced",
 
9
  "dtype": "T",
10
+ "rank": "ranks.x if attrs.keepdims == 1 or ((attrs.axes | length) == 0 and attrs.noop_with_empty_axes == 1) else (ranks.x - (attrs.axes | length) if (attrs.axes | length) > 0 else 0)"
 
11
  }
 
 
 
 
 
 
12
  },
13
+ "attributes": { "keepdims": { "default": 1 }, "noop_with_empty_axes": { "default": 0 }, "axes": { "default": [] } },
14
  "attributeConstraints": { "keepdims": { "values": [0, 1] }, "noop_with_empty_axes": { "values": [0, 1] } },
15
  "typeConstraints": { "T": ["float32", "float16", "int32"] },
16
+ "tunables": {
17
+ "WORKGROUP_SIZE": { "default": 256 },
18
+ "VECTOR_WIDTH": { "default": 4 },
19
+ "ROW_PARALLEL_MIN_COLS": { "default": 64 },
20
+ "SUBGROUP_MIN_COLS": { "default": 256 },
21
+ "SUBGROUP_SMALL_ROW_LIMIT": { "default": 32768 },
22
+ "AXIS0_SPLIT_MIN_ROWS": { "default": 8192 },
23
+ "AXIS0_SPLIT_TARGET_ROWS": { "default": 256 },
24
+ "AXIS0_MAX_SPLITS": { "default": 128 },
25
+ "AXIS0_TILE_MIN_ROWS": { "default": 64 },
26
+ "AXIS0_TILE_MIN_COLS": { "default": 16 },
27
+ "AXIS0_TILE_COLS": { "default": 16 },
28
+ "AXIS_SPLIT_TILE_COLS": { "default": 8 },
29
+ "FULL_REDUCE_MIN_ELEMENTS": { "default": 8192 },
30
+ "FULL_REDUCE_MAX_SPLITS": { "default": 256 },
31
+ "CONTIGUOUS_SUFFIX_MIN_COLS": { "default": 256 },
32
+ "ROW_SERIAL_MIN_ROWS": { "default": 8192 },
33
+ "ROW_SERIAL_MAX_COLS": { "default": 1024 },
34
+ "MULTI_AXIS_COOP_MAX_OUTPUTS": { "default": 8192 },
35
+ "MULTI_AXIS_COOP_MIN_REDUCED": { "default": 256 }
36
  },
37
  "derive": {
38
  "deviceWorkgroupCap": "min(device.limits.maxComputeInvocationsPerWorkgroup, device.limits.maxComputeWorkgroupSizeX)",
39
  "reduceWorkgroupSize": "min(tunables.WORKGROUP_SIZE, deviceWorkgroupCap)",
40
  "treeWorkgroupOk": "reduceWorkgroupSize > 0 and pow2ceil(reduceWorkgroupSize) == reduceWorkgroupSize and reduceWorkgroupSize * dtypeBytes(\"float32\") <= device.limits.maxComputeWorkgroupStorageSize",
41
  "subgroupWorkgroupFloor": "min(reduceWorkgroupSize, max(1, device.adapterInfo.subgroupMaxSize))",
42
+ "lastAxisRows": "rows(shapes.x, ranks.x - 1) if ranks.x > 0 else 1",
43
+ "lastAxisCols": "dim(shapes.x, ranks.x - 1) if ranks.x > 0 else 1",
44
  "rowSerialPreferred": "lastAxisRows >= tunables.ROW_SERIAL_MIN_ROWS and lastAxisCols <= tunables.ROW_SERIAL_MAX_COLS",
45
+ "axis0Rows": "dim(shapes.x, 0) if ranks.x >= 2 else 0",
46
+ "axis0Cols": "dim(shapes.x, 1) if ranks.x >= 2 else 0",
47
  "axis0SplitCount": "min(tunables.AXIS0_MAX_SPLITS, pow2ceil(ceilDiv(axis0Rows, tunables.AXIS0_SPLIT_TARGET_ROWS)))",
48
  "axis0SplitScratchBytes": "axis0SplitCount * axis0Cols * dtypeBytes(\"float32\")",
49
+ "axis0SplitPathFits": "axis0SplitCount <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535) and ceilDiv(ceilDiv(axis0Cols, reduceWorkgroupSize), min(device.limits.maxComputeWorkgroupsPerDimension, 65535)) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535) and axis0SplitScratchBytes <= device.limits.maxStorageBufferBindingSize and axis0SplitScratchBytes <= device.limits.maxBufferSize",
50
+ "reduceAxis": "(attrs.axes[0] + ranks.x if attrs.axes[0] < 0 else attrs.axes[0]) if ((attrs.axes | length) == 1 and isUniqueIntList(attrs.axes, 0 - ranks.x, ranks.x, 1)) else ranks.x",
51
+ "axisSplitDim": "dim(shapes.x, reduceAxis) if ranks.x >= 2 and reduceAxis < ranks.x else 0",
52
+ "axisSplitInner": "inner(shapes.x, reduceAxis) if ranks.x >= 2 and reduceAxis < ranks.x else 1",
53
+ "axisSplitOutputs": "numel(shapes.y)",
54
  "axisSplitCount": "min(tunables.AXIS0_MAX_SPLITS, pow2ceil(ceilDiv(axisSplitDim, tunables.AXIS0_SPLIT_TARGET_ROWS)))",
55
  "axisSplitScratchBytes": "axisSplitCount * axisSplitOutputs * 4",
56
+ "axisSplitPathFits": "axisSplitCount <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535) and ceilDiv(ceilDiv(axisSplitOutputs, reduceWorkgroupSize), min(device.limits.maxComputeWorkgroupsPerDimension, 65535)) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535) and axisSplitScratchBytes <= device.limits.maxStorageBufferBindingSize and axisSplitScratchBytes <= device.limits.maxBufferSize",
57
  "axis0TilePathFits": "treeWorkgroupOk and tunables.AXIS0_TILE_COLS > 0 and tunables.AXIS0_TILE_COLS <= reduceWorkgroupSize and reduceWorkgroupSize % tunables.AXIS0_TILE_COLS == 0",
58
+ "flatItems": "floor(numel(shapes.x) / tunables.VECTOR_WIDTH)",
59
  "flatSplitCount": "max(1, min(tunables.FULL_REDUCE_MAX_SPLITS, ceilDiv(flatItems, reduceWorkgroupSize)))",
60
  "flatScratchBytes": "flatSplitCount * dtypeBytes(\"float32\")",
61
+ "flatPathFits": "treeWorkgroupOk and flatSplitCount <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535) and flatScratchBytes <= device.limits.maxStorageBufferBindingSize and flatScratchBytes <= device.limits.maxBufferSize",
62
+ "flatParallelCovered": "(dtypes.T == \"f32\" or dtypes.T == \"f16\") and f16Ok(dtypes.T) and numel(shapes.y) == 1 and numel(shapes.x) >= tunables.FULL_REDUCE_MIN_ELEMENTS and flatPathFits",
63
+ "contiguousSuffixParallelCovered": "(dtypes.T == \"f32\" or dtypes.T == \"f16\") and f16Ok(dtypes.T) and numel(shapes.y) > 0 and numel(shapes.x) % numel(shapes.y) == 0 and numel(shapes.x) / numel(shapes.y) >= tunables.CONTIGUOUS_SUFFIX_MIN_COLS and ((ranks.x == 3 and hasAxis(attrs.axes, 0, 3) == false and hasAxis(attrs.axes, 1, 3) and hasAxis(attrs.axes, 2, 3) and numel(shapes.y) == dim(shapes.x, 0)) or (ranks.x == 4 and hasAxis(attrs.axes, 0, 4) == false and hasAxis(attrs.axes, 1, 4) == false and hasAxis(attrs.axes, 2, 4) and hasAxis(attrs.axes, 3, 4) and numel(shapes.y) == dim(shapes.x, 0) * dim(shapes.x, 1)) or (ranks.x == 4 and hasAxis(attrs.axes, 0, 4) == false and hasAxis(attrs.axes, 1, 4) and hasAxis(attrs.axes, 2, 4) and hasAxis(attrs.axes, 3, 4) and numel(shapes.y) == dim(shapes.x, 0)))"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  },
65
+ "bindings": {
66
+ "x": { "buffer": "read-only-storage", "elementType": "$vectorScalar" },
67
+ "y": { "buffer": "storage", "elementType": "$T" },
68
+ "params": {
69
+ "buffer": "uniform",
70
+ "struct": [
71
+ { "name": "rows", "type": "u32", "value": "numel(shapes.y)" },
72
+ { "name": "chunkCount", "type": "u32", "value": "numel(shapes.x) / numel(shapes.y) / tunables.VECTOR_WIDTH" }
73
+ ]
74
+ },
75
+ "x_2": { "name": "x", "buffer": "read-only-storage", "elementType": "$T" },
76
+ "params_2": {
77
+ "name": "params",
78
+ "buffer": "uniform",
79
+ "struct": [
80
+ { "name": "rows", "type": "u32", "value": "numel(shapes.y)" },
81
+ { "name": "cols", "type": "u32", "value": "numel(shapes.x) / numel(shapes.y)" }
82
+ ]
83
+ },
84
+ "params_3": {
85
+ "name": "params",
86
+ "buffer": "uniform",
87
+ "struct": [{ "name": "outCount", "type": "u32", "value": "numel(shapes.y)" }]
88
+ },
89
+ "params_4": {
90
+ "name": "params",
91
+ "buffer": "uniform",
92
+ "struct": [
93
+ { "name": "rows", "type": "u32", "value": "rows(shapes.x, ranks.x - 1)" },
94
+ { "name": "chunkCount", "type": "u32", "value": "dim(shapes.x, ranks.x - 1) / tunables.VECTOR_WIDTH" }
95
+ ]
96
+ },
97
+ "params_5": {
98
+ "name": "params",
99
+ "buffer": "uniform",
100
+ "struct": [
101
+ { "name": "rows", "type": "u32", "value": "1" },
102
+ { "name": "cols", "type": "u32", "value": "1" },
103
+ { "name": "outCount", "type": "u32", "value": "1" }
104
+ ]
105
+ },
106
+ "params_6": {
107
+ "name": "params",
108
+ "buffer": "uniform",
109
+ "struct": [
110
+ { "name": "rows", "type": "u32", "value": "dim(shapes.x, 0)" },
111
+ { "name": "cols", "type": "u32", "value": "1" },
112
+ { "name": "outCount", "type": "u32", "value": "numel(shapes.y)" }
113
+ ]
114
+ },
115
+ "params_7": {
116
+ "name": "params",
117
+ "buffer": "uniform",
118
+ "struct": [
119
+ { "name": "rows", "type": "u32", "value": "rows(shapes.x, ranks.x - 1)" },
120
+ { "name": "cols", "type": "u32", "value": "dim(shapes.x, ranks.x - 1)" }
121
+ ]
122
+ },
123
+ "partials": { "buffer": "storage", "elementType": "$partialElement" },
124
+ "params_8": {
125
+ "name": "params",
126
+ "buffer": "uniform",
127
+ "struct": [
128
+ { "name": "axisDim", "type": "u32", "value": "axisSplitDim" },
129
+ { "name": "inner", "type": "u32", "value": "axisSplitInner" },
130
+ { "name": "outputs", "type": "u32", "value": "axisSplitOutputs" }
131
+ ]
132
+ },
133
+ "partials_2": { "name": "partials", "buffer": "read-only-storage", "elementType": "$partialElement" },
134
+ "params_9": {
135
+ "name": "params",
136
+ "buffer": "uniform",
137
+ "struct": [{ "name": "cols", "type": "u32", "value": "axisSplitOutputs" }]
138
+ },
139
+ "params_10": {
140
+ "name": "params",
141
+ "buffer": "uniform",
142
+ "struct": [
143
+ { "name": "rows", "type": "u32", "value": "dim(shapes.x, 0)" },
144
+ { "name": "cols", "type": "u32", "value": "dim(shapes.x, 1)" }
145
+ ]
146
+ },
147
+ "params_11": {
148
+ "name": "params",
149
+ "buffer": "uniform",
150
+ "struct": [{ "name": "cols", "type": "u32", "value": "dim(shapes.x, 1)" }]
151
+ },
152
+ "params_13": {
153
+ "name": "params",
154
+ "buffer": "uniform",
155
+ "struct": [
156
+ { "name": "count4", "type": "u32", "value": "floor(numel(shapes.x) / tunables.VECTOR_WIDTH)" },
157
+ { "name": "numel", "type": "u32", "value": "numel(shapes.x)" }
158
+ ]
159
+ },
160
+ "params_14": { "name": "params", "buffer": "uniform", "struct": [{ "name": "cols", "type": "u32", "value": "1" }] },
161
+ "params_15": {
162
+ "name": "params",
163
+ "buffer": "uniform",
164
+ "struct": [
165
+ { "name": "axisDim", "type": "u32", "value": "axisSplitDim" },
166
+ { "name": "outCount", "type": "u32", "value": "numel(shapes.y)" }
167
+ ]
168
+ },
169
+ "params_16": {
170
+ "name": "params",
171
+ "buffer": "uniform",
172
+ "struct": [
173
+ { "name": "rows", "type": "u32", "value": "rows(shapes.x, ranks.x - 1)" },
174
+ { "name": "chunkCount", "type": "u32", "value": "dim(shapes.x, ranks.x - 1)" }
175
+ ]
176
+ },
177
+ "params_17": {
178
+ "name": "params",
179
+ "buffer": "uniform",
180
+ "struct": [
181
+ { "name": "rows", "type": "u32", "value": "dim(shapes.x, 0)" },
182
+ { "name": "cols", "type": "u32", "value": "dim(shapes.x, 1)" },
183
+ { "name": "outCount", "type": "u32", "value": "numel(shapes.y)" }
184
+ ]
185
+ },
186
+ "params_18": {
187
+ "name": "params",
188
+ "buffer": "uniform",
189
+ "struct": [
190
+ { "name": "cols", "type": "u32", "value": "dim(shapes.x, 1)" },
191
+ { "name": "outCount", "type": "u32", "value": "numel(shapes.y)" }
192
+ ]
193
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  },
195
  "variants": [
196
  {
197
  "id": "contiguous_suffix_subgroup_vec4",
198
  "priority": 30,
199
+ "when": ["device.wgslLanguageFeatures.has(\"subgroup_id\")", "not flatParallelCovered", "contiguousSuffixParallelCovered", "(numel(shapes.x) / numel(shapes.y)) % tunables.VECTOR_WIDTH == 0"],
200
  "requires": { "features": ["subgroups"] },
201
+ "derive": {
 
202
  "scalar": "dtypes.T",
203
  "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
204
+ "workgroupSize": "min(reduceWorkgroupSize, max(subgroupWorkgroupFloor, pow2ceil(ceilDiv(numel(shapes.x) / numel(shapes.y), tunables.VECTOR_WIDTH))))"
205
  },
206
  "passes": [
207
  {
208
  "id": "main",
209
  "name": "ReduceSum.ContiguousSuffixSubgroupVec4",
210
+ "shader": "reduce-row-subgroup.wgsl.jinja",
211
+ "derive": {
212
+ "op": "\"sum\"",
213
+ "vec4": true,
214
+ "castF32": "dtypes.T == \"f16\"",
215
+ "usesF16Spec": "dtypes.T == \"f16\""
 
 
216
  },
217
+ "bindings": ["x", "y", "params"],
218
+ "dispatch": { "x": "min(numel(shapes.y), 65535)", "y": "ceilDiv(numel(shapes.y), 65535)", "z": 1 },
219
+ "subgroupCollectivesWidth": "portable"
220
  }
221
  ]
222
  },
223
  {
224
  "id": "contiguous_suffix_tree_vec4",
225
  "priority": 22,
226
+ "when": ["not flatParallelCovered", "contiguousSuffixParallelCovered", "(numel(shapes.x) / numel(shapes.y)) % tunables.VECTOR_WIDTH == 0", "treeWorkgroupOk"],
227
+ "derive": {
228
  "scalar": "dtypes.T",
229
  "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
230
+ "workgroupSize": "min(reduceWorkgroupSize, pow2ceil(ceilDiv(numel(shapes.x) / numel(shapes.y), tunables.VECTOR_WIDTH)))"
231
  },
232
  "passes": [
233
  {
234
  "id": "main",
235
  "name": "ReduceSum.ContiguousSuffixTreeVec4",
236
+ "shader": "reduce-row-tree.wgsl.jinja",
237
+ "derive": {
238
+ "op": "\"sum\"",
239
+ "vec4": true,
240
+ "castF32": "dtypes.T == \"f16\"",
241
+ "usesF16Spec": "dtypes.T == \"f16\""
 
 
242
  },
243
+ "bindings": ["x", "y", "params"],
244
+ "dispatch": { "x": "min(numel(shapes.y), 65535)", "y": "ceilDiv(numel(shapes.y), 65535)", "z": 1 }
245
  }
246
  ]
247
  },
 
249
  "id": "contiguous_suffix_tree",
250
  "priority": 21,
251
  "when": ["not flatParallelCovered", "contiguousSuffixParallelCovered", "treeWorkgroupOk"],
252
+ "derive": {
253
+ "workgroupSize": "min(reduceWorkgroupSize, pow2ceil(numel(shapes.x) / numel(shapes.y)))",
254
  "scalar": "dtypes.T"
255
  },
256
  "passes": [
257
  {
258
  "id": "main",
259
  "name": "ReduceSum.ContiguousSuffixTree",
260
+ "shader": "reduce-row-tree.wgsl.jinja",
261
+ "derive": { "op": "\"sum\"", "castF32": "dtypes.T == \"f16\"", "usesF16Spec": "dtypes.T == \"f16\"" },
262
+ "bindings": ["x_2", "y", "params_2"],
263
+ "dispatch": { "x": "min(numel(shapes.y), 65535)", "y": "ceilDiv(numel(shapes.y), 65535)", "z": 1 }
264
+ }
265
+ ]
266
+ },
267
+ {
268
+ "id": "multi_axis_rank3_coop",
269
+ "priority": 9,
270
+ "when": ["not flatParallelCovered", "not contiguousSuffixParallelCovered", "f16Ok(dtypes.T)", "ranks.x == 3", "(attrs.keepdims == 1 and ranks.y == 3 and (dim(shapes.y, 0) == 1 if hasAxis(attrs.axes, 0, 3) else dim(shapes.y, 0) == dim(shapes.x, 0)) and (dim(shapes.y, 1) == 1 if hasAxis(attrs.axes, 1, 3) else dim(shapes.y, 1) == dim(shapes.x, 1)) and (dim(shapes.y, 2) == 1 if hasAxis(attrs.axes, 2, 3) else dim(shapes.y, 2) == dim(shapes.x, 2))) or (attrs.keepdims == 0 and ranks.y == 1)", "numel(shapes.y) >= 1", "numel(shapes.y) <= tunables.MULTI_AXIS_COOP_MAX_OUTPUTS", "numel(shapes.x) >= numel(shapes.y) * tunables.MULTI_AXIS_COOP_MIN_REDUCED", "pow2ceil(reduceWorkgroupSize) == reduceWorkgroupSize", "ceilDiv(numel(shapes.y), min(device.limits.maxComputeWorkgroupsPerDimension, 65535)) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535)"],
271
+ "derive": { "scalar": "dtypes.T", "workgroupSize": "reduceWorkgroupSize" },
272
+ "passes": [
273
+ {
274
+ "id": "main",
275
+ "name": "ReduceSum.MultiAxisRank3Coop",
276
+ "shader": "reduce-multi-axis-coop.wgsl.jinja",
277
+ "derive": {
278
+ "op": "\"sum\"",
279
+ "indexing": "\"multiaxis\"",
280
+ "rank": 3,
281
+ "reduce": ["hasAxis(attrs.axes, 0, 3)", "hasAxis(attrs.axes, 1, 3)", "hasAxis(attrs.axes, 2, 3)"],
282
+ "dataShape": "shapes.x",
283
+ "outputShape": "shapes.y",
284
+ "outputRank": "ranks.y",
285
+ "keepDims": "attrs.keepdims != 0",
286
+ "intMode": "dtypes.T == \"i32\"",
287
+ "castF32": "dtypes.T == \"f16\"",
288
+ "usesF16Spec": "dtypes.T == \"f16\""
289
  },
290
+ "bindings": ["x_2", "y"],
291
+ "dispatch": { "x": "min(numel(shapes.y), 65535)", "y": "ceilDiv(numel(shapes.y), 65535)", "z": 1 }
292
+ }
293
+ ]
294
+ },
295
+ {
296
+ "id": "multi_axis_rank4_coop",
297
+ "priority": 9,
298
+ "when": ["not flatParallelCovered", "not contiguousSuffixParallelCovered", "f16Ok(dtypes.T)", "ranks.x == 4", "attrs.noop_with_empty_axes == 0", "numel(shapes.y) == (1 if hasAxis(attrs.axes, 0, 4) else dim(shapes.x, 0)) * (1 if hasAxis(attrs.axes, 1, 4) else dim(shapes.x, 1)) * (1 if hasAxis(attrs.axes, 2, 4) else dim(shapes.x, 2)) * (1 if hasAxis(attrs.axes, 3, 4) else dim(shapes.x, 3))", "((attrs.keepdims == 1 and ranks.y == 4) or (attrs.keepdims == 0 and ranks.y < 4))", "numel(shapes.y) >= 1", "numel(shapes.y) <= tunables.MULTI_AXIS_COOP_MAX_OUTPUTS", "numel(shapes.x) >= numel(shapes.y) * tunables.MULTI_AXIS_COOP_MIN_REDUCED", "pow2ceil(reduceWorkgroupSize) == reduceWorkgroupSize", "ceilDiv(numel(shapes.y), min(device.limits.maxComputeWorkgroupsPerDimension, 65535)) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535)"],
299
+ "derive": { "scalar": "dtypes.T", "workgroupSize": "reduceWorkgroupSize" },
300
+ "passes": [
301
+ {
302
+ "id": "main",
303
+ "name": "ReduceSum.MultiAxisRank4Coop",
304
+ "shader": "reduce-multi-axis-coop.wgsl.jinja",
305
+ "derive": {
306
+ "op": "\"sum\"",
307
+ "indexing": "\"multiaxis\"",
308
+ "rank": 4,
309
+ "reduce": ["hasAxis(attrs.axes, 0, 4)", "hasAxis(attrs.axes, 1, 4)", "hasAxis(attrs.axes, 2, 4)", "hasAxis(attrs.axes, 3, 4)"],
310
+ "dataShape": "shapes.x",
311
+ "outputShape": "shapes.y",
312
+ "outputRank": "ranks.y",
313
+ "keepDims": "attrs.keepdims != 0",
314
+ "intMode": "dtypes.T == \"i32\"",
315
+ "castF32": "dtypes.T == \"f16\"",
316
+ "usesF16Spec": "dtypes.T == \"f16\""
317
+ },
318
+ "bindings": ["x_2", "y"],
319
+ "dispatch": { "x": "min(numel(shapes.y), 65535)", "y": "ceilDiv(numel(shapes.y), 65535)", "z": 1 }
320
  }
321
  ]
322
  },
323
  {
324
  "id": "multi_axis_rank3",
325
  "priority": 8,
326
+ "when": ["not flatParallelCovered", "not contiguousSuffixParallelCovered", "f16Ok(dtypes.T)", "ranks.x == 3", "(attrs.keepdims == 1 and ranks.y == 3 and (dim(shapes.y, 0) == 1 if hasAxis(attrs.axes, 0, 3) else dim(shapes.y, 0) == dim(shapes.x, 0)) and (dim(shapes.y, 1) == 1 if hasAxis(attrs.axes, 1, 3) else dim(shapes.y, 1) == dim(shapes.x, 1)) and (dim(shapes.y, 2) == 1 if hasAxis(attrs.axes, 2, 3) else dim(shapes.y, 2) == dim(shapes.x, 2))) or (attrs.keepdims == 0 and ranks.y == 1)"],
327
+ "derive": { "scalar": "dtypes.T", "reduceWorkgroupSize": "reduceWorkgroupSize" },
328
  "passes": [
329
  {
330
  "id": "main",
331
  "name": "ReduceSum.MultiAxisRank3",
332
+ "shader": "reduce-serial-axis.wgsl.jinja",
333
+ "derive": {
334
+ "op": "\"sum\"",
335
+ "indexing": "\"multiaxis\"",
336
+ "rank": 3,
337
+ "reduce": ["hasAxis(attrs.axes, 0, 3)", "hasAxis(attrs.axes, 1, 3)", "hasAxis(attrs.axes, 2, 3)"],
338
+ "dataShape": "shapes.x",
339
+ "outputShape": "shapes.y",
340
+ "outputRank": "ranks.y",
341
+ "keepDims": "attrs.keepdims != 0",
342
+ "intMode": "dtypes.T == \"i32\"",
343
+ "castF32": "dtypes.T == \"f16\"",
344
+ "usesF16Spec": "dtypes.T == \"f16\""
 
 
345
  },
346
+ "bindings": ["x_2", "y", "params_3"],
347
+ "dispatch": {
348
+ "x": "min(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
349
+ "y": "ceilDiv(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
350
+ "z": 1
351
+ }
352
  }
353
+ ]
 
354
  },
355
  {
356
  "id": "multi_axis_rank4",
357
  "priority": 8,
358
+ "when": ["not flatParallelCovered", "not contiguousSuffixParallelCovered", "f16Ok(dtypes.T)", "ranks.x == 4", "attrs.noop_with_empty_axes == 0", "numel(shapes.y) == (1 if hasAxis(attrs.axes, 0, 4) else dim(shapes.x, 0)) * (1 if hasAxis(attrs.axes, 1, 4) else dim(shapes.x, 1)) * (1 if hasAxis(attrs.axes, 2, 4) else dim(shapes.x, 2)) * (1 if hasAxis(attrs.axes, 3, 4) else dim(shapes.x, 3))", "((attrs.keepdims == 1 and ranks.y == 4) or (attrs.keepdims == 0 and ranks.y < 4))"],
359
+ "derive": { "scalar": "dtypes.T", "reduceWorkgroupSize": "reduceWorkgroupSize" },
360
  "passes": [
361
  {
362
  "id": "main",
363
  "name": "ReduceSum.MultiAxisRank4",
364
+ "shader": "reduce-serial-axis.wgsl.jinja",
365
+ "derive": {
366
+ "op": "\"sum\"",
367
+ "indexing": "\"multiaxis\"",
368
+ "rank": 4,
369
+ "reduce": ["hasAxis(attrs.axes, 0, 4)", "hasAxis(attrs.axes, 1, 4)", "hasAxis(attrs.axes, 2, 4)", "hasAxis(attrs.axes, 3, 4)"],
370
+ "dataShape": "shapes.x",
371
+ "outputShape": "shapes.y",
372
+ "outputRank": "ranks.y",
373
+ "keepDims": "attrs.keepdims != 0",
374
+ "intMode": "dtypes.T == \"i32\"",
375
+ "castF32": "dtypes.T == \"f16\"",
376
+ "usesF16Spec": "dtypes.T == \"f16\""
 
 
377
  },
378
+ "bindings": ["x_2", "y", "params_3"],
379
+ "dispatch": {
380
+ "x": "min(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
381
+ "y": "ceilDiv(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
382
+ "z": 1
383
+ }
384
  }
385
+ ]
386
+ },
387
+ {
388
+ "id": "subgroup_rows_last_axis_vec4",
389
+ "priority": 26,
390
+ "when": ["not flatParallelCovered", "(dtypes.T == \"f32\" or dtypes.T == \"f16\" or dtypes.T == \"i32\")", "f16Ok(dtypes.T)", "ranks.x >= 1", "reduceAxis == ranks.x - 1", "numel(shapes.y) == rows(shapes.x, ranks.x - 1)", "attrs.noop_with_empty_axes == 0", "lastAxisCols % tunables.VECTOR_WIDTH == 0", "lastAxisCols >= tunables.VECTOR_WIDTH", "has(device.adapterInfo, \"subgroupMinSize\")", "has(device.adapterInfo, \"subgroupMaxSize\")", "device.adapterInfo.subgroupMinSize >= 16", "ceilDiv(lastAxisCols / tunables.VECTOR_WIDTH, device.adapterInfo.subgroupMinSize) <= 8", "lastAxisRows >= 64", "treeWorkgroupOk", "device.adapterInfo.subgroupMaxSize <= reduceWorkgroupSize", "(not rowSerialPreferred or lastAxisCols >= tunables.SUBGROUP_MIN_COLS)"],
391
+ "requires": { "features": ["subgroups"] },
392
+ "derive": {
393
+ "scalar": "dtypes.T",
394
+ "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
395
+ "workgroupSize": "reduceWorkgroupSize",
396
+ "vecsPerLane": "ceilDiv(lastAxisCols / tunables.VECTOR_WIDTH, device.adapterInfo.subgroupMinSize)"
397
+ },
398
+ "passes": [
399
+ {
400
+ "id": "main",
401
+ "name": "ReduceSum.SubgroupRowsVec4",
402
+ "shader": "reduce-row-subgroup-rows.wgsl.jinja",
403
+ "derive": { "op": "\"sum\"", "castF32": "dtypes.T == \"f16\"", "usesF16Spec": "dtypes.T == \"f16\"" },
404
+ "bindings": ["x", "y", "params_4"],
405
+ "dispatch": {
406
+ "x": "min(ceilDiv(lastAxisRows, reduceWorkgroupSize / device.adapterInfo.subgroupMaxSize), 65535)",
407
+ "y": "ceilDiv(ceilDiv(lastAxisRows, reduceWorkgroupSize / device.adapterInfo.subgroupMaxSize), 65535)",
408
+ "z": 1
409
+ },
410
+ "subgroupCollectivesWidth": "portable"
411
+ }
412
+ ]
413
  },
414
  {
415
  "id": "tree_last_axis_vec4",
416
  "priority": 23,
417
+ "when": ["not flatParallelCovered", "(dtypes.T == \"f32\" or dtypes.T == \"f16\" or dtypes.T == \"i32\")", "f16Ok(dtypes.T)", "ranks.x >= 1", "reduceAxis == ranks.x - 1", "numel(shapes.y) == rows(shapes.x, ranks.x - 1)", "attrs.noop_with_empty_axes == 0", "lastAxisCols >= tunables.ROW_PARALLEL_MIN_COLS", "lastAxisCols % tunables.VECTOR_WIDTH == 0", "treeWorkgroupOk"],
418
  "demoteWhen": ["rowSerialPreferred"],
419
+ "derive": {
 
420
  "scalar": "dtypes.T",
421
  "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
422
  "workgroupSize": "min(reduceWorkgroupSize, pow2ceil(ceilDiv(lastAxisCols, tunables.VECTOR_WIDTH)))"
 
425
  {
426
  "id": "main",
427
  "name": "ReduceSum.TreeRowVec4",
428
+ "shader": "reduce-row-tree.wgsl.jinja",
429
+ "derive": {
430
+ "op": "\"sum\"",
431
+ "vec4": true,
432
+ "castF32": "dtypes.T == \"f16\"",
433
+ "usesF16Spec": "dtypes.T == \"f16\""
 
 
434
  },
435
+ "bindings": ["x", "y", "params_4"],
436
+ "dispatch": { "x": "min(lastAxisRows, 65535)", "y": "ceilDiv(lastAxisRows, 65535)", "z": 1 }
437
  }
438
  ]
439
  },
440
  {
441
  "id": "rank0_scalar",
442
  "priority": 40,
443
+ "when": ["f16Ok(dtypes.T)", "ranks.x == 0", "ranks.y == 0"],
444
+ "derive": { "axis": 0, "scalar": "dtypes.T", "reduceWorkgroupSize": "reduceWorkgroupSize" },
445
  "passes": [
446
  {
447
  "id": "main",
448
  "name": "ReduceSum.Rank0Scalar",
449
+ "shader": "reduce-serial-axis.wgsl.jinja",
450
+ "derive": {
451
+ "op": "\"sum\"",
452
+ "indexing": "\"axis2d\"",
453
+ "intMode": "dtypes.T == \"i32\"",
454
+ "castF32": "dtypes.T == \"f16\"",
455
+ "usesF16Spec": "dtypes.T == \"f16\"",
456
+ "logicalBool": "tensorDtypes.x == \"bool\""
 
 
457
  },
458
+ "bindings": ["x_2", "y", "params_5"],
459
  "dispatch": { "x": 1 }
460
  }
461
  ]
462
  },
463
  {
464
  "id": "rank1_axis0",
465
+ "when": ["not flatParallelCovered", "f16Ok(dtypes.T)", "ranks.x == 1", "reduceAxis == 0", "((attrs.keepdims == 0 and ranks.y == 0) or (attrs.keepdims == 1 and ranks.y == 1 and dim(shapes.y, 0) == 1))"],
466
+ "derive": { "axis": 0, "scalar": "dtypes.T", "reduceWorkgroupSize": "reduceWorkgroupSize" },
467
  "passes": [
468
  {
469
  "id": "main",
470
  "name": "ReduceSum.Rank1Axis0",
471
+ "shader": "reduce-serial-axis.wgsl.jinja",
472
+ "derive": {
473
+ "op": "\"sum\"",
474
+ "indexing": "\"axis2d\"",
475
+ "intMode": "dtypes.T == \"i32\"",
476
+ "castF32": "dtypes.T == \"f16\"",
477
+ "usesF16Spec": "dtypes.T == \"f16\"",
478
+ "logicalBool": "tensorDtypes.x == \"bool\""
 
 
479
  },
480
+ "bindings": ["x_2", "y", "params_6"],
481
+ "dispatch": {
482
+ "x": "min(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
483
+ "y": "ceilDiv(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
484
+ "z": 1
485
+ }
486
  }
487
  ]
488
  },
489
  {
490
  "id": "axis1_parallel",
491
  "priority": 20,
492
+ "when": ["not flatParallelCovered", "f16Ok(dtypes.T)", "ranks.x >= 2", "reduceAxis == ranks.x - 1", "numel(shapes.y) == rows(shapes.x, ranks.x - 1)", "lastAxisCols >= tunables.ROW_PARALLEL_MIN_COLS", "treeWorkgroupOk"],
493
  "demoteWhen": ["rowSerialPreferred"],
494
+ "derive": {
 
495
  "scalar": "dtypes.T",
496
+ "workgroupSize": "min(reduceWorkgroupSize, pow2ceil(dim(shapes.x, ranks.x - 1)))"
497
  },
498
  "passes": [
499
  {
500
  "id": "main",
501
  "name": "ReduceSum.Axis1Parallel",
502
+ "shader": "reduce-row-tree.wgsl.jinja",
503
+ "derive": { "op": "\"sum\"", "castF32": "dtypes.T == \"f16\"", "usesF16Spec": "dtypes.T == \"f16\"" },
504
+ "bindings": ["x_2", "y", "params_7"],
505
+ "dispatch": {
506
+ "x": "min(rows(shapes.x, ranks.x - 1), 65535)",
507
+ "y": "ceilDiv(rows(shapes.x, ranks.x - 1), 65535)",
508
+ "z": 1
509
+ }
510
  }
511
  ]
512
  },
513
  {
514
  "id": "axis_split",
515
  "priority": 24,
516
+ "when": ["not flatParallelCovered", "(dtypes.T == \"f32\" or dtypes.T == \"f16\")", "f16Ok(dtypes.T)", "attrs.noop_with_empty_axes == 0", "ranks.x >= 2", "reduceAxis < ranks.x - 1", "not (ranks.x == 2 and reduceAxis == 0)", "axisSplitDim >= tunables.AXIS0_SPLIT_MIN_ROWS", "axisSplitOutputs >= 1", "axisSplitOutputs <= 4096", "axisSplitOutputs == rows(shapes.x, reduceAxis)", "axisSplitPathFits"],
517
+ "derive": {
518
+ "splitCount": "axisSplitCount",
519
+ "partialElement": "\"f32\"",
520
+ "workgroupSize": "reduceWorkgroupSize",
521
+ "split": "splitCount"
522
+ },
523
  "intermediates": [{ "id": "partials", "dtype": "float32", "shape": "[splitCount * axisSplitOutputs]" }],
524
  "passes": [
525
  {
526
  "id": "split_reduce",
527
  "name": "ReduceSum.AxisSplitReduce",
528
+ "shader": "reduce-axis-split-reduce.wgsl.jinja",
529
+ "derive": {
530
+ "op": "\"sum\"",
531
+ "splitSpec": "splitCount",
532
+ "castF32": "dtypes.T == \"f16\"",
533
+ "usesF16Spec": "dtypes.T == \"f16\""
 
 
 
534
  },
535
+ "bindings": ["x_2", "partials", "params_8"],
536
+ "dispatch": {
537
+ "x": "min(ceilDiv((axisSplitOutputs), (reduceWorkgroupSize)), DISPATCH_FOLD_WIDTH)",
538
+ "y": "splitCount",
539
+ "z": "ceilDiv(ceilDiv((axisSplitOutputs), (reduceWorkgroupSize)), DISPATCH_FOLD_WIDTH)"
540
+ }
541
  },
542
  {
543
  "id": "combine",
544
  "name": "ReduceSum.AxisSplitCombine",
545
+ "shader": "reduce-axis0-splitk-combine.wgsl.jinja",
546
+ "derive": { "op": "\"sum\"", "splitSpec": "splitCount", "outputF16": "dtypes.T == \"f16\"" },
547
+ "bindings": ["partials_2", "y", "params_9"],
548
+ "dispatch": {
549
+ "x": "min(ceilDiv((axisSplitOutputs), (reduceWorkgroupSize)), 65535)",
550
+ "y": "ceilDiv(ceilDiv((axisSplitOutputs), (reduceWorkgroupSize)), 65535)",
551
+ "z": 1
552
+ }
 
 
 
553
  }
554
  ]
555
  },
556
  {
557
  "id": "axis_split_tiled_narrow",
558
  "priority": 25,
559
+ "when": ["not flatParallelCovered", "(dtypes.T == \"f32\" or dtypes.T == \"f16\")", "f16Ok(dtypes.T)", "attrs.noop_with_empty_axes == 0", "ranks.x >= 2", "reduceAxis < ranks.x - 1", "axisSplitDim >= tunables.AXIS0_SPLIT_MIN_ROWS", "axisSplitOutputs >= 1", "axisSplitOutputs <= 2 * tunables.AXIS_SPLIT_TILE_COLS", "reduceWorkgroupSize % tunables.AXIS_SPLIT_TILE_COLS == 0", "axisSplitOutputs == rows(shapes.x, reduceAxis)", "axisSplitPathFits"],
560
+ "derive": {
561
+ "splitCount": "axisSplitCount",
562
  "partialElement": "\"f32\"",
563
  "scalar": "dtypes.T",
564
  "workgroupSize": "reduceWorkgroupSize",
 
570
  {
571
  "id": "split_reduce",
572
  "name": "ReduceSum.AxisSplitTiledReduce",
573
+ "shader": "reduce-axis0-tilecols.wgsl.jinja",
574
+ "derive": {
575
+ "op": "\"sum\"",
576
+ "splitSpec": "splitCount",
577
+ "tileCols": "tunables.AXIS_SPLIT_TILE_COLS",
578
+ "castF32": "dtypes.T == \"f16\"",
579
+ "usesF16Spec": "dtypes.T == \"f16\""
 
 
 
580
  },
581
+ "bindings": ["x_2", "partials", "params_8"],
582
+ "dispatch": {
583
+ "x": "min(ceilDiv((axisSplitOutputs), (tileCols)), DISPATCH_FOLD_WIDTH)",
584
+ "y": "splitCount",
585
+ "z": "ceilDiv(ceilDiv((axisSplitOutputs), (tileCols)), DISPATCH_FOLD_WIDTH)"
586
+ }
587
  },
588
  {
589
  "id": "combine",
590
  "name": "ReduceSum.AxisSplitCombine",
591
+ "shader": "reduce-axis0-splitk-combine.wgsl.jinja",
592
+ "derive": { "op": "\"sum\"", "splitSpec": "splitCount", "outputF16": "dtypes.T == \"f16\"" },
593
+ "bindings": ["partials_2", "y", "params_9"],
594
+ "dispatch": {
595
+ "x": "min(ceilDiv((axisSplitOutputs), (reduceWorkgroupSize)), 65535)",
596
+ "y": "ceilDiv(ceilDiv((axisSplitOutputs), (reduceWorkgroupSize)), 65535)",
597
+ "z": 1
598
+ }
 
 
 
599
  }
600
  ]
601
  },
602
  {
603
  "id": "axis0_splitk",
604
  "priority": 22,
605
+ "when": ["not flatParallelCovered", "(dtypes.T == \"f32\" or dtypes.T == \"f16\")", "f16Ok(dtypes.T)", "ranks.x == 2", "reduceAxis == 0", "axis0Rows >= tunables.AXIS0_SPLIT_MIN_ROWS", "dim(shapes.x, 1) > 0", "((attrs.keepdims == 0 and ranks.y == 1 and dim(shapes.y, 0) == dim(shapes.x, 1)) or (attrs.keepdims == 1 and ranks.y == 2 and dim(shapes.y, 0) == 1 and dim(shapes.y, 1) == dim(shapes.x, 1)))", "axis0SplitPathFits"],
606
+ "derive": {
607
+ "splitCount": "axis0SplitCount",
608
+ "partialElement": "\"f32\"",
609
+ "workgroupSize": "reduceWorkgroupSize",
610
+ "split": "splitCount"
611
+ },
612
+ "intermediates": [{ "id": "partials", "dtype": "float32", "shape": "[splitCount * dim(shapes.x, 1)]" }],
613
  "passes": [
614
  {
615
  "id": "split_reduce",
616
  "name": "ReduceSum.Axis0SplitKReduce",
617
+ "shader": "reduce-axis0-splitk-reduce.wgsl.jinja",
618
+ "derive": {
619
+ "op": "\"sum\"",
620
+ "splitSpec": "splitCount",
621
+ "castF32": "dtypes.T == \"f16\"",
622
+ "usesF16Spec": "dtypes.T == \"f16\""
 
 
 
623
  },
624
+ "bindings": ["x_2", "partials", "params_10"],
625
+ "dispatch": {
626
+ "x": "min(ceilDiv((dim(shapes.x, 1)), (reduceWorkgroupSize)), DISPATCH_FOLD_WIDTH)",
627
+ "y": "splitCount",
628
+ "z": "ceilDiv(ceilDiv((dim(shapes.x, 1)), (reduceWorkgroupSize)), DISPATCH_FOLD_WIDTH)"
629
+ }
630
  },
631
  {
632
  "id": "combine",
633
  "name": "ReduceSum.Axis0SplitKCombine",
634
+ "shader": "reduce-axis0-splitk-combine.wgsl.jinja",
635
+ "derive": { "op": "\"sum\"", "splitSpec": "splitCount", "outputF16": "dtypes.T == \"f16\"" },
636
+ "bindings": ["partials_2", "y", "params_11"],
637
+ "dispatch": {
638
+ "x": "min(ceilDiv((dim(shapes.x, 1)), (reduceWorkgroupSize)), 65535)",
639
+ "y": "ceilDiv(ceilDiv((dim(shapes.x, 1)), (reduceWorkgroupSize)), 65535)",
640
+ "z": 1
641
+ }
 
 
 
642
  }
643
  ]
644
  },
645
  {
646
  "id": "axis0_splitk_i32",
 
647
  "priority": 23,
648
+ "when": ["dtypes.T == \"i32\"", "ranks.x == 2", "reduceAxis == 0", "axis0Rows >= tunables.AXIS0_SPLIT_MIN_ROWS", "dim(shapes.x, 1) > 0", "((attrs.keepdims == 0 and ranks.y == 1 and dim(shapes.y, 0) == dim(shapes.x, 1)) or (attrs.keepdims == 1 and ranks.y == 2 and dim(shapes.y, 0) == 1 and dim(shapes.y, 1) == dim(shapes.x, 1)))", "axis0SplitPathFits"],
649
+ "derive": {
650
+ "splitCount": "axis0SplitCount",
651
+ "partialElement": "dtypes.T",
652
+ "workgroupSize": "reduceWorkgroupSize",
653
+ "split": "splitCount"
654
+ },
655
+ "intermediates": [{ "id": "partials", "dtype": "int32", "shape": "[splitCount * dim(shapes.x, 1)]" }],
656
  "passes": [
657
  {
658
  "id": "split_reduce",
659
  "name": "ReduceSum.Axis0SplitKReduceI32",
660
+ "shader": "reduce-axis0-splitk-reduce.wgsl.jinja",
661
+ "derive": { "op": "\"sum\"", "intMode": true, "splitSpec": "splitCount" },
662
+ "bindings": ["x_2", "partials", "params_10"],
663
+ "dispatch": {
664
+ "x": "min(ceilDiv((dim(shapes.x, 1)), (reduceWorkgroupSize)), DISPATCH_FOLD_WIDTH)",
665
+ "y": "splitCount",
666
+ "z": "ceilDiv(ceilDiv((dim(shapes.x, 1)), (reduceWorkgroupSize)), DISPATCH_FOLD_WIDTH)"
667
+ }
 
 
 
668
  },
669
  {
670
  "id": "combine",
671
  "name": "ReduceSum.Axis0SplitKCombineI32",
672
+ "shader": "reduce-axis0-splitk-combine.wgsl.jinja",
673
+ "derive": { "op": "\"sum\"", "intMode": true, "splitSpec": "splitCount" },
674
+ "bindings": ["partials_2", "y", "params_11"],
675
+ "dispatch": {
676
+ "x": "min(ceilDiv((dim(shapes.x, 1)), (reduceWorkgroupSize)), 65535)",
677
+ "y": "ceilDiv(ceilDiv((dim(shapes.x, 1)), (reduceWorkgroupSize)), 65535)",
678
+ "z": 1
679
+ }
 
 
 
680
  }
681
  ]
682
  },
683
  {
684
  "id": "axis0_tilecols",
685
  "priority": 20,
686
+ "when": ["not flatParallelCovered", "f16Ok(dtypes.T)", "ranks.x == 2", "reduceAxis == 0", "axis0Rows >= tunables.AXIS0_TILE_MIN_ROWS", "axis0Cols >= tunables.AXIS0_TILE_MIN_COLS", "((attrs.keepdims == 0 and ranks.y == 1 and dim(shapes.y, 0) == dim(shapes.x, 1)) or (attrs.keepdims == 1 and ranks.y == 2 and dim(shapes.y, 0) == 1 and dim(shapes.y, 1) == dim(shapes.x, 1)))", "axis0TilePathFits"],
687
+ "derive": { "scalar": "dtypes.T", "workgroupSize": "reduceWorkgroupSize", "tileCols": "tunables.AXIS0_TILE_COLS" },
 
 
 
 
688
  "passes": [
689
  {
690
  "id": "main",
691
  "name": "ReduceSum.Axis0TileCols",
692
+ "shader": "reduce-axis0-tilecols.wgsl.jinja",
693
+ "derive": {
694
+ "op": "\"sum\"",
695
+ "intMode": "dtypes.T == \"i32\"",
696
+ "castF32": "dtypes.T == \"f16\"",
697
+ "usesF16Spec": "dtypes.T == \"f16\""
 
 
698
  },
699
+ "bindings": ["x_2", "y", "params_10"],
700
+ "dispatch": {
701
+ "x": "min(ceilDiv((dim(shapes.x, 1)), (tileCols)), 65535)",
702
+ "y": "ceilDiv(ceilDiv((dim(shapes.x, 1)), (tileCols)), 65535)",
703
+ "z": 1
704
+ }
705
  }
706
  ]
707
  },
708
  {
709
  "id": "noop_empty_axes",
710
  "priority": 40,
711
+ "when": ["dtypes.T == \"f32\"", "attrs.noop_with_empty_axes == 1", "(attrs.axes | length) == 0", "sameShape(shapes.x, shapes.y)"],
712
  "passes": [
713
  {
714
  "id": "main",
715
  "name": "ReduceSum.NoopEmptyAxes",
716
  "shader": "datamove-elementwise-copy.wgsl.jinja",
717
+ "bindings": [
718
+ "x_2",
719
+ "y",
720
+ { "name": "params", "struct": [{ "name": "count", "type": "u32", "value": "numel(shapes.y)" }] }
721
+ ],
722
+ "dispatch": {
723
+ "x": "min(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), min(device.limits.maxComputeWorkgroupsPerDimension, 65535))",
724
+ "y": 1,
725
+ "z": 1
726
+ }
727
  }
728
  ]
729
  },
730
  {
731
  "id": "all_axes_flat",
732
  "priority": 31,
733
+ "when": ["flatParallelCovered"],
734
+ "derive": {
735
  "partialElement": "\"f32\"",
736
  "scalar": "dtypes.T",
737
  "workgroupSize": "reduceWorkgroupSize",
738
  "split": "flatSplitCount"
739
  },
 
740
  "intermediates": [{ "id": "partials", "dtype": "float32", "shape": "[flatSplitCount]" }],
741
  "passes": [
742
  {
743
  "id": "flat_partial",
744
  "name": "ReduceSum.AllAxesFlatPartial",
745
+ "shader": "reduce-flat-partial.wgsl.jinja",
746
+ "derive": { "op": "\"sum\"", "castF32": "dtypes.T == \"f16\"", "usesF16Spec": "dtypes.T == \"f16\"" },
747
+ "bindings": ["x_2", "partials", "params_13"],
 
 
748
  "dispatch": { "x": "flatSplitCount" }
749
  },
750
  {
751
  "id": "combine",
752
  "name": "ReduceSum.AllAxesFlatCombine",
753
+ "shader": "reduce-axis0-splitk-combine.wgsl.jinja",
754
+ "derive": { "op": "\"sum\"", "outputF16": "dtypes.T == \"f16\"" },
755
+ "bindings": ["partials_2", "y", "params_14"],
 
 
756
  "dispatch": { "x": 1 }
757
  }
758
  ]
 
760
  {
761
  "id": "all_axes_flat_i32",
762
  "priority": 32,
763
+ "when": ["dtypes.T == \"i32\"", "numel(shapes.y) == 1", "numel(shapes.x) >= tunables.FULL_REDUCE_MIN_ELEMENTS", "flatPathFits"],
764
+ "derive": { "partialElement": "dtypes.T", "workgroupSize": "reduceWorkgroupSize", "split": "flatSplitCount" },
765
  "intermediates": [{ "id": "partials", "dtype": "int32", "shape": "[flatSplitCount]" }],
766
  "passes": [
767
  {
768
  "id": "flat_partial",
769
  "name": "ReduceSum.AllAxesFlatPartialI32",
770
+ "shader": "reduce-flat-partial.wgsl.jinja",
771
+ "derive": { "op": "\"sum\"", "intMode": true },
772
+ "bindings": ["x_2", "partials", "params_13"],
773
  "dispatch": { "x": "flatSplitCount" }
774
  },
775
  {
776
  "id": "combine",
777
  "name": "ReduceSum.AllAxesFlatCombineI32",
778
+ "shader": "reduce-axis0-splitk-combine.wgsl.jinja",
779
+ "derive": { "op": "\"sum\"", "intMode": true, "splitSpec": "flatSplitCount" },
780
+ "bindings": ["partials_2", "y", "params_14"],
 
 
781
  "dispatch": { "x": 1 }
782
  }
783
  ]
 
785
  {
786
  "id": "rankn_single_axis_generic",
787
  "priority": 12,
788
+ "when": ["not flatParallelCovered", "f16Ok(dtypes.T)", "ranks.x >= 3", "attrs.noop_with_empty_axes == 0", "reduceAxis < ranks.x", "numel(shapes.y) == rows(shapes.x, reduceAxis)", "((attrs.keepdims == 0 and ranks.y == ranks.x - 1) or (attrs.keepdims == 1 and ranks.y == ranks.x and dim(shapes.y, reduceAxis) == 1))"],
789
  "supersededBy": ["axis_split_tiled_narrow", "axis_split", "subgroup_last_axis_vec4", "subgroup_last_axis", "tree_last_axis_vec4", "rank3_axis1", "rank3_axis2", "rank4_axis1"],
790
+ "derive": { "scalar": "dtypes.T", "reduceWorkgroupSize": "reduceWorkgroupSize" },
791
  "passes": [
792
  {
793
  "id": "main",
794
  "name": "ReduceSum.RankNSingleAxisGeneric",
795
+ "shader": "reduce-serial-axis.wgsl.jinja",
796
+ "derive": {
797
+ "op": "\"sum\"",
798
+ "indexing": "\"rankn\"",
799
+ "rank": "ranks.x",
800
+ "axisSpec": "reduceAxis",
801
+ "dataShape": "shapes.x",
802
+ "outputShape": "shapes.y",
803
+ "outputRank": "ranks.y",
804
+ "keepDims": "attrs.keepdims != 0",
805
+ "intMode": "dtypes.T == \"i32\"",
806
+ "castF32": "dtypes.T == \"f16\"",
807
+ "usesF16Spec": "dtypes.T == \"f16\""
 
 
808
  },
809
+ "bindings": ["x_2", "y", "params_15"],
810
+ "dispatch": {
811
+ "x": "min(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
812
+ "y": "ceilDiv(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
813
+ "z": 1
814
+ }
815
  }
816
+ ]
 
817
  },
818
  {
819
  "id": "subgroup_last_axis_vec4",
 
820
  "priority": 25,
821
+ "when": ["device.wgslLanguageFeatures.has(\"subgroup_id\")", "not flatParallelCovered", "(dtypes.T == \"f32\" or dtypes.T == \"f16\")", "f16Ok(dtypes.T)", "ranks.x >= 1", "reduceAxis == ranks.x - 1", "numel(shapes.y) == rows(shapes.x, ranks.x - 1)", "attrs.noop_with_empty_axes == 0", "dim(shapes.x, ranks.x - 1) >= 4", "dim(shapes.x, ranks.x - 1) % tunables.VECTOR_WIDTH == 0"],
822
  "demoteWhen": ["rowSerialPreferred", "lastAxisCols < tunables.SUBGROUP_MIN_COLS and lastAxisRows >= tunables.SUBGROUP_SMALL_ROW_LIMIT"],
823
+ "requires": { "features": ["subgroups"] },
824
+ "derive": {
825
  "scalar": "dtypes.T",
826
  "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
827
  "workgroupSize": "min(reduceWorkgroupSize, max(subgroupWorkgroupFloor, pow2ceil(ceilDiv(lastAxisCols, tunables.VECTOR_WIDTH))))"
 
830
  {
831
  "id": "main",
832
  "name": "ReduceSum.SubgroupRowVec4",
833
+ "shader": "reduce-row-subgroup.wgsl.jinja",
834
+ "derive": {
835
+ "op": "\"sum\"",
836
+ "vec4": true,
837
+ "castF32": "dtypes.T == \"f16\"",
838
+ "usesF16Spec": "dtypes.T == \"f16\""
839
+ },
840
+ "bindings": ["x", "y", "params_4"],
841
+ "dispatch": {
842
+ "x": "min(rows(shapes.x, ranks.x - 1), 65535)",
843
+ "y": "ceilDiv(rows(shapes.x, ranks.x - 1), 65535)",
844
+ "z": 1
845
  },
846
+ "subgroupCollectivesWidth": "portable"
 
 
847
  }
848
  ]
849
  },
850
  {
851
  "id": "subgroup_last_axis",
 
852
  "priority": 24,
853
+ "when": ["device.wgslLanguageFeatures.has(\"subgroup_id\")", "not flatParallelCovered", "(dtypes.T == \"f32\" or dtypes.T == \"f16\")", "f16Ok(dtypes.T)", "ranks.x >= 1", "reduceAxis == ranks.x - 1", "numel(shapes.y) == rows(shapes.x, ranks.x - 1)", "attrs.noop_with_empty_axes == 0", "dim(shapes.x, ranks.x - 1) > 0", "dim(shapes.x, ranks.x - 1) % tunables.VECTOR_WIDTH != 0"],
854
  "demoteWhen": ["rowSerialPreferred", "lastAxisCols < tunables.SUBGROUP_MIN_COLS and lastAxisRows >= tunables.SUBGROUP_SMALL_ROW_LIMIT"],
855
+ "requires": { "features": ["subgroups"] },
856
+ "derive": {
857
  "scalar": "dtypes.T",
858
  "workgroupSize": "min(reduceWorkgroupSize, max(subgroupWorkgroupFloor, pow2ceil(lastAxisCols)))"
859
  },
 
861
  {
862
  "id": "main",
863
  "name": "ReduceSum.SubgroupRow",
864
+ "shader": "reduce-row-subgroup.wgsl.jinja",
865
+ "derive": {
866
+ "op": "\"sum\"",
867
+ "vec4": false,
868
+ "castF32": "dtypes.T == \"f16\"",
869
+ "usesF16Spec": "dtypes.T == \"f16\""
 
 
870
  },
871
+ "bindings": ["x_2", "y", "params_16"],
872
+ "dispatch": {
873
+ "x": "min(rows(shapes.x, ranks.x - 1), 65535)",
874
+ "y": "ceilDiv(rows(shapes.x, ranks.x - 1), 65535)",
875
+ "z": 1
876
+ },
877
+ "subgroupCollectivesWidth": "portable"
878
  }
879
  ]
880
  },
881
  {
882
  "id": "rank3_axis1",
883
  "priority": 15,
884
+ "when": ["not flatParallelCovered", "f16Ok(dtypes.T)", "ranks.x == 3", "reduceAxis == 1", "((attrs.keepdims == 0 and ranks.y == 2 and dim(shapes.y, 0) == dim(shapes.x, 0) and dim(shapes.y, 1) == dim(shapes.x, 2)) or (attrs.keepdims == 1 and ranks.y == 3 and dim(shapes.y, 0) == dim(shapes.x, 0) and dim(shapes.y, 1) == 1 and dim(shapes.y, 2) == dim(shapes.x, 2)))"],
885
  "supersededBy": ["axis_split"],
886
+ "derive": { "scalar": "dtypes.T" },
887
  "passes": [
888
  {
889
  "id": "main",
890
  "name": "ReduceSum.Rank3Axis1",
891
+ "shader": "reduce-serial-axis.wgsl.jinja",
892
+ "derive": {
893
+ "op": "\"sum\"",
894
+ "indexing": "\"rankn\"",
895
+ "rank": 3,
896
+ "axisSpec": 1,
897
+ "dataShape": "shapes.x",
898
+ "outputShape": "shapes.y",
899
+ "outputRank": "ranks.y",
900
+ "keepDims": "attrs.keepdims != 0",
901
+ "intMode": "dtypes.T == \"i32\"",
902
+ "castF32": "dtypes.T == \"f16\"",
903
+ "usesF16Spec": "dtypes.T == \"f16\""
 
 
904
  },
905
+ "bindings": ["x_2", "y", "params_15"],
906
+ "dispatch": {
907
+ "x": "min(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
908
+ "y": "ceilDiv(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
909
+ "z": 1
910
+ }
911
  }
912
+ ]
 
913
  },
914
  {
915
  "id": "rank3_axis2",
916
  "priority": 15,
917
+ "when": ["not flatParallelCovered", "f16Ok(dtypes.T)", "ranks.x == 3", "reduceAxis == 2", "((attrs.keepdims == 0 and ranks.y == 2 and dim(shapes.y, 0) == dim(shapes.x, 0) and dim(shapes.y, 1) == dim(shapes.x, 1)) or (attrs.keepdims == 1 and ranks.y == 3 and dim(shapes.y, 0) == dim(shapes.x, 0) and dim(shapes.y, 1) == dim(shapes.x, 1) and dim(shapes.y, 2) == 1))"],
918
  "supersededBy": ["axis_split"],
919
+ "derive": { "scalar": "dtypes.T" },
920
  "passes": [
921
  {
922
  "id": "main",
923
  "name": "ReduceSum.Rank3Axis2",
924
+ "shader": "reduce-serial-axis.wgsl.jinja",
925
+ "derive": {
926
+ "op": "\"sum\"",
927
+ "indexing": "\"rankn\"",
928
+ "rank": 3,
929
+ "axisSpec": 2,
930
+ "dataShape": "shapes.x",
931
+ "outputShape": "shapes.y",
932
+ "outputRank": "ranks.y",
933
+ "keepDims": "attrs.keepdims != 0",
934
+ "intMode": "dtypes.T == \"i32\"",
935
+ "castF32": "dtypes.T == \"f16\"",
936
+ "usesF16Spec": "dtypes.T == \"f16\""
 
 
937
  },
938
+ "bindings": ["x_2", "y", "params_15"],
939
+ "dispatch": {
940
+ "x": "min(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
941
+ "y": "ceilDiv(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
942
+ "z": 1
943
+ }
944
  }
945
+ ]
 
946
  },
947
  {
948
  "id": "rank4_axis1",
949
  "priority": 15,
950
+ "when": ["not flatParallelCovered", "f16Ok(dtypes.T)", "ranks.x == 4", "reduceAxis == 1", "((attrs.keepdims == 0 and ranks.y == 3 and dim(shapes.y, 0) == dim(shapes.x, 0) and dim(shapes.y, 1) == dim(shapes.x, 2) and dim(shapes.y, 2) == dim(shapes.x, 3)) or (attrs.keepdims == 1 and ranks.y == 4 and dim(shapes.y, 0) == dim(shapes.x, 0) and dim(shapes.y, 1) == 1 and dim(shapes.y, 2) == dim(shapes.x, 2) and dim(shapes.y, 3) == dim(shapes.x, 3)))"],
951
  "supersededBy": ["axis_split"],
952
+ "derive": { "scalar": "dtypes.T" },
953
  "passes": [
954
  {
955
  "id": "main",
956
  "name": "ReduceSum.Rank4Axis1",
957
+ "shader": "reduce-serial-axis.wgsl.jinja",
958
+ "derive": {
959
+ "op": "\"sum\"",
960
+ "indexing": "\"rankn\"",
961
+ "rank": 4,
962
+ "axisSpec": 1,
963
+ "dataShape": "shapes.x",
964
+ "outputShape": "shapes.y",
965
+ "outputRank": "ranks.y",
966
+ "keepDims": "attrs.keepdims != 0",
967
+ "intMode": "dtypes.T == \"i32\"",
968
+ "castF32": "dtypes.T == \"f16\"",
969
+ "usesF16Spec": "dtypes.T == \"f16\""
 
 
970
  },
971
+ "bindings": ["x_2", "y", "params_15"],
972
+ "dispatch": {
973
+ "x": "min(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
974
+ "y": "ceilDiv(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
975
+ "z": 1
976
+ }
977
  }
978
+ ]
 
979
  },
980
  {
981
  "id": "axis0",
982
  "priority": 0,
983
+ "when": ["not flatParallelCovered", "f16Ok(dtypes.T)", "ranks.x == 2", "reduceAxis == 0", "((attrs.keepdims == 0 and ranks.y == 1 and dim(shapes.y, 0) == dim(shapes.x, 1)) or (attrs.keepdims == 1 and ranks.y == 2 and dim(shapes.y, 0) == 1 and dim(shapes.y, 1) == dim(shapes.x, 1)))"],
984
  "supersededBy": ["axis_split_tiled_narrow", "axis0_splitk_i32", "axis0_splitk", "axis0_tilecols"],
985
+ "derive": { "axis": 0, "scalar": "dtypes.T" },
 
986
  "passes": [
987
  {
988
  "id": "main",
989
  "name": "axis0",
990
+ "shader": "reduce-serial-axis.wgsl.jinja",
991
+ "derive": {
992
+ "axis": 0,
993
+ "op": "\"sum\"",
994
+ "indexing": "\"axis2d\"",
995
+ "intMode": "dtypes.T == \"i32\"",
996
+ "castF32": "dtypes.T == \"f16\"",
997
+ "usesF16Spec": "dtypes.T == \"f16\""
 
998
  },
999
+ "bindings": ["x_2", "y", "params_17"],
1000
+ "dispatch": {
1001
+ "x": "min(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
1002
+ "y": "ceilDiv(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
1003
+ "z": 1
1004
+ }
1005
  }
1006
  ]
1007
  },
1008
  {
1009
  "id": "axis1",
1010
  "priority": 0,
1011
+ "when": ["not flatParallelCovered", "f16Ok(dtypes.T)", "ranks.x == 2", "reduceAxis == 1", "((attrs.keepdims == 0 and ranks.y == 1 and dim(shapes.y, 0) == dim(shapes.x, 0)) or (attrs.keepdims == 1 and ranks.y == 2 and dim(shapes.y, 0) == dim(shapes.x, 0) and dim(shapes.y, 1) == 1))"],
1012
+ "derive": { "axis": 1, "scalar": "dtypes.T" },
1013
  "passes": [
1014
  {
1015
  "id": "main",
1016
  "name": "axis1",
1017
+ "shader": "reduce-serial-axis.wgsl.jinja",
1018
+ "derive": {
1019
+ "axis": 1,
1020
+ "op": "\"sum\"",
1021
+ "indexing": "\"axis2d\"",
1022
+ "intMode": "dtypes.T == \"i32\"",
1023
+ "castF32": "dtypes.T == \"f16\"",
1024
+ "usesF16Spec": "dtypes.T == \"f16\""
 
1025
  },
1026
+ "bindings": ["x_2", "y", "params_18"],
1027
+ "dispatch": {
1028
+ "x": "min(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
1029
+ "y": "ceilDiv(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
1030
+ "z": 1
1031
+ }
1032
  }
1033
  ]
1034
  },
1035
  {
1036
  "id": "all_axes_no_keepdims",
1037
  "priority": 30,
1038
+ "when": ["not flatParallelCovered", "f16Ok(dtypes.T)", "ranks.x >= 3", "attrs.keepdims == 0", "attrs.noop_with_empty_axes == 0", "ranks.y == 0"],
1039
+ "derive": { "axis": 0, "scalar": "dtypes.T" },
1040
  "passes": [
1041
  {
1042
  "id": "main",
1043
  "name": "ReduceSum.Rank3AllAxesNoKeepdims",
1044
+ "shader": "reduce-serial-axis.wgsl.jinja",
1045
+ "derive": {
1046
+ "op": "\"sum\"",
1047
+ "indexing": "\"axis2d\"",
1048
+ "intMode": "dtypes.T == \"i32\"",
1049
+ "castF32": "dtypes.T == \"f16\"",
1050
+ "usesF16Spec": "dtypes.T == \"f16\""
 
 
1051
  },
1052
+ "bindings": [
1053
+ "x_2",
1054
+ "y",
1055
+ {
1056
+ "name": "params",
1057
+ "struct": [
1058
+ { "name": "rows", "type": "u32", "value": "numel(shapes.x)" },
1059
+ { "name": "cols", "type": "u32", "value": "1" },
1060
+ { "name": "outCount", "type": "u32", "value": "numel(shapes.y)" }
1061
+ ]
1062
+ }
1063
+ ],
1064
+ "dispatch": {
1065
+ "x": "min(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
1066
+ "y": "ceilDiv(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
1067
+ "z": 1
1068
+ }
1069
  }
1070
  ]
1071
  },
1072
  {
1073
  "id": "all_axes_keepdims",
1074
  "priority": 30,
1075
+ "when": ["not flatParallelCovered", "f16Ok(dtypes.T)", "ranks.x >= 3", "attrs.keepdims == 1", "ranks.y == ranks.x", "numel(shapes.y) == 1"],
1076
+ "derive": { "axis": 0, "scalar": "dtypes.T" },
1077
  "passes": [
1078
  {
1079
  "id": "main",
1080
  "name": "ReduceSum.Rank3AllAxesKeepdims",
1081
+ "shader": "reduce-serial-axis.wgsl.jinja",
1082
+ "derive": {
1083
+ "op": "\"sum\"",
1084
+ "indexing": "\"axis2d\"",
1085
+ "intMode": "dtypes.T == \"i32\"",
1086
+ "castF32": "dtypes.T == \"f16\"",
1087
+ "usesF16Spec": "dtypes.T == \"f16\""
 
 
1088
  },
1089
+ "bindings": [
1090
+ "x_2",
1091
+ "y",
1092
+ {
1093
+ "name": "params",
1094
+ "struct": [
1095
+ { "name": "rows", "type": "u32", "value": "numel(shapes.x)" },
1096
+ { "name": "cols", "type": "u32", "value": "1" },
1097
+ { "name": "outCount", "type": "u32", "value": "numel(shapes.y)" }
1098
+ ]
1099
+ }
1100
+ ],
1101
+ "dispatch": {
1102
+ "x": "min(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
1103
+ "y": "ceilDiv(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
1104
+ "z": 1
1105
+ }
1106
  }
1107
  ]
1108
  }
build/webgpu/metadata.json CHANGED
@@ -1,26 +1,62 @@
1
  {
2
  "name": "ai.onnx.ReduceSum",
3
- "id": "_ai_onnx_reducesum_webgpu_af45eb9",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
- "bench.json": "l6QAUKLa8vIxVVA0rZdVOMUrh/1wu9YyXFkaAvC3Pf0=",
11
  "datamove-elementwise-copy.wgsl.jinja": "Q1WuZCqcDf9b6cbT4rLilpeWj6327t/bSFmKr6IVBoQ=",
12
- "manifest.json": "a28ZCFG58JYKI9Fz/xskSTbtmTzgBjjg1hnj11Qkhhw=",
13
- "reduce-axis-split-reduce.wgsl.jinja": "Q50AhcbpYvCADMPrbGtPXKbmfT8f6LmQY+6+/CEX224=",
14
- "reduce-axis0-splitk-combine.wgsl.jinja": "uBBEUUxLWHWFF9sdlIgcdZ3y5SlI46mwaa0qtq4oP6E=",
15
- "reduce-axis0-splitk-reduce.wgsl.jinja": "Dsb/fe0TUftbM5lc45iDV/3aku1avSvAU1cYjHXAYrY=",
16
- "reduce-axis0-tilecols.wgsl.jinja": "FaAQTXiES4TyKHlXhGBz2uc00pBlObzW5v8CxtnPTQo=",
17
- "reduce-flat-partial.wgsl.jinja": "8Isi9lEY0j5IW8JpC0bjE4q+FxgdoJYMb+mSIg4mpTo=",
18
- "reduce-row-subgroup.wgsl.jinja": "lRRp3W2sqq1oJk92iUiXcHzGsA/jZK3uxGfw+WUt+GE=",
19
- "reduce-row-tree.wgsl.jinja": "eBfbOUoLED/RUxjWBKVEUfFXDlo1Nl3l30oqu0ljujc=",
20
- "reduce-serial-axis.wgsl.jinja": "k7HGCftlMjDpZju1Yb57Ook5NJICOCtK2kCSsFoWXkc=",
21
- "test.json": "/y+5OIa1acHzNtp0IHM424n+1/ptM4LM/9ZpFaEhZe0="
 
 
22
  }
23
  },
24
- "provenance": { "kernel": { "sha": "c928d21e6cc1310861cba3bafb75f5f679ecf5f3", "dirty": false } },
25
- "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/ai.onnx.ReduceSum" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  }
 
1
  {
2
  "name": "ai.onnx.ReduceSum",
3
+ "id": "_ai_onnx_reducesum_webgpu_7169ef2",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
+ "bench.json": "BI0nPERvR8p4G7FuvF0DHPAoslAI4pxFyIhmFGRy49A=",
11
  "datamove-elementwise-copy.wgsl.jinja": "Q1WuZCqcDf9b6cbT4rLilpeWj6327t/bSFmKr6IVBoQ=",
12
+ "manifest.json": "5i7cYgcHv/CRPtZy+xfWNEkFink8TlfQwSb8cKmvyaw=",
13
+ "reduce-axis-split-reduce.wgsl.jinja": "4+ep9xH4pHZOfaZ8abJhA8SW5M4mUhDC+y1CtDz6vjY=",
14
+ "reduce-axis0-splitk-combine.wgsl.jinja": "Yz1hjK55R/kndUrw3ugPqgPaOBwKmYupqZoVdJTHO5Q=",
15
+ "reduce-axis0-splitk-reduce.wgsl.jinja": "jB2h58emn6rfKhd8ALzqylSDsmBbrrkEOhtIoNhcQM0=",
16
+ "reduce-axis0-tilecols.wgsl.jinja": "PjYkEUQJBeG70td3W2xxmexH9x6XNIfeSzXBY47HbaU=",
17
+ "reduce-flat-partial.wgsl.jinja": "+qToL+wFi9QOxvY887aBAEwZK6Xu/eLQkucKvJYNwSk=",
18
+ "reduce-multi-axis-coop.wgsl.jinja": "ypqU1bkYaYU8YlNy9k4htstN7eLzpsRT2iJ7ErlExYQ=",
19
+ "reduce-row-subgroup-rows.wgsl.jinja": "76u7rAvFoZZKrFDs2A2jk0vkB0uNrPL6twdOBE9b+v8=",
20
+ "reduce-row-subgroup.wgsl.jinja": "2mu9LEsk8HfaLvucBCfcB1/ENpXkD6ELiCtRt+5UqiU=",
21
+ "reduce-row-tree.wgsl.jinja": "Bwa5xcI0bTmKXb4r9Cc1bfVbM5rNqqpQVrWWVqcb8xA=",
22
+ "reduce-serial-axis.wgsl.jinja": "fvUV9htqzKzt4Pg05pYtRmup/5QIHYaUGQHJZnthXKo=",
23
+ "test.json": "F7Iaw2W2jjR+gGYmEDa+6DCBeNv5zF6FSfLEmWfYx2A="
24
  }
25
  },
26
+ "provenance": { "kernel": { "sha": "91d990483a174128daf7673f3f37a7c890493ae1", "dirty": false } },
27
+ "webgpu": {
28
+ "manifestSpec": "2.0",
29
+ "variants": {
30
+ "contiguous_suffix_subgroup_vec4": ["reduce-row-subgroup.wgsl.jinja"],
31
+ "contiguous_suffix_tree_vec4": ["reduce-row-tree.wgsl.jinja"],
32
+ "contiguous_suffix_tree": ["reduce-row-tree.wgsl.jinja"],
33
+ "multi_axis_rank3_coop": ["reduce-multi-axis-coop.wgsl.jinja"],
34
+ "multi_axis_rank4_coop": ["reduce-multi-axis-coop.wgsl.jinja"],
35
+ "multi_axis_rank3": ["reduce-serial-axis.wgsl.jinja"],
36
+ "multi_axis_rank4": ["reduce-serial-axis.wgsl.jinja"],
37
+ "subgroup_rows_last_axis_vec4": ["reduce-row-subgroup-rows.wgsl.jinja"],
38
+ "tree_last_axis_vec4": ["reduce-row-tree.wgsl.jinja"],
39
+ "rank0_scalar": ["reduce-serial-axis.wgsl.jinja"],
40
+ "rank1_axis0": ["reduce-serial-axis.wgsl.jinja"],
41
+ "axis1_parallel": ["reduce-row-tree.wgsl.jinja"],
42
+ "axis_split": ["reduce-axis-split-reduce.wgsl.jinja", "reduce-axis0-splitk-combine.wgsl.jinja"],
43
+ "axis_split_tiled_narrow": ["reduce-axis0-splitk-combine.wgsl.jinja", "reduce-axis0-tilecols.wgsl.jinja"],
44
+ "axis0_splitk": ["reduce-axis0-splitk-combine.wgsl.jinja", "reduce-axis0-splitk-reduce.wgsl.jinja"],
45
+ "axis0_splitk_i32": ["reduce-axis0-splitk-combine.wgsl.jinja", "reduce-axis0-splitk-reduce.wgsl.jinja"],
46
+ "axis0_tilecols": ["reduce-axis0-tilecols.wgsl.jinja"],
47
+ "noop_empty_axes": ["datamove-elementwise-copy.wgsl.jinja"],
48
+ "all_axes_flat": ["reduce-axis0-splitk-combine.wgsl.jinja", "reduce-flat-partial.wgsl.jinja"],
49
+ "all_axes_flat_i32": ["reduce-axis0-splitk-combine.wgsl.jinja", "reduce-flat-partial.wgsl.jinja"],
50
+ "rankn_single_axis_generic": ["reduce-serial-axis.wgsl.jinja"],
51
+ "subgroup_last_axis_vec4": ["reduce-row-subgroup.wgsl.jinja"],
52
+ "subgroup_last_axis": ["reduce-row-subgroup.wgsl.jinja"],
53
+ "rank3_axis1": ["reduce-serial-axis.wgsl.jinja"],
54
+ "rank3_axis2": ["reduce-serial-axis.wgsl.jinja"],
55
+ "rank4_axis1": ["reduce-serial-axis.wgsl.jinja"],
56
+ "axis0": ["reduce-serial-axis.wgsl.jinja"],
57
+ "axis1": ["reduce-serial-axis.wgsl.jinja"],
58
+ "all_axes_no_keepdims": ["reduce-serial-axis.wgsl.jinja"],
59
+ "all_axes_keepdims": ["reduce-serial-axis.wgsl.jinja"]
60
+ }
61
+ }
62
  }
build/webgpu/reduce-axis-split-reduce.wgsl.jinja CHANGED
@@ -4,30 +4,53 @@
4
  // inner-axis elements. The combine pass folds the segments and finalizes the
5
  // selected reduction.
6
  //
7
- // logsumexp writes three partial planes per output and segment: the segment
8
- // maximum, the sum of exp(x - maximum), and a packed NaN marker.
9
- {% set castF32 = source.castF32 is defined and source.castF32 %}
 
 
10
  {% set xa = "f32(" if castF32 else "" %}
11
  {% set ax = ")" if castF32 else "" %}
12
- {% if source.usesF16 is defined and source.usesF16 %}
13
  enable f16;
14
  {% endif %}
15
  {{ env.wgsl.resourceDeclarations }}
16
- /* Exact max/min reduction identities. WGSL rejects infinity during constant
17
- * evaluation, so f32 identities are constructed at runtime from their
18
- * IEEE-754 bit patterns. */
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
 
21
  const WG: u32 = {{ workgroupSize }}u;
22
  const SPLIT: u32 = {{ split }}u;
 
 
 
 
 
 
 
 
 
 
23
 
24
  @compute @workgroup_size(WG, 1, 1)
25
  fn main(@builtin(global_invocation_id) gid: vec3<u32>,
26
- @builtin(workgroup_id) wg: vec3<u32>,
27
- @builtin(num_workgroups) nwg: vec3<u32>) {
28
  // 2D-folded output index: wg.z carries the high bits past the
29
  // per-dimension dispatch limit on the x dimension.
30
- let output_index = (wg.x + wg.z * nwg.x) * WG + (gid.x % WG);
31
  let seg = wg.y;
32
  if (output_index >= params.outputs) { return; }
33
 
@@ -41,9 +64,52 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>,
41
  var a1 = a0 + chunk;
42
  if (a1 > params.axisDim) { a1 = params.axisDim; }
43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  var acc = 0.0;
 
45
  for (var axis_index = a0; axis_index < a1; axis_index = axis_index + 1u) {
 
 
 
 
 
 
 
 
 
 
46
  acc = acc + {{ xa }}x[input_base + axis_index * params.inner]{{ ax }};
 
47
  }
48
  partials[seg * params.outputs + output_index] = acc;
 
49
  }
 
4
  // inner-axis elements. The combine pass folds the segments and finalizes the
5
  // selected reduction.
6
  //
7
+ {% if op == "logsumexp" %}
8
+ // Each output segment writes three partial planes: its maximum, the sum of
9
+ // exp(x - maximum), and a packed NaN marker.
10
+ {% endif %}
11
+ {% set castF32 = castF32 is defined and castF32 %}
12
  {% set xa = "f32(" if castF32 else "" %}
13
  {% set ax = ")" if castF32 else "" %}
14
+ {% if usesF16Spec is defined and usesF16Spec %}
15
  enable f16;
16
  {% endif %}
17
  {{ env.wgsl.resourceDeclarations }}
18
+ {% macro wgsl_minmax_identity(name, op, scalar="f32") %}
19
+ /* Exact {{ op }} reduction identity. WGSL rejects infinity during constant
20
+ * evaluation, so an f32 identity is constructed from its IEEE-754 bits. */
21
+ {% set logicalBoolIdentity = logicalBool is defined and logicalBool %}
22
+ fn {{ name }}() -> {{ scalar }} {
23
+ {% if scalar == "i32" %}
24
+ return {{ "-2147483647i - 1i" if op == "max" else "2147483647i" }};
25
+ {% elif scalar == "u32" %}
26
+ return {{ "1u" if logicalBoolIdentity and op == "min" else "0u" if op == "max" else "4294967295u" }};
27
+ {% else %}
28
+ var bits = {{ "0xff800000u" if op == "max" else "0x7f800000u" }};
29
+ return bitcast<f32>(bits);
30
+ {% endif %}
31
+ }
32
+ {%- endmacro %}
33
 
34
 
35
  const WG: u32 = {{ workgroupSize }}u;
36
  const SPLIT: u32 = {{ split }}u;
37
+ {% if op == "logsumexp" %}
38
+ const F32_MIN: f32 = -3.4028234663852886e38;
39
+
40
+ fn is_nan_f32(value: f32) -> bool {
41
+ let bits = bitcast<u32>(value);
42
+ return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
43
+ }
44
+ {% elif op == "max" or op == "min" %}
45
+ {{ wgsl_minmax_identity("reduction_identity", op) }}
46
+ {% endif %}
47
 
48
  @compute @workgroup_size(WG, 1, 1)
49
  fn main(@builtin(global_invocation_id) gid: vec3<u32>,
50
+ @builtin(workgroup_id) wg: vec3<u32>) {
 
51
  // 2D-folded output index: wg.z carries the high bits past the
52
  // per-dimension dispatch limit on the x dimension.
53
+ let output_index = (wg.x + wg.z * {{ DISPATCH_FOLD_WIDTH }}u) * WG + (gid.x % WG);
54
  let seg = wg.y;
55
  if (output_index >= params.outputs) { return; }
56
 
 
64
  var a1 = a0 + chunk;
65
  if (a1 > params.axisDim) { a1 = params.axisDim; }
66
 
67
+ {% if op == "logsumexp" %}
68
+ var local_max = F32_MIN;
69
+ var local_nan_count = 0.0;
70
+ var local_nan_value = 0.0;
71
+ for (var axis_index = a0; axis_index < a1; axis_index = axis_index + 1u) {
72
+ let value = {{ xa }}x[input_base + axis_index * params.inner]{{ ax }};
73
+ if (is_nan_f32(value)) {
74
+ local_nan_count = local_nan_count + 1.0;
75
+ local_nan_value = value;
76
+ } else {
77
+ local_max = max(local_max, value);
78
+ }
79
+ }
80
+ var acc = 0.0;
81
+ if (local_nan_count == 0.0) {
82
+ for (var axis_index = a0; axis_index < a1; axis_index = axis_index + 1u) {
83
+ acc = acc + exp({{ xa }}x[input_base + axis_index * params.inner]{{ ax }} - local_max);
84
+ }
85
+ }
86
+ partials[seg * params.outputs + output_index] = local_max;
87
+ partials[(SPLIT + seg) * params.outputs + output_index] = acc;
88
+ partials[(2u * SPLIT + seg) * params.outputs + output_index] = select(0.0, local_nan_value, local_nan_count > 0.0);
89
+ {% else %}
90
+ {% if op == "max" %}
91
+ var acc = reduction_identity();
92
+ {% elif op == "min" %}
93
+ var acc = reduction_identity();
94
+ {% elif op == "prod" %}
95
+ var acc = 1.0;
96
+ {% else %}
97
  var acc = 0.0;
98
+ {% endif %}
99
  for (var axis_index = a0; axis_index < a1; axis_index = axis_index + 1u) {
100
+ {% if op == "max" or op == "min" %}
101
+ acc = {{ op }}(acc, {{ xa }}x[input_base + axis_index * params.inner]{{ ax }});
102
+ {% elif op == "prod" %}
103
+ acc = acc * {{ xa }}x[input_base + axis_index * params.inner]{{ ax }};
104
+ {% elif op == "l1" %}
105
+ acc = acc + abs({{ xa }}x[input_base + axis_index * params.inner]{{ ax }});
106
+ {% elif op == "l2" or op == "sumsquare" %}
107
+ let value = {{ xa }}x[input_base + axis_index * params.inner]{{ ax }};
108
+ acc = acc + value * value;
109
+ {% else %}
110
  acc = acc + {{ xa }}x[input_base + axis_index * params.inner]{{ ax }};
111
+ {% endif %}
112
  }
113
  partials[seg * params.outputs + output_index] = acc;
114
+ {% endif %}
115
  }
build/webgpu/reduce-axis0-splitk-combine.wgsl.jinja CHANGED
@@ -2,37 +2,121 @@
2
  // folds the segment partials and applies the selected reduction's final step.
3
  // Segments are folded in ascending order for deterministic results. This order
4
  // differs from the single-pass reduction but remains within the f32 tolerance.
5
- {% if source.outputF16 %}
 
 
 
 
 
6
  enable f16;
7
  {% endif %}
8
  {{ env.wgsl.resourceDeclarations }}
9
- /* Exact max/min reduction identities. WGSL rejects infinity during constant
10
- * evaluation, so f32 identities are constructed at runtime from their
11
- * IEEE-754 bit patterns. */
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
 
14
  const WG: u32 = {{ workgroupSize }}u;
15
  const SPLIT: u32 = {{ split }}u;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  @compute @workgroup_size(WG, 1, 1)
18
  fn main(@builtin(global_invocation_id) gid: vec3<u32>,
19
  @builtin(num_workgroups) nwg: vec3<u32>) {
20
  let stride = nwg.x * WG;
21
- let start = (gid.y * nwg.x * WG) + gid.x;
22
  for (var col = start; col < params.cols; col = col + stride) {
23
- {% if source.intMode %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  var total = 0i;
 
 
 
 
 
 
 
 
25
  {% else %}
26
  var total = 0.0;
 
27
  {% endif %}
28
  for (var seg = 0u; seg < SPLIT; seg = seg + 1u) {
29
  let p = partials[seg * params.cols + col];
 
 
 
 
 
30
  total = total + p;
 
31
  }
32
- {% if source.outputF16 %}
 
 
 
 
 
 
 
 
 
 
33
  y[col] = f16(total);
34
  {% else %}
35
  y[col] = total;
 
 
36
  {% endif %}
37
  }
38
  }
 
2
  // folds the segment partials and applies the selected reduction's final step.
3
  // Segments are folded in ascending order for deterministic results. This order
4
  // differs from the single-pass reduction but remains within the f32 tolerance.
5
+ {% set addBias = addBias is defined and addBias %}
6
+ {% set biasCols = biasCols | default(0) %}
7
+ {% set intMode = intMode is defined and intMode %}
8
+ {% set yv = "f16(" if outputF16 else "" %}
9
+ {% set vy = ")" if outputF16 else "" %}
10
+ {% if outputF16 %}
11
  enable f16;
12
  {% endif %}
13
  {{ env.wgsl.resourceDeclarations }}
14
+ {% macro wgsl_minmax_identity(name, op, scalar="f32") %}
15
+ /* Exact {{ op }} reduction identity. WGSL rejects infinity during constant
16
+ * evaluation, so an f32 identity is constructed from its IEEE-754 bits. */
17
+ {% set logicalBoolIdentity = logicalBool is defined and logicalBool %}
18
+ fn {{ name }}() -> {{ scalar }} {
19
+ {% if scalar == "i32" %}
20
+ return {{ "-2147483647i - 1i" if op == "max" else "2147483647i" }};
21
+ {% elif scalar == "u32" %}
22
+ return {{ "1u" if logicalBoolIdentity and op == "min" else "0u" if op == "max" else "4294967295u" }};
23
+ {% else %}
24
+ var bits = {{ "0xff800000u" if op == "max" else "0x7f800000u" }};
25
+ return bitcast<f32>(bits);
26
+ {% endif %}
27
+ }
28
+ {%- endmacro %}
29
 
30
 
31
  const WG: u32 = {{ workgroupSize }}u;
32
  const SPLIT: u32 = {{ split }}u;
33
+ {% if addBias %}
34
+ const BIAS_COLS: u32 = {{ biasCols }}u;
35
+ {% endif %}
36
+ {% if op == "logsumexp" %}
37
+ const F32_MIN: f32 = -3.4028234663852886e38;
38
+ const F32_MAX: f32 = 3.4028234663852886e38;
39
+
40
+ fn is_nan_f32(value: f32) -> bool {
41
+ let bits = bitcast<u32>(value);
42
+ return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
43
+ }
44
+ {% elif op == "max" or op == "min" %}
45
+ {{ wgsl_minmax_identity("reduction_identity", op) }}
46
+ {% endif %}
47
 
48
  @compute @workgroup_size(WG, 1, 1)
49
  fn main(@builtin(global_invocation_id) gid: vec3<u32>,
50
  @builtin(num_workgroups) nwg: vec3<u32>) {
51
  let stride = nwg.x * WG;
52
+ let start = (gid.y * {{ DISPATCH_FOLD_WIDTH }}u * WG) + gid.x;
53
  for (var col = start; col < params.cols; col = col + stride) {
54
+ {% if op == "logsumexp" %}
55
+ // Merge SPLIT (segMax, segSumExp) pairs stably; carry NaN / +Inf markers.
56
+ var nan_value = 0.0;
57
+ var has_nan = false;
58
+ var global_max = F32_MIN;
59
+ for (var seg = 0u; seg < SPLIT; seg = seg + 1u) {
60
+ let nv = partials[(2u * SPLIT + seg) * params.cols + col];
61
+ if (nv != 0.0 || is_nan_f32(nv)) {
62
+ has_nan = true;
63
+ nan_value = nv;
64
+ }
65
+ global_max = max(global_max, partials[seg * params.cols + col]);
66
+ }
67
+ var sum = 0.0;
68
+ for (var seg = 0u; seg < SPLIT; seg = seg + 1u) {
69
+ let seg_max = partials[seg * params.cols + col];
70
+ let seg_sum = partials[(SPLIT + seg) * params.cols + col];
71
+ sum = sum + seg_sum * exp(seg_max - global_max);
72
+ }
73
+ let has_positive_inf = global_max > F32_MAX;
74
+ let finite_or_inf = select(global_max + log(sum), global_max, has_positive_inf);
75
+ y[col] = {{ yv }}select(finite_or_inf, nan_value, has_nan){{ vy }};
76
+ {% else %}
77
+ {% if intMode %}
78
+ {% if op == "prod" %}
79
+ var total = 1i;
80
+ {% else %}
81
  var total = 0i;
82
+ {% endif %}
83
+ {% else %}
84
+ {% if op == "max" %}
85
+ var total = reduction_identity();
86
+ {% elif op == "min" %}
87
+ var total = reduction_identity();
88
+ {% elif op == "prod" %}
89
+ var total = 1.0;
90
  {% else %}
91
  var total = 0.0;
92
+ {% endif %}
93
  {% endif %}
94
  for (var seg = 0u; seg < SPLIT; seg = seg + 1u) {
95
  let p = partials[seg * params.cols + col];
96
+ {% if op == "max" or op == "min" %}
97
+ total = {{ op }}(total, p);
98
+ {% elif op == "prod" %}
99
+ total = total * p;
100
+ {% else %}
101
  total = total + p;
102
+ {% endif %}
103
  }
104
+ {% if addBias %}
105
+ total = total + f32(bias[col % BIAS_COLS]);
106
+ {% endif %}
107
+ {% if op == "l2" %}
108
+ y[col] = {{ yv }}sqrt(total){{ vy }};
109
+ {% elif op == "logsum" %}
110
+ y[col] = {{ yv }}log(total){{ vy }};
111
+ {% elif op == "mean" %}
112
+ y[col] = {{ yv }}total / f32(params.rows){{ vy }};
113
+ {% else %}
114
+ {% if outputF16 %}
115
  y[col] = f16(total);
116
  {% else %}
117
  y[col] = total;
118
+ {% endif %}
119
+ {% endif %}
120
  {% endif %}
121
  }
122
  }
build/webgpu/reduce-axis0-splitk-reduce.wgsl.jinja CHANGED
@@ -2,31 +2,49 @@
2
  // segments increases residency for tall matrices. Each (column, segment)
3
  // invocation reduces one row slice and writes partials[segment * columns +
4
  // column]. Adjacent column threads keep row reads coalesced.
5
- //
6
- // logsumexp writes the segment maximum and sum of exp(x - maximum) as separate
7
- // partial planes; the combine pass merges them stably and handles NaN and +Inf.
8
- {% set castF32 = source.castF32 is defined and source.castF32 %}
9
  {% set xa = "f32(" if castF32 else "" %}
10
  {% set ax = ")" if castF32 else "" %}
11
- {% if source.usesF16 is defined and source.usesF16 %}
12
  enable f16;
13
  {% endif %}
14
  {{ env.wgsl.resourceDeclarations }}
15
- /* Exact max/min reduction identities. WGSL rejects infinity during constant
16
- * evaluation, so f32 identities are constructed at runtime from their
17
- * IEEE-754 bit patterns. */
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
 
20
  const WG: u32 = {{ workgroupSize }}u;
21
  const SPLIT: u32 = {{ split }}u;
 
 
 
 
 
 
 
 
 
 
22
 
23
  @compute @workgroup_size(WG, 1, 1)
24
  fn main(@builtin(global_invocation_id) gid: vec3<u32>,
25
- @builtin(workgroup_id) wg: vec3<u32>,
26
- @builtin(num_workgroups) nwg: vec3<u32>) {
27
- // 2D-folded column index: wg.z carries the high bits past the maxComputeWorkgroupsPerDimension
28
- // workgroup-per-dimension dispatch limit on the x dimension.
29
- let col = (wg.x + wg.z * nwg.x) * WG + (gid.x % WG);
30
  let seg = wg.y;
31
  if (col >= params.cols) { return; }
32
 
@@ -36,13 +54,62 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>,
36
  var r1 = r0 + chunk;
37
  if (r1 > params.rows) { r1 = params.rows; }
38
 
39
- {% if source.intMode %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  var acc = 0i;
 
 
 
 
 
 
 
 
41
  {% else %}
42
  var acc = 0.0;
 
43
  {% endif %}
44
  for (var row = r0; row < r1; row = row + 1u) {
 
 
 
 
 
 
 
 
 
 
45
  acc = acc + {{ xa }}x[row * params.cols + col]{{ ax }};
 
46
  }
47
  partials[seg * params.cols + col] = acc;
 
48
  }
 
2
  // segments increases residency for tall matrices. Each (column, segment)
3
  // invocation reduces one row slice and writes partials[segment * columns +
4
  // column]. Adjacent column threads keep row reads coalesced.
5
+ {% set castF32 = castF32 is defined and castF32 %}
 
 
 
6
  {% set xa = "f32(" if castF32 else "" %}
7
  {% set ax = ")" if castF32 else "" %}
8
+ {% if usesF16Spec is defined and usesF16Spec %}
9
  enable f16;
10
  {% endif %}
11
  {{ env.wgsl.resourceDeclarations }}
12
+ {% macro wgsl_minmax_identity(name, op, scalar="f32") %}
13
+ /* Exact {{ op }} reduction identity. WGSL rejects infinity during constant
14
+ * evaluation, so an f32 identity is constructed from its IEEE-754 bits. */
15
+ {% set logicalBoolIdentity = logicalBool is defined and logicalBool %}
16
+ fn {{ name }}() -> {{ scalar }} {
17
+ {% if scalar == "i32" %}
18
+ return {{ "-2147483647i - 1i" if op == "max" else "2147483647i" }};
19
+ {% elif scalar == "u32" %}
20
+ return {{ "1u" if logicalBoolIdentity and op == "min" else "0u" if op == "max" else "4294967295u" }};
21
+ {% else %}
22
+ var bits = {{ "0xff800000u" if op == "max" else "0x7f800000u" }};
23
+ return bitcast<f32>(bits);
24
+ {% endif %}
25
+ }
26
+ {%- endmacro %}
27
 
28
 
29
  const WG: u32 = {{ workgroupSize }}u;
30
  const SPLIT: u32 = {{ split }}u;
31
+ {% if op == "logsumexp" %}
32
+ const F32_MIN: f32 = -3.4028234663852886e38;
33
+
34
+ fn is_nan_f32(value: f32) -> bool {
35
+ let bits = bitcast<u32>(value);
36
+ return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
37
+ }
38
+ {% elif op == "max" or op == "min" %}
39
+ {{ wgsl_minmax_identity("reduction_identity", op) }}
40
+ {% endif %}
41
 
42
  @compute @workgroup_size(WG, 1, 1)
43
  fn main(@builtin(global_invocation_id) gid: vec3<u32>,
44
+ @builtin(workgroup_id) wg: vec3<u32>) {
45
+ // 2D-folded column index: wg.z carries the high bits past the per-axis dispatch
46
+ // fold width on the x dimension.
47
+ let col = (wg.x + wg.z * {{ DISPATCH_FOLD_WIDTH }}u) * WG + (gid.x % WG);
 
48
  let seg = wg.y;
49
  if (col >= params.cols) { return; }
50
 
 
54
  var r1 = r0 + chunk;
55
  if (r1 > params.rows) { r1 = params.rows; }
56
 
57
+ {% if op == "logsumexp" %}
58
+ var local_max = F32_MIN;
59
+ var local_nan_count = 0.0;
60
+ var local_nan_value = 0.0;
61
+ for (var row = r0; row < r1; row = row + 1u) {
62
+ let value = {{ xa }}x[row * params.cols + col]{{ ax }};
63
+ if (is_nan_f32(value)) {
64
+ local_nan_count = local_nan_count + 1.0;
65
+ local_nan_value = value;
66
+ } else {
67
+ local_max = max(local_max, value);
68
+ }
69
+ }
70
+ var acc = 0.0;
71
+ if (local_nan_count == 0.0) {
72
+ for (var row = r0; row < r1; row = row + 1u) {
73
+ acc = acc + exp({{ xa }}x[row * params.cols + col]{{ ax }} - local_max);
74
+ }
75
+ }
76
+ // Three partial planes: seg max, seg sumexp(x - max), and a packed nan
77
+ // marker (count in the low slot, the nan bit-pattern smuggled as f32).
78
+ partials[seg * params.cols + col] = local_max;
79
+ partials[(SPLIT + seg) * params.cols + col] = acc;
80
+ partials[(2u * SPLIT + seg) * params.cols + col] = select(0.0, local_nan_value, local_nan_count > 0.0);
81
+ {% else %}
82
+ {% if intMode | default(false) %}
83
+ {% if op == "prod" %}
84
+ var acc = 1i;
85
+ {% else %}
86
  var acc = 0i;
87
+ {% endif %}
88
+ {% else %}
89
+ {% if op == "max" %}
90
+ var acc = reduction_identity();
91
+ {% elif op == "min" %}
92
+ var acc = reduction_identity();
93
+ {% elif op == "prod" %}
94
+ var acc = 1.0;
95
  {% else %}
96
  var acc = 0.0;
97
+ {% endif %}
98
  {% endif %}
99
  for (var row = r0; row < r1; row = row + 1u) {
100
+ {% if op == "max" or op == "min" %}
101
+ acc = {{ op }}(acc, {{ xa }}x[row * params.cols + col]{{ ax }});
102
+ {% elif op == "prod" %}
103
+ acc = acc * {{ xa }}x[row * params.cols + col]{{ ax }};
104
+ {% elif op == "l1" %}
105
+ acc = acc + abs({{ xa }}x[row * params.cols + col]{{ ax }});
106
+ {% elif op == "l2" or op == "sumsquare" %}
107
+ let value = {{ xa }}x[row * params.cols + col]{{ ax }};
108
+ acc = acc + value * value;
109
+ {% else %}
110
  acc = acc + {{ xa }}x[row * params.cols + col]{{ ax }};
111
+ {% endif %}
112
  }
113
  partials[seg * params.cols + col] = acc;
114
+ {% endif %}
115
  }
build/webgpu/reduce-axis0-tilecols.wgsl.jinja CHANGED
@@ -1,9 +1,9 @@
1
  // Tiled column-wise reduction. Each workgroup owns TILE_COLS columns;
2
  // ROW_LANES threads stride the rows of one column, then lane 0 folds their
3
  // partials. In split mode, workgroup y selects an axis segment and finalization
4
- // is deferred to the combine pass. Cooperative row lanes preserve occupancy
5
- // when the flattened output has only a few elements.
6
- {% set splitMode = source.split is defined %}
7
  {% if splitMode %}
8
  {% set rowBegin = "row_begin + row_lane" %}
9
  {% set rowEnd = "row_end" %}
@@ -13,34 +13,66 @@
13
  {% set rowEnd = "params.rows" %}
14
  {% set elem = "x[inputBase + row * params.cols + col]" %}
15
  {% endif %}
16
- {% set castF32 = source.castF32 is defined and source.castF32 %}
17
- {% set intMode = source.intMode is defined and source.intMode %}
18
  {% set scalar = "f32" if castF32 else scalar %}
19
  {% if castF32 %}
20
  {% set elem = "f32(" ~ elem ~ ")" %}
21
  {% endif %}
22
  {% set yv = "f16(" if castF32 else "" %}
23
  {% set vy = ")" if castF32 else "" %}
24
- {% if source.usesF16 is defined and source.usesF16 %}
25
  enable f16;
26
  {% endif %}
27
  {{ env.wgsl.resourceDeclarations }}
28
- /* Exact max/min reduction identities. WGSL rejects infinity during constant
29
- * evaluation, so f32 identities are constructed at runtime from their
30
- * IEEE-754 bit patterns. */
 
 
 
 
 
 
 
 
 
 
 
 
31
 
 
 
 
 
 
 
 
32
 
33
  const WG: u32 = {{ workgroupSize }}u;
34
  const TILE_COLS: u32 = {{ tileCols }}u;
35
  const ROW_LANES: u32 = WG / TILE_COLS;
36
  {% if splitMode %}
37
- const SPLIT: u32 = {{ source.split }}u;
 
 
 
 
 
 
38
  {% endif %}
39
 
40
- var<workgroup> partial: array<{{ scalar if (source.op == "max" or source.op == "min" or intMode) else "f32" }}, WG>;
 
 
 
 
 
 
 
 
41
  @compute @workgroup_size(WG, 1, 1)
42
- fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>{% if not splitMode %},
43
- @builtin(num_workgroups) nwg: vec3<u32>{% endif %}) {
44
  let tid = lid.x;
45
  let col_lane = tid % TILE_COLS;
46
  let row_lane = tid / TILE_COLS;
@@ -61,17 +93,134 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid:
61
  // 2D-folded tile index: wg.y carries the high bits past the dispatch limit.
62
  // The batched form reuses this same coalesced axis-0 reduction for a middle
63
  // axis by assigning consecutive tiles to each outer slice.
64
- let tile = wg.x + wg.y * nwg.x;
65
  let col = tile * TILE_COLS + col_lane;
66
  let inputBase = 0u;
67
  let outputIndex = col;
68
  let in_bounds = col < params.cols;
69
  {% endif %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
 
 
 
 
 
71
  var acc = {% if intMode %}{{ scalar }}(0){% else %}0.0{% endif %};
 
72
  if (in_bounds) {
73
  for (var row = {{ rowBegin }}; row < {{ rowEnd }}; row = row + ROW_LANES) {
 
 
 
 
 
 
 
 
 
 
74
  acc = acc + {{ elem }};
 
75
  }
76
  }
77
  partial[tid] = acc;
@@ -80,12 +229,32 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid:
80
  if (row_lane == 0u && in_bounds) {
81
  var total = partial[col_lane];
82
  for (var lane = 1u; lane < ROW_LANES; lane = lane + 1u) {
 
 
 
 
 
83
  total = total + partial[lane * TILE_COLS + col_lane];
 
84
  }
85
  {% if splitMode %}
86
  partials[seg * params.outputs + outputIndex] = total;
 
 
 
 
 
 
 
 
 
 
 
 
87
  {% else %}
88
  y[outputIndex] = {{ yv }}total{{ vy }};
 
89
  {% endif %}
90
  }
 
91
  }
 
1
  // Tiled column-wise reduction. Each workgroup owns TILE_COLS columns;
2
  // ROW_LANES threads stride the rows of one column, then lane 0 folds their
3
  // partials. In split mode, workgroup y selects an axis segment and finalization
4
+ // is deferred to the combine pass. Cooperative row lanes expose independent
5
+ // reduction work when the flattened output has only a few elements.
6
+ {% set splitMode = splitSpec is defined %}
7
  {% if splitMode %}
8
  {% set rowBegin = "row_begin + row_lane" %}
9
  {% set rowEnd = "row_end" %}
 
13
  {% set rowEnd = "params.rows" %}
14
  {% set elem = "x[inputBase + row * params.cols + col]" %}
15
  {% endif %}
16
+ {% set castF32 = castF32 is defined and castF32 %}
17
+ {% set intMode = intMode is defined and intMode %}
18
  {% set scalar = "f32" if castF32 else scalar %}
19
  {% if castF32 %}
20
  {% set elem = "f32(" ~ elem ~ ")" %}
21
  {% endif %}
22
  {% set yv = "f16(" if castF32 else "" %}
23
  {% set vy = ")" if castF32 else "" %}
24
+ {% if usesF16Spec is defined and usesF16Spec %}
25
  enable f16;
26
  {% endif %}
27
  {{ env.wgsl.resourceDeclarations }}
28
+ {% macro wgsl_minmax_identity(name, op, scalar="f32") %}
29
+ /* Exact {{ op }} reduction identity. WGSL rejects infinity during constant
30
+ * evaluation, so an f32 identity is constructed from its IEEE-754 bits. */
31
+ {% set logicalBoolIdentity = logicalBool is defined and logicalBool %}
32
+ fn {{ name }}() -> {{ scalar }} {
33
+ {% if scalar == "i32" %}
34
+ return {{ "-2147483647i - 1i" if op == "max" else "2147483647i" }};
35
+ {% elif scalar == "u32" %}
36
+ return {{ "1u" if logicalBoolIdentity and op == "min" else "0u" if op == "max" else "4294967295u" }};
37
+ {% else %}
38
+ var bits = {{ "0xff800000u" if op == "max" else "0x7f800000u" }};
39
+ return bitcast<f32>(bits);
40
+ {% endif %}
41
+ }
42
+ {%- endmacro %}
43
 
44
+ {% if not splitMode and not intMode and (op == "logsum" or op == "logsumexp") %}
45
+ fn negative_infinity() -> f32 {
46
+ var bits = 0xff800000u;
47
+ return bitcast<f32>(bits);
48
+ }
49
+
50
+ {% endif %}
51
 
52
  const WG: u32 = {{ workgroupSize }}u;
53
  const TILE_COLS: u32 = {{ tileCols }}u;
54
  const ROW_LANES: u32 = WG / TILE_COLS;
55
  {% if splitMode %}
56
+ const SPLIT: u32 = {{ splitSpec }}u;
57
+ {% endif %}
58
+ {% if op == "max" or op == "min" %}
59
+ {{ wgsl_minmax_identity("lane_identity", op, scalar) }}
60
+ {% elif op == "logsumexp" %}
61
+ const F32_MIN: f32 = -3.4028234663852886e38;
62
+ const F32_MAX: f32 = 3.4028234663852886e38;
63
  {% endif %}
64
 
65
+ var<workgroup> partial: array<{{ scalar if (op == "max" or op == "min" or intMode) else "f32" }}, WG>;
66
+ {% if op == "logsumexp" %}
67
+
68
+ fn is_nan_f32(value: f32) -> bool {
69
+ let bits = bitcast<u32>(value);
70
+ return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
71
+ }
72
+
73
+ {% endif %}
74
  @compute @workgroup_size(WG, 1, 1)
75
+ fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
 
76
  let tid = lid.x;
77
  let col_lane = tid % TILE_COLS;
78
  let row_lane = tid / TILE_COLS;
 
93
  // 2D-folded tile index: wg.y carries the high bits past the dispatch limit.
94
  // The batched form reuses this same coalesced axis-0 reduction for a middle
95
  // axis by assigning consecutive tiles to each outer slice.
96
+ let tile = wg.x + wg.y * {{ DISPATCH_FOLD_WIDTH }}u;
97
  let col = tile * TILE_COLS + col_lane;
98
  let inputBase = 0u;
99
  let outputIndex = col;
100
  let in_bounds = col < params.cols;
101
  {% endif %}
102
+ {% if op == "logsumexp" %}
103
+ {% if not splitMode %}
104
+
105
+ if (params.rows == 0u) {
106
+ if (row_lane == 0u && in_bounds) {
107
+ y[outputIndex] = {{ yv }}negative_infinity(){{ vy }};
108
+ }
109
+ return;
110
+ }
111
+ {% endif %}
112
+
113
+ var local_max = F32_MIN;
114
+ var local_nan_count = 0.0;
115
+ var local_nan_value = 0.0;
116
+ if (in_bounds) {
117
+ for (var row = {{ rowBegin }}; row < {{ rowEnd }}; row = row + ROW_LANES) {
118
+ let value = {{ elem }};
119
+ if (is_nan_f32(value)) {
120
+ local_nan_count = local_nan_count + 1.0;
121
+ local_nan_value = value;
122
+ } else {
123
+ local_max = max(local_max, value);
124
+ }
125
+ }
126
+ }
127
+
128
+ partial[tid] = local_max;
129
+ workgroupBarrier();
130
+ if (row_lane == 0u && in_bounds) {
131
+ var max_lanes = partial[col_lane];
132
+ for (var lane = 1u; lane < ROW_LANES; lane = lane + 1u) {
133
+ max_lanes = max(max_lanes, partial[lane * TILE_COLS + col_lane]);
134
+ }
135
+ partial[col_lane] = max_lanes;
136
+ }
137
+ workgroupBarrier();
138
+ let max_value = partial[col_lane];
139
+ workgroupBarrier();
140
+
141
+ partial[tid] = local_nan_count;
142
+ workgroupBarrier();
143
+ if (row_lane == 0u && in_bounds) {
144
+ var nan_lanes = partial[col_lane];
145
+ for (var lane = 1u; lane < ROW_LANES; lane = lane + 1u) {
146
+ nan_lanes = nan_lanes + partial[lane * TILE_COLS + col_lane];
147
+ }
148
+ partial[col_lane] = nan_lanes;
149
+ }
150
+ workgroupBarrier();
151
+ let nan_count = partial[col_lane];
152
+ workgroupBarrier();
153
+
154
+ partial[tid] = local_nan_value;
155
+ workgroupBarrier();
156
+ if (row_lane == 0u && in_bounds) {
157
+ var nan_value_lanes = partial[col_lane];
158
+ for (var lane = 1u; lane < ROW_LANES; lane = lane + 1u) {
159
+ nan_value_lanes = nan_value_lanes + partial[lane * TILE_COLS + col_lane];
160
+ }
161
+ partial[col_lane] = nan_value_lanes;
162
+ }
163
+ workgroupBarrier();
164
+ let nan_value = partial[col_lane];
165
+ let has_nan = nan_count > 0.0;
166
+ let has_positive_inf = max_value > F32_MAX;
167
+ workgroupBarrier();
168
+
169
+ var acc = 0.0;
170
+ if (in_bounds && !has_nan && !has_positive_inf) {
171
+ for (var row = {{ rowBegin }}; row < {{ rowEnd }}; row = row + ROW_LANES) {
172
+ acc = acc + exp({{ elem }} - max_value);
173
+ }
174
+ }
175
+ partial[tid] = acc;
176
+ workgroupBarrier();
177
+
178
+ if (row_lane == 0u && in_bounds) {
179
+ var sum = partial[col_lane];
180
+ for (var lane = 1u; lane < ROW_LANES; lane = lane + 1u) {
181
+ sum = sum + partial[lane * TILE_COLS + col_lane];
182
+ }
183
+ {% if splitMode %}
184
+ partials[seg * params.outputs + outputIndex] = max_value;
185
+ partials[(SPLIT + seg) * params.outputs + outputIndex] = sum;
186
+ partials[(2u * SPLIT + seg) * params.outputs + outputIndex] = select(0.0, nan_value, has_nan);
187
+ {% else %}
188
+ let finite_or_inf = select(max_value + log(sum), max_value, has_positive_inf);
189
+ y[outputIndex] = {{ yv }}select(finite_or_inf, nan_value, has_nan){{ vy }};
190
+ {% endif %}
191
+ }
192
+ {% else %}
193
+ {% if op == "logsum" and not splitMode %}
194
+
195
+ if (params.rows == 0u) {
196
+ if (row_lane == 0u && in_bounds) {
197
+ y[outputIndex] = {{ yv }}negative_infinity(){{ vy }};
198
+ }
199
+ return;
200
+ }
201
+ {% endif %}
202
 
203
+ {% if op == "max" or op == "min" %}
204
+ var acc = lane_identity();
205
+ {% elif op == "prod" %}
206
+ var acc = {% if intMode %}{{ scalar }}(1){% else %}1.0{% endif %};
207
+ {% else %}
208
  var acc = {% if intMode %}{{ scalar }}(0){% else %}0.0{% endif %};
209
+ {% endif %}
210
  if (in_bounds) {
211
  for (var row = {{ rowBegin }}; row < {{ rowEnd }}; row = row + ROW_LANES) {
212
+ {% if op == "max" or op == "min" %}
213
+ acc = {{ op }}(acc, {{ elem }});
214
+ {% elif op == "prod" %}
215
+ acc = acc * {{ elem }};
216
+ {% elif op == "l1" %}
217
+ acc = acc + abs({{ elem }});
218
+ {% elif op == "l2" or op == "sumsquare" %}
219
+ let value = {{ elem }};
220
+ acc = acc + value * value;
221
+ {% else %}
222
  acc = acc + {{ elem }};
223
+ {% endif %}
224
  }
225
  }
226
  partial[tid] = acc;
 
229
  if (row_lane == 0u && in_bounds) {
230
  var total = partial[col_lane];
231
  for (var lane = 1u; lane < ROW_LANES; lane = lane + 1u) {
232
+ {% if op == "max" or op == "min" %}
233
+ total = {{ op }}(total, partial[lane * TILE_COLS + col_lane]);
234
+ {% elif op == "prod" %}
235
+ total = total * partial[lane * TILE_COLS + col_lane];
236
+ {% else %}
237
  total = total + partial[lane * TILE_COLS + col_lane];
238
+ {% endif %}
239
  }
240
  {% if splitMode %}
241
  partials[seg * params.outputs + outputIndex] = total;
242
+ {% else %}
243
+ {% if op == "l2" and intMode %}
244
+ y[outputIndex] = {{ scalar }}(sqrt(f32(total)));
245
+ {% elif op == "l2" %}
246
+ y[outputIndex] = {{ yv }}sqrt(total){{ vy }};
247
+ {% elif op == "logsum" %}
248
+ y[outputIndex] = {{ yv }}log(total){{ vy }};
249
+ {% elif op == "mean" and intMode %}
250
+ // Integer division truncates toward zero.
251
+ y[outputIndex] = total / {{ scalar }}(params.rows);
252
+ {% elif op == "mean" %}
253
+ y[outputIndex] = {{ yv }}total / f32(params.rows){{ vy }};
254
  {% else %}
255
  y[outputIndex] = {{ yv }}total{{ vy }};
256
+ {% endif %}
257
  {% endif %}
258
  }
259
+ {% endif %}
260
  }
build/webgpu/reduce-flat-partial.wgsl.jinja CHANGED
@@ -6,20 +6,37 @@
6
  // Scalar f32 bindings keep arbitrary element counts legal. The grid-stride loop
7
  // manually assembles full vec4 groups from contiguous scalars, and one global
8
  // thread folds the final zero-to-three scalar elements exactly once.
9
- {% set castF32 = source.castF32 is defined and source.castF32 %}
 
10
  {% set xa = "f32(" if castF32 else "" %}
11
  {% set ax = ")" if castF32 else "" %}
12
- {% if source.usesF16 is defined and source.usesF16 %}
13
  enable f16;
14
  {% endif %}
15
  {{ env.wgsl.resourceDeclarations }}
16
- /* Exact max/min reduction identities. WGSL rejects infinity during constant
17
- * evaluation, so f32 identities are constructed at runtime from their
18
- * IEEE-754 bit patterns. */
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
 
21
  const WG: u32 = {{ workgroupSize }}u;
22
- var<workgroup> red: array<{{ "i32" if source.intMode else "f32" }}, WG>;
 
 
 
 
23
  @compute @workgroup_size(WG)
24
  fn main(@builtin(global_invocation_id) gid: vec3<u32>,
25
  @builtin(local_invocation_id) lid: vec3<u32>,
@@ -27,16 +44,38 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>,
27
  @builtin(num_workgroups) nwg: vec3<u32>) {
28
  let tid = lid.x;
29
  let gstride = nwg.x * WG;
30
- {% if source.intMode %}
 
 
 
31
  var acc = 0i;
 
 
 
 
 
 
 
32
  {% else %}
33
  var acc = 0.0;
34
  {% endif %}
35
  // Grid-stride over the flat vec4 groups (params.count4 = numel / 4, floored).
36
  for (var i = gid.x; i < params.count4; i = i + gstride) {
37
  let b = 4u * i;
38
- let v = vec4<{{ "i32" if source.intMode else "f32" }}>({{ xa }}x[b]{{ ax }}, {{ xa }}x[b + 1u]{{ ax }}, {{ xa }}x[b + 2u]{{ ax }}, {{ xa }}x[b + 3u]{{ ax }});
 
 
 
 
 
 
 
 
 
 
 
39
  acc = acc + v.x + v.y + v.z + v.w;
 
40
  }
41
  // Scalar tail (the 0..3 elements past the last full vec4). One global thread
42
  // folds it so it is counted exactly once; the count is tiny so serializing it
@@ -44,7 +83,19 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>,
44
  if (gid.x == 0u) {
45
  for (var i = 4u * params.count4; i < params.numel; i = i + 1u) {
46
  let s = {{ xa }}x[i]{{ ax }};
 
 
 
 
 
 
 
 
 
 
 
47
  acc = acc + s;
 
48
  }
49
  }
50
  red[tid] = acc;
@@ -53,7 +104,15 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>,
53
  loop {
54
  if (stride == 0u) { break; }
55
  if (tid < stride) {
 
 
 
 
 
 
 
56
  red[tid] = red[tid] + red[tid + stride];
 
57
  }
58
  stride = stride / 2u;
59
  workgroupBarrier();
 
6
  // Scalar f32 bindings keep arbitrary element counts legal. The grid-stride loop
7
  // manually assembles full vec4 groups from contiguous scalars, and one global
8
  // thread folds the final zero-to-three scalar elements exactly once.
9
+ {% set intMode = intMode is defined and intMode %}
10
+ {% set castF32 = castF32 is defined and castF32 %}
11
  {% set xa = "f32(" if castF32 else "" %}
12
  {% set ax = ")" if castF32 else "" %}
13
+ {% if usesF16Spec is defined and usesF16Spec %}
14
  enable f16;
15
  {% endif %}
16
  {{ env.wgsl.resourceDeclarations }}
17
+ {% macro wgsl_minmax_identity(name, op, scalar="f32") %}
18
+ /* Exact {{ op }} reduction identity. WGSL rejects infinity during constant
19
+ * evaluation, so an f32 identity is constructed from its IEEE-754 bits. */
20
+ {% set logicalBoolIdentity = logicalBool is defined and logicalBool %}
21
+ fn {{ name }}() -> {{ scalar }} {
22
+ {% if scalar == "i32" %}
23
+ return {{ "-2147483647i - 1i" if op == "max" else "2147483647i" }};
24
+ {% elif scalar == "u32" %}
25
+ return {{ "1u" if logicalBoolIdentity and op == "min" else "0u" if op == "max" else "4294967295u" }};
26
+ {% else %}
27
+ var bits = {{ "0xff800000u" if op == "max" else "0x7f800000u" }};
28
+ return bitcast<f32>(bits);
29
+ {% endif %}
30
+ }
31
+ {%- endmacro %}
32
 
33
 
34
  const WG: u32 = {{ workgroupSize }}u;
35
+ var<workgroup> red: array<{{ "i32" if intMode else "f32" }}, WG>;
36
+ {% if op == "max" or op == "min" %}
37
+ {{ wgsl_minmax_identity("reduction_identity", op) }}
38
+
39
+ {% endif %}
40
  @compute @workgroup_size(WG)
41
  fn main(@builtin(global_invocation_id) gid: vec3<u32>,
42
  @builtin(local_invocation_id) lid: vec3<u32>,
 
44
  @builtin(num_workgroups) nwg: vec3<u32>) {
45
  let tid = lid.x;
46
  let gstride = nwg.x * WG;
47
+ {% if intMode %}
48
+ {% if op == "prod" %}
49
+ var acc = 1i;
50
+ {% else %}
51
  var acc = 0i;
52
+ {% endif %}
53
+ {% elif op == "max" %}
54
+ var acc = reduction_identity();
55
+ {% elif op == "min" %}
56
+ var acc = reduction_identity();
57
+ {% elif op == "prod" %}
58
+ var acc = 1.0;
59
  {% else %}
60
  var acc = 0.0;
61
  {% endif %}
62
  // Grid-stride over the flat vec4 groups (params.count4 = numel / 4, floored).
63
  for (var i = gid.x; i < params.count4; i = i + gstride) {
64
  let b = 4u * i;
65
+ let v = vec4<{{ "i32" if intMode else "f32" }}>({{ xa }}x[b]{{ ax }}, {{ xa }}x[b + 1u]{{ ax }}, {{ xa }}x[b + 2u]{{ ax }}, {{ xa }}x[b + 3u]{{ ax }});
66
+ {% if op == "max" %}
67
+ acc = max(acc, max(max(v.x, v.y), max(v.z, v.w)));
68
+ {% elif op == "min" %}
69
+ acc = min(acc, min(min(v.x, v.y), min(v.z, v.w)));
70
+ {% elif op == "prod" %}
71
+ acc = acc * v.x * v.y * v.z * v.w;
72
+ {% elif op == "l1" %}
73
+ acc = acc + abs(v.x) + abs(v.y) + abs(v.z) + abs(v.w);
74
+ {% elif op == "l2" or op == "sumsquare" %}
75
+ acc = acc + dot(v, v);
76
+ {% else %}
77
  acc = acc + v.x + v.y + v.z + v.w;
78
+ {% endif %}
79
  }
80
  // Scalar tail (the 0..3 elements past the last full vec4). One global thread
81
  // folds it so it is counted exactly once; the count is tiny so serializing it
 
83
  if (gid.x == 0u) {
84
  for (var i = 4u * params.count4; i < params.numel; i = i + 1u) {
85
  let s = {{ xa }}x[i]{{ ax }};
86
+ {% if op == "max" %}
87
+ acc = max(acc, s);
88
+ {% elif op == "min" %}
89
+ acc = min(acc, s);
90
+ {% elif op == "prod" %}
91
+ acc = acc * s;
92
+ {% elif op == "l1" %}
93
+ acc = acc + abs(s);
94
+ {% elif op == "l2" or op == "sumsquare" %}
95
+ acc = acc + s * s;
96
+ {% else %}
97
  acc = acc + s;
98
+ {% endif %}
99
  }
100
  }
101
  red[tid] = acc;
 
104
  loop {
105
  if (stride == 0u) { break; }
106
  if (tid < stride) {
107
+ {% if op == "max" %}
108
+ red[tid] = max(red[tid], red[tid + stride]);
109
+ {% elif op == "min" %}
110
+ red[tid] = min(red[tid], red[tid + stride]);
111
+ {% elif op == "prod" %}
112
+ red[tid] = red[tid] * red[tid + stride];
113
+ {% else %}
114
  red[tid] = red[tid] + red[tid + stride];
115
+ {% endif %}
116
  }
117
  stride = stride / 2u;
118
  workgroupBarrier();
build/webgpu/reduce-multi-axis-coop.wgsl.jinja ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% macro reduce_multi_axis_offset(hasReduced) %}
2
+
3
+ // One thread per output element walks the Cartesian product of the reduced axes,
4
+ // linearized as reduce_linear. Specialized shapes make every input offset a sum
5
+ // of coordinate-times-constant terms.
6
+ fn input_offset(out_index: u32{% if hasReduced %}, reduce_linear: u32{% endif %}) -> u32 {
7
+ var rem = out_index;
8
+ {% for oaxis in range(outputRank) %}
9
+ {% set ostride = namespace(value=1) %}
10
+ {% for j in range(oaxis + 1, outputRank) %}
11
+ {% set ostride.value = ostride.value * outputShape[j] %}
12
+ {% endfor %}
13
+ {% set osafe = 1 if ostride.value == 0 else ostride.value %}
14
+ {% if not keepDims or not reduce[oaxis] %}
15
+ let out_coord{{ oaxis }} = rem / {{ osafe }}u;
16
+ {% endif %}
17
+ rem = rem % {{ osafe }}u;
18
+ {% endfor %}
19
+ {% if hasReduced %}
20
+ var rrem = reduce_linear;
21
+ {% endif %}
22
+ {% for a in range(rank) if reduce[a] %}
23
+ {% set rstride = namespace(value=1) %}
24
+ {% for b in range(a + 1, rank) if reduce[b] %}
25
+ {% set rstride.value = rstride.value * dataShape[b] %}
26
+ {% endfor %}
27
+ {% set rsafe = 1 if rstride.value == 0 else rstride.value %}
28
+ let red_coord{{ a }} = rrem / {{ rsafe }}u;
29
+ rrem = rrem % {{ rsafe }}u;
30
+ {% endfor %}
31
+ {% set oc = namespace(i=0) %}
32
+ {% for a in range(rank) %}
33
+ {% if reduce[a] %}
34
+ let coord{{ a }} = red_coord{{ a }};
35
+ {% elif keepDims %}
36
+ let coord{{ a }} = out_coord{{ a }};
37
+ {% else %}
38
+ let coord{{ a }} = out_coord{{ oc.i }};
39
+ {% set oc.i = oc.i + 1 %}
40
+ {% endif %}
41
+ {% endfor %}
42
+ {% set src = namespace(value="coord0") %}
43
+ {% for a in range(1, rank) %}
44
+ {% set src.value = "(" ~ src.value ~ " * " ~ dataShape[a] ~ "u + coord" ~ a ~ ")" %}
45
+ {% endfor %}
46
+ return {{ src.value }};
47
+ }
48
+ {%- endmacro %}
49
+ // Cooperative multi-axis reduction with one workgroup per output element.
50
+ // Threads take strided shares of the reduced index range, then combine their
51
+ // partials through a workgroup tree. The shared offset helper maps each reduced
52
+ // coordinate to the corresponding input element.
53
+ //
54
+ // The tree changes f32 association relative to a left-to-right fold, so the two
55
+ // accumulation orders need not be bit-identical.
56
+ {% set castF32 = castF32 is defined and castF32 %}
57
+ {% set intMode = intMode is defined and intMode %}
58
+ {% set yv = "f16(" if castF32 else "" %}
59
+ {% set vy = ")" if castF32 else "" %}
60
+ {% if usesF16Spec is defined and usesF16Spec %}
61
+ enable f16;
62
+ {% endif %}
63
+ {{ env.wgsl.resourceDeclarations }}
64
+ {% set hasReducedAxis = namespace(value=false) %}
65
+ {% for a in range(rank) %}{% if reduce[a] %}{% set hasReducedAxis.value = true %}{% endif %}{% endfor %}
66
+ {{- reduce_multi_axis_offset(hasReducedAxis.value) }}
67
+
68
+ {% set ACC = scalar if intMode else "f32" %}
69
+ const WG: u32 = {{ workgroupSize }}u;
70
+ {% set mcount = namespace(value=1) %}
71
+ {% for a in range(rank) if reduce[a] %}
72
+ {% set mcount.value = mcount.value * dataShape[a] %}
73
+ {% endfor %}
74
+ const REDUCED: u32 = {{ mcount.value }}u;
75
+ {% set ocount = namespace(value=1) %}
76
+ {% for a in range(outputRank) %}
77
+ {% set ocount.value = ocount.value * outputShape[a] %}
78
+ {% endfor %}
79
+ const OUT_COUNT: u32 = {{ ocount.value }}u;
80
+
81
+ var<workgroup> partials: array<{{ ACC }}, WG>;
82
+
83
+ // The reduction's combine, used identically by the per-thread accumulation and by
84
+ // the workgroup fold, so the two cannot disagree about the operator.
85
+ fn combine(a: {{ ACC }}, b: {{ ACC }}) -> {{ ACC }} {
86
+ return a + b;
87
+ }
88
+
89
+ @compute @workgroup_size({{ workgroupSize }})
90
+ fn main(
91
+ @builtin(workgroup_id) wg: vec3<u32>,
92
+ @builtin(local_invocation_id) lid3: vec3<u32>
93
+ ) {
94
+ // 2D-folded output index: wg.y carries the high bits past the per-axis
95
+ // dispatch fold width.
96
+ let i = wg.x + wg.y * {{ DISPATCH_FOLD_WIDTH }}u;
97
+ if (i >= OUT_COUNT) {
98
+ return;
99
+ }
100
+ let lid = lid3.x;
101
+ var acc = {{ ACC }}(0);
102
+
103
+ for (var r = lid; r < REDUCED; r = r + WG) {
104
+ {% set at = "x[input_offset(i, r)]" %}
105
+ {% if castF32 %}
106
+ {% set at = "f32(" ~ at ~ ")" %}
107
+ {% endif %}
108
+ acc = combine(acc, {{ at }});
109
+ }
110
+
111
+ // Fold the per-thread accumulators. WORKGROUP_SIZE is a power of two, and every
112
+ // lane holds the reduction identity when its strided share was empty, so the
113
+ // tree needs no separate active-lane count.
114
+ partials[lid] = acc;
115
+ workgroupBarrier();
116
+ var stride = WG / 2u;
117
+ loop {
118
+ if (stride == 0u) { break; }
119
+ if (lid < stride) {
120
+ partials[lid] = combine(partials[lid], partials[lid + stride]);
121
+ }
122
+ workgroupBarrier();
123
+ stride = stride / 2u;
124
+ }
125
+ if (lid != 0u) {
126
+ return;
127
+ }
128
+ let total = partials[0];
129
+ y[i] = {{ yv }}total{{ vy }};
130
+ }
build/webgpu/reduce-row-subgroup-rows.wgsl.jinja ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Subgroup-per-row reduction for the selected operation on a contiguous last axis.
2
+ // One SUBGROUP owns each output row and a workgroup carries as many rows as it
3
+ // has subgroups, so the row fold is a single subgroup collective with no
4
+ // workgroup memory or barriers. Every lane
5
+ // reads vec4 words strided by the subgroup width, keeps them in registers, and
6
+ // the row is folded by the selected collective.
7
+ {% if op == "logsumexp" %}
8
+ // The row remains in registers across the maximum, NaN census, and exponential
9
+ // sum. Max subtraction and a positive-infinity branch define the stable result.
10
+ {% elif op == "mean" %}
11
+ // The finalizer divides the accumulated sum by the row width.
12
+ {% elif op == "l2" %}
13
+ // The finalizer takes the square root of the sum of squares.
14
+ {% elif op == "logsum" %}
15
+ // The finalizer takes the logarithm of the sum.
16
+ {% endif %}
17
+ // f16 storage widens before accumulation and narrows only at the final store.
18
+ {% set castF32 = castF32 is defined and castF32 %}
19
+ {% set isInt = scalar == "i32" or scalar == "u32" %}
20
+ {% set accScalar = scalar if isInt else "f32" %}
21
+ {% set xv = "vec4<f32>(" if castF32 else "" %}
22
+ {% set vx = ")" if castF32 else "" %}
23
+ {% set yv = "f16(" if castF32 else "" %}
24
+ {% set vy = ")" if castF32 else "" %}
25
+ enable subgroups;
26
+ {% if usesF16Spec is defined and usesF16Spec %}
27
+ enable f16;
28
+ {% endif %}
29
+ {{ env.wgsl.resourceDeclarations }}
30
+ {% macro wgsl_minmax_identity(name, op, scalar="f32") %}
31
+ /* Exact {{ op }} reduction identity. WGSL rejects infinity during constant
32
+ * evaluation, so an f32 identity is constructed from its IEEE-754 bits. */
33
+ {% set logicalBoolIdentity = logicalBool is defined and logicalBool %}
34
+ fn {{ name }}() -> {{ scalar }} {
35
+ {% if scalar == "i32" %}
36
+ return {{ "-2147483647i - 1i" if op == "max" else "2147483647i" }};
37
+ {% elif scalar == "u32" %}
38
+ return {{ "1u" if logicalBoolIdentity and op == "min" else "0u" if op == "max" else "4294967295u" }};
39
+ {% else %}
40
+ var bits = {{ "0xff800000u" if op == "max" else "0x7f800000u" }};
41
+ return bitcast<f32>(bits);
42
+ {% endif %}
43
+ }
44
+ {%- endmacro %}
45
+
46
+
47
+ const WG: u32 = {{ workgroupSize }}u;
48
+ {%- if op == "max" or op == "min" %}
49
+ {{ wgsl_minmax_identity("reduction_identity", op, accScalar) }}
50
+ {%- endif %}
51
+ {%- if op == "logsumexp" %}
52
+
53
+ const F32_MIN: f32 = -3.4028234663852886e38;
54
+ const F32_MAX: f32 = 3.4028234663852886e38;
55
+
56
+ fn is_nan_f32(value: f32) -> bool {
57
+ let bits = bitcast<u32>(value);
58
+ return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
59
+ }
60
+ {%- endif %}
61
+
62
+ @compute @workgroup_size(WG, 1, 1)
63
+ fn main(@builtin(workgroup_id) wg: vec3<u32>,
64
+ @builtin(local_invocation_id) lid: vec3<u32>,
65
+ @builtin(subgroup_invocation_id) sgLane: u32,
66
+ @builtin(subgroup_size) sgSize: u32) {
67
+ // Rows tile the folded workgroup grid; the lanes of one subgroup share a row.
68
+ let rowsPerWorkgroup = WG / sgSize;
69
+ let row = (wg.x + wg.y * {{ DISPATCH_FOLD_WIDTH }}u) * rowsPerWorkgroup + lid.x / sgSize;
70
+ // A subgroup past the last row folds an empty row and stores nothing, so
71
+ // every collective stays in uniform control flow.
72
+ let rowValid = row < params.rows;
73
+ let chunkLimit = select(0u, params.chunkCount, rowValid);
74
+ let base = row * params.chunkCount;
75
+ {% if op == "logsumexp" %}
76
+ // The row stays in registers: the max census and the exp pass read the same
77
+ // words, so the row is fetched from memory once.
78
+ var localMax = F32_MIN;
79
+ var localNan = 0.0;
80
+ var localNanValue = 0.0;
81
+ {% for i in range(vecsPerLane) %}
82
+ var v{{ i }} = vec4<f32>(0.0);
83
+ let c{{ i }} = sgLane + {{ i }}u * sgSize;
84
+ if (c{{ i }} < chunkLimit) {
85
+ v{{ i }} = {{ xv }}x[base + c{{ i }}]{{ vx }};
86
+ {% for comp in ["x", "y", "z", "w"] %}
87
+ if (is_nan_f32(v{{ i }}.{{ comp }})) {
88
+ localNan = 1.0;
89
+ localNanValue = v{{ i }}.{{ comp }};
90
+ } else {
91
+ localMax = max(localMax, v{{ i }}.{{ comp }});
92
+ }
93
+ {% endfor %}
94
+ }
95
+ {% endfor %}
96
+ let rowMax = subgroupMax(localMax);
97
+ let nanCount = subgroupAdd(localNan);
98
+ let nanValue = subgroupAdd(localNanValue);
99
+ let hasPositiveInf = rowMax > F32_MAX;
100
+ let hasNan = nanCount > 0.0;
101
+ var acc = 0.0;
102
+ {% for i in range(vecsPerLane) %}
103
+ if (c{{ i }} < chunkLimit) {
104
+ let e{{ i }} = select(exp(v{{ i }} - vec4<f32>(rowMax)), vec4<f32>(0.0), hasPositiveInf || hasNan);
105
+ acc = acc + ((e{{ i }}.x + e{{ i }}.y) + (e{{ i }}.z + e{{ i }}.w));
106
+ }
107
+ {% endfor %}
108
+ let sum = subgroupAdd(acc);
109
+ if (rowValid && sgLane == 0u) {
110
+ let finiteOrInf = select(rowMax + log(sum), rowMax, hasPositiveInf);
111
+ y[row] = {{ yv }}select(finiteOrInf, nanValue, hasNan){{ vy }};
112
+ }
113
+ {%- else %}
114
+ {% if op == "max" or op == "min" %}
115
+ let INIT: {{ accScalar }} = reduction_identity();
116
+ {%- elif op == "prod" %}
117
+ let INIT: {{ accScalar }} = {{ "1.0" if not isInt else accScalar ~ "(1)" }};
118
+ {%- else %}
119
+ let INIT: {{ accScalar }} = {{ "0.0" if not isInt else accScalar ~ "(0)" }};
120
+ {%- endif %}
121
+ var acc4 = vec4<{{ accScalar }}>(INIT);
122
+ for (var c = sgLane; c < chunkLimit; c = c + sgSize) {
123
+ let v = {{ xv }}x[base + c]{{ vx }};
124
+ {%- if op == "max" %}
125
+ acc4 = max(acc4, v);
126
+ {%- elif op == "min" %}
127
+ acc4 = min(acc4, v);
128
+ {%- elif op == "prod" %}
129
+ acc4 = acc4 * v;
130
+ {%- elif op == "l1" %}
131
+ acc4 = acc4 + abs(v);
132
+ {%- elif op == "l2" or op == "sumsquare" %}
133
+ acc4 = acc4 + v * v;
134
+ {%- else %}
135
+ acc4 = acc4 + v;
136
+ {%- endif %}
137
+ }
138
+ {%- if op == "max" %}
139
+ let acc = max(max(acc4.x, acc4.y), max(acc4.z, acc4.w));
140
+ let total = subgroupMax(acc);
141
+ {%- elif op == "min" %}
142
+ let acc = min(min(acc4.x, acc4.y), min(acc4.z, acc4.w));
143
+ let total = subgroupMin(acc);
144
+ {%- elif op == "prod" %}
145
+ let acc = (acc4.x * acc4.y) * (acc4.z * acc4.w);
146
+ let total = subgroupMul(acc);
147
+ {%- else %}
148
+ let acc = (acc4.x + acc4.y) + (acc4.z + acc4.w);
149
+ let total = subgroupAdd(acc);
150
+ {%- endif %}
151
+ if (rowValid && sgLane == 0u) {
152
+ {%- if op == "mean" and isInt %}
153
+ y[row] = total / {{ accScalar }}(params.chunkCount * 4u);
154
+ {%- elif op == "mean" %}
155
+ y[row] = {{ yv }}total / f32(params.chunkCount * 4u){{ vy }};
156
+ {%- elif op == "l2" and isInt %}
157
+ y[row] = {{ accScalar }}(sqrt(f32(total)));
158
+ {%- elif op == "l2" %}
159
+ y[row] = {{ yv }}sqrt(total){{ vy }};
160
+ {%- elif op == "logsum" %}
161
+ y[row] = {{ yv }}log(total){{ vy }};
162
+ {%- else %}
163
+ y[row] = {{ yv }}total{{ vy }};
164
+ {%- endif %}
165
+ }
166
+ {%- endif %}
167
+ }
build/webgpu/reduce-row-subgroup.wgsl.jinja CHANGED
@@ -1,30 +1,63 @@
1
- // Subgroup row reduction for a contiguous last axis. One workgroup owns each
2
  // output row. Threads reduce strided chunks, subgroup leaders deposit their
3
  // partials in workgroup memory, and thread 0 folds those slots and finalizes.
4
- //
5
- // Accumulators are f32 except for integer max/min. Mean, l2, and logsum apply
6
- // division, square root, or logarithm only at finalization. logsumexp uses
7
- // max-subtraction, bit-exact NaN detection, NaN propagation, and a +Inf
8
- // short-circuit. Runtime bitcasts provide max/min infinity identities because
9
- // WGSL constant evaluation rejects infinite constants. f16 storage is widened
10
- // before accumulation and narrowed only for the final store.
11
- {% set castF32 = source.castF32 is defined and source.castF32 %}
 
 
 
 
 
 
 
12
  {% set scalar = "f32" if castF32 else scalar %}
13
- {% set xv = ("vec4<f32>(" if source.vec4 else "f32(") if castF32 else "" %}
14
  {% set vx = ")" if castF32 else "" %}
15
  {% set yv = "f16(" if castF32 else "" %}
16
  {% set vy = ")" if castF32 else "" %}
17
  enable subgroups;
18
- {% if source.usesF16 is defined and source.usesF16 %}
19
  enable f16;
20
  {% endif %}
21
  {{ env.wgsl.resourceDeclarations }}
22
- /* Exact max/min reduction identities. WGSL rejects infinity during constant
23
- * evaluation, so f32 identities are constructed at runtime from their
24
- * IEEE-754 bit patterns. */
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
 
27
  const WG: u32 = {{ workgroupSize }}u;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  var<workgroup> wgPartial: array<{{ scalar }}, WG>;
29
 
30
  {% macro emit_reduce(name, collective, combine) %}
@@ -44,26 +77,141 @@ fn {{ name }}(value: {{ scalar }}, sgLid: u32, sgId: u32, numSg: u32) -> {{ scal
44
  workgroupBarrier();
45
  return total;
46
  }
47
- {%- endmacro %}{{ emit_reduce("reduce_row", "subgroupAdd", "total = total + wgPartial[i];") }}
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  @compute @workgroup_size(WG, 1, 1)
49
  fn main(@builtin(workgroup_id) wg: vec3<u32>,
50
- @builtin(num_workgroups) nwg: vec3<u32>,
51
  @builtin(local_invocation_id) lid: vec3<u32>,
52
  @builtin(subgroup_invocation_id) sgLid: u32,
53
  @builtin(subgroup_id) sgId: u32,
54
  @builtin(num_subgroups) numSg: u32) {
55
- let row = wg.x + wg.y * nwg.x;
56
  if (row >= params.rows) {
57
  return;
58
  }
59
  let tid = lid.x;
60
- let base = row * params.chunkCount; let INIT: f32 = 0.0;{% if source.vec4 %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  var acc4 = vec4<{{ scalar }}>(INIT);
62
  for (var c = tid; c < params.chunkCount; c = c + WG) {
63
- let v = {{ xv }}x[base + c]{{ vx }}; acc4 = acc4 + v; } let acc = (acc4.x + acc4.y) + (acc4.z + acc4.w);{% else %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  var acc = INIT;
65
  for (var c = tid; c < params.chunkCount; c = c + WG) {
66
- let v = {{ xv }}x[base + c]{{ vx }}; acc = acc + v; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  {%- endif %}
68
  let total = reduce_row(acc, sgLid, sgId, numSg);
69
- if (tid == 0u) { y[row] = {{ yv }}total{{ vy }}; }}
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Subgroup reduction for a contiguous last axis. One workgroup owns each
2
  // output row. Threads reduce strided chunks, subgroup leaders deposit their
3
  // partials in workgroup memory, and thread 0 folds those slots and finalizes.
4
+ {% if op == "mean" %}
5
+ // The finalizer divides the f32 sum by the row width.
6
+ {% elif op == "l2" %}
7
+ // The finalizer takes the square root of the sum of squares.
8
+ {% elif op == "logsum" %}
9
+ // The finalizer takes the logarithm of the sum.
10
+ {% elif op == "logsumexp" %}
11
+ // Max subtraction, explicit NaN propagation, and a positive-infinity branch
12
+ // define the stable log-sum-exp result.
13
+ {% elif op == "max" or op == "min" %}
14
+ // Integer values retain their native type; f32 identities are constructed from
15
+ // IEEE-754 bit patterns because WGSL rejects infinite constants.
16
+ {% endif %}
17
+ // f16 storage widens before accumulation and narrows only at the final store.
18
+ {% set castF32 = castF32 is defined and castF32 %}
19
  {% set scalar = "f32" if castF32 else scalar %}
20
+ {% set xv = ("vec4<f32>(" if vec4 else "f32(") if castF32 else "" %}
21
  {% set vx = ")" if castF32 else "" %}
22
  {% set yv = "f16(" if castF32 else "" %}
23
  {% set vy = ")" if castF32 else "" %}
24
  enable subgroups;
25
+ {% if usesF16Spec is defined and usesF16Spec %}
26
  enable f16;
27
  {% endif %}
28
  {{ env.wgsl.resourceDeclarations }}
29
+ {% macro wgsl_minmax_identity(name, op, scalar="f32") %}
30
+ /* Exact {{ op }} reduction identity. WGSL rejects infinity during constant
31
+ * evaluation, so an f32 identity is constructed from its IEEE-754 bits. */
32
+ {% set logicalBoolIdentity = logicalBool is defined and logicalBool %}
33
+ fn {{ name }}() -> {{ scalar }} {
34
+ {% if scalar == "i32" %}
35
+ return {{ "-2147483647i - 1i" if op == "max" else "2147483647i" }};
36
+ {% elif scalar == "u32" %}
37
+ return {{ "1u" if logicalBoolIdentity and op == "min" else "0u" if op == "max" else "4294967295u" }};
38
+ {% else %}
39
+ var bits = {{ "0xff800000u" if op == "max" else "0x7f800000u" }};
40
+ return bitcast<f32>(bits);
41
+ {% endif %}
42
+ }
43
+ {%- endmacro %}
44
 
45
 
46
  const WG: u32 = {{ workgroupSize }}u;
47
+ {%- if op == "max" or op == "min" %}
48
+ {{ wgsl_minmax_identity("reduction_identity", op, scalar) }}
49
+ {%- endif %}
50
+ {%- if op == "logsumexp" %}
51
+
52
+ const F32_MIN: f32 = -3.4028234663852886e38;
53
+ const F32_MAX: f32 = 3.4028234663852886e38;
54
+
55
+ fn is_nan_f32(value: f32) -> bool {
56
+ let bits = bitcast<u32>(value);
57
+ return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
58
+ }
59
+ {%- endif %}
60
+
61
  var<workgroup> wgPartial: array<{{ scalar }}, WG>;
62
 
63
  {% macro emit_reduce(name, collective, combine) %}
 
77
  workgroupBarrier();
78
  return total;
79
  }
80
+ {%- endmacro %}
81
+ {%- if op == "max" %}
82
+ {{ emit_reduce("reduce_row", "subgroupMax", "total = max(total, wgPartial[i]);") }}
83
+ {%- elif op == "min" %}
84
+ {{ emit_reduce("reduce_row", "subgroupMin", "total = min(total, wgPartial[i]);") }}
85
+ {%- elif op == "prod" %}
86
+ {{ emit_reduce("reduce_row", "subgroupMul", "total = total * wgPartial[i];") }}
87
+ {%- elif op == "logsumexp" %}
88
+ {{ emit_reduce("reduce_row_add", "subgroupAdd", "total = total + wgPartial[i];") }}
89
+ {{ emit_reduce("reduce_row_max", "subgroupMax", "total = max(total, wgPartial[i]);") }}
90
+ {%- else %}
91
+ {{ emit_reduce("reduce_row", "subgroupAdd", "total = total + wgPartial[i];") }}
92
+ {%- endif %}
93
+
94
  @compute @workgroup_size(WG, 1, 1)
95
  fn main(@builtin(workgroup_id) wg: vec3<u32>,
 
96
  @builtin(local_invocation_id) lid: vec3<u32>,
97
  @builtin(subgroup_invocation_id) sgLid: u32,
98
  @builtin(subgroup_id) sgId: u32,
99
  @builtin(num_subgroups) numSg: u32) {
100
+ let row = wg.x + wg.y * {{ DISPATCH_FOLD_WIDTH }}u;
101
  if (row >= params.rows) {
102
  return;
103
  }
104
  let tid = lid.x;
105
+ let base = row * params.chunkCount;
106
+ {%- if op == "logsumexp" %}
107
+ var localMax = F32_MIN;
108
+ var localNan = 0.0;
109
+ var localNanValue = 0.0;
110
+ for (var c = tid; c < params.chunkCount; c = c + WG) {
111
+ let v = {{ xv }}x[base + c]{{ vx }};
112
+ {%- if vec4 %}
113
+ {% for comp in ["x", "y", "z", "w"] %}
114
+ if (is_nan_f32(v.{{ comp }})) {
115
+ localNan = 1.0;
116
+ localNanValue = v.{{ comp }};
117
+ } else {
118
+ localMax = max(localMax, v.{{ comp }});
119
+ }
120
+ {%- endfor %}
121
+ {% else %}
122
+ if (is_nan_f32(v)) {
123
+ localNan = 1.0;
124
+ localNanValue = v;
125
+ } else {
126
+ localMax = max(localMax, v);
127
+ }
128
+ {%- endif %}
129
+ }
130
+ let rowMax = reduce_row_max(localMax, sgLid, sgId, numSg);
131
+ let nanCount = reduce_row_add(localNan, sgLid, sgId, numSg);
132
+ let nanValue = reduce_row_add(localNanValue, sgLid, sgId, numSg);
133
+ let hasPositiveInf = rowMax > F32_MAX;
134
+ let hasNan = nanCount > 0.0;
135
+ var acc = 0.0;
136
+ for (var c = tid; c < params.chunkCount; c = c + WG) {
137
+ let v = {{ xv }}x[base + c]{{ vx }};
138
+ {%- if vec4 %}
139
+ let e = select(exp(v - vec4<f32>(rowMax)), vec4<f32>(0.0), hasPositiveInf || hasNan);
140
+ acc = acc + ((e.x + e.y) + (e.z + e.w));
141
+ {%- else %}
142
+ acc = acc + select(exp(v - rowMax), 0.0, hasPositiveInf || hasNan);
143
+ {%- endif %}
144
+ }
145
+ let sum = reduce_row_add(acc, sgLid, sgId, numSg);
146
+ if (tid == 0u) {
147
+ let finiteOrInf = select(rowMax + log(sum), rowMax, hasPositiveInf);
148
+ y[row] = {{ yv }}select(finiteOrInf, nanValue, hasNan){{ vy }};
149
+ }
150
+ {%- else %}
151
+ {% if op == "max" or op == "min" %}
152
+ let INIT: {{ scalar }} = reduction_identity();
153
+ {%- elif op == "prod" %}
154
+ let INIT: f32 = 1.0;
155
+ {%- else %}
156
+ let INIT: f32 = 0.0;
157
+ {%- endif %}
158
+ {% if vec4 %}
159
  var acc4 = vec4<{{ scalar }}>(INIT);
160
  for (var c = tid; c < params.chunkCount; c = c + WG) {
161
+ let v = {{ xv }}x[base + c]{{ vx }};
162
+ {%- if op == "max" %}
163
+ acc4 = max(acc4, v);
164
+ {%- elif op == "min" %}
165
+ acc4 = min(acc4, v);
166
+ {%- elif op == "prod" %}
167
+ acc4 = acc4 * v;
168
+ {%- elif op == "l1" %}
169
+ acc4 = acc4 + abs(v);
170
+ {%- elif op == "l2" or op == "sumsquare" %}
171
+ acc4 = acc4 + v * v;
172
+ {%- else %}
173
+ acc4 = acc4 + v;
174
+ {%- endif %}
175
+ }
176
+ {%- if op == "max" %}
177
+ let acc = max(max(acc4.x, acc4.y), max(acc4.z, acc4.w));
178
+ {%- elif op == "min" %}
179
+ let acc = min(min(acc4.x, acc4.y), min(acc4.z, acc4.w));
180
+ {%- elif op == "prod" %}
181
+ let acc = (acc4.x * acc4.y) * (acc4.z * acc4.w);
182
+ {%- else %}
183
+ let acc = (acc4.x + acc4.y) + (acc4.z + acc4.w);
184
+ {%- endif %}
185
+ {% else %}
186
  var acc = INIT;
187
  for (var c = tid; c < params.chunkCount; c = c + WG) {
188
+ let v = {{ xv }}x[base + c]{{ vx }};
189
+ {%- if op == "max" %}
190
+ acc = max(acc, v);
191
+ {%- elif op == "min" %}
192
+ acc = min(acc, v);
193
+ {%- elif op == "prod" %}
194
+ acc = acc * v;
195
+ {%- elif op == "l1" %}
196
+ acc = acc + abs(v);
197
+ {%- elif op == "l2" or op == "sumsquare" %}
198
+ acc = acc + v * v;
199
+ {%- else %}
200
+ acc = acc + v;
201
+ {%- endif %}
202
+ }
203
  {%- endif %}
204
  let total = reduce_row(acc, sgLid, sgId, numSg);
205
+ if (tid == 0u) {
206
+ {%- if op == "mean" %}
207
+ y[row] = {{ yv }}total / f32(params.cols){{ vy }};
208
+ {%- elif op == "l2" %}
209
+ y[row] = {{ yv }}sqrt(total){{ vy }};
210
+ {%- elif op == "logsum" %}
211
+ y[row] = {{ yv }}log(total){{ vy }};
212
+ {%- else %}
213
+ y[row] = {{ yv }}total{{ vy }};
214
+ {%- endif %}
215
+ }
216
+ {%- endif %}
217
+ }
build/webgpu/reduce-row-tree.wgsl.jinja CHANGED
@@ -1,70 +1,266 @@
1
- // Portable one-workgroup-per-row reduction for the Reduce value family.
2
  // Threads stride a contiguous row, accumulate locally, and fold their values
3
  // through a shared-memory tree without relying on subgroups.
4
- //
5
- // Max, min, and product use the native f32/i32/u32 value type. Additive
6
- // transforms accumulate float inputs in f32 but retain integer accumulation
7
- // for sum, L1, and sum-of-squares. Mean, L2, log-sum, and log-sum-exp are
8
- // f32-only because their finalizers divide, take a square root, or take a log.
 
 
 
 
 
 
9
  // f16 storage widens through f32 for both accumulation and the shared tree,
10
  // then narrows only at the final store.
11
- {% set castF32 = source.castF32 is defined and source.castF32 %}
 
 
12
  {% set scalar = "f32" if castF32 else scalar %}
13
- {% set xv = ("vec4<f32>(" if source.vec4 else "f32(") if castF32 else "" %}
14
  {% set vx = ")" if castF32 else "" %}
15
  {% set yv = "f16(" if castF32 else "" %}
16
  {% set vy = ")" if castF32 else "" %}
17
- {% if source.usesF16 is defined and source.usesF16 %}
18
  enable f16;
19
  {% endif %}
20
  {{ env.wgsl.resourceDeclarations }}
21
- /* Exact max/min reduction identities. WGSL rejects infinity during constant
22
- * evaluation, so f32 identities are constructed at runtime from their
23
- * IEEE-754 bit patterns. */
 
 
 
 
 
 
 
 
 
 
 
 
24
 
 
 
 
 
 
 
 
25
 
26
  const WG: u32 = {{ workgroupSize }}u;
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  {% set is_int = scalar == "i32" or scalar == "u32" %}
29
- {% set intAdditive = is_int and (source.op == "sum" or source.op == "l1" or source.op == "sumsquare"
30
- or source.op == "l2" or source.op == "mean") %}
31
- {% set accType = scalar if source.op == "max" or source.op == "min" or source.op == "prod" or intAdditive else "f32" %}
 
 
 
 
 
 
 
32
  fn identity() -> {{ accType }} { return {{ accType }}(0); }
 
33
 
34
  fn combine(a: {{ accType }}, b: {{ accType }}) -> {{ accType }} {
 
 
 
 
 
 
 
35
  return a + b;
 
36
  }
37
 
38
  var<workgroup> partial: array<{{ accType }}, WG>;
 
39
 
40
  @compute @workgroup_size(WG, 1, 1)
41
  fn main(@builtin(workgroup_id) wg: vec3<u32>,
42
- @builtin(num_workgroups) nwg: vec3<u32>,
43
  @builtin(local_invocation_id) lid: vec3<u32>) {
44
- let row = wg.x + wg.y * nwg.x;
45
  if (row >= params.rows) {
46
  return;
47
  }
48
  let tid = lid.x;
49
- {% if source.vec4 %}
50
  let base = row * params.chunkCount;
51
  {% else %}
52
  let base = row * params.cols;
53
  {% endif %}
54
 
 
 
 
 
 
 
 
55
 
56
- {% if source.vec4 %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  var acc4 = vec4<{{ accType }}>(identity());
58
  for (var col = tid; col < params.chunkCount; col = col + WG) {
59
  let value = {{ xv }}x[base + col]{{ vx }};
 
 
 
 
 
 
 
 
 
 
 
60
  acc4 = acc4 + value;
 
61
  }
 
 
 
 
 
 
 
62
  let acc = (acc4.x + acc4.y) + (acc4.z + acc4.w);
 
63
  {% else %}
64
  var acc = identity();
65
  for (var col = tid; col < params.cols; col = col + WG) {
66
  let value = {{ xv }}x[base + col]{{ vx }};
 
 
 
 
 
67
  acc = combine(acc, value);
 
68
  }
69
  {% endif %}
70
  partial[tid] = acc;
@@ -79,6 +275,19 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>,
79
 
80
  if (tid == 0u) {
81
  let total = partial[0];
 
 
 
 
 
 
 
 
 
 
 
82
  y[row] = {{ yv }}total{{ vy }};
 
83
  }
 
84
  }
 
1
+ // Portable one-workgroup-per-row reduction for the selected operation.
2
  // Threads stride a contiguous row, accumulate locally, and fold their values
3
  // through a shared-memory tree without relying on subgroups.
4
+ {% if op == "max" or op == "min" or op == "prod" %}
5
+ // Values accumulate in their native f32/i32/u32 type.
6
+ {% elif op == "mean" %}
7
+ // The tree sums values before the final division by the row width.
8
+ {% elif op == "l2" %}
9
+ // The tree sums squared values before the final square root.
10
+ {% elif op == "logsum" %}
11
+ // The tree sums values before the final logarithm.
12
+ {% elif op == "logsumexp" %}
13
+ // A max-subtracted exponential sum handles NaN and positive-infinity rows.
14
+ {% endif %}
15
  // f16 storage widens through f32 for both accumulation and the shared tree,
16
  // then narrows only at the final store.
17
+ {% set isVec4 = vec4 is defined and vec4 %}
18
+ {% set rowIsEmpty = "params.chunkCount == 0u" if isVec4 else "params.cols == 0u" %}
19
+ {% set castF32 = castF32 is defined and castF32 %}
20
  {% set scalar = "f32" if castF32 else scalar %}
21
+ {% set xv = ("vec4<f32>(" if vec4 else "f32(") if castF32 else "" %}
22
  {% set vx = ")" if castF32 else "" %}
23
  {% set yv = "f16(" if castF32 else "" %}
24
  {% set vy = ")" if castF32 else "" %}
25
+ {% if usesF16Spec is defined and usesF16Spec %}
26
  enable f16;
27
  {% endif %}
28
  {{ env.wgsl.resourceDeclarations }}
29
+ {% macro wgsl_minmax_identity(name, op, scalar="f32") %}
30
+ /* Exact {{ op }} reduction identity. WGSL rejects infinity during constant
31
+ * evaluation, so an f32 identity is constructed from its IEEE-754 bits. */
32
+ {% set logicalBoolIdentity = logicalBool is defined and logicalBool %}
33
+ fn {{ name }}() -> {{ scalar }} {
34
+ {% if scalar == "i32" %}
35
+ return {{ "-2147483647i - 1i" if op == "max" else "2147483647i" }};
36
+ {% elif scalar == "u32" %}
37
+ return {{ "1u" if logicalBoolIdentity and op == "min" else "0u" if op == "max" else "4294967295u" }};
38
+ {% else %}
39
+ var bits = {{ "0xff800000u" if op == "max" else "0x7f800000u" }};
40
+ return bitcast<f32>(bits);
41
+ {% endif %}
42
+ }
43
+ {%- endmacro %}
44
 
45
+ {% if scalar != "i32" and scalar != "u32" and (op == "logsum" or op == "logsumexp") %}
46
+ fn negative_infinity() -> f32 {
47
+ var bits = 0xff800000u;
48
+ return bitcast<f32>(bits);
49
+ }
50
+
51
+ {% endif %}
52
 
53
  const WG: u32 = {{ workgroupSize }}u;
54
 
55
+ {% if op == "logsumexp" %}
56
+ const F32_MIN: f32 = -3.4028234663852886e38;
57
+ const F32_MAX: f32 = 3.4028234663852886e38;
58
+
59
+ var<workgroup> partial: array<f32, WG>;
60
+ {% macro wgsl_tree_reduce_f32(name, mode, buffer="partial", wg="WG", trailingBarrier=true) %}
61
+ fn {{ name }}(value: f32, tid: u32) -> f32 {
62
+ {{ buffer }}[tid] = value;
63
+ workgroupBarrier();
64
+ // Ceil-halving keeps every lane when the workgroup size is not a power of
65
+ // two. For even n this matches the power-of-two tree order; for odd n, lanes
66
+ // [0, n-half) fold the upper tail while the middle lane carries forward.
67
+ var n: u32 = {{ wg }};
68
+ loop {
69
+ let half = (n + 1u) / 2u;
70
+ if (tid < n - half) {
71
+ {% if mode == "max" %}
72
+ {{ buffer }}[tid] = max({{ buffer }}[tid], {{ buffer }}[tid + half]);
73
+ {% else %}
74
+ {{ buffer }}[tid] = {{ buffer }}[tid] + {{ buffer }}[tid + half];
75
+ {% endif %}
76
+ }
77
+ workgroupBarrier();
78
+ n = half;
79
+ if (n == 1u) {
80
+ break;
81
+ }
82
+ }
83
+ // The default trailing barrier makes this helper safe for back-to-back calls: every lane reads
84
+ // slot 0 here, so the next call's first store must not run until all lanes have read it.
85
+ // `trailingBarrier=false` is safe only when the buffer is never written again before kernel exit.
86
+ let reduced = {{ buffer }}[0];
87
+ {% if trailingBarrier %}
88
+ workgroupBarrier();
89
+ {% endif %}
90
+ return reduced;
91
+ }
92
+ {% endmacro %}
93
+
94
+ {{ wgsl_tree_reduce_f32("reduce_sum", "add", "partial", "WG") }}
95
+ {{ wgsl_tree_reduce_f32("reduce_max", "max", "partial", "WG") }}
96
+
97
+ fn is_nan_f32(value: f32) -> bool {
98
+ let bits = bitcast<u32>(value);
99
+ return (bits & 0x7f800000u) == 0x7f800000u
100
+ && (bits & 0x007fffffu) != 0u;
101
+ }
102
+ {% else %}
103
  {% set is_int = scalar == "i32" or scalar == "u32" %}
104
+ {% set intAdditive = is_int and (op == "sum" or op == "l1" or op == "sumsquare"
105
+ or op == "l2" or op == "mean") %}
106
+ {% set accType = scalar if op == "max" or op == "min" or op == "prod" or intAdditive else "f32" %}
107
+ {% if op == "max" or op == "min" %}
108
+ {{ wgsl_minmax_identity("identity", op, accType) }}
109
+ {% elif op == "prod" and accType == "i32" %}
110
+ fn identity() -> i32 { return 1i; }
111
+ {% elif op == "prod" %}
112
+ fn identity() -> {{ accType }} { return {{ accType }}(1); }
113
+ {% else %}
114
  fn identity() -> {{ accType }} { return {{ accType }}(0); }
115
+ {% endif %}
116
 
117
  fn combine(a: {{ accType }}, b: {{ accType }}) -> {{ accType }} {
118
+ {% if op == "max" %}
119
+ return max(a, b);
120
+ {% elif op == "min" %}
121
+ return min(a, b);
122
+ {% elif op == "prod" %}
123
+ return a * b;
124
+ {% else %}
125
  return a + b;
126
+ {% endif %}
127
  }
128
 
129
  var<workgroup> partial: array<{{ accType }}, WG>;
130
+ {% endif %}
131
 
132
  @compute @workgroup_size(WG, 1, 1)
133
  fn main(@builtin(workgroup_id) wg: vec3<u32>,
 
134
  @builtin(local_invocation_id) lid: vec3<u32>) {
135
+ let row = wg.x + wg.y * {{ DISPATCH_FOLD_WIDTH }}u;
136
  if (row >= params.rows) {
137
  return;
138
  }
139
  let tid = lid.x;
140
+ {% if vec4 %}
141
  let base = row * params.chunkCount;
142
  {% else %}
143
  let base = row * params.cols;
144
  {% endif %}
145
 
146
+ {% if op == "logsumexp" %}
147
+ if ({{ rowIsEmpty }}) {
148
+ if (tid == 0u) {
149
+ y[row] = {{ yv }}negative_infinity(){{ vy }};
150
+ }
151
+ return;
152
+ }
153
 
154
+ var localMax = F32_MIN;
155
+ var localNan = 0.0;
156
+ var localNanValue = 0.0;
157
+ {% if vec4 %}
158
+ for (var col = tid; col < params.chunkCount; col = col + WG) {
159
+ let value = {{ xv }}x[base + col]{{ vx }};
160
+ {% for component in ["x", "y", "z", "w"] %}
161
+ if (is_nan_f32(value.{{ component }})) {
162
+ localNan = 1.0;
163
+ localNanValue = value.{{ component }};
164
+ } else {
165
+ localMax = max(localMax, value.{{ component }});
166
+ }
167
+ {% endfor %}
168
+ }
169
+ {% else %}
170
+ for (var col = tid; col < params.cols; col = col + WG) {
171
+ let value = {{ xv }}x[base + col]{{ vx }};
172
+ if (is_nan_f32(value)) {
173
+ localNan = 1.0;
174
+ localNanValue = value;
175
+ } else {
176
+ localMax = max(localMax, value);
177
+ }
178
+ }
179
+ {% endif %}
180
+ let rowMax = reduce_max(localMax, tid);
181
+ workgroupBarrier();
182
+ let nanCount = reduce_sum(localNan, tid);
183
+ workgroupBarrier();
184
+ let nanValue = reduce_sum(localNanValue, tid);
185
+ workgroupBarrier();
186
+ let hasPositiveInf = rowMax > F32_MAX;
187
+ let hasNan = nanCount > 0.0;
188
+
189
+ var acc = 0.0;
190
+ {% if vec4 %}
191
+ for (var col = tid; col < params.chunkCount; col = col + WG) {
192
+ let value = {{ xv }}x[base + col]{{ vx }};
193
+ let exponentials = select(exp(value - vec4<f32>(rowMax)), vec4<f32>(0.0),
194
+ hasPositiveInf || hasNan);
195
+ acc = acc + (exponentials.x + exponentials.y)
196
+ + (exponentials.z + exponentials.w);
197
+ }
198
+ {% else %}
199
+ for (var col = tid; col < params.cols; col = col + WG) {
200
+ acc = acc + select(exp({{ xv }}x[base + col]{{ vx }} - rowMax), 0.0,
201
+ hasPositiveInf || hasNan);
202
+ }
203
+ {% endif %}
204
+ let sum = reduce_sum(acc, tid);
205
+ if (tid == 0u) {
206
+ let finiteOrInf = select(rowMax + log(sum), rowMax, hasPositiveInf);
207
+ y[row] = {{ yv }}select(finiteOrInf, nanValue, hasNan){{ vy }};
208
+ }
209
+ {% else %}
210
+ {% if op == "mean" %}
211
+ if ({{ rowIsEmpty }}) {
212
+ {% if is_int %}
213
+ if (tid == 0u) { y[row] = {{ scalar }}(0); }
214
+ {% else %}
215
+ if (tid == 0u) { y[row] = {{ yv }}0.0{{ vy }}; }
216
+ {% endif %}
217
+ return;
218
+ }
219
+ {% elif op == "logsum" %}
220
+ if ({{ rowIsEmpty }}) {
221
+ if (tid == 0u) { y[row] = {{ yv }}negative_infinity(){{ vy }}; }
222
+ return;
223
+ }
224
+ {% endif %}
225
+
226
+ {% if vec4 %}
227
  var acc4 = vec4<{{ accType }}>(identity());
228
  for (var col = tid; col < params.chunkCount; col = col + WG) {
229
  let value = {{ xv }}x[base + col]{{ vx }};
230
+ {% if op == "max" %}
231
+ acc4 = max(acc4, value);
232
+ {% elif op == "min" %}
233
+ acc4 = min(acc4, value);
234
+ {% elif op == "prod" %}
235
+ acc4 = acc4 * value;
236
+ {% elif op == "l1" %}
237
+ acc4 = acc4 + abs(value);
238
+ {% elif op == "l2" or op == "sumsquare" %}
239
+ acc4 = acc4 + value * value;
240
+ {% else %}
241
  acc4 = acc4 + value;
242
+ {% endif %}
243
  }
244
+ {% if op == "max" %}
245
+ let acc = max(max(acc4.x, acc4.y), max(acc4.z, acc4.w));
246
+ {% elif op == "min" %}
247
+ let acc = min(min(acc4.x, acc4.y), min(acc4.z, acc4.w));
248
+ {% elif op == "prod" %}
249
+ let acc = (acc4.x * acc4.y) * (acc4.z * acc4.w);
250
+ {% else %}
251
  let acc = (acc4.x + acc4.y) + (acc4.z + acc4.w);
252
+ {% endif %}
253
  {% else %}
254
  var acc = identity();
255
  for (var col = tid; col < params.cols; col = col + WG) {
256
  let value = {{ xv }}x[base + col]{{ vx }};
257
+ {% if op == "l1" %}
258
+ acc = combine(acc, abs(value));
259
+ {% elif op == "l2" or op == "sumsquare" %}
260
+ acc = combine(acc, value * value);
261
+ {% else %}
262
  acc = combine(acc, value);
263
+ {% endif %}
264
  }
265
  {% endif %}
266
  partial[tid] = acc;
 
275
 
276
  if (tid == 0u) {
277
  let total = partial[0];
278
+ {% if op == "l2" and is_int %}
279
+ y[row] = {{ scalar }}(sqrt(f32(total)));
280
+ {% elif op == "l2" %}
281
+ y[row] = {{ yv }}sqrt(total){{ vy }};
282
+ {% elif op == "logsum" %}
283
+ y[row] = {{ yv }}log(total){{ vy }};
284
+ {% elif op == "mean" and is_int %}
285
+ y[row] = total / {{ scalar }}(params.cols);
286
+ {% elif op == "mean" %}
287
+ y[row] = {{ yv }}total / f32(params.cols){{ vy }};
288
+ {% else %}
289
  y[row] = {{ yv }}total{{ vy }};
290
+ {% endif %}
291
  }
292
+ {% endif %}
293
  }
build/webgpu/reduce-serial-axis.wgsl.jinja CHANGED
@@ -1,87 +1,38 @@
1
- // Serial one-thread-per-output reduction for the no-feature tier. f16 storage
2
- // is widened before every accumulation and narrowed only for the final store.
3
- {% set castF32 = source.castF32 is defined and source.castF32 %}
4
- {% set intMode = source.intMode is defined and source.intMode %}
5
- {% set yv = "f16(" if castF32 else "" %}
6
- {% set vy = ")" if castF32 else "" %}
7
- {% if source.usesF16 is defined and source.usesF16 %}
8
- enable f16;
9
- {% endif %}
10
- {{ env.wgsl.resourceDeclarations }}
11
- /* Exact max/min reduction identities. WGSL rejects infinity during constant
12
- * evaluation, so f32 identities are constructed at runtime from their
13
- * IEEE-754 bit patterns. */
14
-
15
- {% if source.indexing == "rankn" %}
16
-
17
- fn input_offset(out_index: u32, reduce_index: u32) -> u32 {
18
- var rem = out_index;
19
- {% for axis in range(source.outputRank) %}
20
- {% set out_stride = namespace(value=1) %}
21
- {% for j in range(axis + 1, source.outputRank) %}
22
- {% set out_stride.value = out_stride.value * source.outputShape[j] %}
23
- {% endfor %}
24
- {% set safe_out_stride = 1 if out_stride.value == 0 else out_stride.value %}
25
- {% if not source.keepDims or axis != source.axis %}
26
- let out_coord{{ axis }} = rem / {{ safe_out_stride }}u;
27
- {% endif %}
28
- rem = rem % {{ safe_out_stride }}u;
29
- {% endfor %}
30
- {% for axis in range(source.rank) %}
31
- {% if axis == source.axis %}
32
- let coord{{ axis }} = reduce_index;
33
- {% elif source.keepDims %}
34
- let coord{{ axis }} = out_coord{{ axis }};
35
- {% elif axis < source.axis %}
36
- let coord{{ axis }} = out_coord{{ axis }};
37
- {% else %}
38
- let coord{{ axis }} = out_coord{{ axis - 1 }};
39
- {% endif %}
40
- {% endfor %}
41
- {% set src = namespace(value="coord0") %}
42
- {% for axis in range(1, source.rank) %}
43
- {% set src.value = "(" ~ src.value ~ " * " ~ source.dataShape[axis] ~ "u + coord" ~ axis ~ ")" %}
44
- {% endfor %}
45
- return {{ src.value }};
46
- }
47
- {% endif %}
48
- {% if source.indexing == "multiaxis" %}
49
- {% set hasReducedAxis = namespace(value=false) %}
50
- {% for a in range(source.rank) %}{% if source.reduce[a] %}{% set hasReducedAxis.value = true %}{% endif %}{% endfor %}
51
 
52
  // One thread per output element walks the Cartesian product of the reduced axes,
53
  // linearized as reduce_linear. Specialized shapes make every input offset a sum
54
  // of coordinate-times-constant terms.
55
- fn input_offset(out_index: u32{% if hasReducedAxis.value %}, reduce_linear: u32{% endif %}) -> u32 {
56
  var rem = out_index;
57
- {% for oaxis in range(source.outputRank) %}
58
  {% set ostride = namespace(value=1) %}
59
- {% for j in range(oaxis + 1, source.outputRank) %}
60
- {% set ostride.value = ostride.value * source.outputShape[j] %}
61
  {% endfor %}
62
  {% set osafe = 1 if ostride.value == 0 else ostride.value %}
63
- {% if not source.keepDims or not source.reduce[oaxis] %}
64
  let out_coord{{ oaxis }} = rem / {{ osafe }}u;
65
  {% endif %}
66
  rem = rem % {{ osafe }}u;
67
  {% endfor %}
68
- {% if hasReducedAxis.value %}
69
  var rrem = reduce_linear;
70
  {% endif %}
71
- {% for a in range(source.rank) if source.reduce[a] %}
72
  {% set rstride = namespace(value=1) %}
73
- {% for b in range(a + 1, source.rank) if source.reduce[b] %}
74
- {% set rstride.value = rstride.value * source.dataShape[b] %}
75
  {% endfor %}
76
  {% set rsafe = 1 if rstride.value == 0 else rstride.value %}
77
  let red_coord{{ a }} = rrem / {{ rsafe }}u;
78
  rrem = rrem % {{ rsafe }}u;
79
  {% endfor %}
80
  {% set oc = namespace(i=0) %}
81
- {% for a in range(source.rank) %}
82
- {% if source.reduce[a] %}
83
  let coord{{ a }} = red_coord{{ a }};
84
- {% elif source.keepDims %}
85
  let coord{{ a }} = out_coord{{ a }};
86
  {% else %}
87
  let coord{{ a }} = out_coord{{ oc.i }};
@@ -89,54 +40,248 @@ fn input_offset(out_index: u32{% if hasReducedAxis.value %}, reduce_linear: u32{
89
  {% endif %}
90
  {% endfor %}
91
  {% set src = namespace(value="coord0") %}
92
- {% for a in range(1, source.rank) %}
93
- {% set src.value = "(" ~ src.value ~ " * " ~ source.dataShape[a] ~ "u + coord" ~ a ~ ")" %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  {% endfor %}
95
  return {{ src.value }};
96
  }
97
  {% endif %}
98
- {% if source.indexing == "multiaxis" %}
 
 
 
 
 
99
  {% set mcount = namespace(value=1) %}
100
- {% for a in range(source.rank) if source.reduce[a] %}
101
- {% set mcount.value = mcount.value * source.dataShape[a] %}
102
  {% endfor %}
103
  {% set count = mcount.value ~ "u" %}
104
  {% if hasReducedAxis.value %}
105
  {% set at = "x[input_offset(i, r)]" %}
 
106
  {% else %}
107
  {% set at = "x[input_offset(i)]" %}
 
108
  {% endif %}
109
- {% elif source.indexing == "rankn" %}
110
  {% set count = "params.axisDim" %}
111
  {% set at = "x[input_offset(i, r)]" %}
 
112
  {% elif axis == 0 %}
113
  {% set count = "params.rows" %}
114
  {% set at = "x[r * params.cols + i]" %}
 
115
  {% else %}
116
  {% set count = "params.cols" %}
117
  {% set at = "x[i * params.cols + r]" %}
 
118
  {% endif %}
119
  {% if castF32 %}
120
  {% set at = "f32(" ~ at ~ ")" %}
 
121
  {% endif %}
122
 
123
  @compute @workgroup_size({{ reduceWorkgroupSize }})
124
- fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) nwg: vec3<u32>) {
125
  // 2D-folded flat index: gid.y carries the high bits past the
126
- // maxComputeWorkgroupsPerDimension dispatch limit (outputs > 16.7M elements).
127
- let i = gid.x + gid.y * nwg.x * {{ reduceWorkgroupSize }}u;
128
  if (i >= params.outCount) {
129
  return;
130
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  {% if intMode %}
132
  // Integer reduction accumulates in the output type. sum/prod/l1/sumsquare stay
133
  // in integer arithmetic; l2/logsum widen for sqrt/log and truncate afterward.
134
- var acc = {{ scalar }}({{ "1" if source.op == "prod" else "0" }});
135
  {% else %}
136
- var acc = {{ "1.0" if source.op == "prod" else "0.0" }};
137
  {% endif %}
138
  for (var r = 0u; r < {{ count }}; r = r + 1u) {
 
 
 
 
 
 
 
 
139
  acc = acc + {{ at }};
 
140
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  y[i] = {{ yv }}acc{{ vy }};
 
 
142
  }
 
1
+ {% macro reduce_multi_axis_offset(hasReduced) %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  // One thread per output element walks the Cartesian product of the reduced axes,
4
  // linearized as reduce_linear. Specialized shapes make every input offset a sum
5
  // of coordinate-times-constant terms.
6
+ fn input_offset(out_index: u32{% if hasReduced %}, reduce_linear: u32{% endif %}) -> u32 {
7
  var rem = out_index;
8
+ {% for oaxis in range(outputRank) %}
9
  {% set ostride = namespace(value=1) %}
10
+ {% for j in range(oaxis + 1, outputRank) %}
11
+ {% set ostride.value = ostride.value * outputShape[j] %}
12
  {% endfor %}
13
  {% set osafe = 1 if ostride.value == 0 else ostride.value %}
14
+ {% if not keepDims or not reduce[oaxis] %}
15
  let out_coord{{ oaxis }} = rem / {{ osafe }}u;
16
  {% endif %}
17
  rem = rem % {{ osafe }}u;
18
  {% endfor %}
19
+ {% if hasReduced %}
20
  var rrem = reduce_linear;
21
  {% endif %}
22
+ {% for a in range(rank) if reduce[a] %}
23
  {% set rstride = namespace(value=1) %}
24
+ {% for b in range(a + 1, rank) if reduce[b] %}
25
+ {% set rstride.value = rstride.value * dataShape[b] %}
26
  {% endfor %}
27
  {% set rsafe = 1 if rstride.value == 0 else rstride.value %}
28
  let red_coord{{ a }} = rrem / {{ rsafe }}u;
29
  rrem = rrem % {{ rsafe }}u;
30
  {% endfor %}
31
  {% set oc = namespace(i=0) %}
32
+ {% for a in range(rank) %}
33
+ {% if reduce[a] %}
34
  let coord{{ a }} = red_coord{{ a }};
35
+ {% elif keepDims %}
36
  let coord{{ a }} = out_coord{{ a }};
37
  {% else %}
38
  let coord{{ a }} = out_coord{{ oc.i }};
 
40
  {% endif %}
41
  {% endfor %}
42
  {% set src = namespace(value="coord0") %}
43
+ {% for a in range(1, rank) %}
44
+ {% set src.value = "(" ~ src.value ~ " * " ~ dataShape[a] ~ "u + coord" ~ a ~ ")" %}
45
+ {% endfor %}
46
+ return {{ src.value }};
47
+ }
48
+ {%- endmacro %}
49
+ // Serial one-thread-per-output reduction for the no-feature tier. f16 storage
50
+ // is widened before every accumulation and narrowed only for the final store.
51
+ {% set castF32 = castF32 is defined and castF32 %}
52
+ {% set logicalBool = logicalBool is defined and logicalBool %}
53
+ {% set intMode = intMode is defined and intMode %}
54
+ {% set yv = "f16(" if castF32 else "" %}
55
+ {% set vy = ")" if castF32 else "" %}
56
+ {% if usesF16Spec is defined and usesF16Spec %}
57
+ enable f16;
58
+ {% endif %}
59
+ {{ env.wgsl.resourceDeclarations }}
60
+ {% macro wgsl_minmax_identity(name, op, scalar="f32") %}
61
+ /* Exact {{ op }} reduction identity. WGSL rejects infinity during constant
62
+ * evaluation, so an f32 identity is constructed from its IEEE-754 bits. */
63
+ {% set logicalBoolIdentity = logicalBool is defined and logicalBool %}
64
+ fn {{ name }}() -> {{ scalar }} {
65
+ {% if scalar == "i32" %}
66
+ return {{ "-2147483647i - 1i" if op == "max" else "2147483647i" }};
67
+ {% elif scalar == "u32" %}
68
+ return {{ "1u" if logicalBoolIdentity and op == "min" else "0u" if op == "max" else "4294967295u" }};
69
+ {% else %}
70
+ var bits = {{ "0xff800000u" if op == "max" else "0x7f800000u" }};
71
+ return bitcast<f32>(bits);
72
+ {% endif %}
73
+ }
74
+ {%- endmacro %}
75
+
76
+ {% if not intMode and (op == "logsum" or op == "logsumexp") %}
77
+ fn negative_infinity() -> f32 {
78
+ var bits = 0xff800000u;
79
+ return bitcast<f32>(bits);
80
+ }
81
+
82
+ {% endif %}
83
+ {% if op == "max" or op == "min" %}
84
+
85
+ {{ wgsl_minmax_identity("empty_value", op, "f32" if castF32 else scalar) }}
86
+ {% elif op == "logsumexp" %}
87
+
88
+ const F32_MIN: f32 = -3.4028234663852886e38;
89
+ {% if not intMode %}
90
+ const F32_MAX: f32 = 3.4028234663852886e38;
91
+
92
+ fn is_nan_f32(value: f32) -> bool {
93
+ let bits = bitcast<u32>(value);
94
+ return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
95
+ }
96
+ {% endif %}
97
+ {% endif %}
98
+ {% if indexing == "rankn" %}
99
+
100
+ fn input_offset(out_index: u32, reduce_index: u32) -> u32 {
101
+ var rem = out_index;
102
+ {% for axis in range(outputRank) %}
103
+ {% set out_stride = namespace(value=1) %}
104
+ {% for j in range(axis + 1, outputRank) %}
105
+ {% set out_stride.value = out_stride.value * outputShape[j] %}
106
+ {% endfor %}
107
+ {% set safe_out_stride = 1 if out_stride.value == 0 else out_stride.value %}
108
+ {% if not keepDims or axis != axisSpec %}
109
+ let out_coord{{ axis }} = rem / {{ safe_out_stride }}u;
110
+ {% endif %}
111
+ rem = rem % {{ safe_out_stride }}u;
112
+ {% endfor %}
113
+ {% for axis in range(rank) %}
114
+ {% if axis == axisSpec %}
115
+ let coord{{ axis }} = reduce_index;
116
+ {% elif keepDims %}
117
+ let coord{{ axis }} = out_coord{{ axis }};
118
+ {% elif axis < axisSpec %}
119
+ let coord{{ axis }} = out_coord{{ axis }};
120
+ {% else %}
121
+ let coord{{ axis }} = out_coord{{ axis - 1 }};
122
+ {% endif %}
123
+ {% endfor %}
124
+ {% set src = namespace(value="coord0") %}
125
+ {% for axis in range(1, rank) %}
126
+ {% set src.value = "(" ~ src.value ~ " * " ~ dataShape[axis] ~ "u + coord" ~ axis ~ ")" %}
127
  {% endfor %}
128
  return {{ src.value }};
129
  }
130
  {% endif %}
131
+ {% if indexing == "multiaxis" %}
132
+ {% set hasReducedAxis = namespace(value=false) %}
133
+ {% for a in range(rank) %}{% if reduce[a] %}{% set hasReducedAxis.value = true %}{% endif %}{% endfor %}
134
+ {{- reduce_multi_axis_offset(hasReducedAxis.value) }}
135
+ {% endif %}
136
+ {% if indexing == "multiaxis" %}
137
  {% set mcount = namespace(value=1) %}
138
+ {% for a in range(rank) if reduce[a] %}
139
+ {% set mcount.value = mcount.value * dataShape[a] %}
140
  {% endfor %}
141
  {% set count = mcount.value ~ "u" %}
142
  {% if hasReducedAxis.value %}
143
  {% set at = "x[input_offset(i, r)]" %}
144
+ {% set at_first = "x[input_offset(i, 0u)]" %}
145
  {% else %}
146
  {% set at = "x[input_offset(i)]" %}
147
+ {% set at_first = "x[input_offset(i)]" %}
148
  {% endif %}
149
+ {% elif indexing == "rankn" %}
150
  {% set count = "params.axisDim" %}
151
  {% set at = "x[input_offset(i, r)]" %}
152
+ {% set at_first = "x[input_offset(i, 0u)]" %}
153
  {% elif axis == 0 %}
154
  {% set count = "params.rows" %}
155
  {% set at = "x[r * params.cols + i]" %}
156
+ {% set at_first = "x[i]" %}
157
  {% else %}
158
  {% set count = "params.cols" %}
159
  {% set at = "x[i * params.cols + r]" %}
160
+ {% set at_first = "x[i * params.cols]" %}
161
  {% endif %}
162
  {% if castF32 %}
163
  {% set at = "f32(" ~ at ~ ")" %}
164
+ {% set at_first = "f32(" ~ at_first ~ ")" %}
165
  {% endif %}
166
 
167
  @compute @workgroup_size({{ reduceWorkgroupSize }})
168
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
169
  // 2D-folded flat index: gid.y carries the high bits past the
170
+ // per-axis dispatch fold width (outputs > 16.7M elements).
171
+ let i = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ reduceWorkgroupSize }}u;
172
  if (i >= params.outCount) {
173
  return;
174
  }
175
+ {% if op == "logsumexp" %}
176
+ {% if intMode %}
177
+ // Integer logsumexp widens each element for exp/log, then truncates the result
178
+ // back to the integer output type.
179
+ // Integers are never NaN, so the f32 NaN-propagation path is unnecessary here.
180
+ if ({{ count }} == 0u) {
181
+ y[i] = {{ scalar }}(F32_MIN);
182
+ return;
183
+ }
184
+ var m = F32_MIN;
185
+ for (var r = 0u; r < {{ count }}; r = r + 1u) {
186
+ m = max(m, f32({{ at }}));
187
+ }
188
+ var acc = 0.0;
189
+ for (var r = 0u; r < {{ count }}; r = r + 1u) {
190
+ acc = acc + exp(f32({{ at }}) - m);
191
+ }
192
+ y[i] = {{ scalar }}(m + log(acc));
193
+ {% else %}
194
+ if ({{ count }} == 0u) {
195
+ y[i] = {{ yv }}negative_infinity(){{ vy }};
196
+ return;
197
+ }
198
+ var m = F32_MIN;
199
+ var has_nan = false;
200
+ var nan_value = 0.0;
201
+ for (var r = 0u; r < {{ count }}; r = r + 1u) {
202
+ let value = {{ at }};
203
+ if (is_nan_f32(value)) {
204
+ has_nan = true;
205
+ nan_value = value;
206
+ } else {
207
+ m = max(m, value);
208
+ }
209
+ }
210
+ if (has_nan) {
211
+ y[i] = {{ yv }}nan_value{{ vy }};
212
+ return;
213
+ }
214
+ if (m > F32_MAX) {
215
+ y[i] = {{ yv }}m{{ vy }};
216
+ return;
217
+ }
218
+ var acc = 0.0;
219
+ for (var r = 0u; r < {{ count }}; r = r + 1u) {
220
+ acc = acc + exp({{ at }} - m);
221
+ }
222
+ y[i] = {{ yv }}m + log(acc){{ vy }};
223
+ {% endif %}
224
+ {% elif op == "max" or op == "min" %}
225
+ if ({{ count }} == 0u) {
226
+ y[i] = {{ yv }}empty_value(){{ vy }};
227
+ return;
228
+ }
229
+ var acc = {{ at_first }};
230
+ for (var r = 1u; r < {{ count }}; r = r + 1u) {
231
+ acc = {{ op }}(acc, {{ at }});
232
+ }
233
+ y[i] = {{ yv }}acc{{ vy }};
234
+ {% else %}
235
+ {% if op == "mean" %}
236
+ if ({{ count }} == 0u) {
237
+ {% if intMode %}
238
+ y[i] = {{ scalar }}(0);
239
+ {% else %}
240
+ y[i] = {{ yv }}0.0{{ vy }};
241
+ {% endif %}
242
+ return;
243
+ }
244
+ {% elif op == "logsum" %}
245
+ if ({{ count }} == 0u) {
246
+ y[i] = {{ yv }}negative_infinity(){{ vy }};
247
+ return;
248
+ }
249
+ {% endif %}
250
  {% if intMode %}
251
  // Integer reduction accumulates in the output type. sum/prod/l1/sumsquare stay
252
  // in integer arithmetic; l2/logsum widen for sqrt/log and truncate afterward.
253
+ var acc = {{ scalar }}({{ "1" if op == "prod" else "0" }});
254
  {% else %}
255
+ var acc = {{ "1.0" if op == "prod" else "0.0" }};
256
  {% endif %}
257
  for (var r = 0u; r < {{ count }}; r = r + 1u) {
258
+ {% if op == "prod" %}
259
+ acc = acc * {{ at }};
260
+ {% elif op == "l1" %}
261
+ acc = acc + abs({{ at }});
262
+ {% elif op == "l2" or op == "sumsquare" %}
263
+ let value = {{ at }};
264
+ acc = acc + value * value;
265
+ {% else %}
266
  acc = acc + {{ at }};
267
+ {% endif %}
268
  }
269
+ {% if op == "l2" %}
270
+ {% if intMode %}
271
+ y[i] = {{ scalar }}(sqrt(f32(acc)));
272
+ {% else %}
273
+ y[i] = {{ yv }}sqrt(acc){{ vy }};
274
+ {% endif %}
275
+ {% elif op == "logsum" %}
276
+ y[i] = {{ yv }}log(acc){{ vy }};
277
+ {% elif op == "mean" %}
278
+ {% if intMode %}
279
+ y[i] = acc / {{ scalar }}({{ count }});
280
+ {% else %}
281
+ y[i] = {{ yv }}acc / f32({{ count }}){{ vy }};
282
+ {% endif %}
283
+ {% else %}
284
  y[i] = {{ yv }}acc{{ vy }};
285
+ {% endif %}
286
+ {% endif %}
287
  }
build/webgpu/test.json CHANGED
@@ -1,5 +1,4 @@
1
  {
2
- "op": "ai.onnx.ReduceSum",
3
  "fixtureArrays": {
4
  "rank3_axis1_middle_no_keepdims_input_x": [1, 2, 3, 4, -1, -2, -3, -4, 0.5, 1.5, 2.5, 3.5, 10, 20, 30, 40, -10, -20, -30, -40, 2, 4, 6, 8],
5
  "rank3_axis0_no_keepdims_input_x": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]
@@ -8,7 +7,7 @@
8
  {
9
  "name": "all_axes_flat_rank1_boundary_8192",
10
  "provenance": {
11
- "notes": "The parallel full-reduction threshold must supersede the rank1 serial and row-reduction fallbacks."
12
  },
13
  "attrs": { "axes": [0], "keepdims": 0 },
14
  "inputs": { "x": { "dtype": "float32", "shape": [8192], "data": { "kind": "constant", "value": 1.0 } } },
@@ -91,7 +90,7 @@
91
  "provenance": {
92
  "source": "onnxruntime/test/providers/cpu/reduction/reduction_ops_test.cc",
93
  "test": "ReductionOpTest.ReduceSum",
94
- "notes": "Tiled axis-0 companion: 64 finite subnormal values reduce to a larger finite subnormal in each column."
95
  },
96
  "attrs": { "axes": [0], "keepdims": 0 },
97
  "inputs": { "x": { "dtype": "float32", "shape": [64, 16], "data": { "kind": "constant", "value": 1e-40 } } },
@@ -169,7 +168,7 @@
169
  "provenance": {
170
  "source": "onnxruntime/test/providers/cpu/reduction/reduction_ops_test.cc",
171
  "test": "ReductionOpTest.ReduceSum",
172
- "notes": "Axis-0 companion: finite subnormal columns must reduce to finite subnormal sums rather than zero."
173
  },
174
  "attrs": { "axes": [0], "keepdims": 0 },
175
  "inputs": {
@@ -212,7 +211,7 @@
212
  "provenance": {
213
  "source": "onnxruntime/test/providers/cpu/reduction/reduction_ops_test.cc",
214
  "test": "ReductionOpTest.ReduceSum",
215
- "notes": "Vec4 last-axis companion: finite subnormal rows should survive the subgroup/vectorized reducer."
216
  },
217
  "attrs": { "axes": [-1], "keepdims": 0 },
218
  "inputs": {
@@ -240,7 +239,7 @@
240
  "provenance": {
241
  "source": "onnxruntime/test/providers/cpu/reduction/reduction_ops_test.cc",
242
  "test": "ReductionOpTest.ReduceSum",
243
- "notes": "Odd-width last-axis companion: finite subnormal rows should survive the non-vec4 subgroup reducer."
244
  },
245
  "attrs": { "axes": [-1], "keepdims": 0 },
246
  "inputs": {
@@ -268,7 +267,7 @@
268
  "provenance": {
269
  "source": "onnxruntime/test/providers/cpu/reduction/reduction_ops_test.cc",
270
  "test": "ReductionOpTest.ReduceSum",
271
- "notes": "Rank-3 axis-1 companion: the middle-axis indexing path must preserve finite subnormal column sums."
272
  },
273
  "attrs": { "axes": [1], "keepdims": 0 },
274
  "inputs": {
@@ -299,7 +298,7 @@
299
  "provenance": {
300
  "source": "onnxruntime/test/providers/cpu/reduction/reduction_ops_test.cc",
301
  "test": "ReductionOpTest.ReduceSum_default_axes_do_not_keep_dims",
302
- "notes": "Rank-3 default-axes companion: reducing all finite subnormal values to a scalar should preserve the finite subnormal total."
303
  },
304
  "attrs": { "keepdims": 0 },
305
  "inputs": { "x": { "dtype": "float32", "shape": [2, 3, 2], "data": { "kind": "constant", "value": 1e-40 } } },
@@ -316,7 +315,7 @@
316
  "provenance": {
317
  "source": "onnxruntime/test/providers/cpu/reduction/reduction_ops_test.cc",
318
  "test": "ReductionOpTest.ReduceSum_default_axes_keepdims",
319
- "notes": "Rank-3 default-axes keepdims companion: reducing all finite subnormals should preserve the finite total in shape [1,1,1]."
320
  },
321
  "attrs": { "keepdims": 1 },
322
  "inputs": { "x": { "dtype": "float32", "shape": [2, 3, 2], "data": { "kind": "constant", "value": 1e-40 } } },
@@ -384,7 +383,7 @@
384
  {
385
  "name": "rank3_two_axes_trailing_empty_axis_identity_zero",
386
  "provenance": {
387
- "notes": "Two reduced axes where the trailing one is empty. The serial walk divides the reduced linear index by the stride of the reduced axes after each one, and that stride is zero here; the divisor has a floor at one for exactly this shape. Every existing empty-reduce case reduces a single axis, so nothing had reached the floor."
388
  },
389
  "attrs": { "axes": [1, 2], "keepdims": 0 },
390
  "inputs": { "x": { "dtype": "float32", "shape": [2, 3, 0], "data": { "kind": "values", "values": [] } } },
@@ -887,7 +886,7 @@
887
  "shape": [2, 2, 1024],
888
  "data": {
889
  "kind": "cycle",
890
- "values": [1.0, -2.0, 0.5, 3.25, -1.5, 2.0, -0.75, 4.0, -3.5, 1.25, 0.0, -2.25, 5.0, -4.0, 2.75, -1.0]
891
  }
892
  }
893
  },
@@ -897,7 +896,7 @@
897
  "name": "dispatch_cliff_axis0_cols_1048577",
898
  "requires": { "limits": { "maxBufferSize": 268435712, "maxStorageBufferBindingSize": 268435712 } },
899
  "provenance": {
900
- "notes": "rows>=64 selects axis0_tilecols (prio 20); cols=1048577 > 65535*TILE_COLS(16) forces the folded column-tile dispatch (x=65535, y=2). Validates the (wg.x + wg.y*nwg.x)*TILE_COLS reconstruction in the shared reduce-axis0-tilecols.wgsl.jinja (used by all Reduce*/ArgMax/ArgMin axis0 paths). Before the 2D fold the plan-time dispatch-limit guard throws. Its largest tensor is 268435712 bytes, so the case needs an adapter whose maxBufferSize and maxStorageBufferBindingSize both reach it declared, because the WebGPU guaranteed minimums (256 MiB / 128 MiB) do not, and a device at them must report the case inapplicable rather than fail allocating it."
901
  },
902
  "attrs": { "axes": [0], "keepdims": 0 },
903
  "inputs": {
@@ -1000,7 +999,7 @@
1000
  {
1001
  "name": "int32_lastaxis_tree_parallel_3x67",
1002
  "provenance": {
1003
- "notes": "67 cols >= ROW_PARALLEL_MIN_COLS selects the axis1_parallel tree path for i32; values above 2^24 fail if the accumulator smuggles through f32 (regression: f32 combine vs i32 value type mismatch)."
1004
  },
1005
  "attrs": { "axes": [1], "keepdims": 0 },
1006
  "inputs": {
@@ -1037,7 +1036,7 @@
1037
  {
1038
  "name": "all_axes_flat_fullreduce_101x103_nonmul4_keepdims",
1039
  "provenance": {
1040
- "notes": "numel=10403 (%4=3, >=8192) full-reduce: previously fell to the single-lane reduce-serial-axis kernel; now stays on all_axes_flat (f32 binding, manual vec4 groups + 3-element scalar tail). Cross-checks the flat path against the serial all_axes_keepdims reference a dropped tail would change the result."
1041
  },
1042
  "attrs": { "keepdims": 1 },
1043
  "inputs": {
@@ -1061,13 +1060,13 @@
1061
  "inputs": { "x": { "dtype": "int32", "shape": [128, 64], "data": { "kind": "cycle", "values": [7000, -1, 2] } } },
1062
  "outputs": { "y": { "dtype": "int32", "shape": [], "tolerance": 0 } },
1063
  "provenance": {
1064
- "notes": "The cycle [1,-1,2,-2] summed to exactly zero across all 8192 elements, so this case could not distinguish an int32 result from an f32 one - on the very axis its name claims - and referenceReduceAllFloat was in fact returning a Float32Array here. A three-value cycle makes the total 19119729, which is ODD and above 2^24 where f32 has an ulp of 2, so the exact answer is unrepresentable in f32 and the dtype leak becomes visible. It also stays well inside int32, so nothing wraps."
1065
  }
1066
  },
1067
  {
1068
  "name": "axis0_narrow_f32_8192x3_splitk_guard_lock",
1069
  "provenance": {
1070
- "notes": "Compact lock below the historical 16-column split-K guard. It selects the split-K axis0 path; constant ones make the partial reduction and reassociation exact."
1071
  },
1072
  "attrs": { "axes": [0], "keepdims": 0 },
1073
  "inputs": { "x": { "dtype": "float32", "shape": [8192, 3], "data": { "kind": "constant", "value": 1.0 } } },
@@ -1230,10 +1229,13 @@
1230
  "x": {
1231
  "dtype": "float16",
1232
  "shape": [8192, 8],
1233
- "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.07, "scale": 0.2 }
1234
  }
1235
  },
1236
- "outputs": { "y": { "dtype": "float16", "shape": [8], "tolerance": 0.05, "relTolerance": 0.002 } }
 
 
 
1237
  },
1238
  {
1239
  "name": "f16_last_axis_vec4_8x1024",
@@ -1302,10 +1304,13 @@
1302
  "x": {
1303
  "dtype": "float16",
1304
  "shape": [4096, 64],
1305
- "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.07, "scale": 0.2 }
1306
  }
1307
  },
1308
- "outputs": { "y": { "dtype": "float16", "shape": [64], "tolerance": 0.05, "relTolerance": 0.002 } }
 
 
 
1309
  },
1310
  {
1311
  "name": "int32_axis0_tiled_64x32",
@@ -1318,6 +1323,173 @@
1318
  }
1319
  },
1320
  "outputs": { "y": { "dtype": "int32", "shape": [32], "tolerance": 0 } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1321
  }
1322
  ]
1323
  }
 
1
  {
 
2
  "fixtureArrays": {
3
  "rank3_axis1_middle_no_keepdims_input_x": [1, 2, 3, 4, -1, -2, -3, -4, 0.5, 1.5, 2.5, 3.5, 10, 20, 30, 40, -10, -20, -30, -40, 2, 4, 6, 8],
4
  "rank3_axis0_no_keepdims_input_x": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]
 
7
  {
8
  "name": "all_axes_flat_rank1_boundary_8192",
9
  "provenance": {
10
+ "notes": "Exactly 8,192 rank-1 elements exercise the inclusive lower boundary of the parallel full reduction."
11
  },
12
  "attrs": { "axes": [0], "keepdims": 0 },
13
  "inputs": { "x": { "dtype": "float32", "shape": [8192], "data": { "kind": "constant", "value": 1.0 } } },
 
90
  "provenance": {
91
  "source": "onnxruntime/test/providers/cpu/reduction/reduction_ops_test.cc",
92
  "test": "ReductionOpTest.ReduceSum",
93
+ "notes": "An axis-0 reduction over 64 finite subnormal values produces a larger finite subnormal in each column."
94
  },
95
  "attrs": { "axes": [0], "keepdims": 0 },
96
  "inputs": { "x": { "dtype": "float32", "shape": [64, 16], "data": { "kind": "constant", "value": 1e-40 } } },
 
168
  "provenance": {
169
  "source": "onnxruntime/test/providers/cpu/reduction/reduction_ops_test.cc",
170
  "test": "ReductionOpTest.ReduceSum",
171
+ "notes": "An axis-0 reduction of finite subnormal columns must produce finite subnormal sums rather than zero."
172
  },
173
  "attrs": { "axes": [0], "keepdims": 0 },
174
  "inputs": {
 
211
  "provenance": {
212
  "source": "onnxruntime/test/providers/cpu/reduction/reduction_ops_test.cc",
213
  "test": "ReductionOpTest.ReduceSum",
214
+ "notes": "Finite subnormal rows should survive the vectorized last-axis reduction."
215
  },
216
  "attrs": { "axes": [-1], "keepdims": 0 },
217
  "inputs": {
 
239
  "provenance": {
240
  "source": "onnxruntime/test/providers/cpu/reduction/reduction_ops_test.cc",
241
  "test": "ReductionOpTest.ReduceSum",
242
+ "notes": "Finite subnormal rows should survive an odd-width last-axis reduction."
243
  },
244
  "attrs": { "axes": [-1], "keepdims": 0 },
245
  "inputs": {
 
267
  "provenance": {
268
  "source": "onnxruntime/test/providers/cpu/reduction/reduction_ops_test.cc",
269
  "test": "ReductionOpTest.ReduceSum",
270
+ "notes": "A rank-3 axis-1 reduction must preserve finite subnormal column sums through middle-axis indexing."
271
  },
272
  "attrs": { "axes": [1], "keepdims": 0 },
273
  "inputs": {
 
298
  "provenance": {
299
  "source": "onnxruntime/test/providers/cpu/reduction/reduction_ops_test.cc",
300
  "test": "ReductionOpTest.ReduceSum_default_axes_do_not_keep_dims",
301
+ "notes": "A rank-3 default-axes reduction should preserve the finite subnormal scalar total."
302
  },
303
  "attrs": { "keepdims": 0 },
304
  "inputs": { "x": { "dtype": "float32", "shape": [2, 3, 2], "data": { "kind": "constant", "value": 1e-40 } } },
 
315
  "provenance": {
316
  "source": "onnxruntime/test/providers/cpu/reduction/reduction_ops_test.cc",
317
  "test": "ReductionOpTest.ReduceSum_default_axes_keepdims",
318
+ "notes": "A rank-3 default-axes reduction with keepdims should preserve the finite subnormal total in shape [1,1,1]."
319
  },
320
  "attrs": { "keepdims": 1 },
321
  "inputs": { "x": { "dtype": "float32", "shape": [2, 3, 2], "data": { "kind": "constant", "value": 1e-40 } } },
 
383
  {
384
  "name": "rank3_two_axes_trailing_empty_axis_identity_zero",
385
  "provenance": {
386
+ "notes": "Two axes are reduced and the trailing reduced axis is empty. The serial index walk must use its zero-stride safeguard and return the additive identity without dividing by zero."
387
  },
388
  "attrs": { "axes": [1, 2], "keepdims": 0 },
389
  "inputs": { "x": { "dtype": "float32", "shape": [2, 3, 0], "data": { "kind": "values", "values": [] } } },
 
886
  "shape": [2, 2, 1024],
887
  "data": {
888
  "kind": "cycle",
889
+ "values": [1.0, -2.0, 0.5, 3.25, -1.5, 2.0, -0.75, 4.0, -3.5, 1.25, 0.0, -2.25, 5.0, -4.0, 2.75, -1.0, 6.5]
890
  }
891
  }
892
  },
 
896
  "name": "dispatch_cliff_axis0_cols_1048577",
897
  "requires": { "limits": { "maxBufferSize": 268435712, "maxStorageBufferBindingSize": 268435712 } },
898
  "provenance": {
899
+ "notes": "With 1,048,577 columns, the axis-0 reduction requires 65,536 column tiles and therefore a two-row workgroup dispatch. The final tile verifies folded workgroup indexing and the over-dispatch bound. The 268,435,712-byte tensor requires the declared storage limits because it exceeds WebGPU's guaranteed minimum storage-binding size."
900
  },
901
  "attrs": { "axes": [0], "keepdims": 0 },
902
  "inputs": {
 
999
  {
1000
  "name": "int32_lastaxis_tree_parallel_3x67",
1001
  "provenance": {
1002
+ "notes": "A 67-column int32 row exercises parallel last-axis reduction. Values above 2^24 make any unintended f32 accumulation observable."
1003
  },
1004
  "attrs": { "axes": [1], "keepdims": 0 },
1005
  "inputs": {
 
1036
  {
1037
  "name": "all_axes_flat_fullreduce_101x103_nonmul4_keepdims",
1038
  "provenance": {
1039
+ "notes": "A 10,403-element full reduction uses float32 vec4 groups plus a three-element scalar tail. The kept singleton dimensions and independently computed result make any dropped tail element observable."
1040
  },
1041
  "attrs": { "keepdims": 1 },
1042
  "inputs": {
 
1060
  "inputs": { "x": { "dtype": "int32", "shape": [128, 64], "data": { "kind": "cycle", "values": [7000, -1, 2] } } },
1061
  "outputs": { "y": { "dtype": "int32", "shape": [], "tolerance": 0 } },
1062
  "provenance": {
1063
+ "notes": "A three-value int32 cycle over 8,192 elements sums to the odd value 19,119,729. The result is inside int32 range but not exactly representable as f32, exposing any unintended float accumulation."
1064
  }
1065
  },
1066
  {
1067
  "name": "axis0_narrow_f32_8192x3_splitk_guard_lock",
1068
  "provenance": {
1069
+ "notes": "An 8,192-by-3 axis-0 reduction exercises split-K with a narrow output. Constant ones make the partial reduction and reassociation exact."
1070
  },
1071
  "attrs": { "axes": [0], "keepdims": 0 },
1072
  "inputs": { "x": { "dtype": "float32", "shape": [8192, 3], "data": { "kind": "constant", "value": 1.0 } } },
 
1229
  "x": {
1230
  "dtype": "float16",
1231
  "shape": [8192, 8],
1232
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.07, "scale": 0.2, "offset": 0.25 }
1233
  }
1234
  },
1235
+ "outputs": { "y": { "dtype": "float16", "shape": [8], "tolerance": 0.05, "relTolerance": 0.002 } },
1236
+ "provenance": {
1237
+ "notes": "A 8192-row float16 axis-0 sum. Values oscillate about 0.25 so each column total is proportional to the reduction length, making a dropped, duplicated or mis-strided partial observable at float16 resolution."
1238
+ }
1239
  },
1240
  {
1241
  "name": "f16_last_axis_vec4_8x1024",
 
1304
  "x": {
1305
  "dtype": "float16",
1306
  "shape": [4096, 64],
1307
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.07, "scale": 0.2, "offset": 0.25 }
1308
  }
1309
  },
1310
+ "outputs": { "y": { "dtype": "float16", "shape": [64], "tolerance": 0.05, "relTolerance": 0.002 } },
1311
+ "provenance": {
1312
+ "notes": "A 4096-row float16 axis-0 sum. Values oscillate about 0.25 so each column total is proportional to the reduction length, making a dropped, duplicated or mis-strided partial observable at float16 resolution."
1313
+ }
1314
  },
1315
  {
1316
  "name": "int32_axis0_tiled_64x32",
 
1323
  }
1324
  },
1325
  "outputs": { "y": { "dtype": "int32", "shape": [32], "tolerance": 0 } }
1326
+ },
1327
+ {
1328
+ "name": "subgroup_rows_last_axis_f32_96x256",
1329
+ "attrs": { "axes": [-1], "keepdims": 0 },
1330
+ "inputs": {
1331
+ "x": {
1332
+ "dtype": "float32",
1333
+ "shape": [96, 256],
1334
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29 }
1335
+ }
1336
+ },
1337
+ "outputs": { "y": { "dtype": "float32", "shape": [96], "tolerance": 0.0002, "relTolerance": 0.0001 } }
1338
+ },
1339
+ {
1340
+ "name": "subgroup_rows_last_axis_f32_rank3_2x40x1024",
1341
+ "attrs": { "axes": [-1], "keepdims": 0 },
1342
+ "inputs": {
1343
+ "x": {
1344
+ "dtype": "float32",
1345
+ "shape": [2, 40, 1024],
1346
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.25 }
1347
+ }
1348
+ },
1349
+ "outputs": { "y": { "dtype": "float32", "shape": [2, 40], "tolerance": 0.001, "relTolerance": 0.0001 } }
1350
+ },
1351
+ {
1352
+ "name": "subgroup_rows_last_axis_f16_80x1024",
1353
+ "attrs": { "axes": [1], "keepdims": 0 },
1354
+ "inputs": {
1355
+ "x": {
1356
+ "dtype": "float16",
1357
+ "shape": [80, 1024],
1358
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.07, "scale": 0.2 }
1359
+ }
1360
+ },
1361
+ "outputs": { "y": { "dtype": "float16", "shape": [80], "tolerance": 0.05, "relTolerance": 0.002 } }
1362
+ },
1363
+ {
1364
+ "name": "int32_lastaxis_subgroup_rows_64x1024",
1365
+ "provenance": {
1366
+ "notes": "Sixty-four rows of 1,024 int32 values exercise subgroup-per-row reduction in the output type. A five-value cycle shifts phase every row so outputs differ while matching tree accumulation exactly."
1367
+ },
1368
+ "attrs": { "axes": [1], "keepdims": 0 },
1369
+ "inputs": {
1370
+ "x": { "dtype": "int32", "shape": [64, 1024], "data": { "kind": "cycle", "values": [3, 4, 0, -2, 1] } }
1371
+ },
1372
+ "outputs": { "y": { "dtype": "int32", "shape": [64], "tolerance": 0 } }
1373
+ },
1374
+ {
1375
+ "name": "f32_lastaxis_rowserial_band_subgroup_rows_8192x256",
1376
+ "provenance": {
1377
+ "notes": "Route lock for the subgroup-per-row last-axis reduce inside the row-serial band: 8192 rows of 256 f32 values sit at the ROW_SERIAL_MIN_ROWS floor with SUBGROUP_MIN_COLS columns, so the subgroup route outranks the serial one."
1378
+ },
1379
+ "attrs": { "axes": [1], "keepdims": 0 },
1380
+ "inputs": {
1381
+ "x": {
1382
+ "dtype": "float32",
1383
+ "shape": [8192, 256],
1384
+ "data": { "kind": "fillFloat32", "sinStep": 0.013, "cosStep": 0.029, "scale": 1.0 }
1385
+ }
1386
+ },
1387
+ "outputs": { "y": { "dtype": "float32", "shape": [8192], "tolerance": 0.001, "relTolerance": 0.0001 } }
1388
+ },
1389
+ {
1390
+ "name": "rank4_axes023_noncontiguous_multi_axis_2x4x3x3",
1391
+ "provenance": {
1392
+ "notes": "A rank-4 reduction over axes {0,2,3} keeps the middle channel axis. The reduced elements are not a contiguous suffix, so each output uses a serial fold."
1393
+ },
1394
+ "attrs": { "axes": [0, 2, 3], "keepdims": 0 },
1395
+ "inputs": {
1396
+ "x": {
1397
+ "dtype": "float32",
1398
+ "shape": [2, 4, 3, 3],
1399
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.07, "scale": 0.2, "offset": 1.0 }
1400
+ }
1401
+ },
1402
+ "outputs": { "y": { "dtype": "float32", "shape": [4], "tolerance": 0.0001 } }
1403
+ },
1404
+ {
1405
+ "name": "multi_axis_rank4_coop_channel_reduce_axes023",
1406
+ "provenance": {
1407
+ "notes": "A rank-4 reduction over axes {0,2,3} leaves one output per channel and 512 reduced elements per output, exercising cooperative tree accumulation. The tolerance allows its f32 reassociation."
1408
+ },
1409
+ "attrs": { "axes": [0, 2, 3], "keepdims": 0 },
1410
+ "inputs": {
1411
+ "x": {
1412
+ "dtype": "float32",
1413
+ "shape": [2, 8, 16, 16],
1414
+ "data": { "kind": "fillFloat32", "sinStep": 0.017, "cosStep": 0.029, "scale": 1.5 }
1415
+ }
1416
+ },
1417
+ "outputs": { "y": { "dtype": "float32", "shape": [8], "tolerance": 0.0005, "relTolerance": 0.0001 } }
1418
+ },
1419
+ {
1420
+ "name": "multi_axis_rank4_coop_channel_reduce_axes023_keepdims",
1421
+ "provenance": {
1422
+ "notes": "A rank-4 reduction over axes {0,2,3} with keepdims leaves one output per channel and 512 reduced elements per output, exercising cooperative tree accumulation. The tolerance allows its f32 reassociation."
1423
+ },
1424
+ "attrs": { "axes": [0, 2, 3], "keepdims": 1 },
1425
+ "inputs": {
1426
+ "x": {
1427
+ "dtype": "float32",
1428
+ "shape": [2, 8, 16, 16],
1429
+ "data": { "kind": "fillFloat32", "sinStep": 0.017, "cosStep": 0.029, "scale": 1.5 }
1430
+ }
1431
+ },
1432
+ "outputs": { "y": { "dtype": "float32", "shape": [1, 8, 1, 1], "tolerance": 0.0005, "relTolerance": 0.0001 } }
1433
+ },
1434
+ {
1435
+ "name": "multi_axis_rank3_coop_axes02",
1436
+ "provenance": {
1437
+ "notes": "A rank-3 reduction over axes {0,2} leaves one output per channel and 512 reduced elements per output, exercising cooperative tree accumulation. The tolerance allows its f32 reassociation."
1438
+ },
1439
+ "attrs": { "axes": [0, 2], "keepdims": 0 },
1440
+ "inputs": {
1441
+ "x": {
1442
+ "dtype": "float32",
1443
+ "shape": [8, 6, 64],
1444
+ "data": { "kind": "fillFloat32", "sinStep": 0.017, "cosStep": 0.029, "scale": 1.5 }
1445
+ }
1446
+ },
1447
+ "outputs": { "y": { "dtype": "float32", "shape": [6], "tolerance": 0.0005, "relTolerance": 0.0001 } }
1448
+ },
1449
+ {
1450
+ "name": "multi_axis_rank4_coop_channel_reduce_axes023_f16",
1451
+ "provenance": {
1452
+ "notes": "A cooperative multi-axis channel reduction widens f16 storage to f32 for accumulation."
1453
+ },
1454
+ "attrs": { "axes": [0, 2, 3], "keepdims": 0 },
1455
+ "inputs": {
1456
+ "x": {
1457
+ "dtype": "float16",
1458
+ "shape": [2, 8, 16, 16],
1459
+ "data": { "kind": "fillFloat32", "sinStep": 0.017, "cosStep": 0.029, "scale": 1.0 }
1460
+ }
1461
+ },
1462
+ "outputs": { "y": { "dtype": "float16", "shape": [8], "tolerance": 0.05, "relTolerance": 0.002 } }
1463
+ },
1464
+ {
1465
+ "name": "multi_axis_rank4_coop_channel_reduce_axes023_i32",
1466
+ "provenance": {
1467
+ "notes": "A cooperative multi-axis channel reduction accumulates int32 values in the output type."
1468
+ },
1469
+ "attrs": { "axes": [0, 2, 3], "keepdims": 0 },
1470
+ "inputs": {
1471
+ "x": {
1472
+ "dtype": "int32",
1473
+ "shape": [2, 8, 16, 16],
1474
+ "data": { "kind": "cycle", "values": [3, -1, 4, -1, 5, -9, 2] }
1475
+ }
1476
+ },
1477
+ "outputs": { "y": { "dtype": "int32", "shape": [8], "tolerance": 0 } }
1478
+ },
1479
+ {
1480
+ "name": "coop_and_serial_one_large_many_small",
1481
+ "provenance": {
1482
+ "notes": "Each channel contains one 1e8 value and 2,047 ones. Cooperative reassociation at this reduction size must remain within the declared relative tolerance of the sequential sum."
1483
+ },
1484
+ "attrs": { "axes": [0, 2, 3], "keepdims": 0 },
1485
+ "inputs": {
1486
+ "x": {
1487
+ "dtype": "float32",
1488
+ "shape": [8, 64, 16, 16],
1489
+ "data": { "kind": "cycle", "values": [100000000.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0] }
1490
+ }
1491
+ },
1492
+ "outputs": { "y": { "dtype": "float32", "shape": [64], "tolerance": 1, "relTolerance": 0.00001 } }
1493
  }
1494
  ]
1495
  }