Xenova HF Staff commited on
Commit
1986f90
·
verified ·
1 Parent(s): 25fac3d

sync 6fdf6301e2bb

Browse files
README.md CHANGED
@@ -51,7 +51,7 @@ Some implementation variants require `subgroups`. These are route-specific capab
51
  - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, per-variant templates, provenance)
52
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
53
  - [`test.json`](build/webgpu/test.json) — correctness cases
54
- - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
55
  - [`softmax-longrow-normalize.wgsl.jinja`](build/webgpu/softmax-longrow-normalize.wgsl.jinja)
56
  - [`softmax-longrow-stats.wgsl.jinja`](build/webgpu/softmax-longrow-stats.wgsl.jinja)
57
  - [`softmax-normalize.wgsl.jinja`](build/webgpu/softmax-normalize.wgsl.jinja)
@@ -65,7 +65,7 @@ Some implementation variants require `subgroups`. These are route-specific capab
65
  ## Use with `@huggingface/kernels`
66
 
67
  ```sh
68
- npm install --save-exact @huggingface/kernels@0.0.1-preview.2
69
  ```
70
 
71
  Required output shapes and logical data types are inferred from the supplied inputs and attributes; result tensors are allocated automatically.
 
51
  - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, per-variant templates, provenance)
52
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
53
  - [`test.json`](build/webgpu/test.json) — correctness cases
54
+ - [`bench.json`](build/webgpu/bench.json) — benchmark cases
55
  - [`softmax-longrow-normalize.wgsl.jinja`](build/webgpu/softmax-longrow-normalize.wgsl.jinja)
56
  - [`softmax-longrow-stats.wgsl.jinja`](build/webgpu/softmax-longrow-stats.wgsl.jinja)
57
  - [`softmax-normalize.wgsl.jinja`](build/webgpu/softmax-normalize.wgsl.jinja)
 
65
  ## Use with `@huggingface/kernels`
66
 
67
  ```sh
68
+ npm install --save-exact @huggingface/kernels@0.0.1-preview.3
69
  ```
70
 
71
  Required output shapes and logical data types are inferred from the supplied inputs and attributes; result tensors are allocated automatically.
build/webgpu/bench.json CHANGED
@@ -110,7 +110,7 @@
110
  "bench": { "metrics": [{ "type": "bandwidth", "value": "args.rows * args.cols * dtypeBytes(args.dtype) * 2" }] }
111
  },
112
  {
113
- "name": "softmax-f32-packed-rows-131072x32-healthy",
114
  "preset": "stress",
115
  "vars": { "dtype": "float32", "rows": 131072, "cols": 32 },
116
  "attrs": { "axis": 1 },
@@ -166,7 +166,7 @@
166
  "name": "softmax-f32-131072x33-online-row-pathology",
167
  "preset": "stress",
168
  "provenance": {
169
- "notes": "One column past the packed-rows ceiling (<= 32 columns): 33 columns leave online_packed_rows_vec4_nosg for online_wg at one workgroup per row, about 32x the workgroup count of the same-bytes control softmax-f32-packed-rows-131072x32-healthy for a single added column."
170
  },
171
  "vars": { "dtype": "float32", "rows": 131072, "cols": 33 },
172
  "attrs": { "axis": 1 },
@@ -178,7 +178,7 @@
178
  "name": "softmax-f32-131072x64-subgroup-rows-control",
179
  "preset": "stress",
180
  "provenance": {
181
- "notes": "Width-64 context row for the 33-column pathology: the first width the subgroup-per-row vec4 route admits (>= 64 columns, % 4 == 0) at the same row count. Moves twice the bytes of the 32/33-column rows, so compare per element rather than per case."
182
  },
183
  "vars": { "dtype": "float32", "rows": 131072, "cols": 64 },
184
  "attrs": { "axis": 1 },
@@ -202,7 +202,7 @@
202
  "name": "softmax-f32-64x50260-vocab-control",
203
  "preset": "stress",
204
  "provenance": {
205
- "notes": "Nearest multiple-of-4 vocabulary width for softmax-f32-64x50257-vocab-odd-pathology; same bytes within 0.01%, selects online_wg_vec4."
206
  },
207
  "vars": { "dtype": "float32", "rows": 64, "cols": 50260 },
208
  "attrs": { "axis": 1 },
@@ -226,7 +226,7 @@
226
  "name": "softmax-f32-4096x200-vit-tokens-control",
227
  "preset": "stress",
228
  "provenance": {
229
- "notes": "Nearest multiple-of-4 width for softmax-f32-4096x197-vit-tokens-pathology (1.5% more bytes); with subgroups it selects online_subgroup_rows_vec4, without them online_wg_vec4 is demoted below 4096 columns and online_wg runs on both sides."
230
  },
231
  "vars": { "dtype": "float32", "rows": 4096, "cols": 200 },
232
  "attrs": { "axis": 1 },
 
110
  "bench": { "metrics": [{ "type": "bandwidth", "value": "args.rows * args.cols * dtypeBytes(args.dtype) * 2" }] }
111
  },
112
  {
113
+ "name": "softmax-f32-packed-rows-131072x32-control",
114
  "preset": "stress",
115
  "vars": { "dtype": "float32", "rows": 131072, "cols": 32 },
116
  "attrs": { "axis": 1 },
 
166
  "name": "softmax-f32-131072x33-online-row-pathology",
167
  "preset": "stress",
168
  "provenance": {
169
+ "notes": "One column past the packed-rows ceiling (<= 32 columns): 33 columns leave online_packed_rows_vec4_nosg for online_wg at one workgroup per row, about 32x the workgroup count of the same-bytes control softmax-f32-packed-rows-131072x32-control for a single added column."
170
  },
171
  "vars": { "dtype": "float32", "rows": 131072, "cols": 33 },
172
  "attrs": { "axis": 1 },
 
178
  "name": "softmax-f32-131072x64-subgroup-rows-control",
179
  "preset": "stress",
180
  "provenance": {
181
+ "notes": "Measures softmax over 131,072 rows of 64 columns (a multiple of four); its 33-column sibling in this file has the same row count but moves half the bytes, so compare per element rather than per case."
182
  },
183
  "vars": { "dtype": "float32", "rows": 131072, "cols": 64 },
184
  "attrs": { "axis": 1 },
 
202
  "name": "softmax-f32-64x50260-vocab-control",
203
  "preset": "stress",
204
  "provenance": {
205
+ "notes": "The next multiple-of-four vocabulary width above the paired 50,257-column case, with nearly the same input bytes."
206
  },
207
  "vars": { "dtype": "float32", "rows": 64, "cols": 50260 },
208
  "attrs": { "axis": 1 },
 
226
  "name": "softmax-f32-4096x200-vit-tokens-control",
227
  "preset": "stress",
228
  "provenance": {
229
+ "notes": "A 200-column control for the paired 197-column ViT token case; the width changes input size by about 1.5%. Both subgroup and portable tiers must normalize every row."
230
  },
231
  "vars": { "dtype": "float32", "rows": 4096, "cols": 200 },
232
  "attrs": { "axis": 1 },
build/webgpu/manifest.json CHANGED
@@ -20,6 +20,7 @@
20
  "PACKED_ROWS_MAX_COLS": { "default": 256 },
21
  "PACKED_ROWS_MIN_ROW_GROUPS": { "default": 64 },
22
  "PACKED_ROWS_VEC4_MAX_COLS": { "default": 128 },
 
23
  "STRIDED_ONLINE_STREAMS": { "default": 2 }
24
  },
25
  "derive": {
@@ -29,7 +30,6 @@
29
  "axis": "attrs.axis if attrs.axis >= 0 else attrs.axis + ranks.x",
30
  "lastCols": "dim(shapes.x, ranks.x - 1)",
31
  "lastRows": "rows(shapes.x, ranks.x - 1)",
32
- "axisCols": "dim(shapes.x, axis)",
33
  "axisInner": "inner(shapes.x, axis)",
34
  "axisOuter": "outer(shapes.x, axis)",
35
  "axisRows": "axisOuter * axisInner",
@@ -45,37 +45,36 @@
45
  "rowWorkgroupSize": "min(tunables.WORKGROUP_SIZE, max(tunables.ROW_MIN_WORKGROUP, pow2ceil(ceilDiv(lastCols, tunables.ROW_COLS_PER_LANE))))",
46
  "subgroupRowsWorkgroup": "min(tunables.WORKGROUP_SIZE, deviceWorkgroupCap)",
47
  "subgroupRowsLaneVecs": "ceilDiv(lastCols / 4, device.adapterInfo.subgroupMinSize) if (device.features.has(\"subgroups\") and has(device.adapterInfo, \"subgroupMinSize\") and device.adapterInfo.subgroupMinSize > 0) else 0",
 
 
48
  "stridedOnlineStreams": "min(4, pow2ceil(max(1, tunables.STRIDED_ONLINE_STREAMS)), max(1, ceilDiv(dim(shapes.x, axis), 256 / stridedCoopTileCols))) if dtypes.T == \"f16\" else 1"
49
  },
