Xenova HF Staff commited on
Commit
b6fa0da
·
verified ·
1 Parent(s): 8d8c058

sync 91d990483a17

Browse files
README.md CHANGED
@@ -18,15 +18,15 @@ See the [ONNX `ReduceMax` spec](https://onnx.ai/onnx/operators/onnx__ReduceMax.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 reduced output tensor containing the maximum values. | required |
30
 
31
  ## Attributes
32
 
@@ -34,9 +34,9 @@ Default values (overridable per request):
34
 
35
  | Attribute | Default | Description |
36
  | --- | --- | --- |
 
37
  | `keepdims` | `1` | Whether to retain reduced dimensions in the output with size 1 (1) or prune them (0). |
38
  | `noop_with_empty_axes` | `0` | When 1 and `axes` is empty, the op acts as an identity (no-op); when 0 (default), reduction happens over all axes. |
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
 
@@ -50,7 +50,7 @@ Some implementation variants require `subgroups`. These are route-specific capab
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
@@ -61,21 +61,25 @@ Some implementation variants require `subgroups`. These are route-specific capab
61
  - [`reduce-flat-partial.wgsl.jinja`](build/webgpu/reduce-flat-partial.wgsl.jinja)
62
  - [`reduce-narrow-empty-identity.wgsl.jinja`](build/webgpu/reduce-narrow-empty-identity.wgsl.jinja)
63
  - [`reduce-noop-empty-axes.wgsl.jinja`](build/webgpu/reduce-noop-empty-axes.wgsl.jinja)
 
64
  - [`reduce-row-subgroup.wgsl.jinja`](build/webgpu/reduce-row-subgroup.wgsl.jinja)
65
  - [`reduce-row-tree.wgsl.jinja`](build/webgpu/reduce-row-tree.wgsl.jinja)
66
  - [`reduce-serial-axis.wgsl.jinja`](build/webgpu/reduce-serial-axis.wgsl.jinja)
67
 
68
  ## Use with `@huggingface/kernels`
69
 
70
- The loader automatically allocates outputs whose metadata it can derive from the manifest contract and this call.
 
 
71
 
72
- The explicit `outputs` entries provide shape and logical dtype metadata for the results listed below:
73
 
74
- - `y`
75
 
76
- Each entry either requests an optional result or supplies metadata that cannot be inferred from the inputs.
77
 
78
  The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
 
79
 
80
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
81
 
 
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 reduced output tensor containing the maximum values. | 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` | Whether to retain reduced dimensions in the output with size 1 (1) or prune them (0). |
39
  | `noop_with_empty_axes` | `0` | When 1 and `axes` is empty, the op acts as an identity (no-op); when 0 (default), reduction happens over all axes. |
 
40
 
41
  ## Type constraints
42
 
 
50
 
51
  ## Files
52
 
53
+ - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, per-variant templates, 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
 
61
  - [`reduce-flat-partial.wgsl.jinja`](build/webgpu/reduce-flat-partial.wgsl.jinja)
62
  - [`reduce-narrow-empty-identity.wgsl.jinja`](build/webgpu/reduce-narrow-empty-identity.wgsl.jinja)
63
  - [`reduce-noop-empty-axes.wgsl.jinja`](build/webgpu/reduce-noop-empty-axes.wgsl.jinja)
64
+ - [`reduce-row-subgroup-rows.wgsl.jinja`](build/webgpu/reduce-row-subgroup-rows.wgsl.jinja)
65
  - [`reduce-row-subgroup.wgsl.jinja`](build/webgpu/reduce-row-subgroup.wgsl.jinja)
66
  - [`reduce-row-tree.wgsl.jinja`](build/webgpu/reduce-row-tree.wgsl.jinja)
67
  - [`reduce-serial-axis.wgsl.jinja`](build/webgpu/reduce-serial-axis.wgsl.jinja)
68
 
69
  ## Use with `@huggingface/kernels`
70
 
71
+ ```sh
72
+ npm install --save-exact @huggingface/kernels@0.0.1-preview.2
73
+ ```
74
 
75
+ 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.
76
 
77
+ This example supplies explicit metadata for:
78
 
79
+ - `y`
80
 
81
  The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
82
+ It follows the `v1` branch as fixes land. To pin exact artifact bytes, pass a 40-character commit `revision` instead of `version`.
83
 
84
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
85
 
build/webgpu/bench.json CHANGED
@@ -1,5 +1,4 @@
1
  {
2
- "op": "ai.onnx.ReduceMax",
3
  "tunableSpace": { "WORKGROUP_SIZE": [64, 128, 256] },
4
  "cases": [
5
  {
@@ -94,7 +93,9 @@
94
  "preset": "smoke",
95
  "vars": { "dtype": "int32", "rows": 1024, "cols": 4096 },
96
  "attrs": { "axes": [1], "keepdims": 0 },
97
- "inputs": { "x": { "shape": [1024, 4096], "dtype": "int32", "dist": "normal", "seed": 617, "scale": 1000 } },
 
 
98
  "outputs": { "y": { "shape": [1024], "dtype": "int32" } },
99
  "bench": { "metrics": [{ "type": "bandwidth", "value": "args.rows * args.cols * dtypeBytes(args.dtype)" }] }
100
  },
@@ -111,7 +112,7 @@
111
  "name": "reducemax-rank3-spatial-axes12-f32-128x256x256-pathology",
112
  "preset": "stress",
113
  "provenance": {
114
- "source": "authored for branch coverage",
115
  "notes": "Rank-3 multi-axis reduction with 128 output lanes, each serially scanning a 256x256 plane."
116
  },
117
  "attrs": { "axes": [1, 2], "keepdims": 1 },
@@ -123,7 +124,7 @@
123
  "name": "reducemax-spatial-axes23-f32-2x64x256x256-pathology",
124
  "preset": "stress",
125
  "provenance": {
126
- "source": "authored for branch coverage",
127
  "notes": "Rank-4 multi-axis reduction with 128 output lanes, each serially scanning a 256x256 plane."
128
  },
129
  "attrs": { "axes": [2, 3], "keepdims": 1 },
 
1
  {
 
2
  "tunableSpace": { "WORKGROUP_SIZE": [64, 128, 256] },
3
  "cases": [
4
  {
 
93
  "preset": "smoke",
94
  "vars": { "dtype": "int32", "rows": 1024, "cols": 4096 },
95
  "attrs": { "axes": [1], "keepdims": 0 },
96
+ "inputs": {
97
+ "x": { "shape": [1024, 4096], "dtype": "int32", "dist": "uniform", "seed": 617, "min": -1000, "max": 1001 }
98
+ },
99
  "outputs": { "y": { "shape": [1024], "dtype": "int32" } },
100
  "bench": { "metrics": [{ "type": "bandwidth", "value": "args.rows * args.cols * dtypeBytes(args.dtype)" }] }
101
  },
 
112
  "name": "reducemax-rank3-spatial-axes12-f32-128x256x256-pathology",
113
  "preset": "stress",
114
  "provenance": {
115
+ "source": "repository-authored",
116
  "notes": "Rank-3 multi-axis reduction with 128 output lanes, each serially scanning a 256x256 plane."
117
  },
118
  "attrs": { "axes": [1, 2], "keepdims": 1 },
 
124
  "name": "reducemax-spatial-axes23-f32-2x64x256x256-pathology",
125
  "preset": "stress",
126
  "provenance": {
127
+ "source": "repository-authored",
128
  "notes": "Rank-4 multi-axis reduction with 128 output lanes, each serially scanning a 256x256 plane."
129
  },
130
  "attrs": { "axes": [2, 3], "keepdims": 1 },
build/webgpu/manifest.json CHANGED
@@ -2,483 +2,269 @@
2
  "domain": "ai.onnx",
3
  "name": "ReduceMax",
4
  "sinceVersion": 20,
5
- "description": "Computes the maximum of input tensor elements along the specified axes. The output rank matches the input when `keepdims` is 1; reduced dimensions are pruned when `keepdims` is 0. Reduction over an empty set yields negative infinity when the dtype supports it, or the dtype's minimum value otherwise. For Boolean inputs, `false` is less than `true`.",
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 reduced output tensor containing the maximum values."
13
  }
14
- ],
15
- "attributes": { "keepdims": 1, "noop_with_empty_axes": 0, "axes": [] },
16
- "attributeDescriptions": {
17
- "keepdims": "Whether to retain reduced dimensions in the output with size 1 (1) or prune them (0).",
18
- "noop_with_empty_axes": "When 1 and `axes` is empty, the op acts as an identity (no-op); when 0 (default), reduction happens over all axes.",
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", "uint32", "int8", "uint8", "bool"] },
23
- "args": {
24
- "x": { "kind": "tensor", "semantic": "data", "role": "input" },
25
- "y": { "kind": "tensor", "semantic": "reduced", "role": "output" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  },
27
  "derive": {
28
- "emptyNarrowInputOk": "numel(shapes.data) > 0 or (tensorDtypes.data != \"int8\" and tensorDtypes.data != \"uint8\")",
29
  "deviceWorkgroupCap": "min(device.limits.maxComputeInvocationsPerWorkgroup, device.limits.maxComputeWorkgroupSizeX)",
30
  "reduceWorkgroupSize": "min(tunables.WORKGROUP_SIZE, deviceWorkgroupCap)",
31
  "treeWorkgroupOk": "reduceWorkgroupSize > 0 and pow2ceil(reduceWorkgroupSize) == reduceWorkgroupSize and reduceWorkgroupSize * dtypeBytes(\"float32\") <= device.limits.maxComputeWorkgroupStorageSize",
32
  "subgroupWorkgroupFloor": "min(reduceWorkgroupSize, max(1, device.adapterInfo.subgroupMaxSize))",
33
- "lastAxisRows": "rows(shapes.data, ranks.data - 1) if ranks.data > 0 else 1",
34
- "lastAxisCols": "dim(shapes.data, ranks.data - 1) if ranks.data > 0 else 1",
35
  "rowSerialPreferred": "lastAxisRows >= tunables.ROW_SERIAL_MIN_ROWS and lastAxisCols <= tunables.ROW_SERIAL_MAX_COLS",
36
- "axis0Rows": "dim(shapes.data, 0) if ranks.data >= 2 else 0",
37
- "axis0Cols": "dim(shapes.data, 1) if ranks.data >= 2 else 0",
38
  "axis0SplitCount": "min(tunables.AXIS0_MAX_SPLITS, pow2ceil(ceilDiv(axis0Rows, tunables.AXIS0_SPLIT_TARGET_ROWS)))",
39
  "axis0SplitScratchBytes": "axis0SplitCount * axis0Cols * dtypeBytes(\"float32\")",
40
- "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",
41
- "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",
42
- "axisSplitDim": "dim(shapes.data, reduceAxis) if ranks.data >= 2 and reduceAxis < ranks.data else 0",
43
- "axisSplitInner": "inner(shapes.data, reduceAxis) if ranks.data >= 2 and reduceAxis < ranks.data else 1",
44
- "axisSplitOutputs": "numel(shapes.reduced)",
45
  "axisSplitCount": "min(tunables.AXIS0_MAX_SPLITS, pow2ceil(ceilDiv(axisSplitDim, tunables.AXIS0_SPLIT_TARGET_ROWS)))",
46
  "axisSplitScratchBytes": "axisSplitCount * axisSplitOutputs * 4",
47
- "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",
48
  "axis0TilePathFits": "treeWorkgroupOk and tunables.AXIS0_TILE_COLS > 0 and tunables.AXIS0_TILE_COLS <= reduceWorkgroupSize and reduceWorkgroupSize % tunables.AXIS0_TILE_COLS == 0",
49
- "flatItems": "floor(numel(shapes.data) / tunables.VECTOR_WIDTH)",
50
  "flatSplitCount": "max(1, min(tunables.FULL_REDUCE_MAX_SPLITS, ceilDiv(flatItems, reduceWorkgroupSize)))",
51
  "flatScratchBytes": "flatSplitCount * dtypeBytes(\"float32\")",
52
- "flatPathFits": "treeWorkgroupOk and flatSplitCount <= device.limits.maxComputeWorkgroupsPerDimension and flatScratchBytes <= device.limits.maxStorageBufferBindingSize and flatScratchBytes <= device.limits.maxBufferSize",
53
- "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",
54
- "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)))"
55
- },
56
- "tunables": {
57
- "WORKGROUP_SIZE": 256,
58
- "VECTOR_WIDTH": 4,
59
- "ROW_PARALLEL_MIN_COLS": 64,
60
- "SUBGROUP_MIN_COLS": 256,
61
- "SUBGROUP_SMALL_ROW_LIMIT": 32768,
62
- "AXIS0_SPLIT_MIN_ROWS": 8192,
63
- "AXIS0_SPLIT_TARGET_ROWS": 256,
64
- "AXIS0_MAX_SPLITS": 128,
65
- "AXIS0_TILE_MIN_ROWS": 64,
66
- "AXIS0_TILE_MIN_COLS": 16,
67
- "AXIS0_TILE_COLS": 16,
68
- "AXIS_SPLIT_TILE_COLS": 8,
69
- "FULL_REDUCE_MIN_ELEMENTS": 8192,
70
- "FULL_REDUCE_MAX_SPLITS": 256,
71
- "CONTIGUOUS_SUFFIX_MIN_COLS": 256,
72
- "ROW_SERIAL_MIN_ROWS": 8192,
73
- "ROW_SERIAL_MAX_COLS": 1024
74
  },
75
- "bindingSets": {
76
- "narrowEmpty": [
77
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$scalar" },
78
- {
79
- "name": "params",
80
- "semantic": "kernel.params",
81
- "buffer": { "type": "uniform" },
82
- "struct": { "name": "Params", "fields": [{ "name": "count", "type": "u32", "value": "numel(shapes.reduced)" }] }
83
- }
84
- ],
85
- "elementwise": [
86
- { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
87
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
88
- {
89
- "name": "params",
90
- "semantic": "kernel.params",
91
- "buffer": { "type": "uniform" },
92
- "struct": { "name": "Params", "fields": [{ "name": "count", "type": "u32", "value": "numel(shapes.reduced)" }] }
93
- }
94
- ],
95
- "lastAxisVec4": [
96
- {
97
- "name": "x",
98
- "arg": "x",
99
- "semantic": "data",
100
- "buffer": { "type": "read-only-storage" },
101
- "elementType": "$vectorScalar"
102
- },
103
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
104
- {
105
- "name": "params",
106
- "semantic": "kernel.params",
107
- "buffer": { "type": "uniform" },
108
- "struct": {
109
- "name": "Params",
110
- "fields": [
111
- { "name": "rows", "type": "u32", "value": "rows(shapes.data, ranks.data - 1)" },
112
- { "name": "chunkCount", "type": "u32", "value": "dim(shapes.data, ranks.data - 1) / tunables.VECTOR_WIDTH" }
113
- ]
114
- }
115
- }
116
- ],
117
- "lastAxisScalar": [
118
- { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
119
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
120
- {
121
- "name": "params",
122
- "semantic": "kernel.params",
123
- "buffer": { "type": "uniform" },
124
- "struct": {
125
- "name": "Params",
126
- "fields": [
127
- { "name": "rows", "type": "u32", "value": "rows(shapes.data, ranks.data - 1)" },
128
- { "name": "cols", "type": "u32", "value": "dim(shapes.data, ranks.data - 1)" }
129
- ]
130
- }
131
- }
132
- ],
133
- "lastAxisScalarSubgroup": [
134
- { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
135
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
136
- {
137
- "name": "params",
138
- "semantic": "kernel.params",
139
- "buffer": { "type": "uniform" },
140
- "struct": {
141
- "name": "Params",
142
- "fields": [
143
- { "name": "rows", "type": "u32", "value": "rows(shapes.data, ranks.data - 1)" },
144
- { "name": "chunkCount", "type": "u32", "value": "dim(shapes.data, ranks.data - 1)" }
145
- ]
146
- }
147
- }
148
- ],
149
- "scalar": [
150
- { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
151
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
152
- {
153
- "name": "params",
154
- "semantic": "kernel.params",
155
- "buffer": { "type": "uniform" },
156
- "struct": {
157
- "name": "Params",
158
- "fields": [
159
- { "name": "rows", "type": "u32", "value": "1" },
160
- { "name": "cols", "type": "u32", "value": "1" },
161
- { "name": "outCount", "type": "u32", "value": "1" }
162
- ]
163
- }
164
- }
165
- ],
166
- "rank1Axis0": [
167
- { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
168
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
169
- {
170
- "name": "params",
171
- "semantic": "kernel.params",
172
- "buffer": { "type": "uniform" },
173
- "struct": {
174
- "name": "Params",
175
- "fields": [
176
- { "name": "rows", "type": "u32", "value": "dim(shapes.data, 0)" },
177
- { "name": "cols", "type": "u32", "value": "1" },
178
- { "name": "outCount", "type": "u32", "value": "numel(shapes.reduced)" }
179
- ]
180
- }
181
- }
182
- ],
183
- "rank2Serial": [
184
- { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
185
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
186
- {
187
- "name": "params",
188
- "semantic": "kernel.params",
189
- "buffer": { "type": "uniform" },
190
- "struct": {
191
- "name": "Params",
192
- "fields": [
193
- { "name": "rows", "type": "u32", "value": "dim(shapes.data, 0)" },
194
- { "name": "cols", "type": "u32", "value": "dim(shapes.data, 1)" },
195
- { "name": "outCount", "type": "u32", "value": "numel(shapes.reduced)" }
196
- ]
197
- }
198
- }
199
- ],
200
- "rank2SerialAxis1": [
201
- { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
202
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
203
- {
204
- "name": "params",
205
- "semantic": "kernel.params",
206
- "buffer": { "type": "uniform" },
207
- "struct": {
208
- "name": "Params",
209
- "fields": [
210
- { "name": "cols", "type": "u32", "value": "dim(shapes.data, 1)" },
211
- { "name": "outCount", "type": "u32", "value": "numel(shapes.reduced)" }
212
- ]
213
- }
214
- }
215
- ],
216
- "axis0Parallel": [
217
- { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
218
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
219
- {
220
- "name": "params",
221
- "semantic": "kernel.params",
222
- "buffer": { "type": "uniform" },
223
- "struct": {
224
- "name": "Params",
225
- "fields": [
226
- { "name": "rows", "type": "u32", "value": "dim(shapes.data, 0)" },
227
- { "name": "cols", "type": "u32", "value": "dim(shapes.data, 1)" }
228
- ]
229
- }
230
- }
231
- ],
232
- "fullReduceSerial": [
233
- { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
234
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
235
- {
236
- "name": "params",
237
- "semantic": "kernel.params",
238
- "buffer": { "type": "uniform" },
239
- "struct": {
240
- "name": "Params",
241
- "fields": [
242
- { "name": "rows", "type": "u32", "value": "numel(shapes.data)" },
243
- { "name": "cols", "type": "u32", "value": "1" },
244
- { "name": "outCount", "type": "u32", "value": "numel(shapes.reduced)" }
245
- ]
246
- }
247
- }
248
- ],
249
- "axisSplitReduce": [
250
- { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
251
- { "name": "partials", "semantic": "partials", "buffer": { "type": "storage" }, "elementType": "$partialElement" },
252
- {
253
- "name": "params",
254
- "semantic": "kernel.params",
255
- "buffer": { "type": "uniform" },
256
- "struct": {
257
- "name": "Params",
258
- "fields": [
259
- { "name": "axisDim", "type": "u32", "value": "axisSplitDim" },
260
- { "name": "inner", "type": "u32", "value": "axisSplitInner" },
261
- { "name": "outputs", "type": "u32", "value": "axisSplitOutputs" }
262
- ]
263
- }
264
- }
265
- ],
266
- "axisSplitCombine": [
267
- {
268
- "name": "partials",
269
- "semantic": "partials",
270
- "buffer": { "type": "read-only-storage" },
271
- "elementType": "$partialElement"
272
- },
273
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
274
- {
275
- "name": "params",
276
- "semantic": "kernel.params",
277
- "buffer": { "type": "uniform" },
278
- "struct": { "name": "Params", "fields": [{ "name": "cols", "type": "u32", "value": "axisSplitOutputs" }] }
279
- }
280
- ],
281
- "axis0SplitReduce": [
282
- { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
283
- { "name": "partials", "semantic": "partials", "buffer": { "type": "storage" }, "elementType": "$partialElement" },
284
- {
285
- "name": "params",
286
- "semantic": "kernel.params",
287
- "buffer": { "type": "uniform" },
288
- "struct": {
289
- "name": "Params",
290
- "fields": [
291
- { "name": "rows", "type": "u32", "value": "dim(shapes.data, 0)" },
292
- { "name": "cols", "type": "u32", "value": "dim(shapes.data, 1)" }
293
- ]
294
- }
295
- }
296
- ],
297
- "axis0SplitCombine": [
298
- {
299
- "name": "partials",
300
- "semantic": "partials",
301
- "buffer": { "type": "read-only-storage" },
302
- "elementType": "$partialElement"
303
- },
304
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
305
- {
306
- "name": "params",
307
- "semantic": "kernel.params",
308
- "buffer": { "type": "uniform" },
309
- "struct": { "name": "Params", "fields": [{ "name": "cols", "type": "u32", "value": "dim(shapes.data, 1)" }] }
310
- }
311
- ],
312
- "rankNAxis": [
313
- { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
314
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
315
- {
316
- "name": "params",
317
- "semantic": "kernel.params",
318
- "buffer": { "type": "uniform" },
319
- "struct": {
320
- "name": "Params",
321
- "fields": [
322
- { "name": "axisDim", "type": "u32", "value": "axisSplitDim" },
323
- { "name": "outCount", "type": "u32", "value": "numel(shapes.reduced)" }
324
- ]
325
- }
326
- }
327
- ],
328
- "flatPartialF32": [
329
- { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
330
- { "name": "partials", "semantic": "partials", "buffer": { "type": "storage" }, "elementType": "f32" },
331
- {
332
- "name": "params",
333
- "semantic": "kernel.params",
334
- "buffer": { "type": "uniform" },
335
- "struct": {
336
- "name": "Params",
337
- "fields": [
338
- { "name": "count4", "type": "u32", "value": "floor(numel(shapes.data) / tunables.VECTOR_WIDTH)" },
339
- { "name": "numel", "type": "u32", "value": "numel(shapes.data)" }
340
- ]
341
- }
342
- }
343
- ],
344
- "flatCombineF32": [
345
- { "name": "partials", "semantic": "partials", "buffer": { "type": "read-only-storage" }, "elementType": "f32" },
346
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
347
- {
348
- "name": "params",
349
- "semantic": "kernel.params",
350
- "buffer": { "type": "uniform" },
351
- "struct": { "name": "Params", "fields": [{ "name": "cols", "type": "u32", "value": "1" }] }
352
- }
353
- ],
354
- "suffixVec4": [
355
- {
356
- "name": "x",
357
- "arg": "x",
358
- "semantic": "data",
359
- "buffer": { "type": "read-only-storage" },
360
- "elementType": "$vectorScalar"
361
- },
362
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
363
- {
364
- "name": "params",
365
- "semantic": "kernel.params",
366
- "buffer": { "type": "uniform" },
367
- "struct": {
368
- "name": "Params",
369
- "fields": [
370
- { "name": "rows", "type": "u32", "value": "numel(shapes.reduced)" },
371
- {
372
- "name": "chunkCount",
373
- "type": "u32",
374
- "value": "numel(shapes.data) / numel(shapes.reduced) / tunables.VECTOR_WIDTH"
375
- }
376
- ]
377
- }
378
- }
379
- ],
380
- "suffixScalar": [
381
- { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
382
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
383
- {
384
- "name": "params",
385
- "semantic": "kernel.params",
386
- "buffer": { "type": "uniform" },
387
- "struct": {
388
- "name": "Params",
389
- "fields": [
390
- { "name": "rows", "type": "u32", "value": "numel(shapes.reduced)" },
391
- { "name": "cols", "type": "u32", "value": "numel(shapes.data) / numel(shapes.reduced)" }
392
- ]
393
- }
394
- }
395
- ],
396
- "multiAxis": [
397
- { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
398
- { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
399
- {
400
- "name": "params",
401
- "semantic": "kernel.params",
402
- "buffer": { "type": "uniform" },
403
- "struct": {
404
- "name": "Params",
405
- "fields": [{ "name": "outCount", "type": "u32", "value": "numel(shapes.reduced)" }]
406
- }
407
- }
408
- ]
409
  },
410
  "variants": [
411
  {
412
  "id": "narrow_empty_identity",
413
  "priority": 100,
414
- "when": ["numel(shapes.data) == 0", "numel(shapes.reduced) > 0", "tensorDtypes.data == \"int8\" or tensorDtypes.data == \"uint8\" or tensorDtypes.data == \"bool\""],
415
- "constants": { "scalar": "dtypes.T" },
416
  "passes": [
417
  {
418
  "id": "main",
419
  "name": "ReduceMax.NarrowEmptyIdentity",
420
- "source": {
421
- "shader": "reduce-narrow-empty-identity.wgsl.jinja",
422
- "inputs": { "identity": "-128 if tensorDtypes.data == \"int8\" else 0" }
423
- },
424
- "bindings": "narrowEmpty",
425
- "dispatch": { "gridStride": "numel(shapes.reduced)", "workgroupSize": "tunables.WORKGROUP_SIZE" }
 
 
426
  }
427
  ]
428
  },
429
  {
430
  "id": "contiguous_suffix_subgroup_vec4",
431
  "priority": 30,
 
432
  "requires": { "features": ["subgroups"] },
433
- "when": ["device.wgslLanguageFeatures.has(\"subgroup_id\")", "not flatParallelCovered", "contiguousSuffixParallelCovered", "(numel(shapes.data) / numel(shapes.reduced)) % tunables.VECTOR_WIDTH == 0"],
434
- "constants": {
435
  "scalar": "dtypes.T",
436
  "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
437
- "workgroupSize": "min(reduceWorkgroupSize, max(subgroupWorkgroupFloor, pow2ceil(ceilDiv(numel(shapes.data) / numel(shapes.reduced), tunables.VECTOR_WIDTH))))"
438
  },
439
  "passes": [
440
  {
441
  "id": "main",
442
  "name": "ReduceMax.ContiguousSuffixSubgroupVec4",
443
- "source": {
444
- "shader": "reduce-row-subgroup.wgsl.jinja",
445
- "inputs": {
446
- "op": "\"max\"",
447
- "vec4": true,
448
- "castF32": "dtypes.T == \"f16\"",
449
- "usesF16": "dtypes.T == \"f16\""
450
- }
451
  },
452
- "subgroupCollectivesWidth": "portable",
453
- "bindings": "suffixVec4",
454
- "dispatch": { "workgroups": "numel(shapes.reduced)" }
455
  }
456
  ]
457
  },
458
  {
459
  "id": "contiguous_suffix_tree_vec4",
460
  "priority": 22,
461
- "when": ["not flatParallelCovered", "contiguousSuffixParallelCovered", "(numel(shapes.data) / numel(shapes.reduced)) % tunables.VECTOR_WIDTH == 0", "treeWorkgroupOk"],
462
- "constants": {
463
  "scalar": "dtypes.T",
464
  "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
465
- "workgroupSize": "min(reduceWorkgroupSize, pow2ceil(ceilDiv(numel(shapes.data) / numel(shapes.reduced), tunables.VECTOR_WIDTH)))"
466
  },
467
  "passes": [
468
  {
469
  "id": "main",
470
  "name": "ReduceMax.ContiguousSuffixTreeVec4",
471
- "source": {
472
- "shader": "reduce-row-tree.wgsl.jinja",
473
- "inputs": {
474
- "op": "\"max\"",
475
- "vec4": true,
476
- "castF32": "dtypes.T == \"f16\"",
477
- "usesF16": "dtypes.T == \"f16\""
478
- }
479
  },
480
- "bindings": "suffixVec4",
481
- "dispatch": { "workgroups": "numel(shapes.reduced)" }
482
  }
483
  ]
484
  },
@@ -486,62 +272,97 @@
486
  "id": "contiguous_suffix_tree",
487
  "priority": 21,
488
  "when": ["not flatParallelCovered", "contiguousSuffixParallelCovered", "treeWorkgroupOk"],
489
- "constants": {
490
- "workgroupSize": "min(reduceWorkgroupSize, pow2ceil(numel(shapes.data) / numel(shapes.reduced)))",
491
  "scalar": "dtypes.T"
492
  },
493
  "passes": [
494
  {
495
  "id": "main",
496
  "name": "ReduceMax.ContiguousSuffixTree",
497
- "source": {
498
- "shader": "reduce-row-tree.wgsl.jinja",
499
- "inputs": { "op": "\"max\"", "castF32": "dtypes.T == \"f16\"", "usesF16": "dtypes.T == \"f16\"" }
500
- },
501
- "bindings": "suffixScalar",
502
- "dispatch": { "workgroups": "numel(shapes.reduced)" }
503
  }
504
  ]
505
  },
506
  {
507
  "id": "noop_empty_axes",
508
  "priority": 40,
509
- "when": ["dtypes.T == \"f32\"", "attrs.noop_with_empty_axes == 1", "(attrs.axes | length) == 0", "sameShape(shapes.data, shapes.reduced)"],
 
510
  "passes": [
511
  {
512
  "id": "main",
513
  "name": "ReduceMax.NoopEmptyAxes",
514
- "source": { "shader": "reduce-noop-empty-axes.wgsl.jinja" },
515
- "bindings": "elementwise",
516
- "dispatch": { "threads": "numel(shapes.reduced)", "workgroupSize": "reduceWorkgroupSize" }
 
 
 
 
517
  }
518
  ]
519
  },
520
  {
521
  "id": "tree_last_axis",
522
  "priority": 20,
 
523
  "demoteWhen": ["rowSerialPreferred"],
524
- "when": ["not flatParallelCovered", "(dtypes.T == \"f32\" or dtypes.T == \"i32\" or dtypes.T == \"u32\")", "ranks.data >= 2", "reduceAxis == ranks.data - 1", "numel(shapes.reduced) == rows(shapes.data, ranks.data - 1)", "lastAxisCols >= tunables.ROW_PARALLEL_MIN_COLS", "treeWorkgroupOk"],
525
- "constants": {
526
  "scalar": "dtypes.T",
527
- "workgroupSize": "min(reduceWorkgroupSize, pow2ceil(dim(shapes.data, ranks.data - 1)))"
528
  },
529
  "passes": [
530
  {
531
  "id": "main",
532
  "name": "ReduceMax.TreeLastAxis",
533
- "source": { "shader": "reduce-row-tree.wgsl.jinja", "inputs": { "op": "\"max\"" } },
534
- "bindings": "lastAxisScalar",
535
- "dispatch": { "workgroups": "rows(shapes.data, ranks.data - 1)" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
536
  }
537
  ]
538
  },
539
  {
540
  "id": "tree_last_axis_vec4",
541
  "priority": 23,
 
542
  "demoteWhen": ["rowSerialPreferred"],
543
- "when": ["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", "lastAxisCols >= tunables.ROW_PARALLEL_MIN_COLS", "lastAxisCols % tunables.VECTOR_WIDTH == 0", "treeWorkgroupOk"],
544
- "constants": {
545
  "scalar": "dtypes.T",
546
  "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
547
  "workgroupSize": "min(reduceWorkgroupSize, pow2ceil(ceilDiv(lastAxisCols, tunables.VECTOR_WIDTH)))"
@@ -550,114 +371,114 @@
550
  {
551
  "id": "main",
552
  "name": "ReduceMax.TreeRowVec4",
553
- "source": {
554
- "shader": "reduce-row-tree.wgsl.jinja",
555
- "inputs": {
556
- "op": "\"max\"",
557
- "vec4": true,
558
- "castF32": "dtypes.T == \"f16\"",
559
- "usesF16": "dtypes.T == \"f16\""
560
- }
561
  },
562
- "bindings": "lastAxisVec4",
563
- "dispatch": { "workgroups": "lastAxisRows" }
564
  }
565
  ]
566
  },
567
  {
568
  "id": "rank0_scalar",
569
  "priority": 40,
570
- "constants": { "axis": 0, "scalar": "dtypes.T" },
571
- "when": ["emptyNarrowInputOk", "f16Ok(dtypes.T)", "ranks.data == 0", "ranks.reduced == 0"],
572
  "passes": [
573
  {
574
  "id": "main",
575
  "name": "ReduceMax.Rank0Scalar",
576
- "source": {
577
- "shader": "reduce-serial-axis.wgsl.jinja",
578
- "inputs": {
579
- "op": "\"max\"",
580
- "indexing": "\"axis2d\"",
581
- "castF32": "dtypes.T == \"f16\"",
582
- "usesF16": "dtypes.T == \"f16\"",
583
- "logicalBool": "tensorDtypes.data == \"bool\""
584
- }
585
  },
586
- "bindings": "scalar",
587
  "dispatch": { "x": 1 }
588
  }
589
  ]
590
  },
591
  {
592
  "id": "rank1_axis0",
593
- "constants": { "axis": 0, "scalar": "dtypes.T" },
594
- "when": ["not flatParallelCovered", "emptyNarrowInputOk", "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))"],
595
  "passes": [
596
  {
597
  "id": "main",
598
  "name": "ReduceMax.Rank1Axis0",
599
- "source": {
600
- "shader": "reduce-serial-axis.wgsl.jinja",
601
- "inputs": {
602
- "op": "\"max\"",
603
- "indexing": "\"axis2d\"",
604
- "castF32": "dtypes.T == \"f16\"",
605
- "usesF16": "dtypes.T == \"f16\"",
606
- "logicalBool": "tensorDtypes.data == \"bool\""
607
- }
608
  },
609
- "bindings": "rank1Axis0",
610
- "dispatch": { "threads": "numel(shapes.reduced)", "workgroupSize": "reduceWorkgroupSize" }
 
 
 
 
611
  }
612
  ]
613
  },
614
  {
615
  "id": "axis_split",
616
  "priority": 24,
617
- "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"],
618
- "derive": { "splitCount": "axisSplitCount" },
619
- "constants": { "partialElement": "\"f32\"", "workgroupSize": "reduceWorkgroupSize", "split": "splitCount" },
 
 
 
 
620
  "intermediates": [{ "id": "partials", "dtype": "float32", "shape": "[splitCount * axisSplitOutputs]" }],
621
  "passes": [
622
  {
623
  "id": "split_reduce",
624
  "name": "ReduceMax.AxisSplitReduce",
625
- "source": {
626
- "shader": "reduce-axis-split-reduce.wgsl.jinja",
627
- "inputs": {
628
- "op": "\"max\"",
629
- "workgroupSize": "reduceWorkgroupSize",
630
- "split": "splitCount",
631
- "castF32": "dtypes.T == \"f16\"",
632
- "usesF16": "dtypes.T == \"f16\""
633
- }
634
  },
635
- "bindings": "axisSplitReduce",
636
- "dispatch": { "threads": "axisSplitOutputs", "workgroupSize": "reduceWorkgroupSize", "y": "splitCount" }
 
 
 
 
637
  },
638
  {
639
  "id": "combine",
640
  "name": "ReduceMax.AxisSplitCombine",
641
- "source": {
642
- "shader": "reduce-axis0-splitk-combine.wgsl.jinja",
643
- "inputs": {
644
- "op": "\"max\"",
645
- "workgroupSize": "reduceWorkgroupSize",
646
- "split": "splitCount",
647
- "outputF16": "dtypes.T == \"f16\""
648
- }
649
- },
650
- "bindings": "axisSplitCombine",
651
- "dispatch": { "threads": "axisSplitOutputs", "workgroupSize": "reduceWorkgroupSize" }
652
  }
653
  ]
654
  },
655
  {
656
  "id": "axis_split_tiled_narrow",
657
  "priority": 25,
658
- "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"],
659
- "derive": { "splitCount": "axisSplitCount" },
660
- "constants": {
661
  "partialElement": "\"f32\"",
662
  "scalar": "dtypes.T",
663
  "workgroupSize": "reduceWorkgroupSize",
@@ -669,125 +490,119 @@
669
  {
670
  "id": "split_reduce",
671
  "name": "ReduceMax.AxisSplitTiledReduce",
672
- "source": {
673
- "shader": "reduce-axis0-tilecols.wgsl.jinja",
674
- "inputs": {
675
- "op": "\"max\"",
676
- "workgroupSize": "reduceWorkgroupSize",
677
- "split": "splitCount",
678
- "tileCols": "tunables.AXIS_SPLIT_TILE_COLS",
679
- "castF32": "dtypes.T == \"f16\"",
680
- "usesF16": "dtypes.T == \"f16\""
681
- }
682
  },
683
- "bindings": "axisSplitReduce",
684
- "dispatch": { "workgroups": "ceilDiv((axisSplitOutputs), (constants.tileCols))", "y": "splitCount" }
 
 
 
 
685
  },
686
  {
687
  "id": "combine",
688
  "name": "ReduceMax.AxisSplitCombine",
689
- "source": {
690
- "shader": "reduce-axis0-splitk-combine.wgsl.jinja",
691
- "inputs": {
692
- "op": "\"max\"",
693
- "workgroupSize": "reduceWorkgroupSize",
694
- "split": "splitCount",
695
- "outputF16": "dtypes.T == \"f16\""
696
- }
697
- },
698
- "bindings": "axisSplitCombine",
699
- "dispatch": { "threads": "axisSplitOutputs", "workgroupSize": "reduceWorkgroupSize" }
700
  }
701
  ]
702
  },
703
  {
704
  "id": "axis0_splitk",
705
  "priority": 22,
706
- "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"],
707
- "derive": { "splitCount": "axis0SplitCount" },
708
- "constants": { "partialElement": "\"f32\"", "workgroupSize": "reduceWorkgroupSize", "split": "splitCount" },
709
- "intermediates": [{ "id": "partials", "dtype": "float32", "shape": "[splitCount * dim(shapes.data, 1)]" }],
 
 
 
 
710
  "passes": [
711
  {
712
  "id": "split_reduce",
713
  "name": "ReduceMax.Axis0SplitKReduce",
714
- "source": {
715
- "shader": "reduce-axis0-splitk-reduce.wgsl.jinja",
716
- "inputs": {
717
- "op": "\"max\"",
718
- "workgroupSize": "reduceWorkgroupSize",
719
- "split": "splitCount",
720
- "castF32": "dtypes.T == \"f16\"",
721
- "usesF16": "dtypes.T == \"f16\""
722
- }
723
  },
724
- "bindings": "axis0SplitReduce",
725
- "dispatch": { "threads": "dim(shapes.data, 1)", "workgroupSize": "reduceWorkgroupSize", "y": "splitCount" }
 
 
 
 
726
  },
727
  {
728
  "id": "combine",
729
  "name": "ReduceMax.Axis0SplitKCombine",
730
- "source": {
731
- "shader": "reduce-axis0-splitk-combine.wgsl.jinja",
732
- "inputs": {
733
- "op": "\"max\"",
734
- "workgroupSize": "reduceWorkgroupSize",
735
- "split": "splitCount",
736
- "outputF16": "dtypes.T == \"f16\""
737
- }
738
- },
739
- "bindings": "axis0SplitCombine",
740
- "dispatch": { "threads": "dim(shapes.data, 1)", "workgroupSize": "reduceWorkgroupSize" }
741
  }
742
  ]
743
  },
744
  {
745
  "id": "axis0_tilecols",
746
  "priority": 20,
747
- "when": ["not flatParallelCovered", "emptyNarrowInputOk", "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"],
748
- "constants": {
749
- "scalar": "dtypes.T",
750
- "workgroupSize": "reduceWorkgroupSize",
751
- "tileCols": "tunables.AXIS0_TILE_COLS"
752
- },
753
  "passes": [
754
  {
755
  "id": "main",
756
  "name": "ReduceMax.Axis0TileCols",
757
- "source": {
758
- "shader": "reduce-axis0-tilecols.wgsl.jinja",
759
- "inputs": { "op": "\"max\"", "castF32": "dtypes.T == \"f16\"", "usesF16": "dtypes.T == \"f16\"" }
760
- },
761
- "bindings": "axis0Parallel",
762
- "dispatch": { "workgroups": "ceilDiv((dim(shapes.data, 1)), (constants.tileCols))" }
 
 
763
  }
764
  ]
765
  },
766
  {
767
  "id": "all_axes_flat",
768
  "priority": 31,
769
- "constants": { "scalar": "dtypes.T", "workgroupSize": "reduceWorkgroupSize", "split": "flatSplitCount" },
770
  "when": ["flatParallelCovered"],
 
771
  "intermediates": [{ "id": "partials", "dtype": "float32", "shape": "[flatSplitCount]" }],
772
  "passes": [
773
  {
774
  "id": "flat_partial",
775
  "name": "ReduceMax.AllAxesFlatPartial",
776
- "source": {
777
- "shader": "reduce-flat-partial.wgsl.jinja",
778
- "inputs": { "op": "\"max\"", "castF32": "dtypes.T == \"f16\"", "usesF16": "dtypes.T == \"f16\"" }
779
- },
780
- "bindings": "flatPartialF32",
781
  "dispatch": { "x": "flatSplitCount" }
782
  },
783
  {
784
  "id": "combine",
785
  "name": "ReduceMax.AllAxesFlatCombine",
786
- "source": {
787
- "shader": "reduce-axis0-splitk-combine.wgsl.jinja",
788
- "inputs": { "op": "\"max\"", "outputF16": "dtypes.T == \"f16\"" }
789
- },
790
- "bindings": "flatCombineF32",
791
  "dispatch": { "x": 1 }
792
  }
793
  ]
@@ -795,101 +610,110 @@
795
  {
796
  "id": "multi_axis_rank3",
797
  "priority": 8,
798
- "constants": { "scalar": "dtypes.T" },
799
- "when": ["not flatParallelCovered", "not contiguousSuffixParallelCovered", "emptyNarrowInputOk", "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))"],
800
  "passes": [
801
  {
802
  "id": "main",
803
  "name": "ReduceMax.MultiAxisRank3",
804
- "source": {
805
- "shader": "reduce-serial-axis.wgsl.jinja",
806
- "inputs": {
807
- "op": "\"max\"",
808
- "indexing": "\"multiaxis\"",
809
- "castF32": "dtypes.T == \"f16\"",
810
- "usesF16": "dtypes.T == \"f16\"",
811
- "rank": 3,
812
- "reduce": ["hasAxis(attrs.axes, 0, 3)", "hasAxis(attrs.axes, 1, 3)", "hasAxis(attrs.axes, 2, 3)"],
813
- "dataShape": "shapes.data",
814
- "outputShape": "shapes.reduced",
815
- "outputRank": "ranks.reduced",
816
- "keepDims": "attrs.keepdims != 0",
817
- "logicalBool": "tensorDtypes.data == \"bool\""
818
- }
819
  },
820
- "bindings": "multiAxis",
821
- "dispatch": { "threads": "numel(shapes.reduced)", "workgroupSize": "reduceWorkgroupSize" }
 
 
 
 
822
  }
823
  ]
824
  },
825
  {
826
  "id": "multi_axis_rank4",
827
  "priority": 8,
828
- "when": ["not flatParallelCovered", "not contiguousSuffixParallelCovered", "emptyNarrowInputOk", "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))"],
 
829
  "passes": [
830
  {
831
  "id": "main",
832
  "name": "ReduceMax.MultiAxisRank4",
833
- "source": {
834
- "shader": "reduce-serial-axis.wgsl.jinja",
835
- "inputs": {
836
- "op": "\"max\"",
837
- "indexing": "\"multiaxis\"",
838
- "castF32": "dtypes.T == \"f16\"",
839
- "usesF16": "dtypes.T == \"f16\"",
840
- "rank": 4,
841
- "reduce": ["hasAxis(attrs.axes, 0, 4)", "hasAxis(attrs.axes, 1, 4)", "hasAxis(attrs.axes, 2, 4)", "hasAxis(attrs.axes, 3, 4)"],
842
- "dataShape": "shapes.data",
843
- "outputShape": "shapes.reduced",
844
- "outputRank": "ranks.reduced",
845
- "keepDims": "attrs.keepdims != 0",
846
- "logicalBool": "tensorDtypes.data == \"bool\""
847
- }
848
  },
849
- "bindings": "multiAxis",
850
- "dispatch": { "threads": "numel(shapes.reduced)", "workgroupSize": "reduceWorkgroupSize" }
 
 
 
 
851
  }
852
- ],
853
- "constants": { "scalar": "dtypes.T" }
854
  },
855
  {
856
  "id": "rankn_single_axis_generic",
857
  "priority": 12,
 
858
  "supersededBy": ["axis_split_tiled_narrow", "axis_split", "subgroup_last_axis_vec4", "subgroup_last_axis", "tree_last_axis_vec4", "tree_last_axis"],
859
- "when": ["not flatParallelCovered", "emptyNarrowInputOk", "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))"],
860
  "passes": [
861
  {
862
  "id": "main",
863
  "name": "ReduceMax.RankNSingleAxisGeneric",
864
- "source": {
865
- "shader": "reduce-serial-axis.wgsl.jinja",
866
- "inputs": {
867
- "op": "\"max\"",
868
- "indexing": "\"rankn\"",
869
- "castF32": "dtypes.T == \"f16\"",
870
- "usesF16": "dtypes.T == \"f16\"",
871
- "rank": "ranks.data",
872
- "axis": "reduceAxis",
873
- "dataShape": "shapes.data",
874
- "outputShape": "shapes.reduced",
875
- "outputRank": "ranks.reduced",
876
- "keepDims": "attrs.keepdims != 0",
877
- "logicalBool": "tensorDtypes.data == \"bool\""
878
- }
879
  },
880
- "bindings": "rankNAxis",
881
- "dispatch": { "threads": "numel(shapes.reduced)", "workgroupSize": "reduceWorkgroupSize" }
 
 
 
 
882
  }
883
- ],
884
- "constants": { "scalar": "dtypes.T" }
885
  },
886
  {
887
  "id": "subgroup_last_axis_vec4",
888
  "priority": 25,
889
- "requires": { "features": ["subgroups"] },
890
  "demoteWhen": ["rowSerialPreferred"],
891
- "when": ["device.wgslLanguageFeatures.has(\"subgroup_id\")", "not flatParallelCovered", "emptyNarrowInputOk", "f16Ok(dtypes.T)", "ranks.data >= 1", "reduceAxis == ranks.data - 1", "numel(shapes.reduced) == rows(shapes.data, ranks.data - 1)", "dim(shapes.data, ranks.data - 1) >= 4", "dim(shapes.data, ranks.data - 1) % tunables.VECTOR_WIDTH == 0", "(lastAxisCols >= tunables.SUBGROUP_MIN_COLS or lastAxisRows < tunables.SUBGROUP_SMALL_ROW_LIMIT)"],
892
- "constants": {
893
  "scalar": "dtypes.T",
894
  "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
895
  "workgroupSize": "min(reduceWorkgroupSize, max(subgroupWorkgroupFloor, pow2ceil(ceilDiv(lastAxisCols, tunables.VECTOR_WIDTH))))"
@@ -898,28 +722,30 @@
898
  {
899
  "id": "main",
900
  "name": "ReduceMax.SubgroupRowVec4",
901
- "source": {
902
- "shader": "reduce-row-subgroup.wgsl.jinja",
903
- "inputs": {
904
- "op": "\"max\"",
905
- "vec4": true,
906
- "castF32": "dtypes.T == \"f16\"",
907
- "usesF16": "dtypes.T == \"f16\""
908
- }
 
 
 
 
909
  },
910
- "subgroupCollectivesWidth": "portable",
911
- "bindings": "lastAxisVec4",
912
- "dispatch": { "workgroups": "rows(shapes.data, ranks.data - 1)" }
913
  }
914
  ]
915
  },
916
  {
917
  "id": "subgroup_last_axis",
918
  "priority": 24,
919
- "requires": { "features": ["subgroups"] },
920
  "demoteWhen": ["rowSerialPreferred"],
921
- "when": ["device.wgslLanguageFeatures.has(\"subgroup_id\")", "not flatParallelCovered", "emptyNarrowInputOk", "f16Ok(dtypes.T)", "ranks.data >= 1", "reduceAxis == ranks.data - 1", "numel(shapes.reduced) == rows(shapes.data, ranks.data - 1)", "dim(shapes.data, ranks.data - 1) > 0", "dim(shapes.data, ranks.data - 1) % tunables.VECTOR_WIDTH != 0", "(lastAxisCols >= tunables.SUBGROUP_MIN_COLS or lastAxisRows < tunables.SUBGROUP_SMALL_ROW_LIMIT)"],
922
- "constants": {
923
  "scalar": "dtypes.T",
924
  "workgroupSize": "min(reduceWorkgroupSize, max(subgroupWorkgroupFloor, pow2ceil(lastAxisCols)))"
925
  },
@@ -927,114 +753,148 @@
927
  {
928
  "id": "main",
929
  "name": "ReduceMax.SubgroupRow",
930
- "source": {
931
- "shader": "reduce-row-subgroup.wgsl.jinja",
932
- "inputs": {
933
- "op": "\"max\"",
934
- "vec4": false,
935
- "castF32": "dtypes.T == \"f16\"",
936
- "usesF16": "dtypes.T == \"f16\""
937
- }
 
 
 
 
938
  },
939
- "subgroupCollectivesWidth": "portable",
940
- "bindings": "lastAxisScalarSubgroup",
941
- "dispatch": { "workgroups": "rows(shapes.data, ranks.data - 1)" }
942
  }
943
  ]
944
  },
945
  {
946
  "id": "axis0",
947
  "priority": 0,
 
948
  "supersededBy": ["axis_split_tiled_narrow", "axis0_splitk", "axis0_tilecols"],
949
- "constants": { "axis": 0, "scalar": "dtypes.T" },
950
- "when": ["not flatParallelCovered", "emptyNarrowInputOk", "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)))"],
951
  "passes": [
952
  {
953
  "id": "main",
954
  "name": "axis0",
955
- "source": {
956
- "shader": "reduce-serial-axis.wgsl.jinja",
957
- "inputs": {
958
- "op": "\"max\"",
959
- "indexing": "\"axis2d\"",
960
- "castF32": "dtypes.T == \"f16\"",
961
- "usesF16": "dtypes.T == \"f16\""
962
- }
963
  },
964
- "bindings": "rank2Serial",
965
- "constants": { "axis": 0, "scalar": "dtypes.T" },
966
- "dispatch": { "threads": "numel(shapes.reduced)", "workgroupSize": "reduceWorkgroupSize" }
 
 
 
967
  }
968
  ]
969
  },
970
  {
971
  "id": "axis1",
972
  "priority": 0,
 
973
  "supersededBy": ["subgroup_last_axis_vec4", "subgroup_last_axis", "tree_last_axis_vec4", "tree_last_axis"],
974
- "constants": { "axis": 1, "scalar": "dtypes.T" },
975
- "when": ["not flatParallelCovered", "emptyNarrowInputOk", "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))"],
976
  "passes": [
977
  {
978
  "id": "main",
979
  "name": "axis1",
980
- "source": {
981
- "shader": "reduce-serial-axis.wgsl.jinja",
982
- "inputs": {
983
- "op": "\"max\"",
984
- "indexing": "\"axis2d\"",
985
- "castF32": "dtypes.T == \"f16\"",
986
- "usesF16": "dtypes.T == \"f16\""
987
- }
988
  },
989
- "bindings": "rank2SerialAxis1",
990
- "constants": { "axis": 1, "scalar": "dtypes.T" },
991
- "dispatch": { "threads": "numel(shapes.reduced)", "workgroupSize": "reduceWorkgroupSize" }
 
 
 
992
  }
993
  ]
994
  },
995
  {
996
  "id": "all_axes_keepdims",
997
  "priority": 30,
998
- "constants": { "axis": 0, "scalar": "dtypes.T" },
999
- "when": ["not flatParallelCovered", "emptyNarrowInputOk", "f16Ok(dtypes.T)", "ranks.data >= 3", "attrs.keepdims == 1 and ranks.reduced == ranks.data and numel(shapes.reduced) == 1"],
1000
  "passes": [
1001
  {
1002
  "id": "main",
1003
  "name": "ReduceMax.Rank3AllAxesKeepdims",
1004
- "source": {
1005
- "shader": "reduce-serial-axis.wgsl.jinja",
1006
- "inputs": {
1007
- "op": "\"max\"",
1008
- "indexing": "\"axis2d\"",
1009
- "castF32": "dtypes.T == \"f16\"",
1010
- "usesF16": "dtypes.T == \"f16\""
1011
- }
1012
  },
1013
- "bindings": "fullReduceSerial",
1014
- "dispatch": { "threads": "numel(shapes.reduced)", "workgroupSize": "reduceWorkgroupSize" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1015
  }
1016
  ]
1017
  },
1018
  {
1019
  "id": "all_axes_no_keepdims",
1020
  "priority": 30,
1021
- "constants": { "axis": 0, "scalar": "dtypes.T" },
1022
- "when": ["not flatParallelCovered", "emptyNarrowInputOk", "f16Ok(dtypes.T)", "ranks.data >= 3", "attrs.keepdims == 0 and ranks.reduced == 0"],
1023
  "passes": [
1024
  {
1025
  "id": "main",
1026
  "name": "ReduceMax.Rank3AllAxesNoKeepdims",
1027
- "source": {
1028
- "shader": "reduce-serial-axis.wgsl.jinja",
1029
- "inputs": {
1030
- "op": "\"max\"",
1031
- "indexing": "\"axis2d\"",
1032
- "castF32": "dtypes.T == \"f16\"",
1033
- "usesF16": "dtypes.T == \"f16\""
1034
- }
1035
  },
1036
- "bindings": "fullReduceSerial",
1037
- "dispatch": { "threads": "numel(shapes.reduced)", "workgroupSize": "reduceWorkgroupSize" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1038
  }
1039
  ]
1040
  }
 
2
  "domain": "ai.onnx",
3
  "name": "ReduceMax",
4
  "sinceVersion": 20,
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", "uint32", "int8", "uint8", "bool"] },
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
  },
35
  "derive": {
36
+ "emptyNarrowInputOk": "numel(shapes.x) > 0 or (tensorDtypes.x != \"int8\" and tensorDtypes.x != \"uint8\")",
37
  "deviceWorkgroupCap": "min(device.limits.maxComputeInvocationsPerWorkgroup, device.limits.maxComputeWorkgroupSizeX)",
38
  "reduceWorkgroupSize": "min(tunables.WORKGROUP_SIZE, deviceWorkgroupCap)",
39
  "treeWorkgroupOk": "reduceWorkgroupSize > 0 and pow2ceil(reduceWorkgroupSize) == reduceWorkgroupSize and reduceWorkgroupSize * dtypeBytes(\"float32\") <= device.limits.maxComputeWorkgroupStorageSize",
40
  "subgroupWorkgroupFloor": "min(reduceWorkgroupSize, max(1, device.adapterInfo.subgroupMaxSize))",
41
+ "lastAxisRows": "rows(shapes.x, ranks.x - 1) if ranks.x > 0 else 1",
42
+ "lastAxisCols": "dim(shapes.x, ranks.x - 1) if ranks.x > 0 else 1",
43
  "rowSerialPreferred": "lastAxisRows >= tunables.ROW_SERIAL_MIN_ROWS and lastAxisCols <= tunables.ROW_SERIAL_MAX_COLS",
44
+ "axis0Rows": "dim(shapes.x, 0) if ranks.x >= 2 else 0",
45
+ "axis0Cols": "dim(shapes.x, 1) if ranks.x >= 2 else 0",
46
  "axis0SplitCount": "min(tunables.AXIS0_MAX_SPLITS, pow2ceil(ceilDiv(axis0Rows, tunables.AXIS0_SPLIT_TARGET_ROWS)))",
47
  "axis0SplitScratchBytes": "axis0SplitCount * axis0Cols * dtypeBytes(\"float32\")",
48
+ "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",
49
+ "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",
50
+ "axisSplitDim": "dim(shapes.x, reduceAxis) if ranks.x >= 2 and reduceAxis < ranks.x else 0",
51
+ "axisSplitInner": "inner(shapes.x, reduceAxis) if ranks.x >= 2 and reduceAxis < ranks.x else 1",
52
+ "axisSplitOutputs": "numel(shapes.y)",
53
  "axisSplitCount": "min(tunables.AXIS0_MAX_SPLITS, pow2ceil(ceilDiv(axisSplitDim, tunables.AXIS0_SPLIT_TARGET_ROWS)))",
54
  "axisSplitScratchBytes": "axisSplitCount * axisSplitOutputs * 4",
55
+ "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",
56
  "axis0TilePathFits": "treeWorkgroupOk and tunables.AXIS0_TILE_COLS > 0 and tunables.AXIS0_TILE_COLS <= reduceWorkgroupSize and reduceWorkgroupSize % tunables.AXIS0_TILE_COLS == 0",
57
+ "flatItems": "floor(numel(shapes.x) / tunables.VECTOR_WIDTH)",
58
  "flatSplitCount": "max(1, min(tunables.FULL_REDUCE_MAX_SPLITS, ceilDiv(flatItems, reduceWorkgroupSize)))",
59
  "flatScratchBytes": "flatSplitCount * dtypeBytes(\"float32\")",
60
+ "flatPathFits": "treeWorkgroupOk and flatSplitCount <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535) and flatScratchBytes <= device.limits.maxStorageBufferBindingSize and flatScratchBytes <= device.limits.maxBufferSize",
61
+ "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",
62
+ "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)))"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  },
64
+ "bindings": {
65
+ "params": { "buffer": "uniform", "struct": [{ "name": "count", "type": "u32", "value": "numel(shapes.y)" }] },
66
+ "x": { "buffer": "read-only-storage", "elementType": "$vectorScalar" },
67
+ "y_2": { "name": "y", "buffer": "storage", "elementType": "$T" },
68
+ "params_2": {
69
+ "name": "params",
70
+ "buffer": "uniform",
71
+ "struct": [
72
+ { "name": "rows", "type": "u32", "value": "numel(shapes.y)" },
73
+ { "name": "chunkCount", "type": "u32", "value": "numel(shapes.x) / numel(shapes.y) / tunables.VECTOR_WIDTH" }
74
+ ]
75
+ },
76
+ "x_2": { "name": "x", "buffer": "read-only-storage", "elementType": "$T" },
77
+ "params_3": {
78
+ "name": "params",
79
+ "buffer": "uniform",
80
+ "struct": [
81
+ { "name": "rows", "type": "u32", "value": "numel(shapes.y)" },
82
+ { "name": "cols", "type": "u32", "value": "numel(shapes.x) / numel(shapes.y)" }
83
+ ]
84
+ },
85
+ "params_4": {
86
+ "name": "params",
87
+ "buffer": "uniform",
88
+ "struct": [
89
+ { "name": "rows", "type": "u32", "value": "rows(shapes.x, ranks.x - 1)" },
90
+ { "name": "cols", "type": "u32", "value": "dim(shapes.x, ranks.x - 1)" }
91
+ ]
92
+ },
93
+ "params_5": {
94
+ "name": "params",
95
+ "buffer": "uniform",
96
+ "struct": [
97
+ { "name": "rows", "type": "u32", "value": "rows(shapes.x, ranks.x - 1)" },
98
+ { "name": "chunkCount", "type": "u32", "value": "dim(shapes.x, ranks.x - 1) / tunables.VECTOR_WIDTH" }
99
+ ]
100
+ },
101
+ "params_6": {
102
+ "name": "params",
103
+ "buffer": "uniform",
104
+ "struct": [
105
+ { "name": "rows", "type": "u32", "value": "1" },
106
+ { "name": "cols", "type": "u32", "value": "1" },
107
+ { "name": "outCount", "type": "u32", "value": "1" }
108
+ ]
109
+ },
110
+ "params_7": {
111
+ "name": "params",
112
+ "buffer": "uniform",
113
+ "struct": [
114
+ { "name": "rows", "type": "u32", "value": "dim(shapes.x, 0)" },
115
+ { "name": "cols", "type": "u32", "value": "1" },
116
+ { "name": "outCount", "type": "u32", "value": "numel(shapes.y)" }
117
+ ]
118
+ },
119
+ "partials": { "buffer": "storage", "elementType": "$partialElement" },
120
+ "params_8": {
121
+ "name": "params",
122
+ "buffer": "uniform",
123
+ "struct": [
124
+ { "name": "axisDim", "type": "u32", "value": "axisSplitDim" },
125
+ { "name": "inner", "type": "u32", "value": "axisSplitInner" },
126
+ { "name": "outputs", "type": "u32", "value": "axisSplitOutputs" }
127
+ ]
128
+ },
129
+ "partials_2": { "name": "partials", "buffer": "read-only-storage", "elementType": "$partialElement" },
130
+ "params_9": {
131
+ "name": "params",
132
+ "buffer": "uniform",
133
+ "struct": [{ "name": "cols", "type": "u32", "value": "axisSplitOutputs" }]
134
+ },
135
+ "params_10": {
136
+ "name": "params",
137
+ "buffer": "uniform",
138
+ "struct": [
139
+ { "name": "rows", "type": "u32", "value": "dim(shapes.x, 0)" },
140
+ { "name": "cols", "type": "u32", "value": "dim(shapes.x, 1)" }
141
+ ]
142
+ },
143
+ "params_11": {
144
+ "name": "params",
145
+ "buffer": "uniform",
146
+ "struct": [{ "name": "cols", "type": "u32", "value": "dim(shapes.x, 1)" }]
147
+ },
148
+ "partials_3": { "name": "partials", "buffer": "storage", "elementType": "f32" },
149
+ "params_12": {
150
+ "name": "params",
151
+ "buffer": "uniform",
152
+ "struct": [
153
+ { "name": "count4", "type": "u32", "value": "floor(numel(shapes.x) / tunables.VECTOR_WIDTH)" },
154
+ { "name": "numel", "type": "u32", "value": "numel(shapes.x)" }
155
+ ]
156
+ },
157
+ "partials_4": { "name": "partials", "buffer": "read-only-storage", "elementType": "f32" },
158
+ "params_13": { "name": "params", "buffer": "uniform", "struct": [{ "name": "cols", "type": "u32", "value": "1" }] },
159
+ "params_14": {
160
+ "name": "params",
161
+ "buffer": "uniform",
162
+ "struct": [{ "name": "outCount", "type": "u32", "value": "numel(shapes.y)" }]
163
+ },
164
+ "params_15": {
165
+ "name": "params",
166
+ "buffer": "uniform",
167
+ "struct": [
168
+ { "name": "axisDim", "type": "u32", "value": "axisSplitDim" },
169
+ { "name": "outCount", "type": "u32", "value": "numel(shapes.y)" }
170
+ ]
171
+ },
172
+ "params_16": {
173
+ "name": "params",
174
+ "buffer": "uniform",
175
+ "struct": [
176
+ { "name": "rows", "type": "u32", "value": "rows(shapes.x, ranks.x - 1)" },
177
+ { "name": "chunkCount", "type": "u32", "value": "dim(shapes.x, ranks.x - 1)" }
178
+ ]
179
+ },
180
+ "params_17": {
181
+ "name": "params",
182
+ "buffer": "uniform",
183
+ "struct": [
184
+ { "name": "rows", "type": "u32", "value": "dim(shapes.x, 0)" },
185
+ { "name": "cols", "type": "u32", "value": "dim(shapes.x, 1)" },
186
+ { "name": "outCount", "type": "u32", "value": "numel(shapes.y)" }
187
+ ]
188
+ },
189
+ "params_18": {
190
+ "name": "params",
191
+ "buffer": "uniform",
192
+ "struct": [
193
+ { "name": "cols", "type": "u32", "value": "dim(shapes.x, 1)" },
194
+ { "name": "outCount", "type": "u32", "value": "numel(shapes.y)" }
195
+ ]
196
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  },
198
  "variants": [
199
  {
200
  "id": "narrow_empty_identity",
201
  "priority": 100,
202
+ "when": ["numel(shapes.x) == 0", "numel(shapes.y) > 0", "tensorDtypes.x == \"int8\" or tensorDtypes.x == \"uint8\" or tensorDtypes.x == \"bool\""],
203
+ "derive": { "scalar": "dtypes.T" },
204
  "passes": [
205
  {
206
  "id": "main",
207
  "name": "ReduceMax.NarrowEmptyIdentity",
208
+ "shader": "reduce-narrow-empty-identity.wgsl.jinja",
209
+ "derive": { "identity": "-128 if tensorDtypes.x == \"int8\" else 0" },
210
+ "bindings": [{ "arg": "y", "elementType": "$scalar" }, "params"],
211
+ "dispatch": {
212
+ "x": "min(ceilDiv((numel(shapes.y)), (tunables.WORKGROUP_SIZE)), min(device.limits.maxComputeWorkgroupsPerDimension, 65535))",
213
+ "y": 1,
214
+ "z": 1
215
+ }
216
  }
217
  ]
218
  },
219
  {
220
  "id": "contiguous_suffix_subgroup_vec4",
221
  "priority": 30,
222
+ "when": ["device.wgslLanguageFeatures.has(\"subgroup_id\")", "not flatParallelCovered", "contiguousSuffixParallelCovered", "(numel(shapes.x) / numel(shapes.y)) % tunables.VECTOR_WIDTH == 0"],
223
  "requires": { "features": ["subgroups"] },
224
+ "derive": {
 
225
  "scalar": "dtypes.T",
226
  "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
227
+ "workgroupSize": "min(reduceWorkgroupSize, max(subgroupWorkgroupFloor, pow2ceil(ceilDiv(numel(shapes.x) / numel(shapes.y), tunables.VECTOR_WIDTH))))"
228
  },
229
  "passes": [
230
  {
231
  "id": "main",
232
  "name": "ReduceMax.ContiguousSuffixSubgroupVec4",
233
+ "shader": "reduce-row-subgroup.wgsl.jinja",
234
+ "derive": {
235
+ "op": "\"max\"",
236
+ "vec4": true,
237
+ "castF32": "dtypes.T == \"f16\"",
238
+ "usesF16Spec": "dtypes.T == \"f16\""
 
 
239
  },
240
+ "bindings": ["x", "y_2", "params_2"],
241
+ "dispatch": { "x": "min(numel(shapes.y), 65535)", "y": "ceilDiv(numel(shapes.y), 65535)", "z": 1 },
242
+ "subgroupCollectivesWidth": "portable"
243
  }
244
  ]
245
  },
246
  {
247
  "id": "contiguous_suffix_tree_vec4",
248
  "priority": 22,
249
+ "when": ["not flatParallelCovered", "contiguousSuffixParallelCovered", "(numel(shapes.x) / numel(shapes.y)) % tunables.VECTOR_WIDTH == 0", "treeWorkgroupOk"],
250
+ "derive": {
251
  "scalar": "dtypes.T",
252
  "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
253
+ "workgroupSize": "min(reduceWorkgroupSize, pow2ceil(ceilDiv(numel(shapes.x) / numel(shapes.y), tunables.VECTOR_WIDTH)))"
254
  },
255
  "passes": [
256
  {
257
  "id": "main",
258
  "name": "ReduceMax.ContiguousSuffixTreeVec4",
259
+ "shader": "reduce-row-tree.wgsl.jinja",
260
+ "derive": {
261
+ "op": "\"max\"",
262
+ "vec4": true,
263
+ "castF32": "dtypes.T == \"f16\"",
264
+ "usesF16Spec": "dtypes.T == \"f16\""
 
 
265
  },
266
+ "bindings": ["x", "y_2", "params_2"],
267
+ "dispatch": { "x": "min(numel(shapes.y), 65535)", "y": "ceilDiv(numel(shapes.y), 65535)", "z": 1 }
268
  }
269
  ]
270
  },
 
272
  "id": "contiguous_suffix_tree",
273
  "priority": 21,
274
  "when": ["not flatParallelCovered", "contiguousSuffixParallelCovered", "treeWorkgroupOk"],
275
+ "derive": {
276
+ "workgroupSize": "min(reduceWorkgroupSize, pow2ceil(numel(shapes.x) / numel(shapes.y)))",
277
  "scalar": "dtypes.T"
278
  },
279
  "passes": [
280
  {
281
  "id": "main",
282
  "name": "ReduceMax.ContiguousSuffixTree",
283
+ "shader": "reduce-row-tree.wgsl.jinja",
284
+ "derive": { "op": "\"max\"", "castF32": "dtypes.T == \"f16\"", "usesF16Spec": "dtypes.T == \"f16\"" },
285
+ "bindings": ["x_2", "y_2", "params_3"],
286
+ "dispatch": { "x": "min(numel(shapes.y), 65535)", "y": "ceilDiv(numel(shapes.y), 65535)", "z": 1 }
 
 
287
  }
288
  ]
289
  },
290
  {
291
  "id": "noop_empty_axes",
292
  "priority": 40,
293
+ "when": ["dtypes.T == \"f32\"", "attrs.noop_with_empty_axes == 1", "(attrs.axes | length) == 0", "sameShape(shapes.x, shapes.y)"],
294
+ "derive": { "reduceWorkgroupSize": "reduceWorkgroupSize" },
295
  "passes": [
296
  {
297
  "id": "main",
298
  "name": "ReduceMax.NoopEmptyAxes",
299
+ "shader": "reduce-noop-empty-axes.wgsl.jinja",
300
+ "bindings": ["x_2", "y_2", "params"],
301
+ "dispatch": {
302
+ "x": "min(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
303
+ "y": "ceilDiv(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
304
+ "z": 1
305
+ }
306
  }
307
  ]
308
  },
309
  {
310
  "id": "tree_last_axis",
311
  "priority": 20,
312
+ "when": ["not flatParallelCovered", "(dtypes.T == \"f32\" or dtypes.T == \"i32\" or dtypes.T == \"u32\")", "ranks.x >= 2", "reduceAxis == ranks.x - 1", "numel(shapes.y) == rows(shapes.x, ranks.x - 1)", "lastAxisCols >= tunables.ROW_PARALLEL_MIN_COLS", "treeWorkgroupOk"],
313
  "demoteWhen": ["rowSerialPreferred"],
314
+ "derive": {
 
315
  "scalar": "dtypes.T",
316
+ "workgroupSize": "min(reduceWorkgroupSize, pow2ceil(dim(shapes.x, ranks.x - 1)))"
317
  },
318
  "passes": [
319
  {
320
  "id": "main",
321
  "name": "ReduceMax.TreeLastAxis",
322
+ "shader": "reduce-row-tree.wgsl.jinja",
323
+ "derive": { "op": "\"max\"" },
324
+ "bindings": ["x_2", "y_2", "params_4"],
325
+ "dispatch": {
326
+ "x": "min(rows(shapes.x, ranks.x - 1), 65535)",
327
+ "y": "ceilDiv(rows(shapes.x, ranks.x - 1), 65535)",
328
+ "z": 1
329
+ }
330
+ }
331
+ ]
332
+ },
333
+ {
334
+ "id": "subgroup_rows_last_axis_vec4",
335
+ "priority": 26,
336
+ "when": ["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", "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)"],
337
+ "requires": { "features": ["subgroups"] },
338
+ "derive": {
339
+ "scalar": "dtypes.T",
340
+ "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
341
+ "workgroupSize": "reduceWorkgroupSize",
342
+ "vecsPerLane": "ceilDiv(lastAxisCols / tunables.VECTOR_WIDTH, device.adapterInfo.subgroupMinSize)"
343
+ },
344
+ "passes": [
345
+ {
346
+ "id": "main",
347
+ "name": "ReduceMax.SubgroupRowsVec4",
348
+ "shader": "reduce-row-subgroup-rows.wgsl.jinja",
349
+ "derive": { "op": "\"max\"", "castF32": "dtypes.T == \"f16\"", "usesF16Spec": "dtypes.T == \"f16\"" },
350
+ "bindings": ["x", "y_2", "params_5"],
351
+ "dispatch": {
352
+ "x": "min(ceilDiv(lastAxisRows, reduceWorkgroupSize / device.adapterInfo.subgroupMaxSize), 65535)",
353
+ "y": "ceilDiv(ceilDiv(lastAxisRows, reduceWorkgroupSize / device.adapterInfo.subgroupMaxSize), 65535)",
354
+ "z": 1
355
+ },
356
+ "subgroupCollectivesWidth": "portable"
357
  }
358
  ]
359
  },
360
  {
361
  "id": "tree_last_axis_vec4",
362
  "priority": 23,
363
+ "when": ["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", "lastAxisCols >= tunables.ROW_PARALLEL_MIN_COLS", "lastAxisCols % tunables.VECTOR_WIDTH == 0", "treeWorkgroupOk"],
364
  "demoteWhen": ["rowSerialPreferred"],
365
+ "derive": {
 
366
  "scalar": "dtypes.T",
367
  "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
368
  "workgroupSize": "min(reduceWorkgroupSize, pow2ceil(ceilDiv(lastAxisCols, tunables.VECTOR_WIDTH)))"
 
371
  {
372
  "id": "main",
373
  "name": "ReduceMax.TreeRowVec4",
374
+ "shader": "reduce-row-tree.wgsl.jinja",
375
+ "derive": {
376
+ "op": "\"max\"",
377
+ "vec4": true,
378
+ "castF32": "dtypes.T == \"f16\"",
379
+ "usesF16Spec": "dtypes.T == \"f16\""
 
 
380
  },
381
+ "bindings": ["x", "y_2", "params_5"],
382
+ "dispatch": { "x": "min(lastAxisRows, 65535)", "y": "ceilDiv(lastAxisRows, 65535)", "z": 1 }
383
  }
384
  ]
385
  },
386
  {
387
  "id": "rank0_scalar",
388
  "priority": 40,
389
+ "when": ["emptyNarrowInputOk", "f16Ok(dtypes.T)", "ranks.x == 0", "ranks.y == 0"],
390
+ "derive": { "axis": 0, "scalar": "dtypes.T", "reduceWorkgroupSize": "reduceWorkgroupSize" },
391
  "passes": [
392
  {
393
  "id": "main",
394
  "name": "ReduceMax.Rank0Scalar",
395
+ "shader": "reduce-serial-axis.wgsl.jinja",
396
+ "derive": {
397
+ "op": "\"max\"",
398
+ "indexing": "\"axis2d\"",
399
+ "castF32": "dtypes.T == \"f16\"",
400
+ "usesF16Spec": "dtypes.T == \"f16\"",
401
+ "logicalBool": "tensorDtypes.x == \"bool\""
 
 
402
  },
403
+ "bindings": ["x_2", "y_2", "params_6"],
404
  "dispatch": { "x": 1 }
405
  }
406
  ]
407
  },
408
  {
409
  "id": "rank1_axis0",
410
+ "when": ["not flatParallelCovered", "emptyNarrowInputOk", "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))"],
411
+ "derive": { "axis": 0, "scalar": "dtypes.T", "reduceWorkgroupSize": "reduceWorkgroupSize" },
412
  "passes": [
413
  {
414
  "id": "main",
415
  "name": "ReduceMax.Rank1Axis0",
416
+ "shader": "reduce-serial-axis.wgsl.jinja",
417
+ "derive": {
418
+ "op": "\"max\"",
419
+ "indexing": "\"axis2d\"",
420
+ "castF32": "dtypes.T == \"f16\"",
421
+ "usesF16Spec": "dtypes.T == \"f16\"",
422
+ "logicalBool": "tensorDtypes.x == \"bool\""
 
 
423
  },
424
+ "bindings": ["x_2", "y_2", "params_7"],
425
+ "dispatch": {
426
+ "x": "min(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
427
+ "y": "ceilDiv(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
428
+ "z": 1
429
+ }
430
  }
431
  ]
432
  },
433
  {
434
  "id": "axis_split",
435
  "priority": 24,
436
+ "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"],
437
+ "derive": {
438
+ "splitCount": "axisSplitCount",
439
+ "partialElement": "\"f32\"",
440
+ "workgroupSize": "reduceWorkgroupSize",
441
+ "split": "splitCount"
442
+ },
443
  "intermediates": [{ "id": "partials", "dtype": "float32", "shape": "[splitCount * axisSplitOutputs]" }],
444
  "passes": [
445
  {
446
  "id": "split_reduce",
447
  "name": "ReduceMax.AxisSplitReduce",
448
+ "shader": "reduce-axis-split-reduce.wgsl.jinja",
449
+ "derive": {
450
+ "op": "\"max\"",
451
+ "splitSpec": "splitCount",
452
+ "castF32": "dtypes.T == \"f16\"",
453
+ "usesF16Spec": "dtypes.T == \"f16\""
 
 
 
454
  },
455
+ "bindings": ["x_2", "partials", "params_8"],
456
+ "dispatch": {
457
+ "x": "min(ceilDiv((axisSplitOutputs), (reduceWorkgroupSize)), DISPATCH_FOLD_WIDTH)",
458
+ "y": "splitCount",
459
+ "z": "ceilDiv(ceilDiv((axisSplitOutputs), (reduceWorkgroupSize)), DISPATCH_FOLD_WIDTH)"
460
+ }
461
  },
462
  {
463
  "id": "combine",
464
  "name": "ReduceMax.AxisSplitCombine",
465
+ "shader": "reduce-axis0-splitk-combine.wgsl.jinja",
466
+ "derive": { "op": "\"max\"", "splitSpec": "splitCount", "outputF16": "dtypes.T == \"f16\"" },
467
+ "bindings": ["partials_2", "y_2", "params_9"],
468
+ "dispatch": {
469
+ "x": "min(ceilDiv((axisSplitOutputs), (reduceWorkgroupSize)), 65535)",
470
+ "y": "ceilDiv(ceilDiv((axisSplitOutputs), (reduceWorkgroupSize)), 65535)",
471
+ "z": 1
472
+ }
 
 
 
473
  }
474
  ]
475
  },
476
  {
477
  "id": "axis_split_tiled_narrow",
478
  "priority": 25,
479
+ "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"],
480
+ "derive": {
481
+ "splitCount": "axisSplitCount",
482
  "partialElement": "\"f32\"",
483
  "scalar": "dtypes.T",
484
  "workgroupSize": "reduceWorkgroupSize",
 
490
  {
491
  "id": "split_reduce",
492
  "name": "ReduceMax.AxisSplitTiledReduce",
493
+ "shader": "reduce-axis0-tilecols.wgsl.jinja",
494
+ "derive": {
495
+ "op": "\"max\"",
496
+ "splitSpec": "splitCount",
497
+ "tileCols": "tunables.AXIS_SPLIT_TILE_COLS",
498
+ "castF32": "dtypes.T == \"f16\"",
499
+ "usesF16Spec": "dtypes.T == \"f16\""
 
 
 
500
  },
501
+ "bindings": ["x_2", "partials", "params_8"],
502
+ "dispatch": {
503
+ "x": "min(ceilDiv((axisSplitOutputs), (tileCols)), DISPATCH_FOLD_WIDTH)",
504
+ "y": "splitCount",
505
+ "z": "ceilDiv(ceilDiv((axisSplitOutputs), (tileCols)), DISPATCH_FOLD_WIDTH)"
506
+ }
507
  },
508
  {
509
  "id": "combine",
510
  "name": "ReduceMax.AxisSplitCombine",
511
+ "shader": "reduce-axis0-splitk-combine.wgsl.jinja",
512
+ "derive": { "op": "\"max\"", "splitSpec": "splitCount", "outputF16": "dtypes.T == \"f16\"" },
513
+ "bindings": ["partials_2", "y_2", "params_9"],
514
+ "dispatch": {
515
+ "x": "min(ceilDiv((axisSplitOutputs), (reduceWorkgroupSize)), 65535)",
516
+ "y": "ceilDiv(ceilDiv((axisSplitOutputs), (reduceWorkgroupSize)), 65535)",
517
+ "z": 1
518
+ }
 
 
 
519
  }
520
  ]
521
  },
522
  {
523
  "id": "axis0_splitk",
524
  "priority": 22,
525
+ "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"],
526
+ "derive": {
527
+ "splitCount": "axis0SplitCount",
528
+ "partialElement": "\"f32\"",
529
+ "workgroupSize": "reduceWorkgroupSize",
530
+ "split": "splitCount"
531
+ },
532
+ "intermediates": [{ "id": "partials", "dtype": "float32", "shape": "[splitCount * dim(shapes.x, 1)]" }],
533
  "passes": [
534
  {
535
  "id": "split_reduce",
536
  "name": "ReduceMax.Axis0SplitKReduce",
537
+ "shader": "reduce-axis0-splitk-reduce.wgsl.jinja",
538
+ "derive": {
539
+ "op": "\"max\"",
540
+ "splitSpec": "splitCount",
541
+ "castF32": "dtypes.T == \"f16\"",
542
+ "usesF16Spec": "dtypes.T == \"f16\""
 
 
 
543
  },
544
+ "bindings": ["x_2", "partials", "params_10"],
545
+ "dispatch": {
546
+ "x": "min(ceilDiv((dim(shapes.x, 1)), (reduceWorkgroupSize)), DISPATCH_FOLD_WIDTH)",
547
+ "y": "splitCount",
548
+ "z": "ceilDiv(ceilDiv((dim(shapes.x, 1)), (reduceWorkgroupSize)), DISPATCH_FOLD_WIDTH)"
549
+ }
550
  },
551
  {
552
  "id": "combine",
553
  "name": "ReduceMax.Axis0SplitKCombine",
554
+ "shader": "reduce-axis0-splitk-combine.wgsl.jinja",
555
+ "derive": { "op": "\"max\"", "splitSpec": "splitCount", "outputF16": "dtypes.T == \"f16\"" },
556
+ "bindings": ["partials_2", "y_2", "params_11"],
557
+ "dispatch": {
558
+ "x": "min(ceilDiv((dim(shapes.x, 1)), (reduceWorkgroupSize)), 65535)",
559
+ "y": "ceilDiv(ceilDiv((dim(shapes.x, 1)), (reduceWorkgroupSize)), 65535)",
560
+ "z": 1
561
+ }
 
 
 
562
  }
563
  ]
564
  },
565
  {
566
  "id": "axis0_tilecols",
567
  "priority": 20,
568
+ "when": ["not flatParallelCovered", "emptyNarrowInputOk", "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"],
569
+ "derive": { "scalar": "dtypes.T", "workgroupSize": "reduceWorkgroupSize", "tileCols": "tunables.AXIS0_TILE_COLS" },
 
 
 
 
570
  "passes": [
571
  {
572
  "id": "main",
573
  "name": "ReduceMax.Axis0TileCols",
574
+ "shader": "reduce-axis0-tilecols.wgsl.jinja",
575
+ "derive": { "op": "\"max\"", "castF32": "dtypes.T == \"f16\"", "usesF16Spec": "dtypes.T == \"f16\"" },
576
+ "bindings": ["x_2", "y_2", "params_10"],
577
+ "dispatch": {
578
+ "x": "min(ceilDiv((dim(shapes.x, 1)), (tileCols)), 65535)",
579
+ "y": "ceilDiv(ceilDiv((dim(shapes.x, 1)), (tileCols)), 65535)",
580
+ "z": 1
581
+ }
582
  }
583
  ]
584
  },
585
  {
586
  "id": "all_axes_flat",
587
  "priority": 31,
 
588
  "when": ["flatParallelCovered"],
589
+ "derive": { "scalar": "dtypes.T", "workgroupSize": "reduceWorkgroupSize", "split": "flatSplitCount" },
590
  "intermediates": [{ "id": "partials", "dtype": "float32", "shape": "[flatSplitCount]" }],
591
  "passes": [
592
  {
593
  "id": "flat_partial",
594
  "name": "ReduceMax.AllAxesFlatPartial",
595
+ "shader": "reduce-flat-partial.wgsl.jinja",
596
+ "derive": { "op": "\"max\"", "castF32": "dtypes.T == \"f16\"", "usesF16Spec": "dtypes.T == \"f16\"" },
597
+ "bindings": ["x_2", "partials_3", "params_12"],
 
 
598
  "dispatch": { "x": "flatSplitCount" }
599
  },
600
  {
601
  "id": "combine",
602
  "name": "ReduceMax.AllAxesFlatCombine",
603
+ "shader": "reduce-axis0-splitk-combine.wgsl.jinja",
604
+ "derive": { "op": "\"max\"", "outputF16": "dtypes.T == \"f16\"" },
605
+ "bindings": ["partials_4", "y_2", "params_13"],
 
 
606
  "dispatch": { "x": 1 }
607
  }
608
  ]
 
610
  {
611
  "id": "multi_axis_rank3",
612
  "priority": 8,
613
+ "when": ["not flatParallelCovered", "not contiguousSuffixParallelCovered", "emptyNarrowInputOk", "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))"],
614
+ "derive": { "scalar": "dtypes.T" },
615
  "passes": [
616
  {
617
  "id": "main",
618
  "name": "ReduceMax.MultiAxisRank3",
619
+ "shader": "reduce-serial-axis.wgsl.jinja",
620
+ "derive": {
621
+ "reduceWorkgroupSize": "reduceWorkgroupSize",
622
+ "op": "\"max\"",
623
+ "indexing": "\"multiaxis\"",
624
+ "castF32": "dtypes.T == \"f16\"",
625
+ "usesF16Spec": "dtypes.T == \"f16\"",
626
+ "rank": 3,
627
+ "reduce": ["hasAxis(attrs.axes, 0, 3)", "hasAxis(attrs.axes, 1, 3)", "hasAxis(attrs.axes, 2, 3)"],
628
+ "dataShape": "shapes.x",
629
+ "outputShape": "shapes.y",
630
+ "outputRank": "ranks.y",
631
+ "keepDims": "attrs.keepdims != 0",
632
+ "logicalBool": "tensorDtypes.x == \"bool\""
 
633
  },
634
+ "bindings": ["x_2", "y_2", "params_14"],
635
+ "dispatch": {
636
+ "x": "min(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
637
+ "y": "ceilDiv(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
638
+ "z": 1
639
+ }
640
  }
641
  ]
642
  },
643
  {
644
  "id": "multi_axis_rank4",
645
  "priority": 8,
646
+ "when": ["not flatParallelCovered", "not contiguousSuffixParallelCovered", "emptyNarrowInputOk", "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))"],
647
+ "derive": { "scalar": "dtypes.T" },
648
  "passes": [
649
  {
650
  "id": "main",
651
  "name": "ReduceMax.MultiAxisRank4",
652
+ "shader": "reduce-serial-axis.wgsl.jinja",
653
+ "derive": {
654
+ "reduceWorkgroupSize": "reduceWorkgroupSize",
655
+ "op": "\"max\"",
656
+ "indexing": "\"multiaxis\"",
657
+ "castF32": "dtypes.T == \"f16\"",
658
+ "usesF16Spec": "dtypes.T == \"f16\"",
659
+ "rank": 4,
660
+ "reduce": ["hasAxis(attrs.axes, 0, 4)", "hasAxis(attrs.axes, 1, 4)", "hasAxis(attrs.axes, 2, 4)", "hasAxis(attrs.axes, 3, 4)"],
661
+ "dataShape": "shapes.x",
662
+ "outputShape": "shapes.y",
663
+ "outputRank": "ranks.y",
664
+ "keepDims": "attrs.keepdims != 0",
665
+ "logicalBool": "tensorDtypes.x == \"bool\""
 
666
  },
667
+ "bindings": ["x_2", "y_2", "params_14"],
668
+ "dispatch": {
669
+ "x": "min(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
670
+ "y": "ceilDiv(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
671
+ "z": 1
672
+ }
673
  }
674
+ ]
 
675
  },
676
  {
677
  "id": "rankn_single_axis_generic",
678
  "priority": 12,
679
+ "when": ["not flatParallelCovered", "emptyNarrowInputOk", "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))"],
680
  "supersededBy": ["axis_split_tiled_narrow", "axis_split", "subgroup_last_axis_vec4", "subgroup_last_axis", "tree_last_axis_vec4", "tree_last_axis"],
681
+ "derive": { "scalar": "dtypes.T" },
682
  "passes": [
683
  {
684
  "id": "main",
685
  "name": "ReduceMax.RankNSingleAxisGeneric",
686
+ "shader": "reduce-serial-axis.wgsl.jinja",
687
+ "derive": {
688
+ "reduceWorkgroupSize": "reduceWorkgroupSize",
689
+ "op": "\"max\"",
690
+ "indexing": "\"rankn\"",
691
+ "castF32": "dtypes.T == \"f16\"",
692
+ "usesF16Spec": "dtypes.T == \"f16\"",
693
+ "rank": "ranks.x",
694
+ "axisSpec": "reduceAxis",
695
+ "dataShape": "shapes.x",
696
+ "outputShape": "shapes.y",
697
+ "outputRank": "ranks.y",
698
+ "keepDims": "attrs.keepdims != 0",
699
+ "logicalBool": "tensorDtypes.x == \"bool\""
 
700
  },
701
+ "bindings": ["x_2", "y_2", "params_15"],
702
+ "dispatch": {
703
+ "x": "min(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
704
+ "y": "ceilDiv(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
705
+ "z": 1
706
+ }
707
  }
708
+ ]
 
709
  },
710
  {
711
  "id": "subgroup_last_axis_vec4",
712
  "priority": 25,
713
+ "when": ["device.wgslLanguageFeatures.has(\"subgroup_id\")", "not flatParallelCovered", "emptyNarrowInputOk", "f16Ok(dtypes.T)", "ranks.x >= 1", "reduceAxis == ranks.x - 1", "numel(shapes.y) == rows(shapes.x, ranks.x - 1)", "dim(shapes.x, ranks.x - 1) >= 4", "dim(shapes.x, ranks.x - 1) % tunables.VECTOR_WIDTH == 0", "(lastAxisCols >= tunables.SUBGROUP_MIN_COLS or lastAxisRows < tunables.SUBGROUP_SMALL_ROW_LIMIT)"],
714
  "demoteWhen": ["rowSerialPreferred"],
715
+ "requires": { "features": ["subgroups"] },
716
+ "derive": {
717
  "scalar": "dtypes.T",
718
  "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
719
  "workgroupSize": "min(reduceWorkgroupSize, max(subgroupWorkgroupFloor, pow2ceil(ceilDiv(lastAxisCols, tunables.VECTOR_WIDTH))))"
 
722
  {
723
  "id": "main",
724
  "name": "ReduceMax.SubgroupRowVec4",
725
+ "shader": "reduce-row-subgroup.wgsl.jinja",
726
+ "derive": {
727
+ "op": "\"max\"",
728
+ "vec4": true,
729
+ "castF32": "dtypes.T == \"f16\"",
730
+ "usesF16Spec": "dtypes.T == \"f16\""
731
+ },
732
+ "bindings": ["x", "y_2", "params_5"],
733
+ "dispatch": {
734
+ "x": "min(rows(shapes.x, ranks.x - 1), 65535)",
735
+ "y": "ceilDiv(rows(shapes.x, ranks.x - 1), 65535)",
736
+ "z": 1
737
  },
738
+ "subgroupCollectivesWidth": "portable"
 
 
739
  }
740
  ]
741
  },
742
  {
743
  "id": "subgroup_last_axis",
744
  "priority": 24,
745
+ "when": ["device.wgslLanguageFeatures.has(\"subgroup_id\")", "not flatParallelCovered", "emptyNarrowInputOk", "f16Ok(dtypes.T)", "ranks.x >= 1", "reduceAxis == ranks.x - 1", "numel(shapes.y) == rows(shapes.x, ranks.x - 1)", "dim(shapes.x, ranks.x - 1) > 0", "dim(shapes.x, ranks.x - 1) % tunables.VECTOR_WIDTH != 0", "(lastAxisCols >= tunables.SUBGROUP_MIN_COLS or lastAxisRows < tunables.SUBGROUP_SMALL_ROW_LIMIT)"],
746
  "demoteWhen": ["rowSerialPreferred"],
747
+ "requires": { "features": ["subgroups"] },
748
+ "derive": {
749
  "scalar": "dtypes.T",
750
  "workgroupSize": "min(reduceWorkgroupSize, max(subgroupWorkgroupFloor, pow2ceil(lastAxisCols)))"
751
  },
 
753
  {
754
  "id": "main",
755
  "name": "ReduceMax.SubgroupRow",
756
+ "shader": "reduce-row-subgroup.wgsl.jinja",
757
+ "derive": {
758
+ "op": "\"max\"",
759
+ "vec4": false,
760
+ "castF32": "dtypes.T == \"f16\"",
761
+ "usesF16Spec": "dtypes.T == \"f16\""
762
+ },
763
+ "bindings": ["x_2", "y_2", "params_16"],
764
+ "dispatch": {
765
+ "x": "min(rows(shapes.x, ranks.x - 1), 65535)",
766
+ "y": "ceilDiv(rows(shapes.x, ranks.x - 1), 65535)",
767
+ "z": 1
768
  },
769
+ "subgroupCollectivesWidth": "portable"
 
 
770
  }
771
  ]
772
  },
773
  {
774
  "id": "axis0",
775
  "priority": 0,
776
+ "when": ["not flatParallelCovered", "emptyNarrowInputOk", "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)))"],
777
  "supersededBy": ["axis_split_tiled_narrow", "axis0_splitk", "axis0_tilecols"],
778
+ "derive": { "axis": 0, "scalar": "dtypes.T" },
 
779
  "passes": [
780
  {
781
  "id": "main",
782
  "name": "axis0",
783
+ "shader": "reduce-serial-axis.wgsl.jinja",
784
+ "derive": {
785
+ "axis": 0,
786
+ "scalar": "dtypes.T",
787
+ "op": "\"max\"",
788
+ "indexing": "\"axis2d\"",
789
+ "castF32": "dtypes.T == \"f16\"",
790
+ "usesF16Spec": "dtypes.T == \"f16\""
791
  },
792
+ "bindings": ["x_2", "y_2", "params_17"],
793
+ "dispatch": {
794
+ "x": "min(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
795
+ "y": "ceilDiv(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
796
+ "z": 1
797
+ }
798
  }
799
  ]
800
  },
801
  {
802
  "id": "axis1",
803
  "priority": 0,
804
+ "when": ["not flatParallelCovered", "emptyNarrowInputOk", "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))"],
805
  "supersededBy": ["subgroup_last_axis_vec4", "subgroup_last_axis", "tree_last_axis_vec4", "tree_last_axis"],
806
+ "derive": { "axis": 1, "scalar": "dtypes.T" },
 
807
  "passes": [
808
  {
809
  "id": "main",
810
  "name": "axis1",
811
+ "shader": "reduce-serial-axis.wgsl.jinja",
812
+ "derive": {
813
+ "axis": 1,
814
+ "scalar": "dtypes.T",
815
+ "op": "\"max\"",
816
+ "indexing": "\"axis2d\"",
817
+ "castF32": "dtypes.T == \"f16\"",
818
+ "usesF16Spec": "dtypes.T == \"f16\""
819
  },
820
+ "bindings": ["x_2", "y_2", "params_18"],
821
+ "dispatch": {
822
+ "x": "min(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
823
+ "y": "ceilDiv(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
824
+ "z": 1
825
+ }
826
  }
827
  ]
828
  },
829
  {
830
  "id": "all_axes_keepdims",
831
  "priority": 30,
832
+ "when": ["not flatParallelCovered", "emptyNarrowInputOk", "f16Ok(dtypes.T)", "ranks.x >= 3", "attrs.keepdims == 1 and ranks.y == ranks.x and numel(shapes.y) == 1"],
833
+ "derive": { "axis": 0, "scalar": "dtypes.T" },
834
  "passes": [
835
  {
836
  "id": "main",
837
  "name": "ReduceMax.Rank3AllAxesKeepdims",
838
+ "shader": "reduce-serial-axis.wgsl.jinja",
839
+ "derive": {
840
+ "op": "\"max\"",
841
+ "indexing": "\"axis2d\"",
842
+ "castF32": "dtypes.T == \"f16\"",
843
+ "usesF16Spec": "dtypes.T == \"f16\""
 
 
844
  },
845
+ "bindings": [
846
+ "x_2",
847
+ "y_2",
848
+ {
849
+ "name": "params",
850
+ "struct": [
851
+ { "name": "rows", "type": "u32", "value": "numel(shapes.x)" },
852
+ { "name": "cols", "type": "u32", "value": "1" },
853
+ { "name": "outCount", "type": "u32", "value": "numel(shapes.y)" }
854
+ ]
855
+ }
856
+ ],
857
+ "dispatch": {
858
+ "x": "min(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
859
+ "y": "ceilDiv(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
860
+ "z": 1
861
+ }
862
  }
863
  ]
864
  },
865
  {
866
  "id": "all_axes_no_keepdims",
867
  "priority": 30,
868
+ "when": ["not flatParallelCovered", "emptyNarrowInputOk", "f16Ok(dtypes.T)", "ranks.x >= 3", "attrs.keepdims == 0 and ranks.y == 0"],
869
+ "derive": { "axis": 0, "scalar": "dtypes.T" },
870
  "passes": [
871
  {
872
  "id": "main",
873
  "name": "ReduceMax.Rank3AllAxesNoKeepdims",
874
+ "shader": "reduce-serial-axis.wgsl.jinja",
875
+ "derive": {
876
+ "op": "\"max\"",
877
+ "indexing": "\"axis2d\"",
878
+ "castF32": "dtypes.T == \"f16\"",
879
+ "usesF16Spec": "dtypes.T == \"f16\""
 
 
880
  },
881
+ "bindings": [
882
+ "x_2",
883
+ "y_2",
884
+ {
885
+ "name": "params",
886
+ "struct": [
887
+ { "name": "rows", "type": "u32", "value": "numel(shapes.x)" },
888
+ { "name": "cols", "type": "u32", "value": "1" },
889
+ { "name": "outCount", "type": "u32", "value": "numel(shapes.y)" }
890
+ ]
891
+ }
892
+ ],
893
+ "dispatch": {
894
+ "x": "min(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
895
+ "y": "ceilDiv(ceilDiv((numel(shapes.y)), (reduceWorkgroupSize)), 65535)",
896
+ "z": 1
897
+ }
898
  }
899
  ]
900
  }
build/webgpu/metadata.json CHANGED
@@ -1,27 +1,56 @@
1
  {
2
  "name": "ai.onnx.ReduceMax",
3
- "id": "_ai_onnx_reducemax_webgpu_6358767",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
- "bench.json": "RUQwdpKHNbPROeebYI6Fx3gfjio+NVhXg04SxOIzwOE=",
11
- "manifest.json": "CDYT3XU3TUZBQDiVWBvsizcqagUbcFXYgIDYLgnsTp8=",
12
- "reduce-axis-split-reduce.wgsl.jinja": "rjy37ckoB9/9a88KxgKKjpZYjmIjy8vmI34yLvPzxuo=",
13
- "reduce-axis0-splitk-combine.wgsl.jinja": "YOriZzB3yMakRsErMyOJhirJBBtgSenl+Gd4rBsHZsM=",
14
- "reduce-axis0-splitk-reduce.wgsl.jinja": "QbHcxtUfEvqMJQRstIC89pBdlD7gDzTFZSdYH1Wldn8=",
15
- "reduce-axis0-tilecols.wgsl.jinja": "ldhjG9yaosrcAFdq6OCglYkvJEaQWi38KdCNqx4N4Yw=",
16
- "reduce-flat-partial.wgsl.jinja": "GyLYqu3kGWSy+ch6zuImCGx9EVbNeh2VNGC7F7qk5iQ=",
17
- "reduce-narrow-empty-identity.wgsl.jinja": "S6UnLW+6dyCb9ciRezkgXE9mlYmqdfBCjPRO9iPm3Jk=",
18
- "reduce-noop-empty-axes.wgsl.jinja": "IO2MEzyBGJr3Z8AVP7ELJXqfhwcG/vtBfa0wCdkbuwo=",
19
- "reduce-row-subgroup.wgsl.jinja": "2x8tEHQzb0bvjLuk5yKCk4tfkr1YsswRQI0vcmgnlO8=",
20
- "reduce-row-tree.wgsl.jinja": "FoHxcz6JQrvjR1VKtBkmLX1YYso4OcBFBSDinftyYLE=",
21
- "reduce-serial-axis.wgsl.jinja": "cLrTc0ite8YZmx/wn7PBwaxTp9dLWWUIUdQQ3tH8+4o=",
22
- "test.json": "u4hWEOrfmTrnNihqbwdSeT7eSdx1RMefdEFkdb85hWM="
 
23
  }
24
  },
25
- "provenance": { "kernel": { "sha": "c928d21e6cc1310861cba3bafb75f5f679ecf5f3", "dirty": false } },
26
- "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/ai.onnx.ReduceMax" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  }
 
1
  {
2
  "name": "ai.onnx.ReduceMax",
3
+ "id": "_ai_onnx_reducemax_webgpu_e5fe52d",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
+ "bench.json": "XyzUAY0JwjZnawPpr0mLdxRiYEBNVbVHXpcRY2h6a7g=",
11
+ "manifest.json": "JsnHeiXV5pD3+aVqXZtm+oiXjafnbGUMusuHh+9tLV0=",
12
+ "reduce-axis-split-reduce.wgsl.jinja": "4+ep9xH4pHZOfaZ8abJhA8SW5M4mUhDC+y1CtDz6vjY=",
13
+ "reduce-axis0-splitk-combine.wgsl.jinja": "Yz1hjK55R/kndUrw3ugPqgPaOBwKmYupqZoVdJTHO5Q=",
14
+ "reduce-axis0-splitk-reduce.wgsl.jinja": "iLqQIC+Sa+EUzZG4Z3BqZOT2XA1TQEuRcFR2JbkEa30=",
15
+ "reduce-axis0-tilecols.wgsl.jinja": "PjYkEUQJBeG70td3W2xxmexH9x6XNIfeSzXBY47HbaU=",
16
+ "reduce-flat-partial.wgsl.jinja": "+qToL+wFi9QOxvY887aBAEwZK6Xu/eLQkucKvJYNwSk=",
17
+ "reduce-narrow-empty-identity.wgsl.jinja": "XmLNK4zp0ru14DEm8TL5CAL74RVu7aVPrFo3WSJX3wo=",
18
+ "reduce-noop-empty-axes.wgsl.jinja": "5bKufrcfYt3BagZ6WxbUvqwKKKVMU7d+SUshgUFWTDI=",
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": "bPR3qTzwoRpt/ZxyB7i2Dd1K8Sp+WZEWJELbK4zv3H4="
24
  }
25
  },
26
+ "provenance": { "kernel": { "sha": "91d990483a174128daf7673f3f37a7c890493ae1", "dirty": false } },
27
+ "webgpu": {
28
+ "manifestSpec": "2.0",
29
+ "variants": {
30
+ "narrow_empty_identity": ["reduce-narrow-empty-identity.wgsl.jinja"],
31
+ "contiguous_suffix_subgroup_vec4": ["reduce-row-subgroup.wgsl.jinja"],
32
+ "contiguous_suffix_tree_vec4": ["reduce-row-tree.wgsl.jinja"],
33
+ "contiguous_suffix_tree": ["reduce-row-tree.wgsl.jinja"],
34
+ "noop_empty_axes": ["reduce-noop-empty-axes.wgsl.jinja"],
35
+ "tree_last_axis": ["reduce-row-tree.wgsl.jinja"],
36
+ "subgroup_rows_last_axis_vec4": ["reduce-row-subgroup-rows.wgsl.jinja"],
37
+ "tree_last_axis_vec4": ["reduce-row-tree.wgsl.jinja"],
38
+ "rank0_scalar": ["reduce-serial-axis.wgsl.jinja"],
39
+ "rank1_axis0": ["reduce-serial-axis.wgsl.jinja"],
40
+ "axis_split": ["reduce-axis-split-reduce.wgsl.jinja", "reduce-axis0-splitk-combine.wgsl.jinja"],
41
+ "axis_split_tiled_narrow": ["reduce-axis0-splitk-combine.wgsl.jinja", "reduce-axis0-tilecols.wgsl.jinja"],
42
+ "axis0_splitk": ["reduce-axis0-splitk-combine.wgsl.jinja", "reduce-axis0-splitk-reduce.wgsl.jinja"],
43
+ "axis0_tilecols": ["reduce-axis0-tilecols.wgsl.jinja"],
44
+ "all_axes_flat": ["reduce-axis0-splitk-combine.wgsl.jinja", "reduce-flat-partial.wgsl.jinja"],
45
+ "multi_axis_rank3": ["reduce-serial-axis.wgsl.jinja"],
46
+ "multi_axis_rank4": ["reduce-serial-axis.wgsl.jinja"],
47
+ "rankn_single_axis_generic": ["reduce-serial-axis.wgsl.jinja"],
48
+ "subgroup_last_axis_vec4": ["reduce-row-subgroup.wgsl.jinja"],
49
+ "subgroup_last_axis": ["reduce-row-subgroup.wgsl.jinja"],
50
+ "axis0": ["reduce-serial-axis.wgsl.jinja"],
51
+ "axis1": ["reduce-serial-axis.wgsl.jinja"],
52
+ "all_axes_keepdims": ["reduce-serial-axis.wgsl.jinja"],
53
+ "all_axes_no_keepdims": ["reduce-serial-axis.wgsl.jinja"]
54
+ }
55
+ }
56
  }
build/webgpu/reduce-axis-split-reduce.wgsl.jinja CHANGED
@@ -4,19 +4,20 @@
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
  {% macro wgsl_minmax_identity(name, op, scalar="f32") %}
 
 
20
  {% set logicalBoolIdentity = logicalBool is defined and logicalBool %}
21
  fn {{ name }}() -> {{ scalar }} {
22
  {% if scalar == "i32" %}
@@ -33,15 +34,23 @@ fn {{ name }}() -> {{ scalar }} {
33
 
34
  const WG: u32 = {{ workgroupSize }}u;
35
  const SPLIT: u32 = {{ split }}u;
36
- {{ wgsl_minmax_identity("reduction_identity", source.op) }}
 
 
 
 
 
 
 
 
 
37
 
38
  @compute @workgroup_size(WG, 1, 1)
39
  fn main(@builtin(global_invocation_id) gid: vec3<u32>,
40
- @builtin(workgroup_id) wg: vec3<u32>,
41
- @builtin(num_workgroups) nwg: vec3<u32>) {
42
  // 2D-folded output index: wg.z carries the high bits past the
43
  // per-dimension dispatch limit on the x dimension.
44
- let output_index = (wg.x + wg.z * nwg.x) * WG + (gid.x % WG);
45
  let seg = wg.y;
46
  if (output_index >= params.outputs) { return; }
47
 
@@ -55,9 +64,52 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>,
55
  var a1 = a0 + chunk;
56
  if (a1 > params.axisDim) { a1 = params.axisDim; }
57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  var acc = reduction_identity();
 
 
 
 
 
59
  for (var axis_index = a0; axis_index < a1; axis_index = axis_index + 1u) {
60
- acc = {{ source.op }}(acc, {{ xa }}x[input_base + axis_index * params.inner]{{ ax }});
 
 
 
 
 
 
 
 
 
 
 
61
  }
62
  partials[seg * params.outputs + output_index] = acc;
 
63
  }
 
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" %}
 
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,14 +2,18 @@
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
  {% macro wgsl_minmax_identity(name, op, scalar="f32") %}
 
 
13
  {% set logicalBoolIdentity = logicalBool is defined and logicalBool %}
14
  fn {{ name }}() -> {{ scalar }} {
15
  {% if scalar == "i32" %}
@@ -26,23 +30,93 @@ fn {{ name }}() -> {{ scalar }} {
26
 
27
  const WG: u32 = {{ workgroupSize }}u;
28
  const SPLIT: u32 = {{ split }}u;
29
- {{ wgsl_minmax_identity("reduction_identity", source.op) }}
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
  @compute @workgroup_size(WG, 1, 1)
32
  fn main(@builtin(global_invocation_id) gid: vec3<u32>,
33
  @builtin(num_workgroups) nwg: vec3<u32>) {
34
  let stride = nwg.x * WG;
35
- let start = (gid.y * nwg.x * WG) + gid.x;
36
  for (var col = start; col < params.cols; col = col + stride) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  var total = reduction_identity();
 
 
 
 
 
 
38
  for (var seg = 0u; seg < SPLIT; seg = seg + 1u) {
39
  let p = partials[seg * params.cols + col];
40
- total = {{ source.op }}(total, p);
 
 
 
 
 
 
41
  }
42
- {% if source.outputF16 %}
 
 
 
 
 
 
 
 
 
 
43
  y[col] = f16(total);
44
  {% else %}
45
  y[col] = total;
 
 
46
  {% endif %}
47
  }
48
  }
 
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" %}
 
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,20 +2,16 @@
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
  {% macro wgsl_minmax_identity(name, op, scalar="f32") %}
 
 
19
  {% set logicalBoolIdentity = logicalBool is defined and logicalBool %}
20
  fn {{ name }}() -> {{ scalar }} {
21
  {% if scalar == "i32" %}
@@ -32,15 +28,23 @@ fn {{ name }}() -> {{ scalar }} {
32
 
33
  const WG: u32 = {{ workgroupSize }}u;
34
  const SPLIT: u32 = {{ split }}u;
35
- {{ wgsl_minmax_identity("reduction_identity", source.op) }}
 
 
 
 
 
 
 
 
 
36
 
37
  @compute @workgroup_size(WG, 1, 1)
38
  fn main(@builtin(global_invocation_id) gid: vec3<u32>,
39
- @builtin(workgroup_id) wg: vec3<u32>,
40
- @builtin(num_workgroups) nwg: vec3<u32>) {
41
- // 2D-folded column index: wg.z carries the high bits past the maxComputeWorkgroupsPerDimension
42
- // workgroup-per-dimension dispatch limit on the x dimension.
43
- let col = (wg.x + wg.z * nwg.x) * WG + (gid.x % WG);
44
  let seg = wg.y;
45
  if (col >= params.cols) { return; }
46
 
@@ -50,9 +54,54 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>,
50
  var r1 = r0 + chunk;
51
  if (r1 > params.rows) { r1 = params.rows; }
52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  var acc = reduction_identity();
 
 
 
 
 
 
 
54
  for (var row = r0; row < r1; row = row + 1u) {
55
- acc = {{ source.op }}(acc, {{ xa }}x[row * params.cols + col]{{ ax }});
 
 
 
 
 
 
 
 
 
 
 
56
  }
57
  partials[seg * params.cols + col] = acc;
 
58
  }
 
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" %}
 
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 op == "max" %}
83
  var acc = reduction_identity();
84
+ {% elif op == "min" %}
85
+ var acc = reduction_identity();
86
+ {% elif op == "prod" %}
87
+ var acc = 1.0;
88
+ {% else %}
89
+ var acc = 0.0;
90
+ {% endif %}
91
  for (var row = r0; row < r1; row = row + 1u) {
92
+ {% if op == "max" or op == "min" %}
93
+ acc = {{ op }}(acc, {{ xa }}x[row * params.cols + col]{{ ax }});
94
+ {% elif op == "prod" %}
95
+ acc = acc * {{ xa }}x[row * params.cols + col]{{ ax }};
96
+ {% elif op == "l1" %}
97
+ acc = acc + abs({{ xa }}x[row * params.cols + col]{{ ax }});
98
+ {% elif op == "l2" or op == "sumsquare" %}
99
+ let value = {{ xa }}x[row * params.cols + col]{{ ax }};
100
+ acc = acc + value * value;
101
+ {% else %}
102
+ acc = acc + {{ xa }}x[row * params.cols + col]{{ ax }};
103
+ {% endif %}
104
  }
105
  partials[seg * params.cols + col] = acc;
106
+ {% endif %}
107
  }
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,22 +13,21 @@
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
  {% macro wgsl_minmax_identity(name, op, scalar="f32") %}
 
 
32
  {% set logicalBoolIdentity = logicalBool is defined and logicalBool %}
33
  fn {{ name }}() -> {{ scalar }} {
34
  {% if scalar == "i32" %}
@@ -42,19 +41,38 @@ fn {{ name }}() -> {{ scalar }} {
42
  }
43
  {%- endmacro %}
44
 
 
 
 
 
 
 
 
45
 
46
  const WG: u32 = {{ workgroupSize }}u;
47
  const TILE_COLS: u32 = {{ tileCols }}u;
48
  const ROW_LANES: u32 = WG / TILE_COLS;
49
  {% if splitMode %}
50
- const SPLIT: u32 = {{ source.split }}u;
 
 
 
 
 
 
51
  {% endif %}
52
- {{ wgsl_minmax_identity("lane_identity", source.op, scalar) }}
53
 
54
- var<workgroup> partial: array<{{ scalar if (source.op == "max" or source.op == "min" or intMode) else "f32" }}, WG>;
 
 
 
 
 
 
 
 
55
  @compute @workgroup_size(WG, 1, 1)
56
- fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>{% if not splitMode %},
57
- @builtin(num_workgroups) nwg: vec3<u32>{% endif %}) {
58
  let tid = lid.x;
59
  let col_lane = tid % TILE_COLS;
60
  let row_lane = tid / TILE_COLS;
@@ -75,17 +93,134 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid:
75
  // 2D-folded tile index: wg.y carries the high bits past the dispatch limit.
76
  // The batched form reuses this same coalesced axis-0 reduction for a middle
77
  // axis by assigning consecutive tiles to each outer slice.
78
- let tile = wg.x + wg.y * nwg.x;
79
  let col = tile * TILE_COLS + col_lane;
80
  let inputBase = 0u;
81
  let outputIndex = col;
82
  let in_bounds = col < params.cols;
83
  {% endif %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
 
 
 
 
 
 
 
 
 
85
  var acc = lane_identity();
 
 
 
 
 
86
  if (in_bounds) {
87
  for (var row = {{ rowBegin }}; row < {{ rowEnd }}; row = row + ROW_LANES) {
88
- acc = {{ source.op }}(acc, {{ elem }});
 
 
 
 
 
 
 
 
 
 
 
89
  }
90
  }
91
  partial[tid] = acc;
@@ -94,12 +229,32 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid:
94
  if (row_lane == 0u && in_bounds) {
95
  var total = partial[col_lane];
96
  for (var lane = 1u; lane < ROW_LANES; lane = lane + 1u) {
97
- total = {{ source.op }}(total, partial[lane * TILE_COLS + col_lane]);
 
 
 
 
 
 
98
  }
99
  {% if splitMode %}
100
  partials[seg * params.outputs + outputIndex] = total;
 
 
 
 
 
 
 
 
 
 
 
 
101
  {% else %}
102
  y[outputIndex] = {{ yv }}total{{ vy }};
 
103
  {% endif %}
104
  }
 
105
  }
 
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" %}
 
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,17 +6,17 @@
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
  {% macro wgsl_minmax_identity(name, op, scalar="f32") %}
 
 
20
  {% set logicalBoolIdentity = logicalBool is defined and logicalBool %}
21
  fn {{ name }}() -> {{ scalar }} {
22
  {% if scalar == "i32" %}
@@ -32,9 +32,11 @@ fn {{ name }}() -> {{ scalar }} {
32
 
33
 
34
  const WG: u32 = {{ workgroupSize }}u;
35
- var<workgroup> red: array<{{ "i32" if source.intMode else "f32" }}, WG>;
36
- {{ wgsl_minmax_identity("reduction_identity", source.op) }}
 
37
 
 
38
  @compute @workgroup_size(WG)
39
  fn main(@builtin(global_invocation_id) gid: vec3<u32>,
40
  @builtin(local_invocation_id) lid: vec3<u32>,
@@ -42,12 +44,38 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>,
42
  @builtin(num_workgroups) nwg: vec3<u32>) {
43
  let tid = lid.x;
44
  let gstride = nwg.x * WG;
 
 
 
 
 
 
 
45
  var acc = reduction_identity();
 
 
 
 
 
 
 
46
  // Grid-stride over the flat vec4 groups (params.count4 = numel / 4, floored).
47
  for (var i = gid.x; i < params.count4; i = i + gstride) {
48
  let b = 4u * i;
49
- 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 }});
 
50
  acc = max(acc, max(max(v.x, v.y), max(v.z, v.w)));
 
 
 
 
 
 
 
 
 
 
 
51
  }
52
  // Scalar tail (the 0..3 elements past the last full vec4). One global thread
53
  // folds it so it is counted exactly once; the count is tiny so serializing it
@@ -55,7 +83,19 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>,
55
  if (gid.x == 0u) {
56
  for (var i = 4u * params.count4; i < params.numel; i = i + 1u) {
57
  let s = {{ xa }}x[i]{{ ax }};
 
58
  acc = max(acc, s);
 
 
 
 
 
 
 
 
 
 
 
59
  }
60
  }
61
  red[tid] = acc;
@@ -64,7 +104,15 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>,
64
  loop {
65
  if (stride == 0u) { break; }
66
  if (tid < stride) {
 
67
  red[tid] = max(red[tid], red[tid + stride]);
 
 
 
 
 
 
 
68
  }
69
  stride = stride / 2u;
70
  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" %}
 
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-narrow-empty-identity.wgsl.jinja CHANGED
@@ -11,6 +11,6 @@ fn main(
11
  ) {
12
  let stride = nwg.x * WG;
13
  for (var i = gid.x; i < params.count; i += stride) {
14
- y[i] = {{ scalar }}({{ source.identity }});
15
  }
16
  }
 
11
  ) {
12
  let stride = nwg.x * WG;
13
  for (var i = gid.x; i < params.count; i += stride) {
14
+ y[i] = {{ scalar }}({{ identity }});
15
  }
16
  }
build/webgpu/reduce-noop-empty-axes.wgsl.jinja CHANGED
@@ -1,13 +1,21 @@
1
  {{ env.wgsl.resourceDeclarations }}
2
 
3
  @compute @workgroup_size({{ reduceWorkgroupSize }})
4
- fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) nwg: vec3<u32>) {
5
  // 2D-folded flat index: gid.y carries the high bits past the
6
- // maxComputeWorkgroupsPerDimension dispatch limit (outputs > 16.7M elements).
7
- let i = gid.x + gid.y * nwg.x * {{ reduceWorkgroupSize }}u;
8
  if (i >= params.count) {
9
  return;
10
  }
11
  let v = x[i];
 
 
 
 
 
 
 
12
  y[i] = v;
 
13
  }
 
1
  {{ env.wgsl.resourceDeclarations }}
2
 
3
  @compute @workgroup_size({{ reduceWorkgroupSize }})
4
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
5
  // 2D-folded flat index: gid.y carries the high bits past the
6
+ // per-axis dispatch fold width (outputs > 16.7M elements).
7
+ let i = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ reduceWorkgroupSize }}u;
8
  if (i >= params.count) {
9
  return;
10
  }
11
  let v = x[i];
12
+ {% if op == "abs" %}
13
+ y[i] = abs(v);
14
+ {% elif op == "square" %}
15
+ y[i] = v * v;
16
+ {% elif op == "log" %}
17
+ y[i] = log(v);
18
+ {% else %}
19
  y[i] = v;
20
+ {% endif %}
21
  }
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,28 +1,34 @@
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
  {% macro wgsl_minmax_identity(name, op, scalar="f32") %}
 
 
26
  {% set logicalBoolIdentity = logicalBool is defined and logicalBool %}
27
  fn {{ name }}() -> {{ scalar }} {
28
  {% if scalar == "i32" %}
@@ -37,7 +43,21 @@ fn {{ name }}() -> {{ scalar }} {
37
  {%- endmacro %}
38
 
39
 
40
- const WG: u32 = {{ workgroupSize }}u;{{ wgsl_minmax_identity("reduction_identity", source.op, scalar) }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  var<workgroup> wgPartial: array<{{ scalar }}, WG>;
42
 
43
  {% macro emit_reduce(name, collective, combine) %}
@@ -57,26 +77,141 @@ fn {{ name }}(value: {{ scalar }}, sgLid: u32, sgId: u32, numSg: u32) -> {{ scal
57
  workgroupBarrier();
58
  return total;
59
  }
60
- {%- endmacro %}{{ emit_reduce("reduce_row", "subgroupMax", "total = max(total, wgPartial[i]);") }}
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  @compute @workgroup_size(WG, 1, 1)
62
  fn main(@builtin(workgroup_id) wg: vec3<u32>,
63
- @builtin(num_workgroups) nwg: vec3<u32>,
64
  @builtin(local_invocation_id) lid: vec3<u32>,
65
  @builtin(subgroup_invocation_id) sgLid: u32,
66
  @builtin(subgroup_id) sgId: u32,
67
  @builtin(num_subgroups) numSg: u32) {
68
- let row = wg.x + wg.y * nwg.x;
69
  if (row >= params.rows) {
70
  return;
71
  }
72
  let tid = lid.x;
73
- let base = row * params.chunkCount; let INIT: {{ scalar }} = reduction_identity();{% if source.vec4 %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  var acc4 = vec4<{{ scalar }}>(INIT);
75
  for (var c = tid; c < params.chunkCount; c = c + WG) {
76
- let v = {{ xv }}x[base + c]{{ vx }}; acc4 = max(acc4, v); } let acc = max(max(acc4.x, acc4.y), max(acc4.z, acc4.w));{% else %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  var acc = INIT;
78
  for (var c = tid; c < params.chunkCount; c = c + WG) {
79
- let v = {{ xv }}x[base + c]{{ vx }}; acc = max(acc, v); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  {%- endif %}
81
  let total = reduce_row(acc, sgLid, sgId, numSg);
82
- 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" %}
 
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,27 +1,34 @@
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
  {% macro wgsl_minmax_identity(name, op, scalar="f32") %}
 
 
25
  {% set logicalBoolIdentity = logicalBool is defined and logicalBool %}
26
  fn {{ name }}() -> {{ scalar }} {
27
  {% if scalar == "i32" %}
@@ -35,49 +42,225 @@ fn {{ name }}() -> {{ scalar }} {
35
  }
36
  {%- endmacro %}
37
 
 
 
 
 
 
 
 
38
 
39
  const WG: u32 = {{ workgroupSize }}u;
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  {% set is_int = scalar == "i32" or scalar == "u32" %}
42
- {% set intAdditive = is_int and (source.op == "sum" or source.op == "l1" or source.op == "sumsquare"
43
- or source.op == "l2" or source.op == "mean") %}
44
- {% set accType = scalar if source.op == "max" or source.op == "min" or source.op == "prod" or intAdditive else "f32" %}
45
- {{ wgsl_minmax_identity("identity", source.op, accType) }}
 
 
 
 
 
 
 
 
46
 
47
  fn combine(a: {{ accType }}, b: {{ accType }}) -> {{ accType }} {
 
48
  return max(a, b);
 
 
 
 
 
 
 
49
  }
50
 
51
  var<workgroup> partial: array<{{ accType }}, WG>;
 
52
 
53
  @compute @workgroup_size(WG, 1, 1)
54
  fn main(@builtin(workgroup_id) wg: vec3<u32>,
55
- @builtin(num_workgroups) nwg: vec3<u32>,
56
  @builtin(local_invocation_id) lid: vec3<u32>) {
57
- let row = wg.x + wg.y * nwg.x;
58
  if (row >= params.rows) {
59
  return;
60
  }
61
  let tid = lid.x;
62
- {% if source.vec4 %}
63
  let base = row * params.chunkCount;
64
  {% else %}
65
  let base = row * params.cols;
66
  {% endif %}
67
 
 
 
 
 
 
 
 
68
 
69
- {% if source.vec4 %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  var acc4 = vec4<{{ accType }}>(identity());
71
  for (var col = tid; col < params.chunkCount; col = col + WG) {
72
  let value = {{ xv }}x[base + col]{{ vx }};
 
73
  acc4 = max(acc4, value);
 
 
 
 
 
 
 
 
 
 
 
74
  }
 
75
  let acc = max(max(acc4.x, acc4.y), max(acc4.z, acc4.w));
 
 
 
 
 
 
 
76
  {% else %}
77
  var acc = identity();
78
  for (var col = tid; col < params.cols; col = col + WG) {
79
  let value = {{ xv }}x[base + col]{{ vx }};
 
 
 
 
 
80
  acc = combine(acc, value);
 
81
  }
82
  {% endif %}
83
  partial[tid] = acc;
@@ -92,6 +275,19 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>,
92
 
93
  if (tid == 0u) {
94
  let total = partial[0];
 
 
 
 
 
 
 
 
 
 
 
95
  y[row] = {{ yv }}total{{ vy }};
 
96
  }
 
97
  }
 
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" %}
 
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,17 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 logicalBool = source.logicalBool is defined and source.logicalBool %}
 
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
  {% macro wgsl_minmax_identity(name, op, scalar="f32") %}
 
 
15
  {% set logicalBoolIdentity = logicalBool is defined and logicalBool %}
16
  fn {{ name }}() -> {{ scalar }} {
17
  {% if scalar == "i32" %}
@@ -25,95 +73,70 @@ fn {{ name }}() -> {{ scalar }} {
25
  }
26
  {%- endmacro %}
27
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
- {{ wgsl_minmax_identity("empty_value", source.op, "f32" if castF32 else scalar) }}
30
- {% if source.indexing == "rankn" %}
 
 
 
 
 
 
 
 
 
31
 
32
  fn input_offset(out_index: u32, reduce_index: u32) -> u32 {
33
  var rem = out_index;
34
- {% for axis in range(source.outputRank) %}
35
  {% set out_stride = namespace(value=1) %}
36
- {% for j in range(axis + 1, source.outputRank) %}
37
- {% set out_stride.value = out_stride.value * source.outputShape[j] %}
38
  {% endfor %}
39
  {% set safe_out_stride = 1 if out_stride.value == 0 else out_stride.value %}
40
- {% if not source.keepDims or axis != source.axis %}
41
  let out_coord{{ axis }} = rem / {{ safe_out_stride }}u;
42
  {% endif %}
43
  rem = rem % {{ safe_out_stride }}u;
44
  {% endfor %}
45
- {% for axis in range(source.rank) %}
46
- {% if axis == source.axis %}
47
  let coord{{ axis }} = reduce_index;
48
- {% elif source.keepDims %}
49
  let coord{{ axis }} = out_coord{{ axis }};
50
- {% elif axis < source.axis %}
51
  let coord{{ axis }} = out_coord{{ axis }};
52
  {% else %}
53
  let coord{{ axis }} = out_coord{{ axis - 1 }};
54
  {% endif %}
55
  {% endfor %}
56
  {% set src = namespace(value="coord0") %}
57
- {% for axis in range(1, source.rank) %}
58
- {% set src.value = "(" ~ src.value ~ " * " ~ source.dataShape[axis] ~ "u + coord" ~ axis ~ ")" %}
59
  {% endfor %}
60
  return {{ src.value }};
61
  }
62
  {% endif %}
63
- {% if source.indexing == "multiaxis" %}
64
  {% set hasReducedAxis = namespace(value=false) %}
65
- {% for a in range(source.rank) %}{% if source.reduce[a] %}{% set hasReducedAxis.value = true %}{% endif %}{% endfor %}
66
-
67
- // One thread per output element walks the Cartesian product of the reduced axes,
68
- // linearized as reduce_linear. Specialized shapes make every input offset a sum
69
- // of coordinate-times-constant terms.
70
- fn input_offset(out_index: u32{% if hasReducedAxis.value %}, reduce_linear: u32{% endif %}) -> u32 {
71
- var rem = out_index;
72
- {% for oaxis in range(source.outputRank) %}
73
- {% set ostride = namespace(value=1) %}
74
- {% for j in range(oaxis + 1, source.outputRank) %}
75
- {% set ostride.value = ostride.value * source.outputShape[j] %}
76
- {% endfor %}
77
- {% set osafe = 1 if ostride.value == 0 else ostride.value %}
78
- {% if not source.keepDims or not source.reduce[oaxis] %}
79
- let out_coord{{ oaxis }} = rem / {{ osafe }}u;
80
- {% endif %}
81
- rem = rem % {{ osafe }}u;
82
- {% endfor %}
83
- {% if hasReducedAxis.value %}
84
- var rrem = reduce_linear;
85
- {% endif %}
86
- {% for a in range(source.rank) if source.reduce[a] %}
87
- {% set rstride = namespace(value=1) %}
88
- {% for b in range(a + 1, source.rank) if source.reduce[b] %}
89
- {% set rstride.value = rstride.value * source.dataShape[b] %}
90
- {% endfor %}
91
- {% set rsafe = 1 if rstride.value == 0 else rstride.value %}
92
- let red_coord{{ a }} = rrem / {{ rsafe }}u;
93
- rrem = rrem % {{ rsafe }}u;
94
- {% endfor %}
95
- {% set oc = namespace(i=0) %}
96
- {% for a in range(source.rank) %}
97
- {% if source.reduce[a] %}
98
- let coord{{ a }} = red_coord{{ a }};
99
- {% elif source.keepDims %}
100
- let coord{{ a }} = out_coord{{ a }};
101
- {% else %}
102
- let coord{{ a }} = out_coord{{ oc.i }};
103
- {% set oc.i = oc.i + 1 %}
104
  {% endif %}
105
- {% endfor %}
106
- {% set src = namespace(value="coord0") %}
107
- {% for a in range(1, source.rank) %}
108
- {% set src.value = "(" ~ src.value ~ " * " ~ source.dataShape[a] ~ "u + coord" ~ a ~ ")" %}
109
- {% endfor %}
110
- return {{ src.value }};
111
- }
112
- {% endif %}
113
- {% if source.indexing == "multiaxis" %}
114
  {% set mcount = namespace(value=1) %}
115
- {% for a in range(source.rank) if source.reduce[a] %}
116
- {% set mcount.value = mcount.value * source.dataShape[a] %}
117
  {% endfor %}
118
  {% set count = mcount.value ~ "u" %}
119
  {% if hasReducedAxis.value %}
@@ -123,7 +146,7 @@ fn input_offset(out_index: u32{% if hasReducedAxis.value %}, reduce_linear: u32{
123
  {% set at = "x[input_offset(i)]" %}
124
  {% set at_first = "x[input_offset(i)]" %}
125
  {% endif %}
126
- {% elif source.indexing == "rankn" %}
127
  {% set count = "params.axisDim" %}
128
  {% set at = "x[input_offset(i, r)]" %}
129
  {% set at_first = "x[input_offset(i, 0u)]" %}
@@ -142,20 +165,123 @@ fn input_offset(out_index: u32{% if hasReducedAxis.value %}, reduce_linear: u32{
142
  {% endif %}
143
 
144
  @compute @workgroup_size({{ reduceWorkgroupSize }})
145
- fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) nwg: vec3<u32>) {
146
  // 2D-folded flat index: gid.y carries the high bits past the
147
- // maxComputeWorkgroupsPerDimension dispatch limit (outputs > 16.7M elements).
148
- let i = gid.x + gid.y * nwg.x * {{ reduceWorkgroupSize }}u;
149
  if (i >= params.outCount) {
150
  return;
151
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  if ({{ count }} == 0u) {
153
  y[i] = {{ yv }}empty_value(){{ vy }};
154
  return;
155
  }
156
  var acc = {{ at_first }};
157
  for (var r = 1u; r < {{ count }}; r = r + 1u) {
158
- acc = {{ source.op }}(acc, {{ at }});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  y[i] = {{ yv }}acc{{ vy }};
 
 
161
  }
 
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
  // 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" %}
 
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 %}
 
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)]" %}
 
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,14 +1,14 @@
1
  {
2
- "op": "ai.onnx.ReduceMax",
3
  "fixtureArrays": {
4
  "int32_axis0_tiled_64x32_input_x": [16777216, 16777245, 16777274, 16777236, 16777265, 16777227, 16777256, 16777218, 16777247, 16777276, 16777238, 16777267, 16777229, 16777258, 16777220, 16777249, 16777278, 16777240, 16777269, 16777231, 16777260, 16777222, 16777251, 16777280, 16777242, 16777271, 16777233, 16777262, 16777224, 16777253, 16777282, 16777244, 16777273, 16777235, 16777264, 16777226, 16777255, 16777217, 16777246, 16777275, 16777237, 16777266, 16777228, 16777257, 16777219, 16777248, 16777277, 16777239, 16777268, 16777230, 16777259, 16777221, 16777250, 16777279, 16777241, 16777270, 16777232, 16777261, 16777223, 16777252, 16777281, 16777243, 16777272, 16777234, 16777263, 16777225, 16777254],
5
- "rank3_axis1_middle_no_keepdims_input_x": [-1, 7, -3, 4, 5, -6, 2, -8, 0, 3, 9, -2, -4, -1, -7, 6, 8, 2, -5, 1, -9, 4, 3, 0]
 
6
  },
7
  "cases": [
8
  {
9
  "name": "all_axes_flat_negative_infinity_identity_8192",
10
  "provenance": {
11
- "notes": "The parallel full-reduction threshold must supersede the rank1 serial and row-reduction fallbacks, while preserving the true -Infinity identity."
12
  },
13
  "attrs": { "axes": [0], "keepdims": 0 },
14
  "inputs": { "x": { "dtype": "float32", "shape": [8192], "data": { "kind": "negativeInfinity" } } },
@@ -113,7 +113,7 @@
113
  "name": "ort_axis1_nan_seed_comparison_gpu_gap",
114
  "skipGpu": {
115
  "category": "todo",
116
- "reason": "The parallel min/max reduction routes do not yet preserve the reference's first-element NaN incumbent semantics; explicit NaN tracking is implementable in WGSL."
117
  },
118
  "provenance": {
119
  "source": "onnxruntime/test/providers/cpu/reduction/reduction_ops_test.cc",
@@ -237,7 +237,7 @@
237
  "provenance": {
238
  "source": "onnxruntime/test/providers/cpu/reduction/reduction_ops_test.cc",
239
  "test": "ReductionOpTest.ReduceMax_int32",
240
- "notes": "Single-axis subset adapted from ORT's multi-axis integer reduction fixture because this framework models one reduction axis per case."
241
  },
242
  "attrs": { "axes": [1], "keepdims": 1 },
243
  "inputs": {
@@ -615,7 +615,7 @@
615
  "provenance": {
616
  "source": "onnxruntime/test/providers/cpu/reduction/reduction_ops_test.cc",
617
  "test": "test_bool_ReduceMax_16",
618
- "notes": "The fixture now uses the standard Boolean route introduced by ONNX ReduceMax-20."
619
  },
620
  "inputs": {
621
  "x": {
@@ -632,7 +632,7 @@
632
  "provenance": {
633
  "source": "onnxruntime/test/providers/cpu/reduction/reduction_ops_test.cc",
634
  "test": "test_bool_ReduceMax_18",
635
- "notes": "The fixture now uses the standard Boolean route introduced by ONNX ReduceMax-20."
636
  },
637
  "inputs": {
638
  "x": {
@@ -662,43 +662,26 @@
662
  "name": "rank3_lastaxis_cols1024_tree_f32",
663
  "attrs": { "axes": [2], "keepdims": 0 },
664
  "inputs": {
665
- "x": {
666
- "dtype": "float32",
667
- "shape": [2, 2, 1024],
668
- "data": {
669
- "kind": "cycle",
670
- "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]
671
- }
672
- }
673
  },
674
  "outputs": { "y": { "dtype": "float32", "shape": [2, 2], "tolerance": 0.00001 } }
675
  },
676
  {
677
  "name": "lastaxis_vocab_width_2x50257_scalar",
678
  "provenance": {
679
- "notes": "Compact correctness sibling for the vocab-width ReduceMax benchmark; preserves a large odd last axis that selects the scalar last-axis reducer and tree fallback on no-subgroup tiers."
680
  },
681
  "attrs": { "axes": [1], "keepdims": 0 },
682
  "inputs": {
683
- "x": {
684
- "dtype": "float32",
685
- "shape": [2, 50257],
686
- "data": { "kind": "cycle", "values": [-7.0, 0.5, 42.0, -13.0, 3.25, 9.5, -1.0] }
687
- }
688
  },
689
- "outputs": {
690
- "y": { "dtype": "float32", "shape": [2], "data": { "kind": "constant", "value": 42.0 }, "tolerance": 0 }
691
- }
692
  },
693
  {
694
  "name": "rank3_lastaxis_cols1024_tree_i32",
695
  "attrs": { "axes": [2], "keepdims": 0 },
696
  "inputs": {
697
- "x": {
698
- "dtype": "int32",
699
- "shape": [2, 2, 1024],
700
- "data": { "kind": "cycle", "values": [3, -7, 11, -2, 5, -13, 8, -1, 19, -4, 6, -9, 2, -15, 10, -6] }
701
- }
702
  },
703
  "outputs": { "y": { "dtype": "int32", "shape": [2, 2], "tolerance": 0.00001 } }
704
  },
@@ -767,16 +750,7 @@
767
  {
768
  "name": "int32_subgroup_scalar_last_axis_2x67",
769
  "attrs": { "axes": [1], "keepdims": 0 },
770
- "inputs": {
771
- "x": {
772
- "dtype": "int32",
773
- "shape": [2, 67],
774
- "data": {
775
- "kind": "cycle",
776
- "values": [3, -7, 11, -2, 5, -13, 8, -1, 19, -4, 6, -9, 2, -15, 10, -6, 14, -11, 1, -5, 17, -8, 4, -12, 13, -3, 7, -10, 9, -14]
777
- }
778
- }
779
- },
780
  "outputs": { "y": { "dtype": "int32", "shape": [2], "tolerance": 0 } }
781
  },
782
  {
@@ -866,7 +840,7 @@
866
  {
867
  "name": "all_axes_flat_fullreduce_101x103_nonmul4_keepdims",
868
  "provenance": {
869
- "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."
870
  },
871
  "attrs": { "keepdims": 1 },
872
  "inputs": {
@@ -986,11 +960,26 @@
986
  "x": {
987
  "dtype": "float16",
988
  "shape": [8192, 8],
989
- "data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.11, "scale": 4.0 }
990
  }
991
  },
992
  "outputs": { "y": { "dtype": "float16", "shape": [8], "tolerance": 0.05, "relTolerance": 0.002 } }
993
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
994
  {
995
  "name": "f16_last_axis_vec4_8x1024",
996
  "attrs": { "axes": [1], "keepdims": 0 },
@@ -1031,11 +1020,7 @@
1031
  "name": "f16_suffix_vec4_4x8x128",
1032
  "attrs": { "axes": [1, 2], "keepdims": 0 },
1033
  "inputs": {
1034
- "x": {
1035
- "dtype": "float16",
1036
- "shape": [4, 8, 128],
1037
- "data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.11, "scale": 4.0 }
1038
- }
1039
  },
1040
  "outputs": { "y": { "dtype": "float16", "shape": [4], "tolerance": 0.05, "relTolerance": 0.002 } }
1041
  },
@@ -1058,7 +1043,7 @@
1058
  "x": {
1059
  "dtype": "float16",
1060
  "shape": [4096, 64],
1061
- "data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.11, "scale": 4.0 }
1062
  }
1063
  },
1064
  "outputs": { "y": { "dtype": "float16", "shape": [64], "tolerance": 0.05, "relTolerance": 0.002 } }
@@ -1141,6 +1126,30 @@
1141
  "data": { "kind": "values", "values": [0, 0, 0, 0, 0, 0] }
1142
  }
1143
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1144
  }
1145
  ]
1146
  }
 
1
  {
 
2
  "fixtureArrays": {
3
  "int32_axis0_tiled_64x32_input_x": [16777216, 16777245, 16777274, 16777236, 16777265, 16777227, 16777256, 16777218, 16777247, 16777276, 16777238, 16777267, 16777229, 16777258, 16777220, 16777249, 16777278, 16777240, 16777269, 16777231, 16777260, 16777222, 16777251, 16777280, 16777242, 16777271, 16777233, 16777262, 16777224, 16777253, 16777282, 16777244, 16777273, 16777235, 16777264, 16777226, 16777255, 16777217, 16777246, 16777275, 16777237, 16777266, 16777228, 16777257, 16777219, 16777248, 16777277, 16777239, 16777268, 16777230, 16777259, 16777221, 16777250, 16777279, 16777241, 16777270, 16777232, 16777261, 16777223, 16777252, 16777281, 16777243, 16777272, 16777234, 16777263, 16777225, 16777254],
4
+ "rank3_axis1_middle_no_keepdims_input_x": [-1, 7, -3, 4, 5, -6, 2, -8, 0, 3, 9, -2, -4, -1, -7, 6, 8, 2, -5, 1, -9, 4, 3, 0],
5
+ "f16_axis0_splitk_8192x8_input_x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
6
  },
7
  "cases": [
8
  {
9
  "name": "all_axes_flat_negative_infinity_identity_8192",
10
  "provenance": {
11
+ "notes": "Exactly 8,192 rank-1 elements exercise the inclusive lower boundary of the parallel full reduction while preserving the negative-infinity identity."
12
  },
13
  "attrs": { "axes": [0], "keepdims": 0 },
14
  "inputs": { "x": { "dtype": "float32", "shape": [8192], "data": { "kind": "negativeInfinity" } } },
 
113
  "name": "ort_axis1_nan_seed_comparison_gpu_gap",
114
  "skipGpu": {
115
  "category": "todo",
116
+ "reason": "This fixture requires first-element NaN incumbent semantics, which the parallel max reduction does not implement."
117
  },
118
  "provenance": {
119
  "source": "onnxruntime/test/providers/cpu/reduction/reduction_ops_test.cc",
 
237
  "provenance": {
238
  "source": "onnxruntime/test/providers/cpu/reduction/reduction_ops_test.cc",
239
  "test": "ReductionOpTest.ReduceMax_int32",
240
+ "notes": "Adapts the integer source fixture to a single reduction axis, which is the supported contract for this package."
241
  },
242
  "attrs": { "axes": [1], "keepdims": 1 },
243
  "inputs": {
 
615
  "provenance": {
616
  "source": "onnxruntime/test/providers/cpu/reduction/reduction_ops_test.cc",
617
  "test": "test_bool_ReduceMax_16",
618
+ "notes": "ONNX ReduceMax-20 permits Boolean input; this case exercises the standard Boolean reduction route."
619
  },
620
  "inputs": {
621
  "x": {
 
632
  "provenance": {
633
  "source": "onnxruntime/test/providers/cpu/reduction/reduction_ops_test.cc",
634
  "test": "test_bool_ReduceMax_18",
635
+ "notes": "ONNX ReduceMax-20 permits Boolean input; this case exercises the standard Boolean reduction route."
636
  },
637
  "inputs": {
638
  "x": {
 
662
  "name": "rank3_lastaxis_cols1024_tree_f32",
663
  "attrs": { "axes": [2], "keepdims": 0 },
664
  "inputs": {
665
+ "x": { "dtype": "float32", "shape": [2, 2, 1024], "data": { "kind": "linspace", "start": -4.0, "end": 5.0 } }
 
 
 
 
 
 
 
666
  },
667
  "outputs": { "y": { "dtype": "float32", "shape": [2, 2], "tolerance": 0.00001 } }
668
  },
669
  {
670
  "name": "lastaxis_vocab_width_2x50257_scalar",
671
  "provenance": {
672
+ "notes": "A 50,257-element odd last axis exercises scalar row reduction, including the tree implementation on tiers without subgroups."
673
  },
674
  "attrs": { "axes": [1], "keepdims": 0 },
675
  "inputs": {
676
+ "x": { "dtype": "float32", "shape": [2, 50257], "data": { "kind": "linspace", "start": -42.0, "end": 42.0 } }
 
 
 
 
677
  },
678
+ "outputs": { "y": { "dtype": "float32", "shape": [2], "tolerance": 0 } }
 
 
679
  },
680
  {
681
  "name": "rank3_lastaxis_cols1024_tree_i32",
682
  "attrs": { "axes": [2], "keepdims": 0 },
683
  "inputs": {
684
+ "x": { "dtype": "int32", "shape": [2, 2, 1024], "data": { "kind": "linspace", "start": -2048, "end": 2047 } }
 
 
 
 
685
  },
686
  "outputs": { "y": { "dtype": "int32", "shape": [2, 2], "tolerance": 0.00001 } }
687
  },
 
750
  {
751
  "name": "int32_subgroup_scalar_last_axis_2x67",
752
  "attrs": { "axes": [1], "keepdims": 0 },
753
+ "inputs": { "x": { "dtype": "int32", "shape": [2, 67], "data": { "kind": "linspace", "start": -67, "end": 66 } } },
 
 
 
 
 
 
 
 
 
754
  "outputs": { "y": { "dtype": "int32", "shape": [2], "tolerance": 0 } }
755
  },
756
  {
 
840
  {
841
  "name": "all_axes_flat_fullreduce_101x103_nonmul4_keepdims",
842
  "provenance": {
843
+ "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."
844
  },
845
  "attrs": { "keepdims": 1 },
846
  "inputs": {
 
960
  "x": {
961
  "dtype": "float16",
962
  "shape": [8192, 8],
963
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/f16_axis0_splitk_8192x8_input_x" } }
964
  }
965
  },
966
  "outputs": { "y": { "dtype": "float16", "shape": [8], "tolerance": 0.05, "relTolerance": 0.002 } }
967
  },
968
+ {
969
+ "name": "f16_axis0_splitk_8192x8_distinct_columns",
970
+ "attrs": { "axes": [0], "keepdims": 0 },
971
+ "inputs": {
972
+ "x": {
973
+ "dtype": "float16",
974
+ "shape": [8192, 8],
975
+ "data": {
976
+ "kind": "cycle",
977
+ "values": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, -1.5, -2.5, -3.5, -4.5, -5.5, -6.5, -7.5, -8.5, 0.0, 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75]
978
+ }
979
+ }
980
+ },
981
+ "outputs": { "y": { "dtype": "float16", "shape": [8], "tolerance": 0 } }
982
+ },
983
  {
984
  "name": "f16_last_axis_vec4_8x1024",
985
  "attrs": { "axes": [1], "keepdims": 0 },
 
1020
  "name": "f16_suffix_vec4_4x8x128",
1021
  "attrs": { "axes": [1, 2], "keepdims": 0 },
1022
  "inputs": {
1023
+ "x": { "dtype": "float16", "shape": [4, 8, 128], "data": { "kind": "linspace", "start": 0.0, "end": 4.0 } }
 
 
 
 
1024
  },
1025
  "outputs": { "y": { "dtype": "float16", "shape": [4], "tolerance": 0.05, "relTolerance": 0.002 } }
1026
  },
 
1043
  "x": {
1044
  "dtype": "float16",
1045
  "shape": [4096, 64],
1046
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/f16_axis0_splitk_8192x8_input_x" } }
1047
  }
1048
  },
1049
  "outputs": { "y": { "dtype": "float16", "shape": [64], "tolerance": 0.05, "relTolerance": 0.002 } }
 
1126
  "data": { "kind": "values", "values": [0, 0, 0, 0, 0, 0] }
1127
  }
1128
  }
1129
+ },
1130
+ {
1131
+ "name": "subgroup_rows_last_axis_f32_96x256",
1132
+ "attrs": { "axes": [-1], "keepdims": 0 },
1133
+ "inputs": {
1134
+ "x": {
1135
+ "dtype": "float32",
1136
+ "shape": [96, 256],
1137
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29 }
1138
+ }
1139
+ },
1140
+ "outputs": { "y": { "dtype": "float32", "shape": [96], "tolerance": 0 } }
1141
+ },
1142
+ {
1143
+ "name": "subgroup_rows_last_axis_f16_80x1024",
1144
+ "attrs": { "axes": [1], "keepdims": 0 },
1145
+ "inputs": {
1146
+ "x": {
1147
+ "dtype": "float16",
1148
+ "shape": [80, 1024],
1149
+ "data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.11, "scale": 4.0 }
1150
+ }
1151
+ },
1152
+ "outputs": { "y": { "dtype": "float16", "shape": [80], "tolerance": 0.05, "relTolerance": 0.002 } }
1153
  }
1154
  ]
1155
  }