Xenova HF Staff commited on
Commit
3ad0db8
·
verified ·
1 Parent(s): eefaea5

sync 2e7068faf55e

Browse files
README.md CHANGED
@@ -1,3 +1,91 @@
1
  ---
 
2
  license: apache-2.0
 
 
 
 
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ library_name: kernels
3
  license: apache-2.0
4
+ tags:
5
+ - kernel
6
+ - webgpu
7
+ - wgsl
8
  ---
9
+ # ai.onnx.ReduceLogSumExp
10
+
11
+ `ai.onnx` · standard ONNX operator · ONNX opset ≥ 18
12
+
13
+ ## Description
14
+
15
+ Computes `log(sum(exp(x)))` over the specified axes of the input tensor. The output rank matches the input when `keepdims` is 1; reduced dimensions are pruned when `keepdims` is 0. Reduction over an empty set of values yields negative infinity.
16
+
17
+ See the [ONNX `ReduceLogSumExp` spec](https://onnx.ai/onnx/operators/onnx__ReduceLogSumExp.html) for the reference semantics.
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. | required |
30
+
31
+ ## Attributes
32
+
33
+ Default values (overridable per request):
34
+
35
+ | Attribute | Default | Description |
36
+ | --- | --- | --- |
37
+ | `keepdims` | `1` | If 1, retains the reduced dimension with size 1 in the output; if 0, the reduced dimension is removed. |
38
+ | `noop_with_empty_axes` | `0` | When 1 and `axes` is empty, acts as an identity (no reduction); when 0 and `axes` is empty, reduces 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
+
43
+ | Variable | Allowed dtypes |
44
+ | --- | --- |
45
+ | `T` | `float32`, `float16`, `int32` |
46
+
47
+ ## Device requirements
48
+
49
+ Some implementation variants require `subgroups`. These are route-specific capabilities, not package-wide requirements; availability also depends on the request shape and dtype.
50
+
51
+ ## Files
52
+
53
+ - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, provenance)
54
+ - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
55
+ - [`test.json`](build/webgpu/test.json) — correctness cases
56
+ - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
57
+ - [`reduce-axis-split-reduce.wgsl.jinja`](build/webgpu/reduce-axis-split-reduce.wgsl.jinja)
58
+ - [`reduce-axis0-splitk-combine.wgsl.jinja`](build/webgpu/reduce-axis0-splitk-combine.wgsl.jinja)
59
+ - [`reduce-axis0-splitk-reduce.wgsl.jinja`](build/webgpu/reduce-axis0-splitk-reduce.wgsl.jinja)
60
+ - [`reduce-axis0-tilecols.wgsl.jinja`](build/webgpu/reduce-axis0-tilecols.wgsl.jinja)
61
+ - [`reduce-flat-combine-logsumexp.wgsl.jinja`](build/webgpu/reduce-flat-combine-logsumexp.wgsl.jinja)
62
+ - [`reduce-flat-partial-logsumexp.wgsl.jinja`](build/webgpu/reduce-flat-partial-logsumexp.wgsl.jinja)
63
+ - [`reduce-i32-axes02.wgsl.jinja`](build/webgpu/reduce-i32-axes02.wgsl.jinja)
64
+ - [`reduce-noop-empty-axes.wgsl.jinja`](build/webgpu/reduce-noop-empty-axes.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
+ The loader automatically allocates outputs whose metadata it can derive from the manifest contract and this call.
72
+
73
+ The explicit `outputs` entries provide shape and logical dtype metadata for the results listed below:
74
+
75
+ - `y`
76
+
77
+ Each entry either requests an optional result or supplies metadata that cannot be inferred from the inputs.
78
+
79
+ The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
80
+
81
+ Replace each `*Data` placeholder with a typed array containing the corresponding input data.
82
+
83
+ ```js
84
+ import { getKernel } from "@huggingface/kernels";
85
+
86
+ const kernel = await getKernel("webgpu-kernels/ai.onnx.ReduceLogSumExp", { version: 1 });
87
+ // Explicit destinations request optional results or supply metadata that cannot be inferred.
88
+ const { y } = await kernel({ x: { data: xData, shape: [] } }, {
89
+ outputs: { y: { shape: [], dtype: "float32" } },
90
+ });
91
+ ```
build/webgpu/bench.json ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "op": "ai.onnx.ReduceLogSumExp",
3
+ "cases": [
4
+ {
5
+ "name": "1024x1024_axis1",
6
+ "attrs": { "axes": [1], "keepdims": 0 },
7
+ "inputs": { "x": { "dtype": "float32", "shape": [1024, 1024] } },
8
+ "outputs": { "y": { "dtype": "float32", "shape": [1024] } }
9
+ },
10
+ {
11
+ "name": "reducelogsumexp-fullreduce-r3-256x256x256-keepdims",
12
+ "preset": "smoke",
13
+ "vars": { "rows": 65536, "cols": 256 },
14
+ "attrs": { "keepdims": 1 },
15
+ "inputs": { "x": { "shape": [256, 256, 256], "dtype": "float32", "dist": "normal", "seed": 811, "scale": 0.5 } },
16
+ "outputs": { "y": { "shape": [1, 1, 1], "dtype": "float32" } },
17
+ "bench": {
18
+ "metrics": [
19
+ { "type": "bandwidth", "name": "stable max-plus-exp traffic", "value": "2 * args.rows * args.cols * 4" }
20
+ ]
21
+ }
22
+ },
23
+ {
24
+ "name": "reducelogsumexp-axis0-f32-1024x512",
25
+ "preset": "smoke",
26
+ "vars": { "rows": 1024, "cols": 512 },
27
+ "attrs": { "axes": [0], "keepdims": 0 },
28
+ "inputs": { "x": { "shape": [1024, 512], "dtype": "float32", "dist": "normal", "seed": 118, "scale": 0.2 } },
29
+ "outputs": { "y": { "shape": [512], "dtype": "float32" } },
30
+ "bench": {
31
+ "primary": true,
32
+ "metrics": [
33
+ { "type": "bandwidth", "name": "stable max-plus-exp traffic", "value": "2 * args.rows * args.cols * 4" }
34
+ ]
35
+ }
36
+ },
37
+ {
38
+ "name": "reducelogsumexp-lastaxis-f32-4096x4096",
39
+ "preset": "smoke",
40
+ "vars": { "rows": 4096, "cols": 4096 },
41
+ "attrs": { "axes": [1], "keepdims": 0 },
42
+ "inputs": { "x": { "shape": [4096, 4096], "dtype": "float32", "dist": "normal", "seed": 117, "scale": 0.2 } },
43
+ "outputs": { "y": { "shape": [4096], "dtype": "float32" } },
44
+ "bench": {
45
+ "primary": true,
46
+ "metrics": [
47
+ { "type": "bandwidth", "name": "stable max-plus-exp traffic", "value": "2 * args.rows * args.cols * 4" }
48
+ ]
49
+ }
50
+ },
51
+ {
52
+ "name": "axis0-large-65536x512",
53
+ "preset": "smoke",
54
+ "vars": { "rows": 65536, "cols": 512 },
55
+ "attrs": { "axes": [0], "keepdims": 0 },
56
+ "inputs": { "x": { "shape": [65536, 512], "dtype": "float32", "dist": "normal", "scale": 0.2, "seed": 311 } },
57
+ "outputs": { "y": { "shape": [512], "dtype": "float32" } },
58
+ "bench": {
59
+ "primary": true,
60
+ "metrics": [
61
+ { "type": "bandwidth", "name": "stable max-plus-exp traffic", "value": "2 * args.rows * args.cols * 4" }
62
+ ]
63
+ }
64
+ },
65
+ {
66
+ "name": "axis0-tall-16384x1024",
67
+ "preset": "smoke",
68
+ "vars": { "rows": 16384, "cols": 1024 },
69
+ "attrs": { "axes": [0], "keepdims": 0 },
70
+ "inputs": { "x": { "shape": [16384, 1024], "dtype": "float32", "dist": "normal", "scale": 0.2, "seed": 312 } },
71
+ "outputs": { "y": { "shape": [1024], "dtype": "float32" } },
72
+ "bench": {
73
+ "primary": true,
74
+ "metrics": [
75
+ { "type": "bandwidth", "name": "stable max-plus-exp traffic", "value": "2 * args.rows * args.cols * 4" }
76
+ ]
77
+ }
78
+ },
79
+ {
80
+ "name": "fullreduce-single-lane-r3-101x101x101-numel-not-mult4",
81
+ "preset": "stress",
82
+ "vars": { "numel": 1030301 },
83
+ "attrs": { "keepdims": 1 },
84
+ "inputs": { "x": { "shape": [101, 101, 101], "dtype": "float32", "dist": "normal", "seed": 733, "scale": 0.3 } },
85
+ "outputs": { "y": { "shape": [1, 1, 1], "dtype": "float32", "dist": "empty" } },
86
+ "bench": {
87
+ "metrics": [{ "type": "bandwidth", "name": "stable max-plus-exp traffic", "value": "2 * numel(shapes.x) * 4" }]
88
+ }
89
+ },
90
+ {
91
+ "name": "axis0-few-lane-starvation-131072x8",
92
+ "preset": "stress",
93
+ "vars": { "rows": 131072, "cols": 8 },
94
+ "attrs": { "axes": [0], "keepdims": 0 },
95
+ "inputs": { "x": { "shape": [131072, 8], "dtype": "float32", "dist": "normal", "seed": 941, "scale": 0.2 } },
96
+ "outputs": { "y": { "shape": [8], "dtype": "float32", "dist": "empty" } },
97
+ "bench": {
98
+ "primary": true,
99
+ "metrics": [
100
+ { "type": "bandwidth", "name": "stable max-plus-exp traffic", "value": "2 * args.rows * args.cols * 4" }
101
+ ]
102
+ }
103
+ },
104
+ {
105
+ "name": "reducelogsumexp-spatial-axes12-f32-16x256x256-low-lane",
106
+ "preset": "stress",
107
+ "provenance": {
108
+ "source": "authored for variant coverage",
109
+ "notes": "Low-lane rank-3 contiguous-suffix reduction that verifies the vec4 subgroup reducer and no-subgroup workgroup-tree fallback."
110
+ },
111
+ "vars": { "batch": 16, "height": 256, "width": 256 },
112
+ "attrs": { "axes": [1, 2], "keepdims": 1 },
113
+ "inputs": { "x": { "shape": [16, 256, 256], "dtype": "float32", "dist": "normal", "seed": 910, "scale": 0.2 } },
114
+ "outputs": { "y": { "shape": [16, 1, 1], "dtype": "float32", "dist": "empty" } },
115
+ "bench": {
116
+ "metrics": [
117
+ {
118
+ "type": "bandwidth",
119
+ "name": "stable max-plus-exp traffic",
120
+ "value": "2 * args.batch * args.height * args.width * 4"
121
+ }
122
+ ]
123
+ }
124
+ }
125
+ ],
126
+ "tunableSpace": { "WORKGROUP_SIZE": [64, 128, 256] }
127
+ }
build/webgpu/manifest.json ADDED
@@ -0,0 +1,1061 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "domain": "ai.onnx",
3
+ "name": "ReduceLogSumExp",
4
+ "sinceVersion": 18,
5
+ "description": "Computes `log(sum(exp(x)))` over the specified axes of the input tensor. The output rank matches the input when `keepdims` is 1; reduced dimensions are pruned when `keepdims` is 0. Reduction over an empty set of values yields negative infinity.",
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."
13
+ }
14
+ ],
15
+ "attributes": { "keepdims": 1, "noop_with_empty_axes": 0, "axes": [] },
16
+ "attributeDescriptions": {
17
+ "keepdims": "If 1, retains the reduced dimension with size 1 in the output; if 0, the reduced dimension is removed.",
18
+ "noop_with_empty_axes": "When 1 and `axes` is empty, acts as an identity (no reduction); when 0 and `axes` is empty, reduces 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"] },
23
+ "args": {
24
+ "x": { "kind": "tensor", "semantic": "data", "role": "input" },
25
+ "y": { "kind": "tensor", "semantic": "reduced", "role": "output" }
26
+ },
27
+ "derive": {
28
+ "deviceWorkgroupCap": "min(device.limits.maxComputeInvocationsPerWorkgroup, device.limits.maxComputeWorkgroupSizeX)",
29
+ "reduceWorkgroupSize": "min(tunables.WORKGROUP_SIZE, deviceWorkgroupCap)",
30
+ "treeWorkgroupOk": "reduceWorkgroupSize > 0 and pow2ceil(reduceWorkgroupSize) == reduceWorkgroupSize and reduceWorkgroupSize * dtypeBytes(\"float32\") <= device.limits.maxComputeWorkgroupStorageSize",
31
+ "subgroupWorkgroupFloor": "min(reduceWorkgroupSize, max(1, device.adapterInfo.subgroupMaxSize))",
32
+ "lastAxisRows": "rows(shapes.data, ranks.data - 1) if ranks.data > 0 else 1",
33
+ "lastAxisCols": "dim(shapes.data, ranks.data - 1) if ranks.data > 0 else 1",
34
+ "rowSerialPreferred": "lastAxisRows >= tunables.ROW_SERIAL_MIN_ROWS and lastAxisCols <= tunables.ROW_SERIAL_MAX_COLS",
35
+ "axis0Rows": "dim(shapes.data, 0) if ranks.data >= 2 else 0",
36
+ "axis0Cols": "dim(shapes.data, 1) if ranks.data >= 2 else 0",
37
+ "axis0SplitCount": "min(tunables.AXIS0_MAX_SPLITS, pow2ceil(ceilDiv(axis0Rows, tunables.AXIS0_SPLIT_TARGET_ROWS)))",
38
+ "axis0SplitScratchBytes": "3 * axis0SplitCount * axis0Cols * dtypeBytes(\"float32\")",
39
+ "axis0SplitPathFits": "axis0SplitCount <= device.limits.maxComputeWorkgroupsPerDimension and ceilDiv(ceilDiv(axis0Cols, reduceWorkgroupSize), device.limits.maxComputeWorkgroupsPerDimension) <= device.limits.maxComputeWorkgroupsPerDimension and axis0SplitScratchBytes <= device.limits.maxStorageBufferBindingSize and axis0SplitScratchBytes <= device.limits.maxBufferSize",
40
+ "reduceAxis": "(attrs.axes[0] + ranks.data if attrs.axes[0] < 0 else attrs.axes[0]) if ((attrs.axes | length) == 1 and isUniqueIntList(attrs.axes, 0 - ranks.data, ranks.data, 1)) else ranks.data",
41
+ "axisSplitDim": "dim(shapes.data, reduceAxis) if ranks.data >= 2 and reduceAxis < ranks.data else 0",
42
+ "axisSplitInner": "inner(shapes.data, reduceAxis) if ranks.data >= 2 and reduceAxis < ranks.data else 1",
43
+ "axisSplitOutputs": "numel(shapes.reduced)",
44
+ "axisSplitCount": "min(tunables.AXIS0_MAX_SPLITS, pow2ceil(ceilDiv(axisSplitDim, tunables.AXIS0_SPLIT_TARGET_ROWS)))",
45
+ "axisSplitScratchBytes": "3 * axisSplitCount * axisSplitOutputs * 4",
46
+ "axisSplitPathFits": "axisSplitCount <= device.limits.maxComputeWorkgroupsPerDimension and ceilDiv(ceilDiv(axisSplitOutputs, reduceWorkgroupSize), device.limits.maxComputeWorkgroupsPerDimension) <= device.limits.maxComputeWorkgroupsPerDimension and axisSplitScratchBytes <= device.limits.maxStorageBufferBindingSize and axisSplitScratchBytes <= device.limits.maxBufferSize",
47
+ "axis0TilePathFits": "treeWorkgroupOk and tunables.AXIS0_TILE_COLS > 0 and tunables.AXIS0_TILE_COLS <= reduceWorkgroupSize and reduceWorkgroupSize % tunables.AXIS0_TILE_COLS == 0",
48
+ "flatItems": "numel(shapes.data) / tunables.VECTOR_WIDTH if numel(shapes.data) % tunables.VECTOR_WIDTH == 0 else numel(shapes.data)",
49
+ "flatSplitCount": "max(1, min(tunables.FULL_REDUCE_MAX_SPLITS, ceilDiv(flatItems, reduceWorkgroupSize)))",
50
+ "flatScratchBytes": "3 * flatSplitCount * dtypeBytes(\"float32\")",
51
+ "flatPathFits": "treeWorkgroupOk and flatSplitCount <= device.limits.maxComputeWorkgroupsPerDimension and flatScratchBytes <= device.limits.maxStorageBufferBindingSize and flatScratchBytes <= device.limits.maxBufferSize",
52
+ "flatParallelCovered": "(dtypes.T == \"f32\" or dtypes.T == \"f16\") and f16Ok(dtypes.T) and numel(shapes.reduced) == 1 and numel(shapes.data) >= tunables.FULL_REDUCE_MIN_ELEMENTS and flatPathFits",
53
+ "contiguousSuffixParallelCovered": "(dtypes.T == \"f32\" or dtypes.T == \"f16\") and f16Ok(dtypes.T) and numel(shapes.reduced) > 0 and numel(shapes.data) % numel(shapes.reduced) == 0 and numel(shapes.data) / numel(shapes.reduced) >= tunables.CONTIGUOUS_SUFFIX_MIN_COLS and ((ranks.data == 3 and hasAxis(attrs.axes, 0, 3) == false and hasAxis(attrs.axes, 1, 3) and hasAxis(attrs.axes, 2, 3) and numel(shapes.reduced) == dim(shapes.data, 0)) or (ranks.data == 4 and hasAxis(attrs.axes, 0, 4) == false and hasAxis(attrs.axes, 1, 4) == false and hasAxis(attrs.axes, 2, 4) and hasAxis(attrs.axes, 3, 4) and numel(shapes.reduced) == dim(shapes.data, 0) * dim(shapes.data, 1)) or (ranks.data == 4 and hasAxis(attrs.axes, 0, 4) == false and hasAxis(attrs.axes, 1, 4) and hasAxis(attrs.axes, 2, 4) and hasAxis(attrs.axes, 3, 4) and numel(shapes.reduced) == dim(shapes.data, 0)))"
54
+ },
55
+ "tunables": {
56
+ "WORKGROUP_SIZE": 256,
57
+ "VECTOR_WIDTH": 4,
58
+ "ROW_PARALLEL_MIN_COLS": 64,
59
+ "SUBGROUP_MIN_COLS": 256,
60
+ "SUBGROUP_SMALL_ROW_LIMIT": 32768,
61
+ "AXIS0_SPLIT_MIN_ROWS": 8192,
62
+ "AXIS0_SPLIT_TARGET_ROWS": 256,
63
+ "AXIS0_MAX_SPLITS": 128,
64
+ "AXIS0_TILE_MIN_ROWS": 64,
65
+ "AXIS0_TILE_MIN_COLS": 16,
66
+ "AXIS0_TILE_COLS": 16,
67
+ "AXIS_SPLIT_TILE_COLS": 8,
68
+ "FULL_REDUCE_MIN_ELEMENTS": 8192,
69
+ "FULL_REDUCE_MAX_SPLITS": 256,
70
+ "CONTIGUOUS_SUFFIX_MIN_COLS": 256,
71
+ "AXES02_WORKGROUP_SIZE": 256,
72
+ "ROW_SERIAL_MIN_ROWS": 8192,
73
+ "ROW_SERIAL_MAX_COLS": 1024
74
+ },
75
+ "bindingSets": {
76
+ "axes02": [
77
+ { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
78
+ { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
79
+ {
80
+ "name": "params",
81
+ "semantic": "kernel.params",
82
+ "buffer": { "type": "uniform" },
83
+ "struct": {
84
+ "name": "Params",
85
+ "fields": [
86
+ { "name": "d0", "type": "u32", "value": "dim(shapes.data, 0)" },
87
+ { "name": "d1", "type": "u32", "value": "dim(shapes.data, 1)" },
88
+ { "name": "d2", "type": "u32", "value": "dim(shapes.data, 2)" },
89
+ { "name": "outCount", "type": "u32", "value": "numel(shapes.reduced)" }
90
+ ]
91
+ }
92
+ }
93
+ ],
94
+ "suffixVec4": [
95
+ {
96
+ "name": "x",
97
+ "arg": "x",
98
+ "semantic": "data",
99
+ "buffer": { "type": "read-only-storage" },
100
+ "elementType": "$vectorScalar"
101
+ },
102
+ { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
103
+ {
104
+ "name": "params",
105
+ "semantic": "kernel.params",
106
+ "buffer": { "type": "uniform" },
107
+ "struct": {
108
+ "name": "Params",
109
+ "fields": [
110
+ { "name": "rows", "type": "u32", "value": "numel(shapes.reduced)" },
111
+ {
112
+ "name": "chunkCount",
113
+ "type": "u32",
114
+ "value": "numel(shapes.data) / numel(shapes.reduced) / tunables.VECTOR_WIDTH"
115
+ }
116
+ ]
117
+ }
118
+ }
119
+ ],
120
+ "suffixScalar": [
121
+ { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
122
+ { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
123
+ {
124
+ "name": "params",
125
+ "semantic": "kernel.params",
126
+ "buffer": { "type": "uniform" },
127
+ "struct": {
128
+ "name": "Params",
129
+ "fields": [
130
+ { "name": "rows", "type": "u32", "value": "numel(shapes.reduced)" },
131
+ { "name": "cols", "type": "u32", "value": "numel(shapes.data) / numel(shapes.reduced)" }
132
+ ]
133
+ }
134
+ }
135
+ ],
136
+ "elementwise": [
137
+ { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
138
+ { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
139
+ {
140
+ "name": "params",
141
+ "semantic": "kernel.params",
142
+ "buffer": { "type": "uniform" },
143
+ "struct": { "name": "Params", "fields": [{ "name": "count", "type": "u32", "value": "numel(shapes.reduced)" }] }
144
+ }
145
+ ],
146
+ "lastAxisVec4": [
147
+ {
148
+ "name": "x",
149
+ "arg": "x",
150
+ "semantic": "data",
151
+ "buffer": { "type": "read-only-storage" },
152
+ "elementType": "$vectorScalar"
153
+ },
154
+ { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
155
+ {
156
+ "name": "params",
157
+ "semantic": "kernel.params",
158
+ "buffer": { "type": "uniform" },
159
+ "struct": {
160
+ "name": "Params",
161
+ "fields": [
162
+ { "name": "rows", "type": "u32", "value": "rows(shapes.data, ranks.data - 1)" },
163
+ { "name": "chunkCount", "type": "u32", "value": "dim(shapes.data, ranks.data - 1) / tunables.VECTOR_WIDTH" }
164
+ ]
165
+ }
166
+ }
167
+ ],
168
+ "lastAxisScalar": [
169
+ { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
170
+ { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
171
+ {
172
+ "name": "params",
173
+ "semantic": "kernel.params",
174
+ "buffer": { "type": "uniform" },
175
+ "struct": {
176
+ "name": "Params",
177
+ "fields": [
178
+ { "name": "rows", "type": "u32", "value": "rows(shapes.data, ranks.data - 1)" },
179
+ { "name": "cols", "type": "u32", "value": "dim(shapes.data, ranks.data - 1)" }
180
+ ]
181
+ }
182
+ }
183
+ ],
184
+ "lastAxisScalarSubgroup": [
185
+ { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
186
+ { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
187
+ {
188
+ "name": "params",
189
+ "semantic": "kernel.params",
190
+ "buffer": { "type": "uniform" },
191
+ "struct": {
192
+ "name": "Params",
193
+ "fields": [
194
+ { "name": "rows", "type": "u32", "value": "rows(shapes.data, ranks.data - 1)" },
195
+ { "name": "chunkCount", "type": "u32", "value": "dim(shapes.data, ranks.data - 1)" }
196
+ ]
197
+ }
198
+ }
199
+ ],
200
+ "scalar": [
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": "rows", "type": "u32", "value": "1" },
211
+ { "name": "cols", "type": "u32", "value": "1" },
212
+ { "name": "outCount", "type": "u32", "value": "1" }
213
+ ]
214
+ }
215
+ }
216
+ ],
217
+ "rank1Axis0": [
218
+ { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
219
+ { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
220
+ {
221
+ "name": "params",
222
+ "semantic": "kernel.params",
223
+ "buffer": { "type": "uniform" },
224
+ "struct": {
225
+ "name": "Params",
226
+ "fields": [
227
+ { "name": "rows", "type": "u32", "value": "dim(shapes.data, 0)" },
228
+ { "name": "cols", "type": "u32", "value": "1" },
229
+ { "name": "outCount", "type": "u32", "value": "numel(shapes.reduced)" }
230
+ ]
231
+ }
232
+ }
233
+ ],
234
+ "rank2Serial": [
235
+ { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
236
+ { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
237
+ {
238
+ "name": "params",
239
+ "semantic": "kernel.params",
240
+ "buffer": { "type": "uniform" },
241
+ "struct": {
242
+ "name": "Params",
243
+ "fields": [
244
+ { "name": "rows", "type": "u32", "value": "dim(shapes.data, 0)" },
245
+ { "name": "cols", "type": "u32", "value": "dim(shapes.data, 1)" },
246
+ { "name": "outCount", "type": "u32", "value": "numel(shapes.reduced)" }
247
+ ]
248
+ }
249
+ }
250
+ ],
251
+ "rank2SerialAxis1": [
252
+ { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
253
+ { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
254
+ {
255
+ "name": "params",
256
+ "semantic": "kernel.params",
257
+ "buffer": { "type": "uniform" },
258
+ "struct": {
259
+ "name": "Params",
260
+ "fields": [
261
+ { "name": "cols", "type": "u32", "value": "dim(shapes.data, 1)" },
262
+ { "name": "outCount", "type": "u32", "value": "numel(shapes.reduced)" }
263
+ ]
264
+ }
265
+ }
266
+ ],
267
+ "axis0Parallel": [
268
+ { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
269
+ { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
270
+ {
271
+ "name": "params",
272
+ "semantic": "kernel.params",
273
+ "buffer": { "type": "uniform" },
274
+ "struct": {
275
+ "name": "Params",
276
+ "fields": [
277
+ { "name": "rows", "type": "u32", "value": "dim(shapes.data, 0)" },
278
+ { "name": "cols", "type": "u32", "value": "dim(shapes.data, 1)" }
279
+ ]
280
+ }
281
+ }
282
+ ],
283
+ "fullReduceSerial": [
284
+ { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
285
+ { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
286
+ {
287
+ "name": "params",
288
+ "semantic": "kernel.params",
289
+ "buffer": { "type": "uniform" },
290
+ "struct": {
291
+ "name": "Params",
292
+ "fields": [
293
+ { "name": "rows", "type": "u32", "value": "numel(shapes.data)" },
294
+ { "name": "cols", "type": "u32", "value": "1" },
295
+ { "name": "outCount", "type": "u32", "value": "numel(shapes.reduced)" }
296
+ ]
297
+ }
298
+ }
299
+ ],
300
+ "axisSplitReduce": [
301
+ { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
302
+ { "name": "partials", "semantic": "partials", "buffer": { "type": "storage" }, "elementType": "$partialElement" },
303
+ {
304
+ "name": "params",
305
+ "semantic": "kernel.params",
306
+ "buffer": { "type": "uniform" },
307
+ "struct": {
308
+ "name": "Params",
309
+ "fields": [
310
+ { "name": "axisDim", "type": "u32", "value": "axisSplitDim" },
311
+ { "name": "inner", "type": "u32", "value": "axisSplitInner" },
312
+ { "name": "outputs", "type": "u32", "value": "axisSplitOutputs" }
313
+ ]
314
+ }
315
+ }
316
+ ],
317
+ "axisSplitCombine": [
318
+ {
319
+ "name": "partials",
320
+ "semantic": "partials",
321
+ "buffer": { "type": "read-only-storage" },
322
+ "elementType": "$partialElement"
323
+ },
324
+ { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
325
+ {
326
+ "name": "params",
327
+ "semantic": "kernel.params",
328
+ "buffer": { "type": "uniform" },
329
+ "struct": { "name": "Params", "fields": [{ "name": "cols", "type": "u32", "value": "axisSplitOutputs" }] }
330
+ }
331
+ ],
332
+ "axis0SplitReduce": [
333
+ { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
334
+ { "name": "partials", "semantic": "partials", "buffer": { "type": "storage" }, "elementType": "$partialElement" },
335
+ {
336
+ "name": "params",
337
+ "semantic": "kernel.params",
338
+ "buffer": { "type": "uniform" },
339
+ "struct": {
340
+ "name": "Params",
341
+ "fields": [
342
+ { "name": "rows", "type": "u32", "value": "dim(shapes.data, 0)" },
343
+ { "name": "cols", "type": "u32", "value": "dim(shapes.data, 1)" }
344
+ ]
345
+ }
346
+ }
347
+ ],
348
+ "axis0SplitCombine": [
349
+ {
350
+ "name": "partials",
351
+ "semantic": "partials",
352
+ "buffer": { "type": "read-only-storage" },
353
+ "elementType": "$partialElement"
354
+ },
355
+ { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
356
+ {
357
+ "name": "params",
358
+ "semantic": "kernel.params",
359
+ "buffer": { "type": "uniform" },
360
+ "struct": { "name": "Params", "fields": [{ "name": "cols", "type": "u32", "value": "dim(shapes.data, 1)" }] }
361
+ }
362
+ ],
363
+ "rankNAxis": [
364
+ { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
365
+ { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
366
+ {
367
+ "name": "params",
368
+ "semantic": "kernel.params",
369
+ "buffer": { "type": "uniform" },
370
+ "struct": {
371
+ "name": "Params",
372
+ "fields": [
373
+ { "name": "axisDim", "type": "u32", "value": "axisSplitDim" },
374
+ { "name": "outCount", "type": "u32", "value": "numel(shapes.reduced)" }
375
+ ]
376
+ }
377
+ }
378
+ ],
379
+ "flatPartialF32": [
380
+ {
381
+ "name": "x",
382
+ "arg": "x",
383
+ "semantic": "data",
384
+ "buffer": { "type": "read-only-storage" },
385
+ "elementType": "$flatScalar"
386
+ },
387
+ { "name": "partials", "semantic": "partials", "buffer": { "type": "storage" }, "elementType": "f32" },
388
+ {
389
+ "name": "params",
390
+ "semantic": "kernel.params",
391
+ "buffer": { "type": "uniform" },
392
+ "struct": { "name": "Params", "fields": [{ "name": "count", "type": "u32", "value": "flatItems" }] }
393
+ }
394
+ ],
395
+ "flatCombineF32": [
396
+ { "name": "partials", "semantic": "partials", "buffer": { "type": "read-only-storage" }, "elementType": "f32" },
397
+ { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
398
+ {
399
+ "name": "params",
400
+ "semantic": "kernel.params",
401
+ "buffer": { "type": "uniform" },
402
+ "struct": { "name": "Params", "fields": [{ "name": "cols", "type": "u32", "value": "1" }] }
403
+ }
404
+ ],
405
+ "multiAxis": [
406
+ { "name": "x", "arg": "x", "semantic": "data", "buffer": { "type": "read-only-storage" }, "elementType": "$T" },
407
+ { "name": "y", "arg": "y", "semantic": "reduced", "buffer": { "type": "storage" }, "elementType": "$T" },
408
+ {
409
+ "name": "params",
410
+ "semantic": "kernel.params",
411
+ "buffer": { "type": "uniform" },
412
+ "struct": {
413
+ "name": "Params",
414
+ "fields": [{ "name": "outCount", "type": "u32", "value": "numel(shapes.reduced)" }]
415
+ }
416
+ }
417
+ ]
418
+ },
419
+ "variants": [
420
+ {
421
+ "id": "contiguous_suffix_subgroup_vec4",
422
+ "priority": 30,
423
+ "requires": { "features": ["subgroups"] },
424
+ "when": ["device.wgslLanguageFeatures.has(\"subgroup_id\")", "not flatParallelCovered", "contiguousSuffixParallelCovered", "(numel(shapes.data) / numel(shapes.reduced)) % tunables.VECTOR_WIDTH == 0"],
425
+ "constants": {
426
+ "scalar": "dtypes.T",
427
+ "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
428
+ "workgroupSize": "min(reduceWorkgroupSize, max(subgroupWorkgroupFloor, pow2ceil(ceilDiv(numel(shapes.data) / numel(shapes.reduced), tunables.VECTOR_WIDTH))))"
429
+ },
430
+ "passes": [
431
+ {
432
+ "id": "main",
433
+ "name": "ReduceLogSumExp.ContiguousSuffixSubgroupVec4",
434
+ "source": {
435
+ "shader": "reduce-row-subgroup.wgsl.jinja",
436
+ "inputs": {
437
+ "op": "\"logsumexp\"",
438
+ "vec4": true,
439
+ "castF32": "dtypes.T == \"f16\"",
440
+ "usesF16": "dtypes.T == \"f16\""
441
+ }
442
+ },
443
+ "subgroupCollectivesWidth": "portable",
444
+ "bindings": "suffixVec4",
445
+ "dispatch": { "workgroups": "numel(shapes.reduced)" }
446
+ }
447
+ ]
448
+ },
449
+ {
450
+ "id": "contiguous_suffix_tree_vec4",
451
+ "priority": 22,
452
+ "when": ["not flatParallelCovered", "contiguousSuffixParallelCovered", "(numel(shapes.data) / numel(shapes.reduced)) % tunables.VECTOR_WIDTH == 0", "treeWorkgroupOk"],
453
+ "constants": {
454
+ "scalar": "dtypes.T",
455
+ "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
456
+ "workgroupSize": "min(reduceWorkgroupSize, pow2ceil(ceilDiv(numel(shapes.data) / numel(shapes.reduced), tunables.VECTOR_WIDTH)))"
457
+ },
458
+ "passes": [
459
+ {
460
+ "id": "main",
461
+ "name": "ReduceLogSumExp.ContiguousSuffixTreeVec4",
462
+ "source": {
463
+ "shader": "reduce-row-tree.wgsl.jinja",
464
+ "inputs": {
465
+ "op": "\"logsumexp\"",
466
+ "vec4": true,
467
+ "castF32": "dtypes.T == \"f16\"",
468
+ "usesF16": "dtypes.T == \"f16\""
469
+ }
470
+ },
471
+ "bindings": "suffixVec4",
472
+ "dispatch": { "workgroups": "numel(shapes.reduced)" }
473
+ }
474
+ ]
475
+ },
476
+ {
477
+ "id": "contiguous_suffix_tree",
478
+ "priority": 21,
479
+ "when": ["not flatParallelCovered", "contiguousSuffixParallelCovered", "treeWorkgroupOk"],
480
+ "constants": {
481
+ "workgroupSize": "min(reduceWorkgroupSize, pow2ceil(numel(shapes.data) / numel(shapes.reduced)))",
482
+ "scalar": "dtypes.T"
483
+ },
484
+ "passes": [
485
+ {
486
+ "id": "main",
487
+ "name": "ReduceLogSumExp.ContiguousSuffixTree",
488
+ "source": {
489
+ "shader": "reduce-row-tree.wgsl.jinja",
490
+ "inputs": { "op": "\"logsumexp\"", "castF32": "dtypes.T == \"f16\"", "usesF16": "dtypes.T == \"f16\"" }
491
+ },
492
+ "bindings": "suffixScalar",
493
+ "dispatch": { "workgroups": "numel(shapes.reduced)" }
494
+ }
495
+ ]
496
+ },
497
+ {
498
+ "id": "multi_axis_rank3",
499
+ "priority": 8,
500
+ "when": ["not flatParallelCovered", "not contiguousSuffixParallelCovered", "f16Ok(dtypes.T)", "ranks.data == 3", "(attrs.keepdims == 1 and ranks.reduced == 3 and (dim(shapes.reduced, 0) == 1 if hasAxis(attrs.axes, 0, 3) else dim(shapes.reduced, 0) == dim(shapes.data, 0)) and (dim(shapes.reduced, 1) == 1 if hasAxis(attrs.axes, 1, 3) else dim(shapes.reduced, 1) == dim(shapes.data, 1)) and (dim(shapes.reduced, 2) == 1 if hasAxis(attrs.axes, 2, 3) else dim(shapes.reduced, 2) == dim(shapes.data, 2))) or (attrs.keepdims == 0 and ranks.reduced == 1)"],
501
+ "passes": [
502
+ {
503
+ "id": "main",
504
+ "name": "ReduceLogSumExp.MultiAxisRank3",
505
+ "source": {
506
+ "shader": "reduce-serial-axis.wgsl.jinja",
507
+ "inputs": {
508
+ "op": "\"logsumexp\"",
509
+ "indexing": "\"multiaxis\"",
510
+ "rank": 3,
511
+ "reduce": ["hasAxis(attrs.axes, 0, 3)", "hasAxis(attrs.axes, 1, 3)", "hasAxis(attrs.axes, 2, 3)"],
512
+ "dataShape": "shapes.data",
513
+ "outputShape": "shapes.reduced",
514
+ "outputRank": "ranks.reduced",
515
+ "keepDims": "attrs.keepdims != 0",
516
+ "intMode": "dtypes.T == \"i32\"",
517
+ "castF32": "dtypes.T == \"f16\"",
518
+ "usesF16": "dtypes.T == \"f16\""
519
+ }
520
+ },
521
+ "bindings": "multiAxis",
522
+ "dispatch": { "threads": "numel(shapes.reduced)", "workgroupSize": "reduceWorkgroupSize" }
523
+ }
524
+ ],
525
+ "constants": { "scalar": "dtypes.T" }
526
+ },
527
+ {
528
+ "id": "multi_axis_rank4",
529
+ "priority": 8,
530
+ "when": ["not flatParallelCovered", "not contiguousSuffixParallelCovered", "f16Ok(dtypes.T)", "ranks.data == 4", "attrs.noop_with_empty_axes == 0", "numel(shapes.reduced) == (1 if hasAxis(attrs.axes, 0, 4) else dim(shapes.data, 0)) * (1 if hasAxis(attrs.axes, 1, 4) else dim(shapes.data, 1)) * (1 if hasAxis(attrs.axes, 2, 4) else dim(shapes.data, 2)) * (1 if hasAxis(attrs.axes, 3, 4) else dim(shapes.data, 3))", "((attrs.keepdims == 1 and ranks.reduced == 4) or (attrs.keepdims == 0 and ranks.reduced < 4))"],
531
+ "passes": [
532
+ {
533
+ "id": "main",
534
+ "name": "ReduceLogSumExp.MultiAxisRank4",
535
+ "source": {
536
+ "shader": "reduce-serial-axis.wgsl.jinja",
537
+ "inputs": {
538
+ "op": "\"logsumexp\"",
539
+ "indexing": "\"multiaxis\"",
540
+ "rank": 4,
541
+ "reduce": ["hasAxis(attrs.axes, 0, 4)", "hasAxis(attrs.axes, 1, 4)", "hasAxis(attrs.axes, 2, 4)", "hasAxis(attrs.axes, 3, 4)"],
542
+ "dataShape": "shapes.data",
543
+ "outputShape": "shapes.reduced",
544
+ "outputRank": "ranks.reduced",
545
+ "keepDims": "attrs.keepdims != 0",
546
+ "intMode": "dtypes.T == \"i32\"",
547
+ "castF32": "dtypes.T == \"f16\"",
548
+ "usesF16": "dtypes.T == \"f16\""
549
+ }
550
+ },
551
+ "bindings": "multiAxis",
552
+ "dispatch": { "threads": "numel(shapes.reduced)", "workgroupSize": "reduceWorkgroupSize" }
553
+ }
554
+ ],
555
+ "constants": { "scalar": "dtypes.T" }
556
+ },
557
+ {
558
+ "id": "int32_rank3_axes02_keepdims",
559
+ "priority": 30,
560
+ "when": ["dtypes.T == \"i32\"", "ranks.data == 3", "attrs.keepdims == 1", "hasAxis(attrs.axes, 0, 3)", "hasAxis(attrs.axes, 2, 3)", "hasAxis(attrs.axes, 1, 3) == false", "dim(shapes.data, 0) > 0", "dim(shapes.data, 2) > 0", "ranks.reduced == 3", "dim(shapes.reduced, 0) == 1", "dim(shapes.reduced, 1) == dim(shapes.data, 1)", "dim(shapes.reduced, 2) == 1"],
561
+ "passes": [
562
+ {
563
+ "id": "main",
564
+ "name": "ReduceLogSumExp.Int32Rank3Axes02Keepdims",
565
+ "bindings": "axes02",
566
+ "dispatch": { "threads": "numel(shapes.reduced)", "workgroupSize": "axes02WorkgroupSize" },
567
+ "source": {
568
+ "shader": "reduce-i32-axes02.wgsl.jinja",
569
+ "inputs": { "op": "\"logsumexp\"", "workgroupSize": "axes02WorkgroupSize" }
570
+ }
571
+ }
572
+ ],
573
+ "derive": {
574
+ "axes02WorkgroupSize": "min(tunables.AXES02_WORKGROUP_SIZE, device.limits.maxComputeInvocationsPerWorkgroup, device.limits.maxComputeWorkgroupSizeX)"
575
+ }
576
+ },
577
+ {
578
+ "id": "noop_empty_axes",
579
+ "priority": 40,
580
+ "when": ["dtypes.T == \"f32\"", "attrs.noop_with_empty_axes == 1", "(attrs.axes | length) == 0", "sameShape(shapes.data, shapes.reduced)"],
581
+ "passes": [
582
+ {
583
+ "id": "main",
584
+ "name": "ReduceLogSumExp.NoopEmptyAxes",
585
+ "source": { "shader": "reduce-noop-empty-axes.wgsl.jinja", "inputs": { "op": "\"identity\"" } },
586
+ "bindings": "elementwise",
587
+ "dispatch": { "threads": "numel(shapes.reduced)", "workgroupSize": "reduceWorkgroupSize" }
588
+ }
589
+ ]
590
+ },
591
+ {
592
+ "id": "tree_last_axis_vec4",
593
+ "priority": 23,
594
+ "demoteWhen": ["rowSerialPreferred"],
595
+ "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"],
596
+ "constants": {
597
+ "scalar": "dtypes.T",
598
+ "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
599
+ "workgroupSize": "min(reduceWorkgroupSize, pow2ceil(ceilDiv(lastAxisCols, tunables.VECTOR_WIDTH)))"
600
+ },
601
+ "passes": [
602
+ {
603
+ "id": "main",
604
+ "name": "ReduceLogSumExp.TreeRowVec4",
605
+ "source": {
606
+ "shader": "reduce-row-tree.wgsl.jinja",
607
+ "inputs": {
608
+ "op": "\"logsumexp\"",
609
+ "vec4": true,
610
+ "castF32": "dtypes.T == \"f16\"",
611
+ "usesF16": "dtypes.T == \"f16\""
612
+ }
613
+ },
614
+ "bindings": "lastAxisVec4",
615
+ "dispatch": { "workgroups": "lastAxisRows" }
616
+ }
617
+ ]
618
+ },
619
+ {
620
+ "id": "rank0_scalar",
621
+ "priority": 40,
622
+ "constants": { "axis": 0, "scalar": "dtypes.T" },
623
+ "when": ["f16Ok(dtypes.T)", "ranks.data == 0", "ranks.reduced == 0"],
624
+ "passes": [
625
+ {
626
+ "id": "main",
627
+ "name": "ReduceLogSumExp.Rank0Scalar",
628
+ "source": {
629
+ "shader": "reduce-serial-axis.wgsl.jinja",
630
+ "inputs": {
631
+ "op": "\"logsumexp\"",
632
+ "indexing": "\"axis2d\"",
633
+ "intMode": "dtypes.T == \"i32\"",
634
+ "castF32": "dtypes.T == \"f16\"",
635
+ "usesF16": "dtypes.T == \"f16\"",
636
+ "logicalBool": "tensorDtypes.data == \"bool\""
637
+ }
638
+ },
639
+ "bindings": "scalar",
640
+ "dispatch": { "x": 1 }
641
+ }
642
+ ]
643
+ },
644
+ {
645
+ "id": "rank1_axis0",
646
+ "constants": { "axis": 0, "scalar": "dtypes.T" },
647
+ "when": ["not flatParallelCovered", "f16Ok(dtypes.T)", "ranks.data == 1", "reduceAxis == 0", "((attrs.keepdims == 0 and ranks.reduced == 0) or (attrs.keepdims == 1 and ranks.reduced == 1 and dim(shapes.reduced, 0) == 1))"],
648
+ "passes": [
649
+ {
650
+ "id": "main",
651
+ "name": "ReduceLogSumExp.Rank1Axis0",
652
+ "source": {
653
+ "shader": "reduce-serial-axis.wgsl.jinja",
654
+ "inputs": {
655
+ "op": "\"logsumexp\"",
656
+ "indexing": "\"axis2d\"",
657
+ "intMode": "dtypes.T == \"i32\"",
658
+ "castF32": "dtypes.T == \"f16\"",
659
+ "usesF16": "dtypes.T == \"f16\"",
660
+ "logicalBool": "tensorDtypes.data == \"bool\""
661
+ }
662
+ },
663
+ "bindings": "rank1Axis0",
664
+ "dispatch": { "threads": "numel(shapes.reduced)", "workgroupSize": "reduceWorkgroupSize" }
665
+ }
666
+ ]
667
+ },
668
+ {
669
+ "id": "axis1_parallel",
670
+ "priority": 20,
671
+ "demoteWhen": ["rowSerialPreferred"],
672
+ "when": ["not flatParallelCovered", "(dtypes.T == \"f32\" or dtypes.T == \"f16\")", "f16Ok(dtypes.T)", "ranks.data >= 2", "reduceAxis == ranks.data - 1", "numel(shapes.reduced) == rows(shapes.data, ranks.data - 1)", "lastAxisCols >= tunables.ROW_PARALLEL_MIN_COLS", "treeWorkgroupOk"],
673
+ "constants": { "workgroupSize": "min(reduceWorkgroupSize, pow2ceil(dim(shapes.data, ranks.data - 1)))" },
674
+ "passes": [
675
+ {
676
+ "id": "main",
677
+ "name": "ReduceLogSumExp.Axis1Parallel",
678
+ "source": {
679
+ "shader": "reduce-row-tree.wgsl.jinja",
680
+ "inputs": { "op": "\"logsumexp\"", "castF32": "dtypes.T == \"f16\"", "usesF16": "dtypes.T == \"f16\"" }
681
+ },
682
+ "bindings": "lastAxisScalar",
683
+ "dispatch": { "workgroups": "rows(shapes.data, ranks.data - 1)" }
684
+ }
685
+ ]
686
+ },
687
+ {
688
+ "id": "axis_split",
689
+ "priority": 24,
690
+ "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"],
691
+ "derive": { "splitCount": "axisSplitCount" },
692
+ "constants": { "partialElement": "\"f32\"", "workgroupSize": "reduceWorkgroupSize", "split": "splitCount" },
693
+ "intermediates": [{ "id": "partials", "dtype": "float32", "shape": "[3 * splitCount * axisSplitOutputs]" }],
694
+ "passes": [
695
+ {
696
+ "id": "split_reduce",
697
+ "name": "ReduceLogSumExp.AxisSplitReduce",
698
+ "source": {
699
+ "shader": "reduce-axis-split-reduce.wgsl.jinja",
700
+ "inputs": {
701
+ "op": "\"logsumexp\"",
702
+ "workgroupSize": "reduceWorkgroupSize",
703
+ "split": "splitCount",
704
+ "castF32": "dtypes.T == \"f16\"",
705
+ "usesF16": "dtypes.T == \"f16\""
706
+ }
707
+ },
708
+ "bindings": "axisSplitReduce",
709
+ "dispatch": { "threads": "axisSplitOutputs", "workgroupSize": "reduceWorkgroupSize", "y": "splitCount" }
710
+ },
711
+ {
712
+ "id": "combine",
713
+ "name": "ReduceLogSumExp.AxisSplitCombine",
714
+ "source": {
715
+ "shader": "reduce-axis0-splitk-combine.wgsl.jinja",
716
+ "inputs": {
717
+ "op": "\"logsumexp\"",
718
+ "workgroupSize": "reduceWorkgroupSize",
719
+ "split": "splitCount",
720
+ "outputF16": "dtypes.T == \"f16\""
721
+ }
722
+ },
723
+ "bindings": "axisSplitCombine",
724
+ "dispatch": { "threads": "axisSplitOutputs", "workgroupSize": "reduceWorkgroupSize" }
725
+ }
726
+ ]
727
+ },
728
+ {
729
+ "id": "axis_split_tiled_narrow",
730
+ "priority": 25,
731
+ "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"],
732
+ "derive": { "splitCount": "axisSplitCount" },
733
+ "constants": {
734
+ "partialElement": "\"f32\"",
735
+ "scalar": "dtypes.T",
736
+ "workgroupSize": "reduceWorkgroupSize",
737
+ "split": "splitCount",
738
+ "tileCols": "tunables.AXIS_SPLIT_TILE_COLS"
739
+ },
740
+ "intermediates": [{ "id": "partials", "dtype": "float32", "shape": "[3 * splitCount * axisSplitOutputs]" }],
741
+ "passes": [
742
+ {
743
+ "id": "split_reduce",
744
+ "name": "ReduceLogSumExp.AxisSplitTiledReduce",
745
+ "source": {
746
+ "shader": "reduce-axis0-tilecols.wgsl.jinja",
747
+ "inputs": {
748
+ "op": "\"logsumexp\"",
749
+ "workgroupSize": "reduceWorkgroupSize",
750
+ "split": "splitCount",
751
+ "tileCols": "tunables.AXIS_SPLIT_TILE_COLS",
752
+ "castF32": "dtypes.T == \"f16\"",
753
+ "usesF16": "dtypes.T == \"f16\""
754
+ }
755
+ },
756
+ "bindings": "axisSplitReduce",
757
+ "dispatch": { "workgroups": "ceilDiv((axisSplitOutputs), (constants.tileCols))", "y": "splitCount" }
758
+ },
759
+ {
760
+ "id": "combine",
761
+ "name": "ReduceLogSumExp.AxisSplitCombine",
762
+ "source": {
763
+ "shader": "reduce-axis0-splitk-combine.wgsl.jinja",
764
+ "inputs": {
765
+ "op": "\"logsumexp\"",
766
+ "workgroupSize": "reduceWorkgroupSize",
767
+ "split": "splitCount",
768
+ "outputF16": "dtypes.T == \"f16\""
769
+ }
770
+ },
771
+ "bindings": "axisSplitCombine",
772
+ "dispatch": { "threads": "axisSplitOutputs", "workgroupSize": "reduceWorkgroupSize" }
773
+ }
774
+ ]
775
+ },
776
+ {
777
+ "id": "axis0_splitk",
778
+ "priority": 22,
779
+ "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"],
780
+ "derive": { "splitCount": "axis0SplitCount" },
781
+ "constants": { "partialElement": "\"f32\"", "workgroupSize": "reduceWorkgroupSize", "split": "splitCount" },
782
+ "intermediates": [{ "id": "partials", "dtype": "float32", "shape": "[3 * splitCount * dim(shapes.data, 1)]" }],
783
+ "passes": [
784
+ {
785
+ "id": "split_reduce",
786
+ "name": "ReduceLogSumExp.Axis0SplitKReduce",
787
+ "source": {
788
+ "shader": "reduce-axis0-splitk-reduce.wgsl.jinja",
789
+ "inputs": {
790
+ "op": "\"logsumexp\"",
791
+ "workgroupSize": "reduceWorkgroupSize",
792
+ "split": "splitCount",
793
+ "castF32": "dtypes.T == \"f16\"",
794
+ "usesF16": "dtypes.T == \"f16\""
795
+ }
796
+ },
797
+ "bindings": "axis0SplitReduce",
798
+ "dispatch": { "threads": "dim(shapes.data, 1)", "workgroupSize": "reduceWorkgroupSize", "y": "splitCount" }
799
+ },
800
+ {
801
+ "id": "combine",
802
+ "name": "ReduceLogSumExp.Axis0SplitKCombine",
803
+ "source": {
804
+ "shader": "reduce-axis0-splitk-combine.wgsl.jinja",
805
+ "inputs": {
806
+ "op": "\"logsumexp\"",
807
+ "workgroupSize": "reduceWorkgroupSize",
808
+ "split": "splitCount",
809
+ "outputF16": "dtypes.T == \"f16\""
810
+ }
811
+ },
812
+ "bindings": "axis0SplitCombine",
813
+ "dispatch": { "threads": "dim(shapes.data, 1)", "workgroupSize": "reduceWorkgroupSize" }
814
+ }
815
+ ]
816
+ },
817
+ {
818
+ "id": "axis0_tilecols",
819
+ "priority": 20,
820
+ "when": ["not flatParallelCovered", "(dtypes.T == \"f32\" or dtypes.T == \"f16\")", "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"],
821
+ "constants": { "workgroupSize": "reduceWorkgroupSize", "tileCols": "tunables.AXIS0_TILE_COLS" },
822
+ "passes": [
823
+ {
824
+ "id": "main",
825
+ "name": "ReduceLogSumExp.Axis0TileCols",
826
+ "source": {
827
+ "shader": "reduce-axis0-tilecols.wgsl.jinja",
828
+ "inputs": { "op": "\"logsumexp\"", "castF32": "dtypes.T == \"f16\"", "usesF16": "dtypes.T == \"f16\"" }
829
+ },
830
+ "bindings": "axis0Parallel",
831
+ "dispatch": { "workgroups": "ceilDiv((dim(shapes.data, 1)), (constants.tileCols))" }
832
+ }
833
+ ]
834
+ },
835
+ {
836
+ "id": "all_axes_flat",
837
+ "priority": 31,
838
+ "constants": {
839
+ "scalar": "dtypes.T",
840
+ "workgroupSize": "reduceWorkgroupSize",
841
+ "flatScalar": "\"vec4<\" ~ dtypes.T ~ \">\" if numel(shapes.data) % tunables.VECTOR_WIDTH == 0 else dtypes.T",
842
+ "split": "flatSplitCount"
843
+ },
844
+ "when": ["flatParallelCovered"],
845
+ "intermediates": [{ "id": "partials", "dtype": "float32", "shape": "[3 * flatSplitCount]" }],
846
+ "passes": [
847
+ {
848
+ "id": "flat_partial",
849
+ "name": "ReduceLogSumExp.AllAxesFlatPartial",
850
+ "source": {
851
+ "shader": "reduce-flat-partial-logsumexp.wgsl.jinja",
852
+ "inputs": {
853
+ "vec4": "numel(shapes.data) % tunables.VECTOR_WIDTH == 0",
854
+ "castF32": "dtypes.T == \"f16\"",
855
+ "usesF16": "dtypes.T == \"f16\""
856
+ }
857
+ },
858
+ "bindings": "flatPartialF32",
859
+ "dispatch": { "x": "flatSplitCount" }
860
+ },
861
+ {
862
+ "id": "combine",
863
+ "name": "ReduceLogSumExp.AllAxesFlatCombine",
864
+ "source": {
865
+ "shader": "reduce-flat-combine-logsumexp.wgsl.jinja",
866
+ "inputs": { "outputF16": "dtypes.T == \"f16\"" }
867
+ },
868
+ "bindings": "flatCombineF32",
869
+ "dispatch": { "x": 1 }
870
+ }
871
+ ]
872
+ },
873
+ {
874
+ "id": "rankn_single_axis_generic",
875
+ "priority": 12,
876
+ "supersededBy": ["axis_split_tiled_narrow", "axis_split", "subgroup_last_axis_vec4", "subgroup_last_axis", "tree_last_axis_vec4"],
877
+ "when": ["not flatParallelCovered", "f16Ok(dtypes.T)", "ranks.data >= 3", "attrs.noop_with_empty_axes == 0", "reduceAxis < ranks.data", "numel(shapes.reduced) == rows(shapes.data, reduceAxis)", "((attrs.keepdims == 0 and ranks.reduced == ranks.data - 1) or (attrs.keepdims == 1 and ranks.reduced == ranks.data and dim(shapes.reduced, reduceAxis) == 1))"],
878
+ "passes": [
879
+ {
880
+ "id": "main",
881
+ "name": "ReduceLogSumExp.RankNSingleAxisGeneric",
882
+ "source": {
883
+ "shader": "reduce-serial-axis.wgsl.jinja",
884
+ "inputs": {
885
+ "op": "\"logsumexp\"",
886
+ "indexing": "\"rankn\"",
887
+ "rank": "ranks.data",
888
+ "axis": "reduceAxis",
889
+ "dataShape": "shapes.data",
890
+ "outputShape": "shapes.reduced",
891
+ "outputRank": "ranks.reduced",
892
+ "keepDims": "attrs.keepdims != 0",
893
+ "intMode": "dtypes.T == \"i32\"",
894
+ "castF32": "dtypes.T == \"f16\"",
895
+ "usesF16": "dtypes.T == \"f16\""
896
+ }
897
+ },
898
+ "bindings": "rankNAxis",
899
+ "dispatch": { "threads": "numel(shapes.reduced)", "workgroupSize": "reduceWorkgroupSize" }
900
+ }
901
+ ],
902
+ "constants": { "scalar": "dtypes.T" }
903
+ },
904
+ {
905
+ "id": "subgroup_last_axis_vec4",
906
+ "priority": 25,
907
+ "requires": { "features": ["subgroups"] },
908
+ "when": ["device.wgslLanguageFeatures.has(\"subgroup_id\")", "not flatParallelCovered", "(dtypes.T == \"f32\" or dtypes.T == \"f16\")", "f16Ok(dtypes.T)", "ranks.data >= 1", "reduceAxis == ranks.data - 1", "numel(shapes.reduced) == rows(shapes.data, ranks.data - 1)", "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)", "not rowSerialPreferred"],
909
+ "constants": {
910
+ "scalar": "dtypes.T",
911
+ "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
912
+ "workgroupSize": "min(reduceWorkgroupSize, max(subgroupWorkgroupFloor, pow2ceil(ceilDiv(lastAxisCols, tunables.VECTOR_WIDTH))))"
913
+ },
914
+ "passes": [
915
+ {
916
+ "id": "main",
917
+ "name": "ReduceLogSumExp.SubgroupRowVec4",
918
+ "source": {
919
+ "shader": "reduce-row-subgroup.wgsl.jinja",
920
+ "inputs": {
921
+ "op": "\"logsumexp\"",
922
+ "vec4": true,
923
+ "castF32": "dtypes.T == \"f16\"",
924
+ "usesF16": "dtypes.T == \"f16\""
925
+ }
926
+ },
927
+ "subgroupCollectivesWidth": "portable",
928
+ "bindings": "lastAxisVec4",
929
+ "dispatch": { "workgroups": "rows(shapes.data, ranks.data - 1)" }
930
+ }
931
+ ]
932
+ },
933
+ {
934
+ "id": "subgroup_last_axis",
935
+ "priority": 24,
936
+ "requires": { "features": ["subgroups"] },
937
+ "when": ["device.wgslLanguageFeatures.has(\"subgroup_id\")", "not flatParallelCovered", "(dtypes.T == \"f32\" or dtypes.T == \"f16\")", "f16Ok(dtypes.T)", "ranks.data >= 1", "reduceAxis == ranks.data - 1", "numel(shapes.reduced) == rows(shapes.data, ranks.data - 1)", "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)", "not rowSerialPreferred"],
938
+ "constants": {
939
+ "scalar": "dtypes.T",
940
+ "workgroupSize": "min(reduceWorkgroupSize, max(subgroupWorkgroupFloor, pow2ceil(lastAxisCols)))"
941
+ },
942
+ "passes": [
943
+ {
944
+ "id": "main",
945
+ "name": "ReduceLogSumExp.SubgroupRow",
946
+ "source": {
947
+ "shader": "reduce-row-subgroup.wgsl.jinja",
948
+ "inputs": {
949
+ "op": "\"logsumexp\"",
950
+ "vec4": false,
951
+ "castF32": "dtypes.T == \"f16\"",
952
+ "usesF16": "dtypes.T == \"f16\""
953
+ }
954
+ },
955
+ "subgroupCollectivesWidth": "portable",
956
+ "bindings": "lastAxisScalarSubgroup",
957
+ "dispatch": { "workgroups": "rows(shapes.data, ranks.data - 1)" }
958
+ }
959
+ ]
960
+ },
961
+ {
962
+ "id": "axis0",
963
+ "priority": 0,
964
+ "supersededBy": ["axis_split_tiled_narrow", "axis0_splitk", "axis0_tilecols"],
965
+ "constants": { "axis": 0, "scalar": "dtypes.T" },
966
+ "when": ["not flatParallelCovered", "f16Ok(dtypes.T)", "ranks.data == 2", "reduceAxis == 0", "((attrs.keepdims == 0 and ranks.reduced == 1 and dim(shapes.reduced, 0) == dim(shapes.data, 1)) or (attrs.keepdims == 1 and ranks.reduced == 2 and dim(shapes.reduced, 0) == 1 and dim(shapes.reduced, 1) == dim(shapes.data, 1)))"],
967
+ "passes": [
968
+ {
969
+ "id": "main",
970
+ "name": "axis0",
971
+ "source": {
972
+ "shader": "reduce-serial-axis.wgsl.jinja",
973
+ "inputs": {
974
+ "op": "\"logsumexp\"",
975
+ "indexing": "\"axis2d\"",
976
+ "intMode": "dtypes.T == \"i32\"",
977
+ "castF32": "dtypes.T == \"f16\"",
978
+ "usesF16": "dtypes.T == \"f16\""
979
+ }
980
+ },
981
+ "bindings": "rank2Serial",
982
+ "constants": { "axis": 0 },
983
+ "dispatch": { "threads": "numel(shapes.reduced)", "workgroupSize": "reduceWorkgroupSize" }
984
+ }
985
+ ]
986
+ },
987
+ {
988
+ "id": "axis1",
989
+ "priority": 0,
990
+ "constants": { "axis": 1, "scalar": "dtypes.T" },
991
+ "when": ["not flatParallelCovered", "f16Ok(dtypes.T)", "ranks.data == 2", "reduceAxis == 1", "((attrs.keepdims == 0 and ranks.reduced == 1 and dim(shapes.reduced, 0) == dim(shapes.data, 0)) or (attrs.keepdims == 1 and ranks.reduced == 2 and dim(shapes.reduced, 0) == dim(shapes.data, 0) and dim(shapes.reduced, 1) == 1))"],
992
+ "passes": [
993
+ {
994
+ "id": "main",
995
+ "name": "axis1",
996
+ "source": {
997
+ "shader": "reduce-serial-axis.wgsl.jinja",
998
+ "inputs": {
999
+ "op": "\"logsumexp\"",
1000
+ "indexing": "\"axis2d\"",
1001
+ "intMode": "dtypes.T == \"i32\"",
1002
+ "castF32": "dtypes.T == \"f16\"",
1003
+ "usesF16": "dtypes.T == \"f16\""
1004
+ }
1005
+ },
1006
+ "bindings": "rank2SerialAxis1",
1007
+ "constants": { "axis": 1 },
1008
+ "dispatch": { "threads": "numel(shapes.reduced)", "workgroupSize": "reduceWorkgroupSize" }
1009
+ }
1010
+ ]
1011
+ },
1012
+ {
1013
+ "id": "all_axes_keepdims",
1014
+ "priority": 30,
1015
+ "constants": { "axis": 0, "scalar": "dtypes.T" },
1016
+ "when": ["not flatParallelCovered", "f16Ok(dtypes.T)", "ranks.data >= 3", "attrs.keepdims == 1", "ranks.reduced == ranks.data", "numel(shapes.reduced) == 1"],
1017
+ "passes": [
1018
+ {
1019
+ "id": "main",
1020
+ "name": "ReduceLogSumExp.Rank3AllAxesKeepdims",
1021
+ "source": {
1022
+ "shader": "reduce-serial-axis.wgsl.jinja",
1023
+ "inputs": {
1024
+ "op": "\"logsumexp\"",
1025
+ "indexing": "\"axis2d\"",
1026
+ "intMode": "dtypes.T == \"i32\"",
1027
+ "castF32": "dtypes.T == \"f16\"",
1028
+ "usesF16": "dtypes.T == \"f16\""
1029
+ }
1030
+ },
1031
+ "bindings": "fullReduceSerial",
1032
+ "dispatch": { "threads": "numel(shapes.reduced)", "workgroupSize": "reduceWorkgroupSize" }
1033
+ }
1034
+ ]
1035
+ },
1036
+ {
1037
+ "id": "all_axes_no_keepdims",
1038
+ "priority": 30,
1039
+ "constants": { "axis": 0, "scalar": "dtypes.T" },
1040
+ "when": ["not flatParallelCovered", "f16Ok(dtypes.T)", "ranks.data >= 3", "attrs.keepdims == 0", "attrs.noop_with_empty_axes == 0", "ranks.reduced == 0"],
1041
+ "passes": [
1042
+ {
1043
+ "id": "main",
1044
+ "name": "ReduceLogSumExp.Rank3AllAxesNoKeepdims",
1045
+ "source": {
1046
+ "shader": "reduce-serial-axis.wgsl.jinja",
1047
+ "inputs": {
1048
+ "op": "\"logsumexp\"",
1049
+ "indexing": "\"axis2d\"",
1050
+ "intMode": "dtypes.T == \"i32\"",
1051
+ "castF32": "dtypes.T == \"f16\"",
1052
+ "usesF16": "dtypes.T == \"f16\""
1053
+ }
1054
+ },
1055
+ "bindings": "fullReduceSerial",
1056
+ "dispatch": { "threads": "numel(shapes.reduced)", "workgroupSize": "reduceWorkgroupSize" }
1057
+ }
1058
+ ]
1059
+ }
1060
+ ]
1061
+ }
build/webgpu/metadata.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "ai.onnx.ReduceLogSumExp",
3
+ "id": "_ai_onnx_reducelogsumexp_webgpu_85769e2",
4
+ "version": 1,
5
+ "license": "Apache-2.0",
6
+ "backend": { "type": "webgpu" },
7
+ "digest": {
8
+ "algorithm": "sha256",
9
+ "files": {
10
+ "bench.json": "osiIimng7rf8c4+lWBGf/4BUtBJ1pMrNA2I08w6ygf0=",
11
+ "manifest.json": "rbJKRnqa38U/J/QPQmwV9MWpWxBCCQQoVVrRzy1fRI0=",
12
+ "reduce-axis-split-reduce.wgsl.jinja": "CKODGEG8mXIkTNFzjDJZwfExy1KzYxG50C3LaUiYd3U=",
13
+ "reduce-axis0-splitk-combine.wgsl.jinja": "P3U2gCak59UZSyakvyuWCDhW27IPcilPg+yE1BPY5nM=",
14
+ "reduce-axis0-splitk-reduce.wgsl.jinja": "cowJ5TK8xFHBwnCPYUYEBScEIKXFiCN4mlu0B2lWaw4=",
15
+ "reduce-axis0-tilecols.wgsl.jinja": "CNwWK3NVE27T0IJo4+HoDfLFDGMuJ0PVDJRk5Jmwq7o=",
16
+ "reduce-flat-combine-logsumexp.wgsl.jinja": "qrI8ma9jUPm+p+oWJejet/DvaNNTgjw4AoUBKIgoaPQ=",
17
+ "reduce-flat-partial-logsumexp.wgsl.jinja": "dl9CTTHHIAjnI0QvdFpNkv4IFWD62Cf0Q3jpgsAfpPo=",
18
+ "reduce-i32-axes02.wgsl.jinja": "qSNKLHjGG9CFO6DRIlBsQ7XETdKrQbbnn3O7NJPxvY0=",
19
+ "reduce-noop-empty-axes.wgsl.jinja": "IO2MEzyBGJr3Z8AVP7ELJXqfhwcG/vtBfa0wCdkbuwo=",
20
+ "reduce-row-subgroup.wgsl.jinja": "GqRcdIa6KA7BPnGGO9ApvQPizeinFAG1XDOSLsutZ6Y=",
21
+ "reduce-row-tree.wgsl.jinja": "yX9bu0RBuTGybmUUcK4bZ9Gf0CyqDsMThC9jIXoXxNU=",
22
+ "reduce-serial-axis.wgsl.jinja": "BGP2SlwMidLS+xcUX/FBC9lAwCi5gLM6DWOZYiqxYQo=",
23
+ "test.json": "nvmxvijOcgGOSHCGvqrJ6/iAdaTeHTSgnc4Ghf7IGCE="
24
+ }
25
+ },
26
+ "provenance": { "kernel": { "sha": "2e7068faf55e7f43df740015f6d1ee49391a41c5", "dirty": false } },
27
+ "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/ai.onnx.ReduceLogSumExp" }
28
+ }
build/webgpu/reduce-axis-split-reduce.wgsl.jinja ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Pass 1 of a split-K reduction for an (outer, axis, inner) flattening. Each
2
+ // (output, segment) invocation reduces one axis slice at stride inner and writes
3
+ // partials[segment * outputs + output]. Adjacent output threads read adjacent
4
+ // inner-axis elements. The combine pass folds the segments and finalizes the
5
+ // selected reduction.
6
+ //
7
+ // logsumexp writes three partial planes per output and segment: the segment
8
+ // maximum, the sum of exp(x - maximum), and a packed NaN marker.
9
+ {% set castF32 = source.castF32 is defined and source.castF32 %}
10
+ {% set xa = "f32(" if castF32 else "" %}
11
+ {% set ax = ")" if castF32 else "" %}
12
+ {% if source.usesF16 is defined and source.usesF16 %}
13
+ enable f16;
14
+ {% endif %}
15
+ {{ env.wgsl.resourceDeclarations }}
16
+ /* Exact max/min reduction identities. WGSL rejects infinity during constant
17
+ * evaluation, so f32 identities are constructed at runtime from their
18
+ * IEEE-754 bit patterns. */
19
+
20
+
21
+ const WG: u32 = {{ workgroupSize }}u;
22
+ const SPLIT: u32 = {{ split }}u;
23
+ const F32_MIN: f32 = -3.4028234663852886e38;
24
+
25
+ fn is_nan_f32(value: f32) -> bool {
26
+ let bits = bitcast<u32>(value);
27
+ return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
28
+ }
29
+
30
+ @compute @workgroup_size(WG, 1, 1)
31
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>,
32
+ @builtin(workgroup_id) wg: vec3<u32>,
33
+ @builtin(num_workgroups) nwg: vec3<u32>) {
34
+ // 2D-folded output index: wg.z carries the high bits past the
35
+ // per-dimension dispatch limit on the x dimension.
36
+ let output_index = (wg.x + wg.z * nwg.x) * WG + (gid.x % WG);
37
+ let seg = wg.y;
38
+ if (output_index >= params.outputs) { return; }
39
+
40
+ let outer_index = output_index / params.inner;
41
+ let inner_index = output_index % params.inner;
42
+ let input_base = outer_index * params.axisDim * params.inner + inner_index;
43
+
44
+ // Even split of [0, axisDim) across SPLIT segments (last may be shorter).
45
+ let chunk = (params.axisDim + SPLIT - 1u) / SPLIT;
46
+ let a0 = seg * chunk;
47
+ var a1 = a0 + chunk;
48
+ if (a1 > params.axisDim) { a1 = params.axisDim; }
49
+
50
+ var local_max = F32_MIN;
51
+ var local_nan_count = 0.0;
52
+ var local_nan_value = 0.0;
53
+ for (var axis_index = a0; axis_index < a1; axis_index = axis_index + 1u) {
54
+ let value = {{ xa }}x[input_base + axis_index * params.inner]{{ ax }};
55
+ if (is_nan_f32(value)) {
56
+ local_nan_count = local_nan_count + 1.0;
57
+ local_nan_value = value;
58
+ } else {
59
+ local_max = max(local_max, value);
60
+ }
61
+ }
62
+ var acc = 0.0;
63
+ if (local_nan_count == 0.0) {
64
+ for (var axis_index = a0; axis_index < a1; axis_index = axis_index + 1u) {
65
+ acc = acc + exp({{ xa }}x[input_base + axis_index * params.inner]{{ ax }} - local_max);
66
+ }
67
+ }
68
+ partials[seg * params.outputs + output_index] = local_max;
69
+ partials[(SPLIT + seg) * params.outputs + output_index] = acc;
70
+ partials[(2u * SPLIT + seg) * params.outputs + output_index] = select(0.0, local_nan_value, local_nan_count > 0.0);
71
+ }
build/webgpu/reduce-axis0-splitk-combine.wgsl.jinja ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Pass 2 of the split-K column-wise reduction. One invocation per output column
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 yv = "f16(" if source.outputF16 else "" %}
6
+ {% set vy = ")" if source.outputF16 else "" %}
7
+ {% if source.outputF16 %}
8
+ enable f16;
9
+ {% endif %}
10
+ {{ env.wgsl.resourceDeclarations }}
11
+ /* Exact max/min reduction identities. WGSL rejects infinity during constant
12
+ * evaluation, so f32 identities are constructed at runtime from their
13
+ * IEEE-754 bit patterns. */
14
+
15
+
16
+ const WG: u32 = {{ workgroupSize }}u;
17
+ const SPLIT: u32 = {{ split }}u;
18
+ const F32_MIN: f32 = -3.4028234663852886e38;
19
+ const F32_MAX: f32 = 3.4028234663852886e38;
20
+
21
+ fn is_nan_f32(value: f32) -> bool {
22
+ let bits = bitcast<u32>(value);
23
+ return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
24
+ }
25
+
26
+ @compute @workgroup_size(WG, 1, 1)
27
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>,
28
+ @builtin(num_workgroups) nwg: vec3<u32>) {
29
+ let stride = nwg.x * WG;
30
+ let start = (gid.y * nwg.x * WG) + gid.x;
31
+ for (var col = start; col < params.cols; col = col + stride) {
32
+ // Merge SPLIT (segMax, segSumExp) pairs stably; carry NaN / +Inf markers.
33
+ var nan_value = 0.0;
34
+ var has_nan = false;
35
+ var global_max = F32_MIN;
36
+ for (var seg = 0u; seg < SPLIT; seg = seg + 1u) {
37
+ let nv = partials[(2u * SPLIT + seg) * params.cols + col];
38
+ if (nv != 0.0 || is_nan_f32(nv)) {
39
+ has_nan = true;
40
+ nan_value = nv;
41
+ }
42
+ global_max = max(global_max, partials[seg * params.cols + col]);
43
+ }
44
+ var sum = 0.0;
45
+ for (var seg = 0u; seg < SPLIT; seg = seg + 1u) {
46
+ let seg_max = partials[seg * params.cols + col];
47
+ let seg_sum = partials[(SPLIT + seg) * params.cols + col];
48
+ sum = sum + seg_sum * exp(seg_max - global_max);
49
+ }
50
+ let has_positive_inf = global_max > F32_MAX;
51
+ let finite_or_inf = select(global_max + log(sum), global_max, has_positive_inf);
52
+ y[col] = {{ yv }}select(finite_or_inf, nan_value, has_nan){{ vy }};
53
+ }
54
+ }
build/webgpu/reduce-axis0-splitk-reduce.wgsl.jinja ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Pass 1 of a split-K column-wise reduction. Splitting rows across workgroup
2
+ // segments increases residency for tall matrices. Each (column, segment)
3
+ // invocation reduces one row slice and writes partials[segment * columns +
4
+ // column]. Adjacent column threads keep row reads coalesced.
5
+ //
6
+ // logsumexp writes the segment maximum and sum of exp(x - maximum) as separate
7
+ // partial planes; the combine pass merges them stably and handles NaN and +Inf.
8
+ {% set castF32 = source.castF32 is defined and source.castF32 %}
9
+ {% set xa = "f32(" if castF32 else "" %}
10
+ {% set ax = ")" if castF32 else "" %}
11
+ {% if source.usesF16 is defined and source.usesF16 %}
12
+ enable f16;
13
+ {% endif %}
14
+ {{ env.wgsl.resourceDeclarations }}
15
+ /* Exact max/min reduction identities. WGSL rejects infinity during constant
16
+ * evaluation, so f32 identities are constructed at runtime from their
17
+ * IEEE-754 bit patterns. */
18
+
19
+
20
+ const WG: u32 = {{ workgroupSize }}u;
21
+ const SPLIT: u32 = {{ split }}u;
22
+ const F32_MIN: f32 = -3.4028234663852886e38;
23
+
24
+ fn is_nan_f32(value: f32) -> bool {
25
+ let bits = bitcast<u32>(value);
26
+ return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
27
+ }
28
+
29
+ @compute @workgroup_size(WG, 1, 1)
30
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>,
31
+ @builtin(workgroup_id) wg: vec3<u32>,
32
+ @builtin(num_workgroups) nwg: vec3<u32>) {
33
+ // 2D-folded column index: wg.z carries the high bits past the maxComputeWorkgroupsPerDimension
34
+ // workgroup-per-dimension dispatch limit on the x dimension.
35
+ let col = (wg.x + wg.z * nwg.x) * WG + (gid.x % WG);
36
+ let seg = wg.y;
37
+ if (col >= params.cols) { return; }
38
+
39
+ // Even split of [0, rows) across SPLIT segments (last segment may be shorter).
40
+ let chunk = (params.rows + SPLIT - 1u) / SPLIT;
41
+ let r0 = seg * chunk;
42
+ var r1 = r0 + chunk;
43
+ if (r1 > params.rows) { r1 = params.rows; }
44
+
45
+ var local_max = F32_MIN;
46
+ var local_nan_count = 0.0;
47
+ var local_nan_value = 0.0;
48
+ for (var row = r0; row < r1; row = row + 1u) {
49
+ let value = {{ xa }}x[row * params.cols + col]{{ ax }};
50
+ if (is_nan_f32(value)) {
51
+ local_nan_count = local_nan_count + 1.0;
52
+ local_nan_value = value;
53
+ } else {
54
+ local_max = max(local_max, value);
55
+ }
56
+ }
57
+ var acc = 0.0;
58
+ if (local_nan_count == 0.0) {
59
+ for (var row = r0; row < r1; row = row + 1u) {
60
+ acc = acc + exp({{ xa }}x[row * params.cols + col]{{ ax }} - local_max);
61
+ }
62
+ }
63
+ // Three partial planes: seg max, seg sumexp(x - max), and a packed nan
64
+ // marker (count in the low slot, the nan bit-pattern smuggled as f32).
65
+ partials[seg * params.cols + col] = local_max;
66
+ partials[(SPLIT + seg) * params.cols + col] = acc;
67
+ partials[(2u * SPLIT + seg) * params.cols + col] = select(0.0, local_nan_value, local_nan_count > 0.0);
68
+ }
build/webgpu/reduce-axis0-tilecols.wgsl.jinja ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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" %}
10
+ {% set elem = "x[input_base + row * params.inner]" %}
11
+ {% else %}
12
+ {% set rowBegin = "row_lane" %}
13
+ {% set rowEnd = "params.rows" %}
14
+ {% set elem = "x[inputBase + row * params.cols + col]" %}
15
+ {% endif %}
16
+ {% set castF32 = source.castF32 is defined and source.castF32 %}
17
+ {% set intMode = source.intMode is defined and source.intMode %}
18
+ {% set scalar = "f32" if castF32 else scalar %}
19
+ {% if castF32 %}
20
+ {% set elem = "f32(" ~ elem ~ ")" %}
21
+ {% endif %}
22
+ {% set yv = "f16(" if castF32 else "" %}
23
+ {% set vy = ")" if castF32 else "" %}
24
+ {% if source.usesF16 is defined and source.usesF16 %}
25
+ enable f16;
26
+ {% endif %}
27
+ {{ env.wgsl.resourceDeclarations }}
28
+ /* Exact max/min reduction identities. WGSL rejects infinity during constant
29
+ * evaluation, so f32 identities are constructed at runtime from their
30
+ * IEEE-754 bit patterns. */
31
+
32
+ {% if not splitMode and not intMode and (source.op == "logsum" or source.op == "logsumexp") %}
33
+ fn negative_infinity() -> f32 {
34
+ var bits = 0xff800000u;
35
+ return bitcast<f32>(bits);
36
+ }
37
+
38
+ {% endif %}
39
+
40
+ const WG: u32 = {{ workgroupSize }}u;
41
+ const TILE_COLS: u32 = {{ tileCols }}u;
42
+ const ROW_LANES: u32 = WG / TILE_COLS;
43
+ {% if splitMode %}
44
+ const SPLIT: u32 = {{ source.split }}u;
45
+ {% endif %}
46
+ const F32_MIN: f32 = -3.4028234663852886e38;
47
+ const F32_MAX: f32 = 3.4028234663852886e38;
48
+
49
+ var<workgroup> partial: array<{{ scalar if (source.op == "max" or source.op == "min" or intMode) else "f32" }}, WG>;
50
+
51
+ fn is_nan_f32(value: f32) -> bool {
52
+ let bits = bitcast<u32>(value);
53
+ return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
54
+ }
55
+
56
+ @compute @workgroup_size(WG, 1, 1)
57
+ fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>{% if not splitMode %},
58
+ @builtin(num_workgroups) nwg: vec3<u32>{% endif %}) {
59
+ let tid = lid.x;
60
+ let col_lane = tid % TILE_COLS;
61
+ let row_lane = tid / TILE_COLS;
62
+ {% if splitMode %}
63
+ // Narrow outputs: wg.x covers every column tile, wg.y is the axis segment.
64
+ let col = wg.x * TILE_COLS + col_lane;
65
+ let outputIndex = col;
66
+ let in_bounds = col < params.outputs;
67
+ let seg = wg.y;
68
+ let outer_index = col / params.inner;
69
+ let inner_index = col % params.inner;
70
+ let input_base = outer_index * params.axisDim * params.inner + inner_index;
71
+ // Even split of [0, axisDim) across SPLIT segments (last may be shorter).
72
+ let chunk = (params.axisDim + SPLIT - 1u) / SPLIT;
73
+ let row_begin = seg * chunk;
74
+ let row_end = min(row_begin + chunk, params.axisDim);
75
+ {% else %}
76
+ // 2D-folded tile index: wg.y carries the high bits past the dispatch limit.
77
+ // The batched form reuses this same coalesced axis-0 reduction for a middle
78
+ // axis by assigning consecutive tiles to each outer slice.
79
+ let tile = wg.x + wg.y * nwg.x;
80
+ let col = tile * TILE_COLS + col_lane;
81
+ let inputBase = 0u;
82
+ let outputIndex = col;
83
+ let in_bounds = col < params.cols;
84
+ {% endif %}
85
+ {% if not splitMode %}
86
+
87
+ if (params.rows == 0u) {
88
+ if (row_lane == 0u && in_bounds) {
89
+ y[outputIndex] = {{ yv }}negative_infinity(){{ vy }};
90
+ }
91
+ return;
92
+ }
93
+ {% endif %}
94
+
95
+ var local_max = F32_MIN;
96
+ var local_nan_count = 0.0;
97
+ var local_nan_value = 0.0;
98
+ if (in_bounds) {
99
+ for (var row = {{ rowBegin }}; row < {{ rowEnd }}; row = row + ROW_LANES) {
100
+ let value = {{ elem }};
101
+ if (is_nan_f32(value)) {
102
+ local_nan_count = local_nan_count + 1.0;
103
+ local_nan_value = value;
104
+ } else {
105
+ local_max = max(local_max, value);
106
+ }
107
+ }
108
+ }
109
+
110
+ partial[tid] = local_max;
111
+ workgroupBarrier();
112
+ if (row_lane == 0u && in_bounds) {
113
+ var max_lanes = partial[col_lane];
114
+ for (var lane = 1u; lane < ROW_LANES; lane = lane + 1u) {
115
+ max_lanes = max(max_lanes, partial[lane * TILE_COLS + col_lane]);
116
+ }
117
+ partial[col_lane] = max_lanes;
118
+ }
119
+ workgroupBarrier();
120
+ let max_value = partial[col_lane];
121
+ workgroupBarrier();
122
+
123
+ partial[tid] = local_nan_count;
124
+ workgroupBarrier();
125
+ if (row_lane == 0u && in_bounds) {
126
+ var nan_lanes = partial[col_lane];
127
+ for (var lane = 1u; lane < ROW_LANES; lane = lane + 1u) {
128
+ nan_lanes = nan_lanes + partial[lane * TILE_COLS + col_lane];
129
+ }
130
+ partial[col_lane] = nan_lanes;
131
+ }
132
+ workgroupBarrier();
133
+ let nan_count = partial[col_lane];
134
+ workgroupBarrier();
135
+
136
+ partial[tid] = local_nan_value;
137
+ workgroupBarrier();
138
+ if (row_lane == 0u && in_bounds) {
139
+ var nan_value_lanes = partial[col_lane];
140
+ for (var lane = 1u; lane < ROW_LANES; lane = lane + 1u) {
141
+ nan_value_lanes = nan_value_lanes + partial[lane * TILE_COLS + col_lane];
142
+ }
143
+ partial[col_lane] = nan_value_lanes;
144
+ }
145
+ workgroupBarrier();
146
+ let nan_value = partial[col_lane];
147
+ let has_nan = nan_count > 0.0;
148
+ let has_positive_inf = max_value > F32_MAX;
149
+ workgroupBarrier();
150
+
151
+ var acc = 0.0;
152
+ if (in_bounds && !has_nan && !has_positive_inf) {
153
+ for (var row = {{ rowBegin }}; row < {{ rowEnd }}; row = row + ROW_LANES) {
154
+ acc = acc + exp({{ elem }} - max_value);
155
+ }
156
+ }
157
+ partial[tid] = acc;
158
+ workgroupBarrier();
159
+
160
+ if (row_lane == 0u && in_bounds) {
161
+ var sum = partial[col_lane];
162
+ for (var lane = 1u; lane < ROW_LANES; lane = lane + 1u) {
163
+ sum = sum + partial[lane * TILE_COLS + col_lane];
164
+ }
165
+ {% if splitMode %}
166
+ partials[seg * params.outputs + outputIndex] = max_value;
167
+ partials[(SPLIT + seg) * params.outputs + outputIndex] = sum;
168
+ partials[(2u * SPLIT + seg) * params.outputs + outputIndex] = select(0.0, nan_value, has_nan);
169
+ {% else %}
170
+ let finite_or_inf = select(max_value + log(sum), max_value, has_positive_inf);
171
+ y[outputIndex] = {{ yv }}select(finite_or_inf, nan_value, has_nan){{ vy }};
172
+ {% endif %}
173
+ }
174
+ }
build/webgpu/reduce-flat-combine-logsumexp.wgsl.jinja ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Parallel scalar combine for the flat LogSumExp full reduction. The partial
2
+ // pass writes SPLIT stable (segment maximum, shifted exponential sum, NaN
3
+ // marker) triples. Unlike the generic axis-0 combine, which assigns one lane
4
+ // to each output column, this scalar-only path has a full workgroup fold the
5
+ // partial planes. That avoids leaving one lane to execute 2*SPLIT
6
+ // transcendental merges when the output has exactly one element.
7
+ {% set yv = "f16(" if source.outputF16 else "" %}
8
+ {% set vy = ")" if source.outputF16 else "" %}
9
+ {% if source.outputF16 %}
10
+ enable f16;
11
+ {% endif %}
12
+ {{ env.wgsl.resourceDeclarations }}
13
+
14
+ const WG: u32 = {{ workgroupSize }}u;
15
+ const SPLIT: u32 = {{ split }}u;
16
+ const F32_MIN: f32 = -3.4028234663852886e38;
17
+ const F32_MAX: f32 = 3.4028234663852886e38;
18
+
19
+ var<workgroup> wmax: array<f32, WG>;
20
+ var<workgroup> wsegmax: array<f32, WG>;
21
+ var<workgroup> wsum: array<f32, WG>;
22
+ var<workgroup> wnan: array<f32, WG>;
23
+
24
+ fn is_nan_f32(value: f32) -> bool {
25
+ let bits = bitcast<u32>(value);
26
+ return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
27
+ }
28
+
29
+ @compute @workgroup_size(WG)
30
+ fn main(@builtin(local_invocation_id) lid: vec3<u32>) {
31
+ let tid = lid.x;
32
+ var seg_max = F32_MIN;
33
+ var seg_sum = 0.0;
34
+ var nan_value = 0.0;
35
+ if (tid < SPLIT) {
36
+ // params.cols is one for this scalar-only path. Keep it in the addressing
37
+ // expression so the binding contract stays identical to the shared axis-0
38
+ // combine used by the other flat reductions.
39
+ let col = 0u;
40
+ seg_max = partials[tid * params.cols + col];
41
+ seg_sum = partials[(SPLIT + tid) * params.cols + col];
42
+ nan_value = partials[(2u * SPLIT + tid) * params.cols + col];
43
+ }
44
+ wmax[tid] = seg_max;
45
+ wsegmax[tid] = seg_max;
46
+ wsum[tid] = seg_sum;
47
+ wnan[tid] = nan_value;
48
+ workgroupBarrier();
49
+
50
+ var stride = WG / 2u;
51
+ loop {
52
+ if (stride == 0u) { break; }
53
+ if (tid < stride) {
54
+ wmax[tid] = max(wmax[tid], wmax[tid + stride]);
55
+ let other_nan = wnan[tid + stride];
56
+ if (other_nan != 0.0 || is_nan_f32(other_nan)) {
57
+ wnan[tid] = other_nan;
58
+ }
59
+ }
60
+ stride = stride / 2u;
61
+ workgroupBarrier();
62
+ }
63
+
64
+ let global_max = wmax[0];
65
+ if (tid < SPLIT) {
66
+ wsum[tid] = wsum[tid] * exp(wsegmax[tid] - global_max);
67
+ } else {
68
+ wsum[tid] = 0.0;
69
+ }
70
+ workgroupBarrier();
71
+
72
+ stride = WG / 2u;
73
+ loop {
74
+ if (stride == 0u) { break; }
75
+ if (tid < stride) {
76
+ wsum[tid] = wsum[tid] + wsum[tid + stride];
77
+ }
78
+ stride = stride / 2u;
79
+ workgroupBarrier();
80
+ }
81
+
82
+ if (tid == 0u) {
83
+ let has_positive_inf = global_max > F32_MAX;
84
+ let finite_or_inf = select(global_max + log(wsum[0]), global_max, has_positive_inf);
85
+ let has_nan = wnan[0] != 0.0 || is_nan_f32(wnan[0]);
86
+ y[0] = {{ yv }}select(finite_or_inf, wnan[0], has_nan){{ vy }};
87
+ }
88
+ }
build/webgpu/reduce-flat-partial-logsumexp.wgsl.jinja ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Pass 1 of a flat LogSumExp reduction to one scalar. SPLIT workgroups traverse
2
+ // disjoint partial segments. Each first finds its segment maximum while
3
+ // tracking NaN, then rereads the segment to sum exp(x - segmentMaximum).
4
+ // Workgroups write three partial planes: segment maximum, shifted exponential
5
+ // sum, and a NaN marker. The combine pass merges them stably and emits
6
+ // globalMaximum + log(sum), preserving NaN and positive infinity.
7
+ {% set vec4 = source.vec4 | default(true) %}
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
+
16
+ const WG: u32 = {{ workgroupSize }}u;
17
+ const SPLIT: u32 = {{ split }}u;
18
+ const F32_MIN: f32 = -3.4028234663852886e38;
19
+
20
+ var<workgroup> wmax: array<f32, WG>;
21
+ var<workgroup> wnan: array<f32, WG>;
22
+ var<workgroup> wsum: array<f32, WG>;
23
+
24
+ fn is_nan_f32(value: f32) -> bool {
25
+ let bits = bitcast<u32>(value);
26
+ return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
27
+ }
28
+
29
+ @compute @workgroup_size(WG)
30
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>,
31
+ @builtin(local_invocation_id) lid: vec3<u32>,
32
+ @builtin(workgroup_id) wg: vec3<u32>,
33
+ @builtin(num_workgroups) nwg: vec3<u32>) {
34
+ let tid = lid.x;
35
+ let gstride = nwg.x * WG;
36
+
37
+ // Pass A: per-lane max over its grid-stride slice of the flat view,
38
+ // tracking NaN separately (NaN never participates in max).
39
+ var local_max = F32_MIN;
40
+ var nan_value = 0.0;
41
+ var has_nan = false;
42
+ for (var i = gid.x; i < params.count; i = i + gstride) {
43
+ {% if vec4 %}
44
+ let v = x[i];
45
+ for (var c = 0u; c < 4u; c = c + 1u) {
46
+ let e = {{ xa }}v[c]{{ ax }};
47
+ if (is_nan_f32(e)) { has_nan = true; nan_value = e; }
48
+ else { local_max = max(local_max, e); }
49
+ }
50
+ {% else %}
51
+ let e = {{ xa }}x[i]{{ ax }};
52
+ if (is_nan_f32(e)) { has_nan = true; nan_value = e; }
53
+ else { local_max = max(local_max, e); }
54
+ {% endif %}
55
+ }
56
+ wmax[tid] = local_max;
57
+ wnan[tid] = select(0.0, nan_value, has_nan);
58
+ workgroupBarrier();
59
+ var stride: u32 = WG / 2u;
60
+ loop {
61
+ if (stride == 0u) { break; }
62
+ if (tid < stride) {
63
+ wmax[tid] = max(wmax[tid], wmax[tid + stride]);
64
+ let other = wnan[tid + stride];
65
+ if (other != 0.0 || is_nan_f32(other)) { wnan[tid] = other; }
66
+ }
67
+ stride = stride / 2u;
68
+ workgroupBarrier();
69
+ }
70
+ let seg_max = wmax[0];
71
+ let seg_nan = wnan[0];
72
+
73
+ // Pass B: sum exp(x - segMax) over the same slice. (When the segment holds a
74
+ // NaN the combine discards this sum, so its value is irrelevant then.)
75
+ var local_sum = 0.0;
76
+ for (var i = gid.x; i < params.count; i = i + gstride) {
77
+ {% if vec4 %}
78
+ let v = x[i];
79
+ local_sum = local_sum
80
+ + exp({{ xa }}v.x{{ ax }} - seg_max) + exp({{ xa }}v.y{{ ax }} - seg_max)
81
+ + exp({{ xa }}v.z{{ ax }} - seg_max) + exp({{ xa }}v.w{{ ax }} - seg_max);
82
+ {% else %}
83
+ local_sum = local_sum + exp({{ xa }}x[i]{{ ax }} - seg_max);
84
+ {% endif %}
85
+ }
86
+ wsum[tid] = local_sum;
87
+ workgroupBarrier();
88
+ stride = WG / 2u;
89
+ loop {
90
+ if (stride == 0u) { break; }
91
+ if (tid < stride) { wsum[tid] = wsum[tid] + wsum[tid + stride]; }
92
+ stride = stride / 2u;
93
+ workgroupBarrier();
94
+ }
95
+
96
+ if (tid == 0u) {
97
+ partials[wg.x] = seg_max;
98
+ partials[SPLIT + wg.x] = wsum[0];
99
+ partials[2u * SPLIT + wg.x] = seg_nan;
100
+ }
101
+ }
build/webgpu/reduce-i32-axes02.wgsl.jinja ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Rank-3 i32 reduction over axes {0, 2}, leaving the middle dimension.
2
+ {{ env.wgsl.resourceDeclarations }}
3
+
4
+ const WG: u32 = {{ source.workgroupSize }}u;
5
+
6
+ @compute @workgroup_size(WG)
7
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>,
8
+ @builtin(num_workgroups) nwg: vec3<u32>) {
9
+ let outputIndex = gid.x + gid.y * nwg.x * WG;
10
+ let d0 = params.d0;
11
+ let d1 = params.d1;
12
+ let d2 = params.d2;
13
+ let outputCount = params.outCount;
14
+ if (outputIndex >= outputCount) {
15
+ return;
16
+ }
17
+
18
+ var maxValue = f32(x[outputIndex * d2]);
19
+ for (var i = 0u; i < d0; i = i + 1u) {
20
+ for (var k = 0u; k < d2; k = k + 1u) {
21
+ maxValue = max(maxValue, f32(x[(i * d1 + outputIndex) * d2 + k]));
22
+ }
23
+ }
24
+
25
+ var sum = 0.0;
26
+ for (var i = 0u; i < d0; i = i + 1u) {
27
+ for (var k = 0u; k < d2; k = k + 1u) {
28
+ let value = f32(x[(i * d1 + outputIndex) * d2 + k]);
29
+ sum = sum + exp(value - maxValue);
30
+ }
31
+ }
32
+ y[outputIndex] = i32(maxValue + log(sum));
33
+ }
build/webgpu/reduce-noop-empty-axes.wgsl.jinja ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }
build/webgpu/reduce-row-subgroup.wgsl.jinja ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Subgroup row reduction for a contiguous last axis. One workgroup owns each
2
+ // output row. Threads reduce strided chunks, subgroup leaders deposit their
3
+ // partials in workgroup memory, and thread 0 folds those slots and finalizes.
4
+ //
5
+ // Accumulators are f32 except for integer max/min. Mean, l2, and logsum apply
6
+ // division, square root, or logarithm only at finalization. logsumexp uses
7
+ // max-subtraction, bit-exact NaN detection, NaN propagation, and a +Inf
8
+ // short-circuit. Runtime bitcasts provide max/min infinity identities because
9
+ // WGSL constant evaluation rejects infinite constants. f16 storage is widened
10
+ // before accumulation and narrowed only for the final store.
11
+ {% set castF32 = source.castF32 is defined and source.castF32 %}
12
+ {% set scalar = "f32" if castF32 else scalar %}
13
+ {% set xv = ("vec4<f32>(" if source.vec4 else "f32(") if castF32 else "" %}
14
+ {% set vx = ")" if castF32 else "" %}
15
+ {% set yv = "f16(" if castF32 else "" %}
16
+ {% set vy = ")" if castF32 else "" %}
17
+ enable subgroups;
18
+ {% if source.usesF16 is defined and source.usesF16 %}
19
+ enable f16;
20
+ {% endif %}
21
+ {{ env.wgsl.resourceDeclarations }}
22
+ /* Exact max/min reduction identities. WGSL rejects infinity during constant
23
+ * evaluation, so f32 identities are constructed at runtime from their
24
+ * IEEE-754 bit patterns. */
25
+
26
+
27
+ const WG: u32 = {{ workgroupSize }}u;
28
+ const F32_MIN: f32 = -3.4028234663852886e38;
29
+ const F32_MAX: f32 = 3.4028234663852886e38;
30
+
31
+ fn is_nan_f32(value: f32) -> bool {
32
+ let bits = bitcast<u32>(value);
33
+ return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
34
+ }
35
+ var<workgroup> wgPartial: array<{{ scalar }}, WG>;
36
+
37
+ {% macro emit_reduce(name, collective, combine) %}
38
+ fn {{ name }}(value: {{ scalar }}, sgLid: u32, sgId: u32, numSg: u32) -> {{ scalar }} {
39
+ let sgVal = {{ collective }}(value);
40
+ if (numSg == 1u) {
41
+ return sgVal;
42
+ }
43
+ if (sgLid == 0u) {
44
+ wgPartial[sgId] = sgVal;
45
+ }
46
+ workgroupBarrier();
47
+ var total = wgPartial[0];
48
+ for (var i = 1u; i < numSg; i = i + 1u) {
49
+ {{ combine }}
50
+ }
51
+ workgroupBarrier();
52
+ return total;
53
+ }
54
+ {%- endmacro %}{{ emit_reduce("reduce_row_add", "subgroupAdd", "total = total + wgPartial[i];") }}
55
+ {{ emit_reduce("reduce_row_max", "subgroupMax", "total = max(total, wgPartial[i]);") }}
56
+ @compute @workgroup_size(WG, 1, 1)
57
+ fn main(@builtin(workgroup_id) wg: vec3<u32>,
58
+ @builtin(num_workgroups) nwg: vec3<u32>,
59
+ @builtin(local_invocation_id) lid: vec3<u32>,
60
+ @builtin(subgroup_invocation_id) sgLid: u32,
61
+ @builtin(subgroup_id) sgId: u32,
62
+ @builtin(num_subgroups) numSg: u32) {
63
+ let row = wg.x + wg.y * nwg.x;
64
+ if (row >= params.rows) {
65
+ return;
66
+ }
67
+ let tid = lid.x;
68
+ let base = row * params.chunkCount; var localMax = F32_MIN;
69
+ var localNan = 0.0;
70
+ var localNanValue = 0.0;
71
+ for (var c = tid; c < params.chunkCount; c = c + WG) {
72
+ let v = {{ xv }}x[base + c]{{ vx }};
73
+ {%- if source.vec4 %}
74
+ {% for comp in ["x", "y", "z", "w"] %}
75
+ if (is_nan_f32(v.{{ comp }})) {
76
+ localNan = 1.0;
77
+ localNanValue = v.{{ comp }};
78
+ } else {
79
+ localMax = max(localMax, v.{{ comp }});
80
+ }
81
+ {%- endfor %}
82
+ {% else %}
83
+ if (is_nan_f32(v)) {
84
+ localNan = 1.0;
85
+ localNanValue = v;
86
+ } else {
87
+ localMax = max(localMax, v);
88
+ }
89
+ {%- endif %}
90
+ }
91
+ let rowMax = reduce_row_max(localMax, sgLid, sgId, numSg);
92
+ let nanCount = reduce_row_add(localNan, sgLid, sgId, numSg);
93
+ let nanValue = reduce_row_add(localNanValue, sgLid, sgId, numSg);
94
+ let hasPositiveInf = rowMax > F32_MAX;
95
+ let hasNan = nanCount > 0.0;
96
+ var acc = 0.0;
97
+ for (var c = tid; c < params.chunkCount; c = c + WG) {
98
+ let v = {{ xv }}x[base + c]{{ vx }};
99
+ {%- if source.vec4 %}
100
+ let e = select(exp(v - vec4<f32>(rowMax)), vec4<f32>(0.0), hasPositiveInf || hasNan);
101
+ acc = acc + ((e.x + e.y) + (e.z + e.w));
102
+ {%- else %}
103
+ acc = acc + select(exp(v - rowMax), 0.0, hasPositiveInf || hasNan);
104
+ {%- endif %}
105
+ }
106
+ let sum = reduce_row_add(acc, sgLid, sgId, numSg);
107
+ if (tid == 0u) {
108
+ let finiteOrInf = select(rowMax + log(sum), rowMax, hasPositiveInf);
109
+ y[row] = {{ yv }}select(finiteOrInf, nanValue, hasNan){{ vy }};
110
+ }}
build/webgpu/reduce-row-tree.wgsl.jinja ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 isVec4 = source.vec4 is defined and source.vec4 %}
12
+ {% set rowIsEmpty = "params.chunkCount == 0u" if isVec4 else "params.cols == 0u" %}
13
+ {% set castF32 = source.castF32 is defined and source.castF32 %}
14
+ {% set scalar = "f32" if castF32 else scalar %}
15
+ {% set xv = ("vec4<f32>(" if source.vec4 else "f32(") if castF32 else "" %}
16
+ {% set vx = ")" if castF32 else "" %}
17
+ {% set yv = "f16(" if castF32 else "" %}
18
+ {% set vy = ")" if castF32 else "" %}
19
+ {% if source.usesF16 is defined and source.usesF16 %}
20
+ enable f16;
21
+ {% endif %}
22
+ {{ env.wgsl.resourceDeclarations }}
23
+ /* Exact max/min reduction identities. WGSL rejects infinity during constant
24
+ * evaluation, so f32 identities are constructed at runtime from their
25
+ * IEEE-754 bit patterns. */
26
+
27
+ {% if scalar != "i32" and scalar != "u32" and (source.op == "logsum" or source.op == "logsumexp") %}
28
+ fn negative_infinity() -> f32 {
29
+ var bits = 0xff800000u;
30
+ return bitcast<f32>(bits);
31
+ }
32
+
33
+ {% endif %}
34
+
35
+ const WG: u32 = {{ workgroupSize }}u;
36
+
37
+ const F32_MIN: f32 = -3.4028234663852886e38;
38
+ const F32_MAX: f32 = 3.4028234663852886e38;
39
+
40
+ var<workgroup> partial: array<f32, WG>;
41
+ {% macro wgsl_tree_reduce_f32(name, mode, buffer="partial", wg="WG", trailingBarrier=true) %}
42
+ fn {{ name }}(value: f32, tid: u32) -> f32 {
43
+ {{ buffer }}[tid] = value;
44
+ workgroupBarrier();
45
+ // Ceil-halving keeps every lane when the workgroup size is not a power of
46
+ // two. For even n this matches the power-of-two tree order; for odd n, lanes
47
+ // [0, n-half) fold the upper tail while the middle lane carries forward.
48
+ var n: u32 = {{ wg }};
49
+ loop {
50
+ let half = (n + 1u) / 2u;
51
+ if (tid < n - half) {
52
+ {% if mode == "max" %}
53
+ {{ buffer }}[tid] = max({{ buffer }}[tid], {{ buffer }}[tid + half]);
54
+ {% else %}
55
+ {{ buffer }}[tid] = {{ buffer }}[tid] + {{ buffer }}[tid + half];
56
+ {% endif %}
57
+ }
58
+ workgroupBarrier();
59
+ n = half;
60
+ if (n == 1u) {
61
+ break;
62
+ }
63
+ }
64
+ // The default trailing barrier makes this helper safe for back-to-back calls: every lane reads
65
+ // slot 0 here, so the next call's first store must not run until all lanes have read it.
66
+ // `trailingBarrier=false` is safe only when the buffer is never written again before kernel exit.
67
+ let reduced = {{ buffer }}[0];
68
+ {% if trailingBarrier %}
69
+ workgroupBarrier();
70
+ {% endif %}
71
+ return reduced;
72
+ }
73
+ {% endmacro %}
74
+
75
+ {{ wgsl_tree_reduce_f32("reduce_sum", "add", "partial", "WG") }}
76
+ {{ wgsl_tree_reduce_f32("reduce_max", "max", "partial", "WG") }}
77
+
78
+ fn is_nan_f32(value: f32) -> bool {
79
+ let bits = bitcast<u32>(value);
80
+ return (bits & 0x7f800000u) == 0x7f800000u
81
+ && (bits & 0x007fffffu) != 0u;
82
+ }
83
+
84
+ @compute @workgroup_size(WG, 1, 1)
85
+ fn main(@builtin(workgroup_id) wg: vec3<u32>,
86
+ @builtin(num_workgroups) nwg: vec3<u32>,
87
+ @builtin(local_invocation_id) lid: vec3<u32>) {
88
+ let row = wg.x + wg.y * nwg.x;
89
+ if (row >= params.rows) {
90
+ return;
91
+ }
92
+ let tid = lid.x;
93
+ {% if source.vec4 %}
94
+ let base = row * params.chunkCount;
95
+ {% else %}
96
+ let base = row * params.cols;
97
+ {% endif %}
98
+
99
+ if ({{ rowIsEmpty }}) {
100
+ if (tid == 0u) {
101
+ y[row] = {{ yv }}negative_infinity(){{ vy }};
102
+ }
103
+ return;
104
+ }
105
+
106
+ var localMax = F32_MIN;
107
+ var localNan = 0.0;
108
+ var localNanValue = 0.0;
109
+ {% if source.vec4 %}
110
+ for (var col = tid; col < params.chunkCount; col = col + WG) {
111
+ let value = {{ xv }}x[base + col]{{ vx }};
112
+ {% for component in ["x", "y", "z", "w"] %}
113
+ if (is_nan_f32(value.{{ component }})) {
114
+ localNan = 1.0;
115
+ localNanValue = value.{{ component }};
116
+ } else {
117
+ localMax = max(localMax, value.{{ component }});
118
+ }
119
+ {% endfor %}
120
+ }
121
+ {% else %}
122
+ for (var col = tid; col < params.cols; col = col + WG) {
123
+ let value = {{ xv }}x[base + col]{{ vx }};
124
+ if (is_nan_f32(value)) {
125
+ localNan = 1.0;
126
+ localNanValue = value;
127
+ } else {
128
+ localMax = max(localMax, value);
129
+ }
130
+ }
131
+ {% endif %}
132
+ let rowMax = reduce_max(localMax, tid);
133
+ workgroupBarrier();
134
+ let nanCount = reduce_sum(localNan, tid);
135
+ workgroupBarrier();
136
+ let nanValue = reduce_sum(localNanValue, tid);
137
+ workgroupBarrier();
138
+ let hasPositiveInf = rowMax > F32_MAX;
139
+ let hasNan = nanCount > 0.0;
140
+
141
+ var acc = 0.0;
142
+ {% if source.vec4 %}
143
+ for (var col = tid; col < params.chunkCount; col = col + WG) {
144
+ let value = {{ xv }}x[base + col]{{ vx }};
145
+ let exponentials = select(exp(value - vec4<f32>(rowMax)), vec4<f32>(0.0),
146
+ hasPositiveInf || hasNan);
147
+ acc = acc + (exponentials.x + exponentials.y)
148
+ + (exponentials.z + exponentials.w);
149
+ }
150
+ {% else %}
151
+ for (var col = tid; col < params.cols; col = col + WG) {
152
+ acc = acc + select(exp({{ xv }}x[base + col]{{ vx }} - rowMax), 0.0,
153
+ hasPositiveInf || hasNan);
154
+ }
155
+ {% endif %}
156
+ let sum = reduce_sum(acc, tid);
157
+ if (tid == 0u) {
158
+ let finiteOrInf = select(rowMax + log(sum), rowMax, hasPositiveInf);
159
+ y[row] = {{ yv }}select(finiteOrInf, nanValue, hasNan){{ vy }};
160
+ }
161
+ }
build/webgpu/reduce-serial-axis.wgsl.jinja ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Serial one-thread-per-output reduction for the no-feature tier. f16 storage
2
+ // is widened before every accumulation and narrowed only for the final store.
3
+ {% set castF32 = source.castF32 is defined and source.castF32 %}
4
+ {% set intMode = source.intMode is defined and source.intMode %}
5
+ {% set yv = "f16(" if castF32 else "" %}
6
+ {% set vy = ")" if castF32 else "" %}
7
+ {% if source.usesF16 is defined and source.usesF16 %}
8
+ enable f16;
9
+ {% endif %}
10
+ {{ env.wgsl.resourceDeclarations }}
11
+ /* Exact max/min reduction identities. WGSL rejects infinity during constant
12
+ * evaluation, so f32 identities are constructed at runtime from their
13
+ * IEEE-754 bit patterns. */
14
+
15
+ {% if not intMode and (source.op == "logsum" or source.op == "logsumexp") %}
16
+ fn negative_infinity() -> f32 {
17
+ var bits = 0xff800000u;
18
+ return bitcast<f32>(bits);
19
+ }
20
+
21
+ {% endif %}
22
+
23
+ const F32_MIN: f32 = -3.4028234663852886e38;
24
+ {% if not intMode %}
25
+ const F32_MAX: f32 = 3.4028234663852886e38;
26
+
27
+ fn is_nan_f32(value: f32) -> bool {
28
+ let bits = bitcast<u32>(value);
29
+ return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
30
+ }
31
+ {% endif %}
32
+ {% if source.indexing == "rankn" %}
33
+
34
+ fn input_offset(out_index: u32, reduce_index: u32) -> u32 {
35
+ var rem = out_index;
36
+ {% for axis in range(source.outputRank) %}
37
+ {% set out_stride = namespace(value=1) %}
38
+ {% for j in range(axis + 1, source.outputRank) %}
39
+ {% set out_stride.value = out_stride.value * source.outputShape[j] %}
40
+ {% endfor %}
41
+ {% set safe_out_stride = 1 if out_stride.value == 0 else out_stride.value %}
42
+ {% if not source.keepDims or axis != source.axis %}
43
+ let out_coord{{ axis }} = rem / {{ safe_out_stride }}u;
44
+ {% endif %}
45
+ rem = rem % {{ safe_out_stride }}u;
46
+ {% endfor %}
47
+ {% for axis in range(source.rank) %}
48
+ {% if axis == source.axis %}
49
+ let coord{{ axis }} = reduce_index;
50
+ {% elif source.keepDims %}
51
+ let coord{{ axis }} = out_coord{{ axis }};
52
+ {% elif axis < source.axis %}
53
+ let coord{{ axis }} = out_coord{{ axis }};
54
+ {% else %}
55
+ let coord{{ axis }} = out_coord{{ axis - 1 }};
56
+ {% endif %}
57
+ {% endfor %}
58
+ {% set src = namespace(value="coord0") %}
59
+ {% for axis in range(1, source.rank) %}
60
+ {% set src.value = "(" ~ src.value ~ " * " ~ source.dataShape[axis] ~ "u + coord" ~ axis ~ ")" %}
61
+ {% endfor %}
62
+ return {{ src.value }};
63
+ }
64
+ {% endif %}
65
+ {% if source.indexing == "multiaxis" %}
66
+ {% set hasReducedAxis = namespace(value=false) %}
67
+ {% for a in range(source.rank) %}{% if source.reduce[a] %}{% set hasReducedAxis.value = true %}{% endif %}{% endfor %}
68
+
69
+ // One thread per output element walks the Cartesian product of the reduced axes,
70
+ // linearized as reduce_linear. Specialized shapes make every input offset a sum
71
+ // of coordinate-times-constant terms.
72
+ fn input_offset(out_index: u32{% if hasReducedAxis.value %}, reduce_linear: u32{% endif %}) -> u32 {
73
+ var rem = out_index;
74
+ {% for oaxis in range(source.outputRank) %}
75
+ {% set ostride = namespace(value=1) %}
76
+ {% for j in range(oaxis + 1, source.outputRank) %}
77
+ {% set ostride.value = ostride.value * source.outputShape[j] %}
78
+ {% endfor %}
79
+ {% set osafe = 1 if ostride.value == 0 else ostride.value %}
80
+ {% if not source.keepDims or not source.reduce[oaxis] %}
81
+ let out_coord{{ oaxis }} = rem / {{ osafe }}u;
82
+ {% endif %}
83
+ rem = rem % {{ osafe }}u;
84
+ {% endfor %}
85
+ {% if hasReducedAxis.value %}
86
+ var rrem = reduce_linear;
87
+ {% endif %}
88
+ {% for a in range(source.rank) if source.reduce[a] %}
89
+ {% set rstride = namespace(value=1) %}
90
+ {% for b in range(a + 1, source.rank) if source.reduce[b] %}
91
+ {% set rstride.value = rstride.value * source.dataShape[b] %}
92
+ {% endfor %}
93
+ {% set rsafe = 1 if rstride.value == 0 else rstride.value %}
94
+ let red_coord{{ a }} = rrem / {{ rsafe }}u;
95
+ rrem = rrem % {{ rsafe }}u;
96
+ {% endfor %}
97
+ {% set oc = namespace(i=0) %}
98
+ {% for a in range(source.rank) %}
99
+ {% if source.reduce[a] %}
100
+ let coord{{ a }} = red_coord{{ a }};
101
+ {% elif source.keepDims %}
102
+ let coord{{ a }} = out_coord{{ a }};
103
+ {% else %}
104
+ let coord{{ a }} = out_coord{{ oc.i }};
105
+ {% set oc.i = oc.i + 1 %}
106
+ {% endif %}
107
+ {% endfor %}
108
+ {% set src = namespace(value="coord0") %}
109
+ {% for a in range(1, source.rank) %}
110
+ {% set src.value = "(" ~ src.value ~ " * " ~ source.dataShape[a] ~ "u + coord" ~ a ~ ")" %}
111
+ {% endfor %}
112
+ return {{ src.value }};
113
+ }
114
+ {% endif %}
115
+ {% if source.indexing == "multiaxis" %}
116
+ {% set mcount = namespace(value=1) %}
117
+ {% for a in range(source.rank) if source.reduce[a] %}
118
+ {% set mcount.value = mcount.value * source.dataShape[a] %}
119
+ {% endfor %}
120
+ {% set count = mcount.value ~ "u" %}
121
+ {% if hasReducedAxis.value %}
122
+ {% set at = "x[input_offset(i, r)]" %}
123
+ {% else %}
124
+ {% set at = "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
+ {% elif axis == 0 %}
130
+ {% set count = "params.rows" %}
131
+ {% set at = "x[r * params.cols + i]" %}
132
+ {% else %}
133
+ {% set count = "params.cols" %}
134
+ {% set at = "x[i * params.cols + r]" %}
135
+ {% endif %}
136
+ {% if castF32 %}
137
+ {% set at = "f32(" ~ at ~ ")" %}
138
+ {% endif %}
139
+
140
+ @compute @workgroup_size({{ reduceWorkgroupSize }})
141
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) nwg: vec3<u32>) {
142
+ // 2D-folded flat index: gid.y carries the high bits past the
143
+ // maxComputeWorkgroupsPerDimension dispatch limit (outputs > 16.7M elements).
144
+ let i = gid.x + gid.y * nwg.x * {{ reduceWorkgroupSize }}u;
145
+ if (i >= params.outCount) {
146
+ return;
147
+ }
148
+ {% if intMode %}
149
+ // Integer logsumexp widens each element for exp/log, then truncates the result
150
+ // back to the integer output type.
151
+ // Integers are never NaN, so the f32 NaN-propagation path is unnecessary here.
152
+ if ({{ count }} == 0u) {
153
+ y[i] = {{ scalar }}(F32_MIN);
154
+ return;
155
+ }
156
+ var m = F32_MIN;
157
+ for (var r = 0u; r < {{ count }}; r = r + 1u) {
158
+ m = max(m, f32({{ at }}));
159
+ }
160
+ var acc = 0.0;
161
+ for (var r = 0u; r < {{ count }}; r = r + 1u) {
162
+ acc = acc + exp(f32({{ at }}) - m);
163
+ }
164
+ y[i] = {{ scalar }}(m + log(acc));
165
+ {% else %}
166
+ if ({{ count }} == 0u) {
167
+ y[i] = {{ yv }}negative_infinity(){{ vy }};
168
+ return;
169
+ }
170
+ var m = F32_MIN;
171
+ var has_nan = false;
172
+ var nan_value = 0.0;
173
+ for (var r = 0u; r < {{ count }}; r = r + 1u) {
174
+ let value = {{ at }};
175
+ if (is_nan_f32(value)) {
176
+ has_nan = true;
177
+ nan_value = value;
178
+ } else {
179
+ m = max(m, value);
180
+ }
181
+ }
182
+ if (has_nan) {
183
+ y[i] = {{ yv }}nan_value{{ vy }};
184
+ return;
185
+ }
186
+ if (m > F32_MAX) {
187
+ y[i] = {{ yv }}m{{ vy }};
188
+ return;
189
+ }
190
+ var acc = 0.0;
191
+ for (var r = 0u; r < {{ count }}; r = r + 1u) {
192
+ acc = acc + exp({{ at }} - m);
193
+ }
194
+ y[i] = {{ yv }}m + log(acc){{ vy }};
195
+ {% endif %}
196
+ }
build/webgpu/test.json ADDED
The diff for this file is too large to render. See raw diff