50
  "bindings": {
51
- "x": { "buffer": "read-only-storage", "elementType": "$T" },
52
- "rowMax": { "buffer": "storage", "elementType": "f32" },
53
- "rowSum": { "buffer": "storage", "elementType": "f32" },
54
- "rowMax_2": { "name": "rowMax", "buffer": "read-only-storage", "elementType": "f32" },
55
- "rowSum_2": { "name": "rowSum", "buffer": "read-only-storage", "elementType": "f32" },
56
- "y": { "buffer": "storage", "elementType": "$T" },
57
- "params_4": {
58
  "name": "params",
59
- "buffer": "uniform",
60
  "struct": [
61
- { "name": "rows", "type": "u32", "value": "lastRows" },
62
  { "name": "cols", "type": "u32", "value": "lastCols" }
63
  ]
64
  },
65
- "x_2": { "name": "x", "buffer": "read-only-storage", "elementType": "$vectorScalar" },
66
- "y_2": { "name": "y", "buffer": "storage", "elementType": "$vectorScalar" },
 
 
 
 
67
  "params": {
68
- "buffer": "uniform",
69
  "struct": [
70
  { "name": "rows", "type": "u32", "value": "lastRows" },
71
  { "name": "vecCols", "type": "u32", "value": "lastCols / 4" }
72
  ]
73
  },
74
- "params_6": {
75
  "name": "params",
76
- "buffer": "uniform",
77
  "struct": [
78
- { "name": "count", "type": "u32", "value": "numel(shapes.x)" },
79
  { "name": "cols", "type": "u32", "value": "lastCols" }
80
  ]
81
  }
@@ -105,11 +104,11 @@
105
  "id": "block_stats",
106
  "name": "Softmax.LongRowBlockStats",
107
  "shader": "softmax-longrow-stats.wgsl.jinja",
108
- "derive": { "stage": "\"block\"" },
109
  "bindings": [
110
  "x",
111
- { "name": "blockMax", "buffer": "storage", "elementType": "f32" },
112
- { "name": "blockSum", "buffer": "storage", "elementType": "f32" },
113
  {
114
  "name": "params",
115
  "struct": [
@@ -127,7 +126,7 @@
127
  "id": "row_stats",
128
  "name": "Softmax.LongRowStats",
129
  "shader": "softmax-longrow-stats.wgsl.jinja",
130
- "derive": { "stage": "\"row\"" },
131
  "bindings": [
132
  { "name": "blockMax", "buffer": "read-only-storage", "elementType": "f32" },
133
  { "name": "blockSum", "buffer": "read-only-storage", "elementType": "f32" },
@@ -147,8 +146,8 @@
147
  "derive": { "op": "\"softmax\"" },
148
  "bindings": [
149
  "x",
150
- "rowMax_2",
151
- "rowSum_2",
152
  "y",
153
  { "name": "params", "struct": [{ "name": "cols", "type": "u32", "value": "lastCols" }] }
154
  ],
@@ -176,13 +175,12 @@
176
  "name": "Softmax.OnlineSubgroupRowsVec4",
177
  "shader": "softmax-subgroup-rows.wgsl.jinja",
178
  "derive": { "op": "\"softmax\"" },
179
- "bindings": ["x_2", "y_2", "params"],
180
  "dispatch": {
181
  "x": "min(ceilDiv(lastRows, subgroupRowsWorkgroup / device.adapterInfo.subgroupMaxSize), 65535)",
182
  "y": "ceilDiv(ceilDiv(lastRows, subgroupRowsWorkgroup / device.adapterInfo.subgroupMaxSize), 65535)",
183
  "z": 1
184
- },
185
- "subgroupCollectivesWidth": "portable"
186
  }
187
  ]
188
  },
@@ -190,7 +188,7 @@
190
  "id": "online_wg_vec4",
191
  "priority": 16,
192
  "when": ["dim(shapes.x, ranks.x - 1) % 4 == 0", "ceilDiv(rows(shapes.x, ranks.x - 1), min(device.limits.maxComputeWorkgroupsPerDimension, 65535)) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535)", "lastAxisOk"],
193
- "demoteWhen": ["dim(shapes.x, ranks.x - 1) < 4096"],
194
  "derive": {
195
  "combineSubgroups": "device.features.has(\"subgroups\") and not narrowSubgroupRange",
196
  "scalar": "dtypes.T",
@@ -202,9 +200,8 @@
202
  "id": "main",
203
  "name": "Softmax.OnlineWorkgroupVec4",
204
  "shader": "softmax-online.wgsl.jinja",
205
- "subgroupCollectivesWidth": "portable",
206
  "derive": { "op": "\"softmax\"", "useVec4": "true" },
207
- "bindings": ["x_2", "y_2", "params"],
208
  "dispatch": {
209
  "x": "min(rows(shapes.x, ranks.x - 1), 65535)",
210
  "y": "ceilDiv(rows(shapes.x, ranks.x - 1), 65535)",
@@ -227,9 +224,8 @@
227
  "id": "main",
228
  "name": "Softmax.OnlineWorkgroup",
229
  "shader": "softmax-online.wgsl.jinja",
230
- "subgroupCollectivesWidth": "portable",
231
  "derive": { "op": "\"softmax\"", "useVec4": "false" },
232
- "bindings": ["x", "y", "params_4"],
233
  "dispatch": {
234
  "x": "min(rows(shapes.x, ranks.x - 1), 65535)",
235
  "y": "ceilDiv(rows(shapes.x, ranks.x - 1), 65535)",
@@ -259,7 +255,7 @@
259
  "rowsPerWorkgroup": "rowsPerWorkgroup",
260
  "useVec4": "true"
261
  },
262
- "bindings": ["x_2", "y_2", "params"],
263
  "dispatch": {
264
  "x": "min(ceilDiv(rows(shapes.x, ranks.x - 1), rowsPerWorkgroup), min(device.limits.maxComputeWorkgroupsPerDimension, 65535))",
265
  "y": 1,
@@ -284,7 +280,7 @@
284
  "lanesPerRow": "lanesPerRow",
285
  "rowsPerWorkgroup": "rowsPerWorkgroup"
286
  },
287
- "bindings": ["x", "y", "params_4"],
288
  "dispatch": {
289
  "x": "min(ceilDiv(rows(shapes.x, ranks.x - 1), rowsPerWorkgroup), min(device.limits.maxComputeWorkgroupsPerDimension, 65535))",
290
  "y": 1,
@@ -310,7 +306,7 @@
310
  "name": "Softmax.OnlineLocalVec4NoSg",
311
  "shader": "softmax-online.wgsl.jinja",
312
  "derive": { "op": "\"softmax\"", "useVec4": "true" },
313
- "bindings": ["x_2", "y_2", "params"],
314
  "dispatch": {
315
  "x": "min(rows(shapes.x, ranks.x - 1), 65535)",
316
  "y": "ceilDiv(rows(shapes.x, ranks.x - 1), 65535)",
@@ -331,7 +327,7 @@
331
  "name": "Softmax.OnlineLocalNoSg",
332
  "shader": "softmax-online.wgsl.jinja",
333
  "derive": { "op": "\"softmax\"", "useVec4": "false" },
334
- "bindings": ["x", "y", "params_4"],
335
  "dispatch": {
336
  "x": "min(rows(shapes.x, ranks.x - 1), 65535)",
337
  "y": "ceilDiv(rows(shapes.x, ranks.x - 1), 65535)",
@@ -343,7 +339,7 @@
343
  {
344
  "id": "strided_online_coop",
345
  "priority": 13,
346
- "when": ["stridedAxisOk", "f16Ok(dtypes.T)", "axisDispatchFits"],
347
  "passes": [
348
  {
349
  "id": "main",
@@ -378,7 +374,7 @@
378
  "name": "Softmax.ReduceMax",
379
  "shader": "softmax-row-stage.wgsl.jinja",
380
  "derive": { "stage": "\"max\"", "strided": "false" },
381
- "bindings": ["x", "rowMax", "params_4"],
382
  "dispatch": {
383
  "x": "min(numel(shapes.x) / dim(shapes.x, ranks.x - 1), 65535)",
384
  "y": "ceilDiv(numel(shapes.x) / dim(shapes.x, ranks.x - 1), 65535)",
@@ -390,7 +386,7 @@
390
  "name": "Softmax.ExpSum",
391
  "shader": "softmax-row-stage.wgsl.jinja",
392
  "derive": { "stage": "\"exp_sum\"", "strided": "false" },
393
- "bindings": ["x", "rowMax_2", "y", "rowSum", "params_4"],
394
  "dispatch": {
395
  "x": "min(numel(shapes.x) / dim(shapes.x, ranks.x - 1), 65535)",
396
  "y": "ceilDiv(numel(shapes.x) / dim(shapes.x, ranks.x - 1), 65535)",
@@ -402,7 +398,7 @@
402
  "name": "Softmax.Normalize",
403
  "shader": "softmax-normalize.wgsl.jinja",
404
  "derive": { "strided": "false" },
405
- "bindings": ["y", "rowSum_2", "params_6"],
406
  "dispatch": {
407
  "x": "min(ceilDiv((numel(shapes.x)), (tunables.WORKGROUP_SIZE)), 65535)",
408
  "y": "ceilDiv(ceilDiv((numel(shapes.x)), (tunables.WORKGROUP_SIZE)), 65535)",
 
20
  "PACKED_ROWS_MAX_COLS": { "default": 256 },
21
  "PACKED_ROWS_MIN_ROW_GROUPS": { "default": 64 },
22
  "PACKED_ROWS_VEC4_MAX_COLS": { "default": 128 },
23
+ "WG_VEC4_MIN_COLS": { "default": 128 },
24
  "STRIDED_ONLINE_STREAMS": { "default": 2 }
25
  },
26
  "derive": {
 
30
  "axis": "attrs.axis if attrs.axis >= 0 else attrs.axis + ranks.x",
31
  "lastCols": "dim(shapes.x, ranks.x - 1)",
32
  "lastRows": "rows(shapes.x, ranks.x - 1)",
 
33
  "axisInner": "inner(shapes.x, axis)",
34
  "axisOuter": "outer(shapes.x, axis)",
35
  "axisRows": "axisOuter * axisInner",
 
45
  "rowWorkgroupSize": "min(tunables.WORKGROUP_SIZE, max(tunables.ROW_MIN_WORKGROUP, pow2ceil(ceilDiv(lastCols, tunables.ROW_COLS_PER_LANE))))",
46
  "subgroupRowsWorkgroup": "min(tunables.WORKGROUP_SIZE, deviceWorkgroupCap)",
47
  "subgroupRowsLaneVecs": "ceilDiv(lastCols / 4, device.adapterInfo.subgroupMinSize) if (device.features.has(\"subgroups\") and has(device.adapterInfo, \"subgroupMinSize\") and device.adapterInfo.subgroupMinSize > 0) else 0",
48
+ "stridedCoopDispatchFits": "min(ceilDiv(axisRows, stridedCoopTileCols), DISPATCH_FOLD_WIDTH) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535) and ceilDiv(ceilDiv(axisRows, stridedCoopTileCols), DISPATCH_FOLD_WIDTH) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535)",
49
+ "axisCols": "dim(shapes.x, axis)",
50
  "stridedOnlineStreams": "min(4, pow2ceil(max(1, tunables.STRIDED_ONLINE_STREAMS)), max(1, ceilDiv(dim(shapes.x, axis), 256 / stridedCoopTileCols))) if dtypes.T == \"f16\" else 1"
51
  },
52
  "bindings": {
53
+ "x": { "elementType": "$T" },
54
+ "y": { "elementType": "$T" },
55
+ "params__uniform": {
 
 
 
 
56
  "name": "params",
 
57
  "struct": [
58
+ { "name": "count", "type": "u32", "value": "numel(shapes.x)" },
59
  { "name": "cols", "type": "u32", "value": "lastCols" }
60
  ]
61
  },
62
+ "rowMax": { "buffer": "storage", "elementType": "f32" },
63
+ "rowSum": { "buffer": "storage", "elementType": "f32" },
64
+ "rowMax_f32": { "name": "rowMax", "buffer": "read-only-storage", "elementType": "f32" },
65
+ "rowSum_f32": { "name": "rowSum", "buffer": "read-only-storage", "elementType": "f32" },
66
+ "x_main": { "name": "x", "elementType": "$vectorScalar" },
67
+ "y_main": { "name": "y", "elementType": "$vectorScalar" },
68
  "params": {
 
69
  "struct": [
70
  { "name": "rows", "type": "u32", "value": "lastRows" },
71
  { "name": "vecCols", "type": "u32", "value": "lastCols / 4" }
72
  ]
73
  },
74
+ "params_main": {
75
  "name": "params",
 
76
  "struct": [
77
+ { "name": "rows", "type": "u32", "value": "lastRows" },
78
  { "name": "cols", "type": "u32", "value": "lastCols" }
79
  ]
80
  }
 
104
  "id": "block_stats",
105
  "name": "Softmax.LongRowBlockStats",
106
  "shader": "softmax-longrow-stats.wgsl.jinja",
107
+ "derive": { "stage": "\"block\"", "combineSubgroups": false },
108
  "bindings": [
109
  "x",
110
+ { "name": "blockMax", "elementType": "f32" },
111
+ { "name": "blockSum", "elementType": "f32" },
112
  {
113
  "name": "params",
114
  "struct": [
 
126
  "id": "row_stats",
127
  "name": "Softmax.LongRowStats",
128
  "shader": "softmax-longrow-stats.wgsl.jinja",
129
+ "derive": { "stage": "\"row\"", "combineSubgroups": false },
130
  "bindings": [
131
  { "name": "blockMax", "buffer": "read-only-storage", "elementType": "f32" },
132
  { "name": "blockSum", "buffer": "read-only-storage", "elementType": "f32" },
 
146
  "derive": { "op": "\"softmax\"" },
147
  "bindings": [
148
  "x",
149
+ "rowMax_f32",
150
+ "rowSum_f32",
151
  "y",
152
  { "name": "params", "struct": [{ "name": "cols", "type": "u32", "value": "lastCols" }] }
153
  ],
 
175
  "name": "Softmax.OnlineSubgroupRowsVec4",
176
  "shader": "softmax-subgroup-rows.wgsl.jinja",
177
  "derive": { "op": "\"softmax\"" },
178
+ "bindings": ["x_main", "y_main", "params"],
179
  "dispatch": {
180
  "x": "min(ceilDiv(lastRows, subgroupRowsWorkgroup / device.adapterInfo.subgroupMaxSize), 65535)",
181
  "y": "ceilDiv(ceilDiv(lastRows, subgroupRowsWorkgroup / device.adapterInfo.subgroupMaxSize), 65535)",
182
  "z": 1
183
+ }
 
184
  }
185
  ]
186
  },
 
188
  "id": "online_wg_vec4",
189
  "priority": 16,
190
  "when": ["dim(shapes.x, ranks.x - 1) % 4 == 0", "ceilDiv(rows(shapes.x, ranks.x - 1), min(device.limits.maxComputeWorkgroupsPerDimension, 65535)) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535)", "lastAxisOk"],
191
+ "demoteWhen": ["dim(shapes.x, ranks.x - 1) < tunables.WG_VEC4_MIN_COLS"],
192
  "derive": {
193
  "combineSubgroups": "device.features.has(\"subgroups\") and not narrowSubgroupRange",
194
  "scalar": "dtypes.T",
 
200
  "id": "main",
201
  "name": "Softmax.OnlineWorkgroupVec4",
202
  "shader": "softmax-online.wgsl.jinja",
 
203
  "derive": { "op": "\"softmax\"", "useVec4": "true" },
204
+ "bindings": ["x_main", "y_main", "params"],
205
  "dispatch": {
206
  "x": "min(rows(shapes.x, ranks.x - 1), 65535)",
207
  "y": "ceilDiv(rows(shapes.x, ranks.x - 1), 65535)",
 
224
  "id": "main",
225
  "name": "Softmax.OnlineWorkgroup",
226
  "shader": "softmax-online.wgsl.jinja",
 
227
  "derive": { "op": "\"softmax\"", "useVec4": "false" },
228
+ "bindings": ["x", "y", "params_main"],
229
  "dispatch": {
230
  "x": "min(rows(shapes.x, ranks.x - 1), 65535)",
231
  "y": "ceilDiv(rows(shapes.x, ranks.x - 1), 65535)",
 
255
  "rowsPerWorkgroup": "rowsPerWorkgroup",
256
  "useVec4": "true"
257
  },
258
+ "bindings": ["x_main", "y_main", "params"],
259
  "dispatch": {
260
  "x": "min(ceilDiv(rows(shapes.x, ranks.x - 1), rowsPerWorkgroup), min(device.limits.maxComputeWorkgroupsPerDimension, 65535))",
261
  "y": 1,
 
280
  "lanesPerRow": "lanesPerRow",
281
  "rowsPerWorkgroup": "rowsPerWorkgroup"
282
  },
283
+ "bindings": ["x", "y", "params_main"],
284
  "dispatch": {
285
  "x": "min(ceilDiv(rows(shapes.x, ranks.x - 1), rowsPerWorkgroup), min(device.limits.maxComputeWorkgroupsPerDimension, 65535))",
286
  "y": 1,
 
306
  "name": "Softmax.OnlineLocalVec4NoSg",
307
  "shader": "softmax-online.wgsl.jinja",
308
  "derive": { "op": "\"softmax\"", "useVec4": "true" },
309
+ "bindings": ["x_main", "y_main", "params"],
310
  "dispatch": {
311
  "x": "min(rows(shapes.x, ranks.x - 1), 65535)",
312
  "y": "ceilDiv(rows(shapes.x, ranks.x - 1), 65535)",
 
327
  "name": "Softmax.OnlineLocalNoSg",
328
  "shader": "softmax-online.wgsl.jinja",
329
  "derive": { "op": "\"softmax\"", "useVec4": "false" },
330
+ "bindings": ["x", "y", "params_main"],
331
  "dispatch": {
332
  "x": "min(rows(shapes.x, ranks.x - 1), 65535)",
333
  "y": "ceilDiv(rows(shapes.x, ranks.x - 1), 65535)",
 
339
  {
340
  "id": "strided_online_coop",
341
  "priority": 13,
342
+ "when": ["stridedAxisOk", "f16Ok(dtypes.T)", "stridedCoopDispatchFits"],
343
  "passes": [
344
  {
345
  "id": "main",
 
374
  "name": "Softmax.ReduceMax",
375
  "shader": "softmax-row-stage.wgsl.jinja",
376
  "derive": { "stage": "\"max\"", "strided": "false" },
377
+ "bindings": ["x", "rowMax", "params_main"],
378
  "dispatch": {
379
  "x": "min(numel(shapes.x) / dim(shapes.x, ranks.x - 1), 65535)",
380
  "y": "ceilDiv(numel(shapes.x) / dim(shapes.x, ranks.x - 1), 65535)",
 
386
  "name": "Softmax.ExpSum",
387
  "shader": "softmax-row-stage.wgsl.jinja",
388
  "derive": { "stage": "\"exp_sum\"", "strided": "false" },
389
+ "bindings": ["x", "rowMax_f32", "y", "rowSum", "params_main"],
390
  "dispatch": {
391
  "x": "min(numel(shapes.x) / dim(shapes.x, ranks.x - 1), 65535)",
392
  "y": "ceilDiv(numel(shapes.x) / dim(shapes.x, ranks.x - 1), 65535)",
 
398
  "name": "Softmax.Normalize",
399
  "shader": "softmax-normalize.wgsl.jinja",
400
  "derive": { "strided": "false" },
401
+ "bindings": ["y", "rowSum_f32", "params__uniform"],
402
  "dispatch": {
403
  "x": "min(ceilDiv((numel(shapes.x)), (tunables.WORKGROUP_SIZE)), 65535)",
404
  "y": "ceilDiv(ceilDiv((numel(shapes.x)), (tunables.WORKGROUP_SIZE)), 65535)",
build/webgpu/metadata.json CHANGED
@@ -1,29 +1,29 @@
1
  {
2
  "name": "ai.onnx.Softmax",
3
- "id": "_ai_onnx_softmax_webgpu_3fa7f40",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
- "bench.json": "aU1fchujehBREF4OEI2lDx52FqWQ7KG71PLit0YBOJk=",
11
- "manifest.json": "QkoJAAKNK3VdbvesprtvngDdr6fwrsUrZXOAzMepQHg=",
12
- "softmax-longrow-normalize.wgsl.jinja": "ttaSzDzYfSH4XuQxo9EyAPOGcYiV0dkbUEug5e7seZg=",
13
- "softmax-longrow-stats.wgsl.jinja": "prBVBse5ULq+Wl8maEVHnHtV24ispCW8XYS6ys3rzmA=",
14
- "softmax-normalize.wgsl.jinja": "HMCOne5hCi5FFNWwfx/cDYOWSX9HbQbxGc64fNCYNeE=",
15
- "softmax-online-packed-rows.wgsl.jinja": "+WeaU/Vi2axEcmzfW9HbLo5W0cLyYskd4QU0JCdZXPU=",
16
- "softmax-online.wgsl.jinja": "HW9xDlnVtDtOuoQ9V0qdCldBRSPksX3OWjGnaAUSuts=",
17
- "softmax-row-stage.wgsl.jinja": "QwJMAIkPQ6XppyMHf8uM5oBCHi/Dk2Nbur2cgTOuNGE=",
18
- "softmax-strided-online-coop.wgsl.jinja": "Stf+8ldvQ37YZRZqK2eMwxFE5IRtSHI5CnDuX6cBaqg=",
19
- "softmax-strided-online-lane.wgsl.jinja": "8g3/Y0koYxoMY2EwXlBYfwV/fxs8Qo9lOxJNcymmRzs=",
20
- "softmax-subgroup-rows.wgsl.jinja": "wU5WO+CSSIcJt5KKD13affqBjFjrNbdvSNglO15sVDg=",
21
- "test.json": "HYBGxPhprRUNZ7v+qibfypCv6gGWJpb30yH2IXo1WL8="
22
  }
23
  },
24
- "provenance": { "kernel": { "sha": "91d990483a174128daf7673f3f37a7c890493ae1", "dirty": false } },
25
  "webgpu": {
26
- "manifestSpec": "2.0",
27
  "variants": {
28
  "longrow_split": ["softmax-longrow-normalize.wgsl.jinja", "softmax-longrow-stats.wgsl.jinja"],
29
  "online_subgroup_rows_vec4": ["softmax-subgroup-rows.wgsl.jinja"],
 
1
  {
2
  "name": "ai.onnx.Softmax",
3
+ "id": "_ai_onnx_softmax_webgpu_9c9600e",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
+ "bench.json": "CnIUtxX0uN+sBSS6S7sXw4PhPgJN0quVT/7N7+m+kvc=",
11
+ "manifest.json": "bvTch1ywn3onsk2ac2P6UQN14EdCTr7IXYOQVJxkwhA=",
12
+ "softmax-longrow-normalize.wgsl.jinja": "GbW6zPjxGeiY5RQZTFwS5pUJ++ZTI/HQ6xtfmrssn18=",
13
+ "softmax-longrow-stats.wgsl.jinja": "S8w3EVvJXQavrn/79+OYm6yMZWERwlyJiZhYF8HkAj0=",
14
+ "softmax-normalize.wgsl.jinja": "WJcysxIi1A/NUtzd8CQXbAGTyVRH8J2D3fBJjH8oQlM=",
15
+ "softmax-online-packed-rows.wgsl.jinja": "uUzYkkedsLP0Jbu7C6puiDpvzvN+h5bSnsSs2lEY2xY=",
16
+ "softmax-online.wgsl.jinja": "/uJEBPE+2Yh0kTputbT4qMMyeB8IbkaWnRwlw+jQ8Qk=",
17
+ "softmax-row-stage.wgsl.jinja": "7+SK74287ITFa+SmGFdWhyjVYO0uKbQ8fN1X2eP/TbA=",
18
+ "softmax-strided-online-coop.wgsl.jinja": "80DwMJFFvyEWnTcPQXUmhFoFPhk1Uj9X/smbmCMn930=",
19
+ "softmax-strided-online-lane.wgsl.jinja": "Z/lssYWxtZYq1aNDCkA3+U4lJUwHce22W7gFaAKi/PQ=",
20
+ "softmax-subgroup-rows.wgsl.jinja": "aD8MbAhJiaMqEWPg64GAAg6af5rUzC2v3LRYJI3ItcI=",
21
+ "test.json": "hNvyxaj27rdwa+Th6fTNY8lBe80QVerINYUaKVO4NO4="
22
  }
23
  },
24
+ "provenance": { "kernel": { "sha": "6fdf6301e2bbcc2f03bf1eaf493b7ad55ef33afc", "dirty": false } },
25
  "webgpu": {
26
+ "manifestSpec": "2.1",
27
  "variants": {
28
  "longrow_split": ["softmax-longrow-normalize.wgsl.jinja", "softmax-longrow-stats.wgsl.jinja"],
29
  "online_subgroup_rows_vec4": ["softmax-subgroup-rows.wgsl.jinja"],
build/webgpu/softmax-longrow-normalize.wgsl.jinja CHANGED
@@ -1,9 +1,6 @@
1
  // Final normalization pass for long-row split softmax. Each workgroup handles
2
  // one block of a row and re-reads the input against the completed row maximum
3
  // and denominator. The epilogue supports softmax and log-softmax.
4
- {% if T == "f16" %}
5
- enable f16;
6
- {% endif %}
7
  {{ env.wgsl.resourceDeclarations }}
8
 
9
  const WG: u32 = {{ tunables.WORKGROUP_SIZE }}u;
@@ -27,6 +24,7 @@ fn shifted_value(value: f32, maxValue: f32) -> f32 {
27
  let equalFiniteMax = select(false, value == maxValue, is_finite_f32(maxValue));
28
  return select(value - maxValue, 0.0, equalFiniteMax);
29
  }
 
30
  fn exp_shift(value: f32, maxValue: f32) -> f32 {
31
  return exp(shifted_value(value, maxValue));
32
  }
@@ -37,7 +35,6 @@ fn is_nan_f32(value: f32) -> bool {
37
  return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
38
  }
39
 
40
-
41
  @compute @workgroup_size(WG)
42
  fn main(@builtin(workgroup_id) wg: vec3<u32>,
43
  @builtin(local_invocation_id) lid: vec3<u32>) {
 
1
  // Final normalization pass for long-row split softmax. Each workgroup handles
2
  // one block of a row and re-reads the input against the completed row maximum
3
  // and denominator. The epilogue supports softmax and log-softmax.
 
 
 
4
  {{ env.wgsl.resourceDeclarations }}
5
 
6
  const WG: u32 = {{ tunables.WORKGROUP_SIZE }}u;
 
24
  let equalFiniteMax = select(false, value == maxValue, is_finite_f32(maxValue));
25
  return select(value - maxValue, 0.0, equalFiniteMax);
26
  }
27
+
28
  fn exp_shift(value: f32, maxValue: f32) -> f32 {
29
  return exp(shifted_value(value, maxValue));
30
  }
 
35
  return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
36
  }
37
 
 
38
  @compute @workgroup_size(WG)
39
  fn main(@builtin(workgroup_id) wg: vec3<u32>,
40
  @builtin(local_invocation_id) lid: vec3<u32>) {
build/webgpu/softmax-longrow-stats.wgsl.jinja CHANGED
@@ -3,9 +3,8 @@
3
  // The block stage assigns one workgroup to each (block, row), merges per-thread
4
  // online pairs, and writes one partial pair. The row stage assigns one
5
  // workgroup per row and folds those block partials into rowMax and rowSum.
6
- {% if stage == "block" and T == "f16" %}
7
- enable f16;
8
- {% endif %}
9
  {{ env.wgsl.resourceDeclarations }}
10
 
11
  const WG: u32 = {{ tunables.WORKGROUP_SIZE }}u;
@@ -31,6 +30,7 @@ fn shifted_value(value: f32, maxValue: f32) -> f32 {
31
  let equalFiniteMax = select(false, value == maxValue, is_finite_f32(maxValue));
32
  return select(value - maxValue, 0.0, equalFiniteMax);
33
  }
 
34
  fn exp_shift(value: f32, maxValue: f32) -> f32 {
35
  return exp(shifted_value(value, maxValue));
36
  }
@@ -40,94 +40,9 @@ fn exp_shift(value: f32, maxValue: f32) -> f32 {
40
  // Both the subgroup and portable barrier-tree engines return the same merged
41
  // pair to every invocation. Repeated merges require a workgroup barrier between
42
  // calls before their shared partial storage is reused.
43
- {% set combineSubgroups = combineSubgroups is defined and combineSubgroups %}
44
- {% if combineSubgroups %}
45
- // Cross-subgroup merge that assumes nothing about which invocations share a
46
- // subgroup or how many subgroups there are: each subgroup's elected lane
47
- // publishes the subgroup pair in the slot at its OWN invocation index and sets
48
- // that index's bit in a workgroup bitmask; thread 0 then folds exactly the
49
- // published slots, in ascending index order (the online (m, d) merge is not
50
- // float-associative, so the order is fixed), and clears the mask for the next
51
- // call as it reads it. Workgroup memory starts zeroed, so the mask needs no
52
- // setup. Same three collectives as a single-subgroup reduce, two barriers.
53
- var<workgroup> partialM: array<f32, WG>;
54
- var<workgroup> partialD: array<f32, WG>;
55
- var<workgroup> leaderMask: array<atomic<u32>, (WG + 31u) / 32u>;
56
- var<workgroup> combinedMD: vec2<f32>;
57
-
58
- // When the whole workgroup is one subgroup the subgroup reduce already covers
59
- // it (no barriers, no shared state). `subgroup_size` is the size of the current
60
- // subgroup and uniform, so the test is exact and may guard the barriers below.
61
- fn combine_partials(m: f32, d: f32, lidx: u32, sgSize: u32) -> vec2<f32> {
62
- let sgM = subgroupMax(m);
63
- // A lane with no elements contributes d == 0 (exact identity). A +inf
64
- // element made exp(inf - inf) = NaN stick in that lane's d; a NaN element
65
- // landed in d via exp(NaN); both survive the merge and are detected by the
66
- // code after the reduction.
67
- let sgD = subgroupAdd(d * exp_shift(m, sgM));
68
- if (sgSize == WG) {
69
- return vec2<f32>(sgM, sgD);
70
- }
71
- if (subgroupElect()) {
72
- partialM[lidx] = sgM;
73
- partialD[lidx] = sgD;
74
- atomicOr(&leaderMask[lidx / 32u], 1u << (lidx % 32u));
75
- }
76
- workgroupBarrier();
77
- if (lidx == 0u) {
78
- var accM = -FLT_MAX;
79
- var accD = 0.0;
80
- for (var w = 0u; w < (WG + 31u) / 32u; w = w + 1u) {
81
- var bits = atomicExchange(&leaderMask[w], 0u);
82
- while (bits != 0u) {
83
- let slot = w * 32u + firstTrailingBit(bits);
84
- bits = bits & (bits - 1u);
85
- let mNew = max(accM, partialM[slot]);
86
- accD = accD * exp_shift(accM, mNew) + partialD[slot] * exp_shift(partialM[slot], mNew);
87
- accM = mNew;
88
- }
89
- }
90
- combinedMD = vec2<f32>(accM, accD);
91
- }
92
- workgroupBarrier();
93
- return combinedMD;
94
- }
95
- {% else %}
96
- {% set mdStreamed = mdStreams is defined %}
97
- {% set mdStreams = mdStreams if mdStreams is defined else 1 %}
98
- {% set mdExtent = "WG" if mdStreams == 1 else "WG * " ~ mdStreams ~ "u" %}
99
  var<workgroup> partialM: array<f32, {{ mdExtent }}>;
100
  var<workgroup> partialD: array<f32, {{ mdExtent }}>;
101
- {% if mdStreamed %}
102
-
103
- // In-place fold of {{ mdStreams }} streams. Input partials occupy
104
- // partialM/partialD; stream s returns its merged pair in slot s * WG.
105
- fn combine_partials_streams(lidx: u32) {
106
- workgroupBarrier();
107
- var stride = WG / 2u;
108
- loop {
109
- if (stride == 0u) {
110
- break;
111
- }
112
- if (lidx < stride) {
113
- {% for s in range(mdStreams) %}
114
- {
115
- let slot = {{ s }}u * WG + lidx;
116
- let m1 = partialM[slot];
117
- let d1 = partialD[slot];
118
- let m2 = partialM[slot + stride];
119
- let d2 = partialD[slot + stride];
120
- let mNew = max(m1, m2);
121
- partialD[slot] = d1 * exp_shift(m1, mNew) + d2 * exp_shift(m2, mNew);
122
- partialM[slot] = mNew;
123
- }
124
- {% endfor %}
125
- }
126
- workgroupBarrier();
127
- stride = stride / 2u;
128
- }
129
- }
130
- {% else %}
131
 
132
  fn combine_partials(m: f32, d: f32, lidx: u32) -> vec2<f32> {
133
  partialM[lidx] = m;
@@ -156,9 +71,6 @@ fn combine_partials(m: f32, d: f32, lidx: u32) -> vec2<f32> {
156
  workgroupBarrier();
157
  return merged;
158
  }
159
- {% endif %}
160
- {% endif %}
161
-
162
 
163
  @compute @workgroup_size(WG)
164
  fn main(@builtin(workgroup_id) wg: vec3<u32>,
@@ -166,14 +78,14 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>,
166
  {% if stage == "block" %}
167
  let block = wg.x;
168
  let row = wg.y;
169
- let rowBase = row * params.cols;
170
  let colStart = block * BLOCK_COLS;
171
- let colEnd = min(params.cols, colStart + BLOCK_COLS);
172
 
173
  var m = -FLT_MAX;
174
  var d = 0.0;
175
  for (var col = colStart + lid.x; col < colEnd; col = col + WG) {
176
- let v = f32(x[rowBase + col]);
177
  let mNew = max(m, v);
178
  d = d * exp_shift(m, mNew) + exp_shift(v, mNew);
179
  m = mNew;
 
3
  // The block stage assigns one workgroup to each (block, row), merges per-thread
4
  // online pairs, and writes one partial pair. The row stage assigns one
5
  // workgroup per row and folds those block partials into rowMax and rowSum.
6
+ {% set biasRow = false %}
7
+ {% set cols = "params.cols" %}
 
8
  {{ env.wgsl.resourceDeclarations }}
9
 
10
  const WG: u32 = {{ tunables.WORKGROUP_SIZE }}u;
 
30
  let equalFiniteMax = select(false, value == maxValue, is_finite_f32(maxValue));
31
  return select(value - maxValue, 0.0, equalFiniteMax);
32
  }
33
+
34
  fn exp_shift(value: f32, maxValue: f32) -> f32 {
35
  return exp(shifted_value(value, maxValue));
36
  }
 
40
  // Both the subgroup and portable barrier-tree engines return the same merged
41
  // pair to every invocation. Repeated merges require a workgroup barrier between
42
  // calls before their shared partial storage is reused.
43
+ {% set mdExtent = "WG" %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  var<workgroup> partialM: array<f32, {{ mdExtent }}>;
45
  var<workgroup> partialD: array<f32, {{ mdExtent }}>;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
  fn combine_partials(m: f32, d: f32, lidx: u32) -> vec2<f32> {
48
  partialM[lidx] = m;
 
71
  workgroupBarrier();
72
  return merged;
73
  }
 
 
 
74
 
75
  @compute @workgroup_size(WG)
76
  fn main(@builtin(workgroup_id) wg: vec3<u32>,
 
78
  {% if stage == "block" %}
79
  let block = wg.x;
80
  let row = wg.y;
81
+ let rowBase = row * {{ cols }};
82
  let colStart = block * BLOCK_COLS;
83
+ let colEnd = min({{ cols }}, colStart + BLOCK_COLS);
84
 
85
  var m = -FLT_MAX;
86
  var d = 0.0;
87
  for (var col = colStart + lid.x; col < colEnd; col = col + WG) {
88
+ let v = {{ "f32(data[rowBase + col]) + f32(bias[biasBase + col])" if biasRow else "f32(x[rowBase + col])" }};
89
  let mNew = max(m, v);
90
  d = d * exp_shift(m, mNew) + exp_shift(v, mNew);
91
  m = mNew;
build/webgpu/softmax-normalize.wgsl.jinja CHANGED
@@ -1,17 +1,19 @@
1
- {% if T == "f16" %}
2
- enable f16;
3
- {% endif %}
 
 
 
 
 
4
  {{ env.wgsl.resourceDeclarations }}
5
  {% if op == "logsoftmax" %}
6
- {% set stableHelperUsage = "shifted" %}
7
- {% set stableUsage = stableHelperUsage if stableHelperUsage is defined else "all" %}
8
  // FLT_MAX, not -inf, as the online (m, d) accumulator init: merges must keep
9
  // `m - m` finite so an empty lane / all--inf row contributes the exact
10
  // accumulator identity (m, d) = (-FLT_MAX, 0). Operator epilogues interpret
11
  // a zero final denominator according to their public semantics. Using -inf
12
  // here changes +inf-row behavior.
13
  const FLT_MAX: f32 = 3.4028234663852886e38;
14
- {% if stableUsage != "constant" %}
15
 
16
  fn is_finite_f32(value: f32) -> bool {
17
  return select(false, value <= FLT_MAX, value >= -FLT_MAX);
@@ -25,31 +27,17 @@ fn shifted_value(value: f32, maxValue: f32) -> f32 {
25
  let equalFiniteMax = select(false, value == maxValue, is_finite_f32(maxValue));
26
  return select(value - maxValue, 0.0, equalFiniteMax);
27
  }
28
- {%- endif %}
29
- {% if stableUsage == "all" %}
30
-
31
- fn exp_shift(value: f32, maxValue: f32) -> f32 {
32
- return exp(shifted_value(value, maxValue));
33
- }
34
- {%- endif %}
35
-
36
  // Bit-exact NaN test: `v != v` can be folded to false by fast-math backends,
37
  // and a constant NaN bit pattern is rejected at WGSL const-evaluation.
38
  fn is_nan_f32(value: f32) -> bool {
39
  let bits = bitcast<u32>(value);
40
  return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
41
  }
42
-
43
  {% endif %}
44
 
45
  @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
46
  fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
47
- // 2D-folded flat index: gid.y carries the high bits past the
48
- // per-axis dispatch fold width (outputs > 16.7M elements).
49
- let i = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ tunables.WORKGROUP_SIZE }}u;
50
- if (i >= params.count) {
51
- return;
52
- }
53
  {% if op == "logsoftmax" %}
54
  // Final element-parallel log-softmax pass. Each element rereads x and writes
55
  // (x - rowMax) - log(rowSum); strided mode changes only the row map.
 
1
+ {% macro flat_index_2d(workgroupSize, name="i", bound="params.count", guardInline=false) %}
2
+ {% set wgTerm = workgroupSize ~ "u" if workgroupSize is number else workgroupSize %}
3
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
4
+ // per-axis workgroup fold width.
5
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ wgTerm }};
6
+ if ({{ name }} >= {{ bound }}) {
7
+ return;
8
+ }{% endmacro %}
9
  {{ env.wgsl.resourceDeclarations }}
10
  {% if op == "logsoftmax" %}
 
 
11
  // FLT_MAX, not -inf, as the online (m, d) accumulator init: merges must keep
12
  // `m - m` finite so an empty lane / all--inf row contributes the exact
13
  // accumulator identity (m, d) = (-FLT_MAX, 0). Operator epilogues interpret
14
  // a zero final denominator according to their public semantics. Using -inf
15
  // here changes +inf-row behavior.
16
  const FLT_MAX: f32 = 3.4028234663852886e38;
 
17
 
18
  fn is_finite_f32(value: f32) -> bool {
19
  return select(false, value <= FLT_MAX, value >= -FLT_MAX);
 
27
  let equalFiniteMax = select(false, value == maxValue, is_finite_f32(maxValue));
28
  return select(value - maxValue, 0.0, equalFiniteMax);
29
  }
 
 
 
 
 
 
 
 
30
  // Bit-exact NaN test: `v != v` can be folded to false by fast-math backends,
31
  // and a constant NaN bit pattern is rejected at WGSL const-evaluation.
32
  fn is_nan_f32(value: f32) -> bool {
33
  let bits = bitcast<u32>(value);
34
  return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
35
  }
 
36
  {% endif %}
37
 
38
  @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
39
  fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
40
+ {{ flat_index_2d(tunables.WORKGROUP_SIZE) }}
 
 
 
 
 
41
  {% if op == "logsoftmax" %}
42
  // Final element-parallel log-softmax pass. Each element rereads x and writes
43
  // (x - rowMax) - log(rowSum); strided mode changes only the row map.
build/webgpu/softmax-online-packed-rows.wgsl.jinja CHANGED
@@ -31,6 +31,7 @@ fn shifted_value(value: f32, maxValue: f32) -> f32 {
31
  let equalFiniteMax = select(false, value == maxValue, is_finite_f32(maxValue));
32
  return select(value - maxValue, 0.0, equalFiniteMax);
33
  }
 
34
  fn exp_shift(value: f32, maxValue: f32) -> f32 {
35
  return exp(shifted_value(value, maxValue));
36
  }
@@ -41,7 +42,6 @@ fn is_nan_f32(value: f32) -> bool {
41
  return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
42
  }
43
 
44
-
45
  // Each logical row owns one contiguous LANES_PER_ROW slice. All reductions
46
  // are workgroup-portable; no subgroup width or subgroup feature is assumed.
47
  var<workgroup> partialM: array<f32, WG>;
 
31
  let equalFiniteMax = select(false, value == maxValue, is_finite_f32(maxValue));
32
  return select(value - maxValue, 0.0, equalFiniteMax);
33
  }
34
+
35
  fn exp_shift(value: f32, maxValue: f32) -> f32 {
36
  return exp(shifted_value(value, maxValue));
37
  }
 
42
  return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
43
  }
44
 
 
45
  // Each logical row owns one contiguous LANES_PER_ROW slice. All reductions
46
  // are workgroup-portable; no subgroup width or subgroup feature is assumed.
47
  var<workgroup> partialM: array<f32, WG>;
build/webgpu/softmax-online.wgsl.jinja CHANGED
@@ -6,9 +6,17 @@ enable subgroups;
6
  // Workgroup-level online normalization over one contiguous last-axis row per
7
  // workgroup. Each thread keeps an online (maximum, denominator) pair over its
8
  // strided chunk. The workgroup merges those pairs, then a fused second sweep
9
- // writes the normalized result. Vectorized rows use vec4 accesses.
 
 
10
  //
11
  // `workgroupSize` fixes the merge-tree width for this specialization.
 
 
 
 
 
 
12
  const WG: u32 = {{ workgroupSize }}u;
13
  // FLT_MAX, not -inf, as the online (m, d) accumulator init: merges must keep
14
  // `m - m` finite so an empty lane / all--inf row contributes the exact
@@ -29,6 +37,7 @@ fn shifted_value(value: f32, maxValue: f32) -> f32 {
29
  let equalFiniteMax = select(false, value == maxValue, is_finite_f32(maxValue));
30
  return select(value - maxValue, 0.0, equalFiniteMax);
31
  }
 
32
  fn exp_shift(value: f32, maxValue: f32) -> f32 {
33
  return exp(shifted_value(value, maxValue));
34
  }
@@ -38,14 +47,12 @@ fn is_nan_f32(value: f32) -> bool {
38
  let bits = bitcast<u32>(value);
39
  return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
40
  }
41
-
42
  // Workgroup-cooperative merge of per-thread online-softmax (m, d) partials:
43
  // mNew = max(m1, m2)
44
  // dNew = d1 * exp(m1 - mNew) + d2 * exp(m2 - mNew)
45
  // Both the subgroup and portable barrier-tree engines return the same merged
46
  // pair to every invocation. Repeated merges require a workgroup barrier between
47
  // calls before their shared partial storage is reused.
48
- {% set combineSubgroups = combineSubgroups is defined and combineSubgroups %}
49
  {% if combineSubgroups %}
50
  // Cross-subgroup merge that assumes nothing about which invocations share a
51
  // subgroup or how many subgroups there are: each subgroup's elected lane
@@ -98,41 +105,9 @@ fn combine_partials(m: f32, d: f32, lidx: u32, sgSize: u32) -> vec2<f32> {
98
  return combinedMD;
99
  }
100
  {% else %}
101
- {% set mdStreamed = mdStreams is defined %}
102
- {% set mdStreams = mdStreams if mdStreams is defined else 1 %}
103
- {% set mdExtent = "WG" if mdStreams == 1 else "WG * " ~ mdStreams ~ "u" %}
104
  var<workgroup> partialM: array<f32, {{ mdExtent }}>;
105
  var<workgroup> partialD: array<f32, {{ mdExtent }}>;
106
- {% if mdStreamed %}
107
-
108
- // In-place fold of {{ mdStreams }} streams. Input partials occupy
109
- // partialM/partialD; stream s returns its merged pair in slot s * WG.
110
- fn combine_partials_streams(lidx: u32) {
111
- workgroupBarrier();
112
- var stride = WG / 2u;
113
- loop {
114
- if (stride == 0u) {
115
- break;
116
- }
117
- if (lidx < stride) {
118
- {% for s in range(mdStreams) %}
119
- {
120
- let slot = {{ s }}u * WG + lidx;
121
- let m1 = partialM[slot];
122
- let d1 = partialD[slot];
123
- let m2 = partialM[slot + stride];
124
- let d2 = partialD[slot + stride];
125
- let mNew = max(m1, m2);
126
- partialD[slot] = d1 * exp_shift(m1, mNew) + d2 * exp_shift(m2, mNew);
127
- partialM[slot] = mNew;
128
- }
129
- {% endfor %}
130
- }
131
- workgroupBarrier();
132
- stride = stride / 2u;
133
- }
134
- }
135
- {% else %}
136
 
137
  fn combine_partials(m: f32, d: f32, lidx: u32) -> vec2<f32> {
138
  partialM[lidx] = m;
@@ -162,8 +137,6 @@ fn combine_partials(m: f32, d: f32, lidx: u32) -> vec2<f32> {
162
  return merged;
163
  }
164
  {% endif %}
165
- {% endif %}
166
-
167
 
168
  @compute @workgroup_size({{ workgroupSize }})
169
  fn main(@builtin(workgroup_id) wid: vec3<u32>,
@@ -186,7 +159,7 @@ fn main(@builtin(workgroup_id) wid: vec3<u32>,
186
  var d = 0.0;
187
  {% if useVec4 %}
188
  for (var c = lid.x; c < params.vecCols; c = c + WG) {
189
- let v = vec4<f32>(x[rowBase + c]);
190
  let mNew = max(m, max(max(v.x, v.y), max(v.z, v.w)));
191
  d = d * exp_shift(m, mNew)
192
  + exp_shift(v.x, mNew) + exp_shift(v.y, mNew) + exp_shift(v.z, mNew) + exp_shift(v.w, mNew);
@@ -217,7 +190,7 @@ fn main(@builtin(workgroup_id) wid: vec3<u32>,
217
  // denominator from an all--infinity row produces an all-NaN row.
218
  {% if useVec4 %}
219
  for (var c = lid.x; c < params.vecCols; c = c + WG) {
220
- let v = vec4<f32>(x[rowBase + c]);
221
  var o = vec4<f32>(nanOut);
222
  if (!rowIsNan) {
223
  o = vec4<f32>(
@@ -226,7 +199,7 @@ fn main(@builtin(workgroup_id) wid: vec3<u32>,
226
  exp_shift(v.z, finalM),
227
  exp_shift(v.w, finalM)) / finalD;
228
  }
229
- y[rowBase + c] = vec4<{{ scalar }}>(o);
230
  }
231
  {% else %}
232
  for (var c = lid.x; c < params.cols; c = c + WG) {
 
6
  // Workgroup-level online normalization over one contiguous last-axis row per
7
  // workgroup. Each thread keeps an online (maximum, denominator) pair over its
8
  // strided chunk. The workgroup merges those pairs, then a fused second sweep
9
+ // writes the normalized result. Vectorized rows use native vec4 storage when
10
+ // aligned, or guarded scalar storage at unaligned row strides. Both paths retain
11
+ // f32 bias and reduction math.
12
  //
13
  // `workgroupSize` fixes the merge-tree width for this specialization.
14
+ // `scalarVec4` selects scalar storage for fused bias rows only.
15
+ {% macro read_vec4(c) %}
16
+ vec4<f32>(x[rowBase + {{ c }}]){% endmacro %}
17
+ {% macro store_vec4(value, c) %}
18
+ y[rowBase + {{ c }}] = vec4<{{ scalar }}>({{ value }});
19
+ {% endmacro %}
20
  const WG: u32 = {{ workgroupSize }}u;
21
  // FLT_MAX, not -inf, as the online (m, d) accumulator init: merges must keep
22
  // `m - m` finite so an empty lane / all--inf row contributes the exact
 
37
  let equalFiniteMax = select(false, value == maxValue, is_finite_f32(maxValue));
38
  return select(value - maxValue, 0.0, equalFiniteMax);
39
  }
40
+
41
  fn exp_shift(value: f32, maxValue: f32) -> f32 {
42
  return exp(shifted_value(value, maxValue));
43
  }
 
47
  let bits = bitcast<u32>(value);
48
  return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
49
  }
 
50
  // Workgroup-cooperative merge of per-thread online-softmax (m, d) partials:
51
  // mNew = max(m1, m2)
52
  // dNew = d1 * exp(m1 - mNew) + d2 * exp(m2 - mNew)
53
  // Both the subgroup and portable barrier-tree engines return the same merged
54
  // pair to every invocation. Repeated merges require a workgroup barrier between
55
  // calls before their shared partial storage is reused.
 
56
  {% if combineSubgroups %}
57
  // Cross-subgroup merge that assumes nothing about which invocations share a
58
  // subgroup or how many subgroups there are: each subgroup's elected lane
 
105
  return combinedMD;
106
  }
107
  {% else %}
108
+ {% set mdExtent = "WG" %}
 
 
109
  var<workgroup> partialM: array<f32, {{ mdExtent }}>;
110
  var<workgroup> partialD: array<f32, {{ mdExtent }}>;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
  fn combine_partials(m: f32, d: f32, lidx: u32) -> vec2<f32> {
113
  partialM[lidx] = m;
 
137
  return merged;
138
  }
139
  {% endif %}
 
 
140
 
141
  @compute @workgroup_size({{ workgroupSize }})
142
  fn main(@builtin(workgroup_id) wid: vec3<u32>,
 
159
  var d = 0.0;
160
  {% if useVec4 %}
161
  for (var c = lid.x; c < params.vecCols; c = c + WG) {
162
+ let v = {{ read_vec4("c") }};
163
  let mNew = max(m, max(max(v.x, v.y), max(v.z, v.w)));
164
  d = d * exp_shift(m, mNew)
165
  + exp_shift(v.x, mNew) + exp_shift(v.y, mNew) + exp_shift(v.z, mNew) + exp_shift(v.w, mNew);
 
190
  // denominator from an all--infinity row produces an all-NaN row.
191
  {% if useVec4 %}
192
  for (var c = lid.x; c < params.vecCols; c = c + WG) {
193
+ let v = {{ read_vec4("c") }};
194
  var o = vec4<f32>(nanOut);
195
  if (!rowIsNan) {
196
  o = vec4<f32>(
 
199
  exp_shift(v.z, finalM),
200
  exp_shift(v.w, finalM)) / finalD;
201
  }
202
+ {{ store_vec4("o", "c") }}
203
  }
204
  {% else %}
205
  for (var c = lid.x; c < params.cols; c = c + WG) {
build/webgpu/softmax-row-stage.wgsl.jinja CHANGED
@@ -1,14 +1,11 @@
1
  // First and second passes of the three-pass softmax, with one workgroup per
2
  // row. The max stage reduces the row maximum. The exp_sum stage writes
3
  // exp(x - rowMax) and reduces the row denominator.
4
- {% if T == "f16" %}
5
- enable f16;
6
- {% endif %}
7
  {{ env.wgsl.resourceDeclarations }}
8
 
9
  const WGS: u32 = {{ tunables.WORKGROUP_SIZE }}u;
10
- {% macro wgsl_tree_reduce_f32(name, mode, buffer="partial", wg="WG", trailingBarrier=true) %}
11
- fn {{ name }}(value: f32, tid: u32) -> f32 {
12
  {{ buffer }}[tid] = value;
13
  workgroupBarrier();
14
  // Ceil-halving keeps every lane when the workgroup size is not a power of
@@ -34,22 +31,20 @@ fn {{ name }}(value: f32, tid: u32) -> f32 {
34
  // slot 0 here, so the next call's first store must not run until all lanes have read it.
35
  // `trailingBarrier=false` is safe only when the buffer is never written again before kernel exit.
36
  let reduced = {{ buffer }}[0];
37
- {% if trailingBarrier %}
38
  workgroupBarrier();
39
- {% endif %}
40
  return reduced;
41
  }
42
  {% endmacro %}
43
 
44
  var<workgroup> partial: array<f32, WGS>;
 
45
  {% if stage == "max" %}
46
  {{ wgsl_tree_reduce_f32("reduce_max_f32", "max", "partial", "WGS") }}
47
  {% else %}
48
  {{ wgsl_tree_reduce_f32("reduce_add_f32", "add", "partial", "WGS") }}
49
  {% endif %}
50
- {% set count = "REDUCE_SIZE" if strided else "params.cols" %}
51
- {% set offset_of = "element_offset(row, k)" if strided else "row * params.cols + k" %}
52
-
53
  @compute @workgroup_size(WGS)
54
  fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
55
  // 2D-folded row index: wg.y carries the high bits once the row count passes the
 
1
  // First and second passes of the three-pass softmax, with one workgroup per
2
  // row. The max stage reduces the row maximum. The exp_sum stage writes
3
  // exp(x - rowMax) and reduces the row denominator.
 
 
 
4
  {{ env.wgsl.resourceDeclarations }}
5
 
6
  const WGS: u32 = {{ tunables.WORKGROUP_SIZE }}u;
7
+ {% macro wgsl_tree_reduce_f32(name, mode, buffer="partial", wg="WG", trailingBarrier=true, valueType="f32") %}
8
+ fn {{ name }}(value: {{ valueType }}, tid: u32) -> {{ valueType }} {
9
  {{ buffer }}[tid] = value;
10
  workgroupBarrier();
11
  // Ceil-halving keeps every lane when the workgroup size is not a power of
 
31
  // slot 0 here, so the next call's first store must not run until all lanes have read it.
32
  // `trailingBarrier=false` is safe only when the buffer is never written again before kernel exit.
33
  let reduced = {{ buffer }}[0];
 
34
  workgroupBarrier();
 
35
  return reduced;
36
  }
37
  {% endmacro %}
38
 
39
  var<workgroup> partial: array<f32, WGS>;
40
+
41
  {% if stage == "max" %}
42
  {{ wgsl_tree_reduce_f32("reduce_max_f32", "max", "partial", "WGS") }}
43
  {% else %}
44
  {{ wgsl_tree_reduce_f32("reduce_add_f32", "add", "partial", "WGS") }}
45
  {% endif %}
46
+ {% set count = "params.cols" %}
47
+ {% set offset_of = "row * params.cols + k" %}
 
48
  @compute @workgroup_size(WGS)
49
  fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
50
  // 2D-folded row index: wg.y carries the high bits once the row count passes the
build/webgpu/softmax-strided-online-coop.wgsl.jinja CHANGED
@@ -6,11 +6,7 @@
6
  // online streams. Their partials and the per-lane partials merge in workgroup memory with the same update form as the serial
7
  // walk, so NaN and +Inf rows reproduce the single-lane kernel's outputs.
8
  {% set operation = op if op is defined else "softmax" %}
9
- {% set tileCols = tileCols %}
10
  {% set axisLanes = (256 / tileCols) | int %}
11
- {% if T == "f16" %}
12
- enable f16;
13
- {% endif %}
14
  {{ env.wgsl.resourceDeclarations }}
15
  {% set stridedAxisUses = ["STRIDED_ROWS", "element_offset"] %}
16
  // Compiled indexing for row-wise reductions over a non-last axis. A row is one
@@ -50,17 +46,17 @@ const INNER_SIZE_SAFE: u32 = {{ inner_size.value if inner_size.value > 0 else 1
50
  {% if usesAxisStrideSafe %}
51
  const AXIS_STRIDE_SAFE: u32 = {{ (reduce_size * inner_size.value) if (reduce_size * inner_size.value) > 0 else 1 }}u;
52
  {% endif %}
53
-
54
  {% if usesElementOffset %}
 
55
  // Flat offset of element k (along the reduce axis) of `row`.
56
  fn element_offset(row: u32, k: u32) -> u32 {
57
  let outer = row / INNER_SIZE_SAFE;
58
  let inner = row % INNER_SIZE_SAFE;
59
  return outer * REDUCE_SIZE * INNER_SIZE + k * INNER_SIZE + inner;
60
  }
61
-
62
  {% endif %}
63
  {% if usesRowForOffset %}
 
64
  // Row that owns the flat element `offset` (inverse of element_offset over
65
  // the non-axis coordinates).
66
  fn row_for_offset(offset: u32) -> u32 {
@@ -68,17 +64,14 @@ fn row_for_offset(offset: u32) -> u32 {
68
  let inner = offset % INNER_SIZE_SAFE;
69
  return outer * INNER_SIZE_SAFE + inner;
70
  }
71
- {%- endif -%}
72
  {% if operation == "logsoftmax" %}
73
- {% set stableHelperUsage = "shifted" %}
74
- {% set stableUsage = stableHelperUsage if stableHelperUsage is defined else "all" %}
75
  // FLT_MAX, not -inf, as the online (m, d) accumulator init: merges must keep
76
  // `m - m` finite so an empty lane / all--inf row contributes the exact
77
  // accumulator identity (m, d) = (-FLT_MAX, 0). Operator epilogues interpret
78
  // a zero final denominator according to their public semantics. Using -inf
79
  // here changes +inf-row behavior.
80
  const FLT_MAX: f32 = 3.4028234663852886e38;
81
- {% if stableUsage != "constant" %}
82
 
83
  fn is_finite_f32(value: f32) -> bool {
84
  return select(false, value <= FLT_MAX, value >= -FLT_MAX);
@@ -92,23 +85,8 @@ fn shifted_value(value: f32, maxValue: f32) -> f32 {
92
  let equalFiniteMax = select(false, value == maxValue, is_finite_f32(maxValue));
93
  return select(value - maxValue, 0.0, equalFiniteMax);
94
  }
95
- {%- endif %}
96
- {% if stableUsage == "all" %}
97
-
98
- fn exp_shift(value: f32, maxValue: f32) -> f32 {
99
- return exp(shifted_value(value, maxValue));
100
- }
101
- {%- endif %}
102
-
103
- {% endif %}
104
-
105
- const TILE_COLS: u32 = {{ tileCols }}u;
106
- const AXIS_LANES: u32 = {{ axisLanes }}u;
107
- {% if operation != "logsoftmax" %}
108
- const F32_MAX: f32 = 3.4028234663852886e38;
109
  {% endif %}
110
  {% set negInit = "-FLT_MAX" if operation == "logsoftmax" else "-F32_MAX" %}
111
-
112
  {% macro update_partial(maxName, sumName, valueName, sumValue) %}
113
  if ({{ valueName }} > {{ maxName }}) {
114
  {{ sumName }} = {{ sumName }} * exp({{ maxName }} - {{ valueName }}) + {{ sumValue }};
@@ -122,6 +100,12 @@ const F32_MAX: f32 = 3.4028234663852886e38;
122
  }
123
  {% endmacro %}
124
 
 
 
 
 
 
 
125
  var<workgroup> part_max: array<f32, TILE_COLS * AXIS_LANES>;
126
  var<workgroup> part_sum: array<f32, TILE_COLS * AXIS_LANES>;
127
 
 
6
  // online streams. Their partials and the per-lane partials merge in workgroup memory with the same update form as the serial
7
  // walk, so NaN and +Inf rows reproduce the single-lane kernel's outputs.
8
  {% set operation = op if op is defined else "softmax" %}
 
9
  {% set axisLanes = (256 / tileCols) | int %}
 
 
 
10
  {{ env.wgsl.resourceDeclarations }}
11
  {% set stridedAxisUses = ["STRIDED_ROWS", "element_offset"] %}
12
  // Compiled indexing for row-wise reductions over a non-last axis. A row is one
 
46
  {% if usesAxisStrideSafe %}
47
  const AXIS_STRIDE_SAFE: u32 = {{ (reduce_size * inner_size.value) if (reduce_size * inner_size.value) > 0 else 1 }}u;
48
  {% endif %}
 
49
  {% if usesElementOffset %}
50
+
51
  // Flat offset of element k (along the reduce axis) of `row`.
52
  fn element_offset(row: u32, k: u32) -> u32 {
53
  let outer = row / INNER_SIZE_SAFE;
54
  let inner = row % INNER_SIZE_SAFE;
55
  return outer * REDUCE_SIZE * INNER_SIZE + k * INNER_SIZE + inner;
56
  }
 
57
  {% endif %}
58
  {% if usesRowForOffset %}
59
+
60
  // Row that owns the flat element `offset` (inverse of element_offset over
61
  // the non-axis coordinates).
62
  fn row_for_offset(offset: u32) -> u32 {
 
64
  let inner = offset % INNER_SIZE_SAFE;
65
  return outer * INNER_SIZE_SAFE + inner;
66
  }
67
+ {% endif %}
68
  {% if operation == "logsoftmax" %}
 
 
69
  // FLT_MAX, not -inf, as the online (m, d) accumulator init: merges must keep
70
  // `m - m` finite so an empty lane / all--inf row contributes the exact
71
  // accumulator identity (m, d) = (-FLT_MAX, 0). Operator epilogues interpret
72
  // a zero final denominator according to their public semantics. Using -inf
73
  // here changes +inf-row behavior.
74
  const FLT_MAX: f32 = 3.4028234663852886e38;
 
75
 
76
  fn is_finite_f32(value: f32) -> bool {
77
  return select(false, value <= FLT_MAX, value >= -FLT_MAX);
 
85
  let equalFiniteMax = select(false, value == maxValue, is_finite_f32(maxValue));
86
  return select(value - maxValue, 0.0, equalFiniteMax);
87
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  {% endif %}
89
  {% set negInit = "-FLT_MAX" if operation == "logsoftmax" else "-F32_MAX" %}
 
90
  {% macro update_partial(maxName, sumName, valueName, sumValue) %}
91
  if ({{ valueName }} > {{ maxName }}) {
92
  {{ sumName }} = {{ sumName }} * exp({{ maxName }} - {{ valueName }}) + {{ sumValue }};
 
100
  }
101
  {% endmacro %}
102
 
103
+ const TILE_COLS: u32 = {{ tileCols }}u;
104
+ const AXIS_LANES: u32 = {{ axisLanes }}u;
105
+ {% if operation != "logsoftmax" %}
106
+ const F32_MAX: f32 = 3.4028234663852886e38;
107
+ {% endif %}
108
+
109
  var<workgroup> part_max: array<f32, TILE_COLS * AXIS_LANES>;
110
  var<workgroup> part_sum: array<f32, TILE_COLS * AXIS_LANES>;
111
 
build/webgpu/softmax-strided-online-lane.wgsl.jinja CHANGED
@@ -1,9 +1,12 @@
1
  // Scratch-free online softmax for a non-last reduction axis. One invocation
2
  // owns one (outer, inner) row. At each reduction step, neighboring invocations
3
  // access consecutive inner positions. Row statistics remain private.
4
- {% if T == "f16" %}
5
- enable f16;
6
- {% endif %}
 
 
 
7
  {{ env.wgsl.resourceDeclarations }}
8
  {% set stridedAxisUses = ["STRIDED_ROWS", "element_offset"] %}
9
  // Compiled indexing for row-wise reductions over a non-last axis. A row is one
@@ -43,17 +46,17 @@ const INNER_SIZE_SAFE: u32 = {{ inner_size.value if inner_size.value > 0 else 1
43
  {% if usesAxisStrideSafe %}
44
  const AXIS_STRIDE_SAFE: u32 = {{ (reduce_size * inner_size.value) if (reduce_size * inner_size.value) > 0 else 1 }}u;
45
  {% endif %}
46
-
47
  {% if usesElementOffset %}
 
48
  // Flat offset of element k (along the reduce axis) of `row`.
49
  fn element_offset(row: u32, k: u32) -> u32 {
50
  let outer = row / INNER_SIZE_SAFE;
51
  let inner = row % INNER_SIZE_SAFE;
52
  return outer * REDUCE_SIZE * INNER_SIZE + k * INNER_SIZE + inner;
53
  }
54
-
55
  {% endif %}
56
  {% if usesRowForOffset %}
 
57
  // Row that owns the flat element `offset` (inverse of element_offset over
58
  // the non-axis coordinates).
59
  fn row_for_offset(offset: u32) -> u32 {
@@ -61,15 +64,14 @@ fn row_for_offset(offset: u32) -> u32 {
61
  let inner = offset % INNER_SIZE_SAFE;
62
  return outer * INNER_SIZE_SAFE + inner;
63
  }
64
- {%- endif %}
65
 
66
  const WGS: u32 = {{ tunables.WORKGROUP_SIZE }}u;
67
  const F32_MAX: f32 = 3.4028234663852886e38;
68
 
69
  @compute @workgroup_size(WGS)
70
  fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
71
- let row = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * WGS;
72
- if (row >= STRIDED_ROWS) { return; }
73
 
74
  var row_max = -F32_MAX;
75
  var row_sum = 0.0;
 
1
  // Scratch-free online softmax for a non-last reduction axis. One invocation
2
  // owns one (outer, inner) row. At each reduction step, neighboring invocations
3
  // access consecutive inner positions. Row statistics remain private.
4
+ {% macro flat_index_2d(workgroupSize, name="i", bound="params.count", guardInline=false) %}
5
+ {% set wgTerm = workgroupSize ~ "u" if workgroupSize is number else workgroupSize %}
6
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
7
+ // per-axis workgroup fold width.
8
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ wgTerm }};
9
+ if ({{ name }} >= {{ bound }}) { return; }{% endmacro %}
10
  {{ env.wgsl.resourceDeclarations }}
11
  {% set stridedAxisUses = ["STRIDED_ROWS", "element_offset"] %}
12
  // Compiled indexing for row-wise reductions over a non-last axis. A row is one
 
46
  {% if usesAxisStrideSafe %}
47
  const AXIS_STRIDE_SAFE: u32 = {{ (reduce_size * inner_size.value) if (reduce_size * inner_size.value) > 0 else 1 }}u;
48
  {% endif %}
 
49
  {% if usesElementOffset %}
50
+
51
  // Flat offset of element k (along the reduce axis) of `row`.
52
  fn element_offset(row: u32, k: u32) -> u32 {
53
  let outer = row / INNER_SIZE_SAFE;
54
  let inner = row % INNER_SIZE_SAFE;
55
  return outer * REDUCE_SIZE * INNER_SIZE + k * INNER_SIZE + inner;
56
  }
 
57
  {% endif %}
58
  {% if usesRowForOffset %}
59
+
60
  // Row that owns the flat element `offset` (inverse of element_offset over
61
  // the non-axis coordinates).
62
  fn row_for_offset(offset: u32) -> u32 {
 
64
  let inner = offset % INNER_SIZE_SAFE;
65
  return outer * INNER_SIZE_SAFE + inner;
66
  }
67
+ {% endif %}
68
 
69
  const WGS: u32 = {{ tunables.WORKGROUP_SIZE }}u;
70
  const F32_MAX: f32 = 3.4028234663852886e38;
71
 
72
  @compute @workgroup_size(WGS)
73
  fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
74
+ {{ flat_index_2d("WGS", "row", "STRIDED_ROWS", guardInline=true) }}
 
75
 
76
  var row_max = -F32_MAX;
77
  var row_sum = 0.0;
build/webgpu/softmax-subgroup-rows.wgsl.jinja CHANGED
@@ -28,6 +28,7 @@ fn shifted_value(value: f32, maxValue: f32) -> f32 {
28
  let equalFiniteMax = select(false, value == maxValue, is_finite_f32(maxValue));
29
  return select(value - maxValue, 0.0, equalFiniteMax);
30
  }
 
31
  fn exp_shift(value: f32, maxValue: f32) -> f32 {
32
  return exp(shifted_value(value, maxValue));
33
  }
@@ -37,7 +38,6 @@ fn is_nan_f32(value: f32) -> bool {
37
  let bits = bitcast<u32>(value);
38
  return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
39
  }
40
-
41
  var<workgroup> sgLaneClaims: atomic<u32>;
42
 
43
  struct SubgroupLogicalLanes {
@@ -47,28 +47,27 @@ struct SubgroupLogicalLanes {
47
  count: u32,
48
  }
49
 
50
- fn subgroup_logical_lanes() -> SubgroupLogicalLanes {
51
- let rank = subgroupExclusiveAdd(1u);
52
- let count = subgroupAdd(1u);
53
- // Every lane performs the atomic so no collective follows a lane guard (a
54
- // subgroup op after a closed `if (rank == 0u)` block is the reconvergence
55
- // hazard the render gate flags): only the rank-0 lane adds its subgroup's
56
- // claim, every other lane adds 0 and discards its snapshot. The rank-0 lane is
57
- // the lowest active lane, which is the lane `subgroupBroadcastFirst` reads.
58
  let ticket = atomicAdd(&sgLaneClaims, select(0u, count | (1u << 16u), rank == 0u));
59
  let claim = subgroupBroadcastFirst(ticket);
60
  return SubgroupLogicalLanes((claim & 0xffffu) + rank, claim >> 16u, rank, count);
61
  }
62
 
63
-
64
  @compute @workgroup_size({{ workgroupSize }})
65
  fn main(@builtin(workgroup_id) wid: vec3<u32>,
 
66
  @builtin(subgroup_size) sgSize: u32) {
67
  // Rows tile the folded workgroup grid; the lanes of one subgroup share a row.
68
  // Which subgroup owns which row, and which lane holds which columns, come from
69
  // the logical lane coordinates, so no assumption is made about which
70
  // invocations share a subgroup.
71
- let L = subgroup_logical_lanes();
72
  let sgLane = L.rank;
73
  let rowsPerWorkgroup = WG / sgSize;
74
  let row = (wid.x + wid.y * {{ DISPATCH_FOLD_WIDTH }}u) * rowsPerWorkgroup + L.ord;
 
28
  let equalFiniteMax = select(false, value == maxValue, is_finite_f32(maxValue));
29
  return select(value - maxValue, 0.0, equalFiniteMax);
30
  }
31
+
32
  fn exp_shift(value: f32, maxValue: f32) -> f32 {
33
  return exp(shifted_value(value, maxValue));
34
  }
 
38
  let bits = bitcast<u32>(value);
39
  return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
40
  }
 
41
  var<workgroup> sgLaneClaims: atomic<u32>;
42
 
43
  struct SubgroupLogicalLanes {
 
47
  count: u32,
48
  }
49
 
50
+ fn subgroup_logical_lanes(rank: u32, count: u32) -> SubgroupLogicalLanes {
51
+ // Every lane performs the atomic so no collective follows a lane guard: a
52
+ // subgroup op after a closed `if (rank == 0u)` block is a reconvergence
53
+ // hazard, since every subgroup operation must be reached in uniform control
54
+ // flow. Only the lane-0 lane adds its subgroup's claim, every other lane
55
+ // adds 0 and discards its snapshot. Lane 0 is the lowest lane, which is the
56
+ // lane `subgroupBroadcastFirst` reads.
 
57
  let ticket = atomicAdd(&sgLaneClaims, select(0u, count | (1u << 16u), rank == 0u));
58
  let claim = subgroupBroadcastFirst(ticket);
59
  return SubgroupLogicalLanes((claim & 0xffffu) + rank, claim >> 16u, rank, count);
60
  }
61
 
 
62
  @compute @workgroup_size({{ workgroupSize }})
63
  fn main(@builtin(workgroup_id) wid: vec3<u32>,
64
+ @builtin(subgroup_invocation_id) sgLaneId: u32,
65
  @builtin(subgroup_size) sgSize: u32) {
66
  // Rows tile the folded workgroup grid; the lanes of one subgroup share a row.
67
  // Which subgroup owns which row, and which lane holds which columns, come from
68
  // the logical lane coordinates, so no assumption is made about which
69
  // invocations share a subgroup.
70
+ let L = subgroup_logical_lanes(sgLaneId, sgSize);
71
  let sgLane = L.rank;
72
  let rowsPerWorkgroup = WG / sgSize;
73
  let row = (wid.x + wid.y * {{ DISPATCH_FOLD_WIDTH }}u) * rowsPerWorkgroup + L.ord;
build/webgpu/test.json CHANGED
@@ -17,7 +17,7 @@
17
  "outputs": { "y": { "dtype": "float32", "shape": [65540, 40], "tolerance": 0.0001, "relTolerance": 0.0001 } }
18
  },
19
  {
20
- "name": "dispatch_cliff_stable_3pass_257x65535",
21
  "attrs": { "axis": -1 },
22
  "inputs": {
23
  "x": {
@@ -28,7 +28,7 @@
28
  },
29
  "outputs": { "y": { "dtype": "float32", "shape": [257, 65535], "tolerance": 1e-8, "relTolerance": 0.0001 } },
30
  "provenance": {
31
- "notes": "A 65,535-column row forces the three-pass stable route past the single-dispatch cliff. The tolerance is scaled to the roughly 1/65,535 outputs so denominator rescaling or a missing partial is observable."
32
  }
33
  },
34
  {
@@ -80,7 +80,7 @@
80
  "provenance": {
81
  "source": "onnxruntime/test/providers/cpu/math/softmax_test.cc",
82
  "test": "SoftmaxOperator.LargeNumber",
83
- "notes": "An 87.5-point logit gap leaves a valid positive subnormal probability tail in ORT CPU; stable softmax should not flush it to zero."
84
  },
85
  "attrs": { "axis": 1 },
86
  "inputs": { "x": { "dtype": "float32", "shape": [1, 2], "data": { "kind": "values", "values": [0.0, -87.5] } } },
@@ -517,7 +517,7 @@
517
  "outputs": { "y": { "dtype": "float32", "shape": [1, 1025], "tolerance": 0.000001 } }
518
  },
519
  {
520
- "name": "ort_gh15949_rank4_axis1_singleton_spatial",
521
  "provenance": {
522
  "source": "onnxruntime/test/providers/cpu/math/softmax_test.cc",
523
  "test": "SoftmaxOperator.GH15949_regression_test"
@@ -748,7 +748,7 @@
748
  {
749
  "name": "rank7_axis1_strided",
750
  "provenance": {
751
- "notes": "Exercises strided Softmax over axis 1 of a rank-7 tensor, with expectations from ORT CPU."
752
  },
753
  "attrs": { "axis": 1 },
754
  "inputs": {
@@ -891,7 +891,7 @@
891
  {
892
  "name": "packed_rows_tail_axis1_33x32",
893
  "provenance": {
894
- "notes": "One row beyond a full 32-row packed workgroup locks the grid-stride tail guard and requires every inactive logical row to keep participating in workgroup barriers."
895
  },
896
  "attrs": { "axis": 1 },
897
  "inputs": {
@@ -936,7 +936,7 @@
936
  {
937
  "name": "strided_scalar4_tail_f32_rank3_axis1_inner6",
938
  "provenance": {
939
- "notes": "Compact correctness lock for coarsening four adjacent strided rows when the inner dimension has a two-lane tail."
940
  },
941
  "attrs": { "axis": 1 },
942
  "inputs": {
@@ -961,7 +961,7 @@
961
  "outputs": { "y": { "dtype": "float16", "shape": [2, 17, 6], "tolerance": 0.003, "relTolerance": 0.003 } }
962
  },
963
  {
964
- "name": "strided_empty_inner_f16_forces_plain_3pass",
965
  "provenance": {
966
  "notes": "A float16 tensor with a zero-length inner dimension exercises the plain three-pass strided implementation, since scalar-four processing requires a nonempty inner dimension and vectorized processing requires float32."
967
  },
@@ -1080,7 +1080,7 @@
1080
  {
1081
  "name": "online_stream_axis31_f16",
1082
  "provenance": {
1083
- "notes": "Exercises the per-lane stream-count boundary, reduction tail, or column tail. f16 comparison includes its storage rounding; existing fixtures and tolerances are unchanged."
1084
  },
1085
  "attrs": { "axis": 1 },
1086
  "inputs": {
@@ -1095,7 +1095,7 @@
1095
  {
1096
  "name": "online_stream_axis32_f16",
1097
  "provenance": {
1098
- "notes": "Exercises the per-lane stream-count boundary, reduction tail, or column tail. f16 comparison includes its storage rounding; existing fixtures and tolerances are unchanged."
1099
  },
1100
  "attrs": { "axis": 1 },
1101
  "inputs": {
@@ -1110,7 +1110,7 @@
1110
  {
1111
  "name": "online_stream_axis33_f16",
1112
  "provenance": {
1113
- "notes": "Exercises the per-lane stream-count boundary, reduction tail, or column tail. f16 comparison includes its storage rounding; existing fixtures and tolerances are unchanged."
1114
  },
1115
  "attrs": { "axis": 1 },
1116
  "inputs": {
@@ -1125,7 +1125,7 @@
1125
  {
1126
  "name": "online_stream_axis64_f16",
1127
  "provenance": {
1128
- "notes": "Exercises the per-lane stream-count boundary, reduction tail, or column tail. f16 comparison includes its storage rounding; existing fixtures and tolerances are unchanged."
1129
  },
1130
  "attrs": { "axis": 1 },
1131
  "inputs": {
@@ -1140,7 +1140,7 @@
1140
  {
1141
  "name": "online_stream_axis65_f16",
1142
  "provenance": {
1143
- "notes": "Exercises the per-lane stream-count boundary, reduction tail, or column tail. f16 comparison includes its storage rounding; existing fixtures and tolerances are unchanged."
1144
  },
1145
  "attrs": { "axis": 1 },
1146
  "inputs": {
@@ -1155,7 +1155,7 @@
1155
  {
1156
  "name": "online_stream_columns_tail_f16",
1157
  "provenance": {
1158
- "notes": "Exercises the per-lane stream-count boundary, reduction tail, or column tail. f16 comparison includes its storage rounding; existing fixtures and tolerances are unchanged."
1159
  },
1160
  "attrs": { "axis": 1 },
1161
  "inputs": {
@@ -1190,7 +1190,7 @@
1190
  {
1191
  "name": "online_stream_four_way_tail_f16",
1192
  "provenance": {
1193
- "notes": "Exercises the per-lane stream-count boundary, reduction tail, or column tail. f16 comparison includes its storage rounding; existing fixtures and tolerances are unchanged."
1194
  },
1195
  "attrs": { "axis": 1 },
1196
  "inputs": {
 
17
  "outputs": { "y": { "dtype": "float32", "shape": [65540, 40], "tolerance": 0.0001, "relTolerance": 0.0001 } }
18
  },
19
  {
20
+ "name": "wide_rows_257x65535",
21
  "attrs": { "axis": -1 },
22
  "inputs": {
23
  "x": {
 
28
  },
29
  "outputs": { "y": { "dtype": "float32", "shape": [257, 65535], "tolerance": 1e-8, "relTolerance": 0.0001 } },
30
  "provenance": {
31
+ "notes": "A 257-row softmax over 65,535 columns; the tolerance, scaled to the roughly 1/65,535 outputs, exposes a dropped column or an incorrectly rescaled denominator."
32
  }
33
  },
34
  {
 
80
  "provenance": {
81
  "source": "onnxruntime/test/providers/cpu/math/softmax_test.cc",
82
  "test": "SoftmaxOperator.LargeNumber",
83
+ "notes": "An 87.5-point logit gap leaves a valid positive subnormal probability tail in ONNX Runtime's CPU provider; stable softmax should not flush it to zero."
84
  },
85
  "attrs": { "axis": 1 },
86
  "inputs": { "x": { "dtype": "float32", "shape": [1, 2], "data": { "kind": "values", "values": [0.0, -87.5] } } },
 
517
  "outputs": { "y": { "dtype": "float32", "shape": [1, 1025], "tolerance": 0.000001 } }
518
  },
519
  {
520
+ "name": "ort_rank4_axis1_singleton_spatial",
521
  "provenance": {
522
  "source": "onnxruntime/test/providers/cpu/math/softmax_test.cc",
523
  "test": "SoftmaxOperator.GH15949_regression_test"
 
748
  {
749
  "name": "rank7_axis1_strided",
750
  "provenance": {
751
+ "notes": "Exercises strided Softmax over axis 1 of a rank-7 tensor, with expectations from ONNX Runtime's CPU provider."
752
  },
753
  "attrs": { "axis": 1 },
754
  "inputs": {
 
891
  {
892
  "name": "packed_rows_tail_axis1_33x32",
893
  "provenance": {
894
+ "notes": "33 rows of 32 columns leave one row past a multiple of 32; that final row must normalize independently of the others."
895
  },
896
  "attrs": { "axis": 1 },
897
  "inputs": {
 
936
  {
937
  "name": "strided_scalar4_tail_f32_rank3_axis1_inner6",
938
  "provenance": {
939
+ "notes": "Four adjacent strided rows with a two-element inner tail check normalized output independently per row."
940
  },
941
  "attrs": { "axis": 1 },
942
  "inputs": {
 
961
  "outputs": { "y": { "dtype": "float16", "shape": [2, 17, 6], "tolerance": 0.003, "relTolerance": 0.003 } }
962
  },
963
  {
964
+ "name": "strided_empty_inner_f16_plain_3pass",
965
  "provenance": {
966
  "notes": "A float16 tensor with a zero-length inner dimension exercises the plain three-pass strided implementation, since scalar-four processing requires a nonempty inner dimension and vectorized processing requires float32."
967
  },
 
1080
  {
1081
  "name": "online_stream_axis31_f16",
1082
  "provenance": {
1083
+ "notes": "Exercises the per-lane stream-count boundary, reduction tail, or column tail. f16 comparison includes its storage rounding."
1084
  },
1085
  "attrs": { "axis": 1 },
1086
  "inputs": {
 
1095
  {
1096
  "name": "online_stream_axis32_f16",
1097
  "provenance": {
1098
+ "notes": "Exercises the per-lane stream-count boundary, reduction tail, or column tail. f16 comparison includes its storage rounding."
1099
  },
1100
  "attrs": { "axis": 1 },
1101
  "inputs": {
 
1110
  {
1111
  "name": "online_stream_axis33_f16",
1112
  "provenance": {
1113
+ "notes": "Exercises the per-lane stream-count boundary, reduction tail, or column tail. f16 comparison includes its storage rounding."
1114
  },
1115
  "attrs": { "axis": 1 },
1116
  "inputs": {
 
1125
  {
1126
  "name": "online_stream_axis64_f16",
1127
  "provenance": {
1128
+ "notes": "Exercises the per-lane stream-count boundary, reduction tail, or column tail. f16 comparison includes its storage rounding."
1129
  },
1130
  "attrs": { "axis": 1 },
1131
  "inputs": {
 
1140
  {
1141
  "name": "online_stream_axis65_f16",
1142
  "provenance": {
1143
+ "notes": "Exercises the per-lane stream-count boundary, reduction tail, or column tail. f16 comparison includes its storage rounding."
1144
  },
1145
  "attrs": { "axis": 1 },
1146
  "inputs": {
 
1155
  {
1156
  "name": "online_stream_columns_tail_f16",
1157
  "provenance": {
1158
+ "notes": "Exercises the per-lane stream-count boundary, reduction tail, or column tail. f16 comparison includes its storage rounding."
1159
  },
1160
  "attrs": { "axis": 1 },
1161
  "inputs": {
 
1190
  {
1191
  "name": "online_stream_four_way_tail_f16",
1192
  "provenance": {
1193
+ "notes": "Exercises the per-lane stream-count boundary, reduction tail, or column tail. f16 comparison includes its storage rounding."
1194
  },
1195
  "attrs": { "axis": 1 },
1196
  "inputs": {