Xenova HF Staff commited on
Commit
2bf2387
·
verified ·
1 Parent(s): a0bbc3b

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)
@@ -66,7 +66,7 @@ Some implementation variants require `subgroups`. These are route-specific capab
66
  ## Use with `@huggingface/kernels`
67
 
68
  ```sh
69
- npm install --save-exact @huggingface/kernels@0.0.1-preview.2
70
  ```
71
 
72
  Required output shapes and logical data types are inferred from the supplied inputs and attributes; result tensors are allocated automatically.
 
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)
 
66
  ## Use with `@huggingface/kernels`
67
 
68
  ```sh
69
+ npm install --save-exact @huggingface/kernels@0.0.1-preview.3
70
  ```
71
 
72
  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
@@ -86,10 +86,7 @@
86
  {
87
  "name": "logsoftmax-f32-strided-axis1-64x2048x130-packed4-control",
88
  "preset": "stress",
89
- "provenance": {
90
- "source": "strided-axis selector crossover",
91
- "notes": "At 8,320 independent strided rows with an inner extent of 130, the selector uses the cooperative packed-four reduction path."
92
- },
93
  "vars": { "dtype": "float32", "outer": 64, "axisDim": 2048, "inner": 130 },
94
  "attrs": { "axis": 1 },
95
  "inputs": { "x": { "shape": [64, 2048, 130], "dtype": "float32", "dist": "normal", "seed": 414, "scale": 2 } },
@@ -104,8 +101,7 @@
104
  "name": "logsoftmax-f32-strided-axis1-96x2048x130-crossover",
105
  "preset": "stress",
106
  "provenance": {
107
- "source": "strided-axis selector crossover",
108
- "notes": "A large-inner tail with 12,480 independent strided rows measures the selector boundary between the serial-lane capacity path and cooperative packed-four reduction."
109
  },
110
  "vars": { "dtype": "float32", "outer": 96, "axisDim": 2048, "inner": 130 },
111
  "attrs": { "axis": 1 },
@@ -136,9 +132,7 @@
136
  },
137
  {
138
  "name": "logsoftmax-f32-8192x512-midwidth",
139
- "provenance": {
140
- "notes": "Covers the 512-column midpoint between the 128- and 2048-column cases and probes the width term in online_wg_vec4's selector."
141
- },
142
  "preset": "smoke",
143
  "vars": { "dtype": "float32", "rows": 8192, "cols": 512 },
144
  "attrs": { "axis": 1 },
@@ -150,7 +144,7 @@
150
  "name": "logsoftmax-f32-64x50257-vocab-odd-pathology",
151
  "preset": "stress",
152
  "provenance": {
153
- "notes": "GPT-2 vocabulary width (50257 % 4 != 0): online_wg scalar because online_wg_vec4 needs cols % 4 == 0 (it is otherwise eligible here, cols >= 1024 with subgroups) and the subgroup-rows route is out of range (> 8 vec4 per lane). Control: logsoftmax-f32-64x50260-vocab-control."
154
  },
155
  "vars": { "dtype": "float32", "rows": 64, "cols": 50257 },
156
  "attrs": { "axis": 1 },
@@ -162,7 +156,7 @@
162
  "name": "logsoftmax-f32-64x50260-vocab-control",
163
  "preset": "stress",
164
  "provenance": {
165
- "notes": "Nearest multiple-of-4 vocabulary width for logsoftmax-f32-64x50257-vocab-odd-pathology; same bytes within 0.01%, selects online_wg_vec4."
166
  },
167
  "vars": { "dtype": "float32", "rows": 64, "cols": 50260 },
168
  "attrs": { "axis": 1 },
 
86
  {
87
  "name": "logsoftmax-f32-strided-axis1-64x2048x130-packed4-control",
88
  "preset": "stress",
89
+ "provenance": { "notes": "Measures LogSoftmax over 8,320 independent strided rows with an inner extent of 130." },
 
 
 
90
  "vars": { "dtype": "float32", "outer": 64, "axisDim": 2048, "inner": 130 },
91
  "attrs": { "axis": 1 },
92
  "inputs": { "x": { "shape": [64, 2048, 130], "dtype": "float32", "dist": "normal", "seed": 414, "scale": 2 } },
 
101
  "name": "logsoftmax-f32-strided-axis1-96x2048x130-crossover",
102
  "preset": "stress",
103
  "provenance": {
104
+ "notes": "A large inner tail across 12,480 independent strided rows measures LogSoftmax on noncontiguous data."
 
105
  },
106
  "vars": { "dtype": "float32", "outer": 96, "axisDim": 2048, "inner": 130 },
107
  "attrs": { "axis": 1 },
 
132
  },
133
  {
134
  "name": "logsoftmax-f32-8192x512-midwidth",
135
+ "provenance": { "notes": "A 512-column case measures LogSoftmax between the 128- and 2048-column shapes." },
 
 
136
  "preset": "smoke",
137
  "vars": { "dtype": "float32", "rows": 8192, "cols": 512 },
138
  "attrs": { "axis": 1 },
 
144
  "name": "logsoftmax-f32-64x50257-vocab-odd-pathology",
145
  "preset": "stress",
146
  "provenance": {
147
+ "notes": "Measures log-softmax over GPT-2's 50,257-token vocabulary width, which is not a multiple of four; its 50,260-wide sibling in this file is the nearest four-aligned control at nearly the same byte count."
148
  },
149
  "vars": { "dtype": "float32", "rows": 64, "cols": 50257 },
150
  "attrs": { "axis": 1 },
 
156
  "name": "logsoftmax-f32-64x50260-vocab-control",
157
  "preset": "stress",
158
  "provenance": {
159
+ "notes": "The next multiple-of-four vocabulary width above the paired 50,257-column case, with nearly the same input bytes."
160
  },
161
  "vars": { "dtype": "float32", "rows": 64, "cols": 50260 },
162
  "attrs": { "axis": 1 },
build/webgpu/manifest.json CHANGED
@@ -29,7 +29,6 @@
29
  "axisRows": "axisOuter * axisInner",
30
  "longrowBlocks": "ceilDiv(lastCols, tunables.BLOCK_COLS)",
31
  "lastRowScratchFits": "4 * lastRows <= storageBufferLimit",
32
- "axisRowScratchFits": "4 * axisRows <= storageBufferLimit",
33
  "longrowBlockScratchFits": "4 * lastRows * longrowBlocks <= storageBufferLimit",
34
  "lastDispatchFits": "ceilDiv(lastRows, min(device.limits.maxComputeWorkgroupsPerDimension, 65535)) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535)",
35
  "axisDispatchFits": "ceilDiv(axisRows, min(device.limits.maxComputeWorkgroupsPerDimension, 65535)) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535)",
@@ -40,37 +39,33 @@
40
  "rowWorkgroupSize": "min(tunables.WORKGROUP_SIZE, max(tunables.ROW_MIN_WORKGROUP, pow2ceil(ceilDiv(lastCols, tunables.ROW_COLS_PER_LANE))))",
41
  "subgroupRowsWorkgroup": "min(tunables.WORKGROUP_SIZE, deviceWorkgroupCap)",
42
  "subgroupRowsLaneVecs": "ceilDiv(lastCols / 4, device.adapterInfo.subgroupMinSize) if (device.features.has(\"subgroups\") and has(device.adapterInfo, \"subgroupMinSize\") and device.adapterInfo.subgroupMinSize > 0) else 0",
 
 
43
  "stridedOnlineStreams": "min(4, pow2ceil(max(1, tunables.STRIDED_ONLINE_STREAMS)), max(1, ceilDiv(dim(shapes.x, axis), 256 / stridedCoopTileCols))) if dtypes.T == \"f32\" else 1"
44
  },
45
  "bindings": {
 
 
 
 
46
  "rowMax": { "buffer": "storage", "elementType": "f32" },
47
  "rowSum": { "buffer": "storage", "elementType": "f32" },
48
- "rowMax_2": { "name": "rowMax", "buffer": "read-only-storage", "elementType": "f32" },
49
- "rowSum_2": { "name": "rowSum", "buffer": "read-only-storage", "elementType": "f32" },
50
- "x_2": { "name": "x", "buffer": "read-only-storage", "elementType": "$vectorScalar" },
51
- "y_2": { "name": "y", "buffer": "storage", "elementType": "$vectorScalar" },
52
  "params": {
53
- "buffer": "uniform",
54
  "struct": [
55
  { "name": "rows", "type": "u32", "value": "lastRows" },
56
  { "name": "vecCols", "type": "u32", "value": "lastCols / 4" }
57
  ]
58
  },
59
- "x_3": { "name": "x", "buffer": "read-only-storage", "elementType": "$scalar" },
60
- "y_3": { "name": "y", "buffer": "storage", "elementType": "$scalar" },
61
- "params_5": {
62
  "name": "params",
63
- "buffer": "uniform",
64
  "struct": [
65
  { "name": "rows", "type": "u32", "value": "lastRows" },
66
  { "name": "cols", "type": "u32", "value": "lastCols" }
67
  ]
68
- },
69
- "x_4": { "name": "x", "buffer": "read-only-storage", "elementType": "vec4<f32>" },
70
- "params_7": {
71
- "name": "params",
72
- "buffer": "uniform",
73
- "struct": [{ "name": "count", "type": "u32", "value": "numel(shapes.x)" }]
74
  }
75
  },
76
  "variants": [
@@ -97,11 +92,11 @@
97
  "id": "block_stats",
98
  "name": "LogSoftmax.LongRowBlockStats",
99
  "shader": "softmax-longrow-stats.wgsl.jinja",
100
- "derive": { "stage": "\"block\"" },
101
  "bindings": [
102
  "x",
103
- { "name": "blockMax", "buffer": "storage", "elementType": "f32" },
104
- { "name": "blockSum", "buffer": "storage", "elementType": "f32" },
105
  {
106
  "name": "params",
107
  "struct": [
@@ -119,7 +114,7 @@
119
  "id": "row_stats",
120
  "name": "LogSoftmax.LongRowStats",
121
  "shader": "softmax-longrow-stats.wgsl.jinja",
122
- "derive": { "stage": "\"row\"" },
123
  "bindings": [
124
  { "name": "blockMax", "buffer": "read-only-storage", "elementType": "f32" },
125
  { "name": "blockSum", "buffer": "read-only-storage", "elementType": "f32" },
@@ -139,8 +134,8 @@
139
  "derive": { "op": "\"logsoftmax\"" },
140
  "bindings": [
141
  "x",
142
- "rowMax_2",
143
- "rowSum_2",
144
  "y",
145
  { "name": "params", "struct": [{ "name": "cols", "type": "u32", "value": "lastCols" }] }
146
  ],
@@ -168,13 +163,12 @@
168
  "name": "LogSoftmax.OnlineSubgroupRowsVec4",
169
  "shader": "softmax-subgroup-rows.wgsl.jinja",
170
  "derive": { "op": "\"logsoftmax\"" },
171
- "bindings": ["x_2", "y_2", "params"],
172
  "dispatch": {
173
  "x": "min(ceilDiv(lastRows, subgroupRowsWorkgroup / device.adapterInfo.subgroupMaxSize), 65535)",
174
  "y": "ceilDiv(ceilDiv(lastRows, subgroupRowsWorkgroup / device.adapterInfo.subgroupMaxSize), 65535)",
175
  "z": 1
176
- },
177
- "subgroupCollectivesWidth": "portable"
178
  }
179
  ]
180
  },
@@ -193,9 +187,8 @@
193
  "id": "main",
194
  "name": "LogSoftmax.OnlineWorkgroupVec4",
195
  "shader": "softmax-online.wgsl.jinja",
196
- "subgroupCollectivesWidth": "portable",
197
  "derive": { "op": "\"logsoftmax\"", "useVec4": "true" },
198
- "bindings": ["x_2", "y_2", "params"],
199
  "dispatch": {
200
  "x": "min(rows(shapes.x, ranks.x - 1), 65535)",
201
  "y": "ceilDiv(rows(shapes.x, ranks.x - 1), 65535)",
@@ -218,9 +211,8 @@
218
  "id": "main",
219
  "name": "LogSoftmax.OnlineWorkgroup",
220
  "shader": "softmax-online.wgsl.jinja",
221
- "subgroupCollectivesWidth": "portable",
222
  "derive": { "op": "\"logsoftmax\"", "useVec4": "false" },
223
- "bindings": ["x_3", "y_3", "params_5"],
224
  "dispatch": {
225
  "x": "min(rows(shapes.x, ranks.x - 1), 65535)",
226
  "y": "ceilDiv(rows(shapes.x, ranks.x - 1), 65535)",
@@ -250,7 +242,7 @@
250
  "rowsPerWorkgroup": "rowsPerWorkgroup",
251
  "useVec4": "true"
252
  },
253
- "bindings": ["x_2", "y_2", "params"],
254
  "dispatch": {
255
  "x": "min(ceilDiv(rows(shapes.x, ranks.x - 1), rowsPerWorkgroup), min(device.limits.maxComputeWorkgroupsPerDimension, 65535))",
256
  "y": 1,
@@ -275,7 +267,7 @@
275
  "lanesPerRow": "lanesPerRow",
276
  "rowsPerWorkgroup": "rowsPerWorkgroup"
277
  },
278
- "bindings": ["x_3", "y_3", "params_5"],
279
  "dispatch": {
280
  "x": "min(ceilDiv(rows(shapes.x, ranks.x - 1), rowsPerWorkgroup), min(device.limits.maxComputeWorkgroupsPerDimension, 65535))",
281
  "y": 1,
@@ -301,7 +293,7 @@
301
  "name": "LogSoftmax.OnlineLocalVec4NoSg",
302
  "shader": "softmax-online.wgsl.jinja",
303
  "derive": { "op": "\"logsoftmax\"", "useVec4": "true" },
304
- "bindings": ["x_2", "y_2", "params"],
305
  "dispatch": {
306
  "x": "min(rows(shapes.x, ranks.x - 1), 65535)",
307
  "y": "ceilDiv(rows(shapes.x, ranks.x - 1), 65535)",
@@ -322,7 +314,7 @@
322
  "name": "LogSoftmax.OnlineLocalNoSg",
323
  "shader": "softmax-online.wgsl.jinja",
324
  "derive": { "op": "\"logsoftmax\"", "useVec4": "false" },
325
- "bindings": ["x_3", "y_3", "params_5"],
326
  "dispatch": {
327
  "x": "min(rows(shapes.x, ranks.x - 1), 65535)",
328
  "y": "ceilDiv(rows(shapes.x, ranks.x - 1), 65535)",
@@ -345,7 +337,7 @@
345
  "name": "LogSoftmax.ReduceMax",
346
  "shader": "softmax-row-stage.wgsl.jinja",
347
  "derive": { "stage": "\"max\"", "strided": "false" },
348
- "bindings": ["x", "rowMax", "params_5"],
349
  "dispatch": {
350
  "x": "min(numel(shapes.x) / dim(shapes.x, ranks.x - 1), 65535)",
351
  "y": "ceilDiv(numel(shapes.x) / dim(shapes.x, ranks.x - 1), 65535)",
@@ -357,7 +349,7 @@
357
  "name": "LogSoftmax.ExpSum",
358
  "shader": "softmax-row-stage.wgsl.jinja",
359
  "derive": { "stage": "\"exp_sum\"", "strided": "false" },
360
- "bindings": ["x", "rowMax_2", "y", "rowSum", "params_5"],
361
  "dispatch": {
362
  "x": "min(numel(shapes.x) / dim(shapes.x, ranks.x - 1), 65535)",
363
  "y": "ceilDiv(numel(shapes.x) / dim(shapes.x, ranks.x - 1), 65535)",
@@ -371,8 +363,8 @@
371
  "derive": { "strided": "false", "op": "\"logsoftmax\"" },
372
  "bindings": [
373
  "x",
374
- "rowMax_2",
375
- "rowSum_2",
376
  "y",
377
  {
378
  "name": "params",
@@ -394,7 +386,6 @@
394
  "id": "strided_3pass_vec4",
395
  "priority": 14,
396
  "when": ["dtypes.T == \"f32\"", "stridedAxisOk", "inner(shapes.x, attrs.axis if attrs.axis >= 0 else attrs.axis + ranks.x) % 4 == 0", "axisDispatchFits", "axisRowScratchFits"],
397
- "derive": { "axis": "attrs.axis if attrs.axis >= 0 else attrs.axis + ranks.x" },
398
  "intermediates": [
399
  { "id": "rowMax", "dtype": "float32", "shape": "[numel(shapes.x) / dim(shapes.x, axis)]" },
400
  { "id": "rowSum", "dtype": "float32", "shape": "[numel(shapes.x) / dim(shapes.x, axis)]" }
@@ -405,7 +396,7 @@
405
  "name": "LogSoftmax.StridedReduceMaxVec4",
406
  "shader": "softmax-row-stage-strided-vec4.wgsl.jinja",
407
  "derive": { "stage": "\"max\"", "xShape": "shapes.x", "axisSpec": "axis" },
408
- "bindings": ["x_4", { "name": "rowMax", "buffer": "storage", "elementType": "vec4<f32>" }],
409
  "dispatch": {
410
  "x": "min(numel(shapes.x) / dim(shapes.x, axis) / 4, 65535)",
411
  "y": "ceilDiv(numel(shapes.x) / dim(shapes.x, axis) / 4, 65535)",
@@ -418,10 +409,10 @@
418
  "shader": "softmax-row-stage-strided-vec4.wgsl.jinja",
419
  "derive": { "stage": "\"exp_sum\"", "xShape": "shapes.x", "axisSpec": "axis" },
420
  "bindings": [
421
- "x_4",
422
  { "name": "rowMax", "buffer": "read-only-storage", "elementType": "vec4<f32>" },
423
  { "arg": "y", "elementType": "vec4<f32>" },
424
- { "name": "rowSum", "buffer": "storage", "elementType": "vec4<f32>" }
425
  ],
426
  "dispatch": {
427
  "x": "min(numel(shapes.x) / dim(shapes.x, axis) / 4, 65535)",
@@ -434,7 +425,7 @@
434
  "name": "LogSoftmax.StridedNormalize",
435
  "shader": "softmax-normalize.wgsl.jinja",
436
  "derive": { "strided": "true", "xShape": "shapes.x", "axisSpec": "axis", "op": "\"logsoftmax\"" },
437
- "bindings": ["x", "rowMax_2", "rowSum_2", "y", "params_7"],
438
  "dispatch": {
439
  "x": "min(ceilDiv((numel(shapes.x)), (tunables.WORKGROUP_SIZE)), 65535)",
440
  "y": "ceilDiv(ceilDiv((numel(shapes.x)), (tunables.WORKGROUP_SIZE)), 65535)",
@@ -446,7 +437,7 @@
446
  {
447
  "id": "strided_online_coop",
448
  "priority": 19,
449
- "when": ["stridedAxisOk", "f16Ok(dtypes.T)", "axisThreadDispatchFits"],
450
  "passes": [
451
  {
452
  "id": "main",
@@ -455,9 +446,9 @@
455
  "derive": {
456
  "xShape": "shapes.x",
457
  "axisSpec": "axis",
458
- "op": "\"logsoftmax\"",
459
  "tileCols": "stridedCoopTileCols",
460
- "onlineStreams": "stridedOnlineStreams"
 
461
  },
462
  "bindings": ["x", "y"],
463
  "dispatch": {
@@ -472,7 +463,6 @@
472
  "id": "strided_online_packed4_tail",
473
  "priority": 18,
474
  "when": ["dtypes.T == \"f32\"", "stridedAxisOk", "inner(shapes.x, attrs.axis if attrs.axis >= 0 else attrs.axis + ranks.x) >= 4", "inner(shapes.x, attrs.axis if attrs.axis >= 0 else attrs.axis + ranks.x) % 4 != 0", "dim(shapes.x, axis) < 2048 or axisRows < tunables.STRIDED_CAPACITY_MIN_ROWS", "axisDispatchFits"],
475
- "derive": { "axis": "attrs.axis if attrs.axis >= 0 else attrs.axis + ranks.x" },
476
  "passes": [
477
  {
478
  "id": "main",
@@ -493,7 +483,6 @@
493
  "priority": 12,
494
  "when": ["stridedAxisOk", "f16Ok(dtypes.T)", "axisDispatchFits", "axisRowScratchFits"],
495
  "demoteWhen": ["dtypes.T == \"f32\" and inner(shapes.x, attrs.axis if attrs.axis >= 0 else attrs.axis + ranks.x) >= 4 and inner(shapes.x, attrs.axis if attrs.axis >= 0 else attrs.axis + ranks.x) % 4 != 0"],
496
- "derive": { "axis": "attrs.axis if attrs.axis >= 0 else attrs.axis + ranks.x" },
497
  "intermediates": [
498
  { "id": "rowMax", "dtype": "float32", "shape": "[numel(shapes.x) / dim(shapes.x, axis)]" },
499
  { "id": "rowSum", "dtype": "float32", "shape": "[numel(shapes.x) / dim(shapes.x, axis)]" }
@@ -516,7 +505,7 @@
516
  "name": "LogSoftmax.StridedExpSum",
517
  "shader": "softmax-row-stage.wgsl.jinja",
518
  "derive": { "stage": "\"exp_sum\"", "strided": "true", "xShape": "shapes.x", "axisSpec": "axis" },
519
- "bindings": ["x", "rowMax_2", "y", "rowSum"],
520
  "dispatch": {
521
  "x": "min(numel(shapes.x) / dim(shapes.x, axis), 65535)",
522
  "y": "ceilDiv(numel(shapes.x) / dim(shapes.x, axis), 65535)",
@@ -528,7 +517,7 @@
528
  "name": "LogSoftmax.StridedNormalize",
529
  "shader": "softmax-normalize.wgsl.jinja",
530
  "derive": { "strided": "true", "xShape": "shapes.x", "axisSpec": "axis", "op": "\"logsoftmax\"" },
531
- "bindings": ["x", "rowMax_2", "rowSum_2", "y", "params_7"],
532
  "dispatch": {
533
  "x": "min(ceilDiv((numel(shapes.x)), (tunables.WORKGROUP_SIZE)), 65535)",
534
  "y": "ceilDiv(ceilDiv((numel(shapes.x)), (tunables.WORKGROUP_SIZE)), 65535)",
 
29
  "axisRows": "axisOuter * axisInner",
30
  "longrowBlocks": "ceilDiv(lastCols, tunables.BLOCK_COLS)",
31
  "lastRowScratchFits": "4 * lastRows <= storageBufferLimit",
 
32
  "longrowBlockScratchFits": "4 * lastRows * longrowBlocks <= storageBufferLimit",
33
  "lastDispatchFits": "ceilDiv(lastRows, min(device.limits.maxComputeWorkgroupsPerDimension, 65535)) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535)",
34
  "axisDispatchFits": "ceilDiv(axisRows, min(device.limits.maxComputeWorkgroupsPerDimension, 65535)) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535)",
 
39
  "rowWorkgroupSize": "min(tunables.WORKGROUP_SIZE, max(tunables.ROW_MIN_WORKGROUP, pow2ceil(ceilDiv(lastCols, tunables.ROW_COLS_PER_LANE))))",
40
  "subgroupRowsWorkgroup": "min(tunables.WORKGROUP_SIZE, deviceWorkgroupCap)",
41
  "subgroupRowsLaneVecs": "ceilDiv(lastCols / 4, device.adapterInfo.subgroupMinSize) if (device.features.has(\"subgroups\") and has(device.adapterInfo, \"subgroupMinSize\") and device.adapterInfo.subgroupMinSize > 0) else 0",
42
+ "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)",
43
+ "axisRowScratchFits": "4 * axisRows <= storageBufferLimit",
44
  "stridedOnlineStreams": "min(4, pow2ceil(max(1, tunables.STRIDED_ONLINE_STREAMS)), max(1, ceilDiv(dim(shapes.x, axis), 256 / stridedCoopTileCols))) if dtypes.T == \"f32\" else 1"
45
  },
46
  "bindings": {
47
+ "x_scalar": { "name": "x", "elementType": "$scalar" },
48
+ "y_scalar": { "name": "y", "elementType": "$scalar" },
49
+ "x_exp_sum": { "name": "x", "elementType": "vec4<f32>" },
50
+ "params_count": { "name": "params", "struct": [{ "name": "count", "type": "u32", "value": "numel(shapes.x)" }] },
51
  "rowMax": { "buffer": "storage", "elementType": "f32" },
52
  "rowSum": { "buffer": "storage", "elementType": "f32" },
53
+ "rowMax_f32": { "name": "rowMax", "buffer": "read-only-storage", "elementType": "f32" },
54
+ "rowSum_f32": { "name": "rowSum", "buffer": "read-only-storage", "elementType": "f32" },
55
+ "x_main": { "name": "x", "elementType": "$vectorScalar" },
56
+ "y_main": { "name": "y", "elementType": "$vectorScalar" },
57
  "params": {
 
58
  "struct": [
59
  { "name": "rows", "type": "u32", "value": "lastRows" },
60
  { "name": "vecCols", "type": "u32", "value": "lastCols / 4" }
61
  ]
62
  },
63
+ "params_main": {
 
 
64
  "name": "params",
 
65
  "struct": [
66
  { "name": "rows", "type": "u32", "value": "lastRows" },
67
  { "name": "cols", "type": "u32", "value": "lastCols" }
68
  ]
 
 
 
 
 
 
69
  }
70
  },
71
  "variants": [
 
92
  "id": "block_stats",
93
  "name": "LogSoftmax.LongRowBlockStats",
94
  "shader": "softmax-longrow-stats.wgsl.jinja",
95
+ "derive": { "stage": "\"block\"", "combineSubgroups": false },
96
  "bindings": [
97
  "x",
98
+ { "name": "blockMax", "elementType": "f32" },
99
+ { "name": "blockSum", "elementType": "f32" },
100
  {
101
  "name": "params",
102
  "struct": [
 
114
  "id": "row_stats",
115
  "name": "LogSoftmax.LongRowStats",
116
  "shader": "softmax-longrow-stats.wgsl.jinja",
117
+ "derive": { "stage": "\"row\"", "combineSubgroups": false },
118
  "bindings": [
119
  { "name": "blockMax", "buffer": "read-only-storage", "elementType": "f32" },
120
  { "name": "blockSum", "buffer": "read-only-storage", "elementType": "f32" },
 
134
  "derive": { "op": "\"logsoftmax\"" },
135
  "bindings": [
136
  "x",
137
+ "rowMax_f32",
138
+ "rowSum_f32",
139
  "y",
140
  { "name": "params", "struct": [{ "name": "cols", "type": "u32", "value": "lastCols" }] }
141
  ],
 
163
  "name": "LogSoftmax.OnlineSubgroupRowsVec4",
164
  "shader": "softmax-subgroup-rows.wgsl.jinja",
165
  "derive": { "op": "\"logsoftmax\"" },
166
+ "bindings": ["x_main", "y_main", "params"],
167
  "dispatch": {
168
  "x": "min(ceilDiv(lastRows, subgroupRowsWorkgroup / device.adapterInfo.subgroupMaxSize), 65535)",
169
  "y": "ceilDiv(ceilDiv(lastRows, subgroupRowsWorkgroup / device.adapterInfo.subgroupMaxSize), 65535)",
170
  "z": 1
171
+ }
 
172
  }
173
  ]
174
  },
 
187
  "id": "main",
188
  "name": "LogSoftmax.OnlineWorkgroupVec4",
189
  "shader": "softmax-online.wgsl.jinja",
 
190
  "derive": { "op": "\"logsoftmax\"", "useVec4": "true" },
191
+ "bindings": ["x_main", "y_main", "params"],
192
  "dispatch": {
193
  "x": "min(rows(shapes.x, ranks.x - 1), 65535)",
194
  "y": "ceilDiv(rows(shapes.x, ranks.x - 1), 65535)",
 
211
  "id": "main",
212
  "name": "LogSoftmax.OnlineWorkgroup",
213
  "shader": "softmax-online.wgsl.jinja",
 
214
  "derive": { "op": "\"logsoftmax\"", "useVec4": "false" },
215
+ "bindings": ["x_scalar", "y_scalar", "params_main"],
216
  "dispatch": {
217
  "x": "min(rows(shapes.x, ranks.x - 1), 65535)",
218
  "y": "ceilDiv(rows(shapes.x, ranks.x - 1), 65535)",
 
242
  "rowsPerWorkgroup": "rowsPerWorkgroup",
243
  "useVec4": "true"
244
  },
245
+ "bindings": ["x_main", "y_main", "params"],
246
  "dispatch": {
247
  "x": "min(ceilDiv(rows(shapes.x, ranks.x - 1), rowsPerWorkgroup), min(device.limits.maxComputeWorkgroupsPerDimension, 65535))",
248
  "y": 1,
 
267
  "lanesPerRow": "lanesPerRow",
268
  "rowsPerWorkgroup": "rowsPerWorkgroup"
269
  },
270
+ "bindings": ["x_scalar", "y_scalar", "params_main"],
271
  "dispatch": {
272
  "x": "min(ceilDiv(rows(shapes.x, ranks.x - 1), rowsPerWorkgroup), min(device.limits.maxComputeWorkgroupsPerDimension, 65535))",
273
  "y": 1,
 
293
  "name": "LogSoftmax.OnlineLocalVec4NoSg",
294
  "shader": "softmax-online.wgsl.jinja",
295
  "derive": { "op": "\"logsoftmax\"", "useVec4": "true" },
296
+ "bindings": ["x_main", "y_main", "params"],
297
  "dispatch": {
298
  "x": "min(rows(shapes.x, ranks.x - 1), 65535)",
299
  "y": "ceilDiv(rows(shapes.x, ranks.x - 1), 65535)",
 
314
  "name": "LogSoftmax.OnlineLocalNoSg",
315
  "shader": "softmax-online.wgsl.jinja",
316
  "derive": { "op": "\"logsoftmax\"", "useVec4": "false" },
317
+ "bindings": ["x_scalar", "y_scalar", "params_main"],
318
  "dispatch": {
319
  "x": "min(rows(shapes.x, ranks.x - 1), 65535)",
320
  "y": "ceilDiv(rows(shapes.x, ranks.x - 1), 65535)",
 
337
  "name": "LogSoftmax.ReduceMax",
338
  "shader": "softmax-row-stage.wgsl.jinja",
339
  "derive": { "stage": "\"max\"", "strided": "false" },
340
+ "bindings": ["x", "rowMax", "params_main"],
341
  "dispatch": {
342
  "x": "min(numel(shapes.x) / dim(shapes.x, ranks.x - 1), 65535)",
343
  "y": "ceilDiv(numel(shapes.x) / dim(shapes.x, ranks.x - 1), 65535)",
 
349
  "name": "LogSoftmax.ExpSum",
350
  "shader": "softmax-row-stage.wgsl.jinja",
351
  "derive": { "stage": "\"exp_sum\"", "strided": "false" },
352
+ "bindings": ["x", "rowMax_f32", "y", "rowSum", "params_main"],
353
  "dispatch": {
354
  "x": "min(numel(shapes.x) / dim(shapes.x, ranks.x - 1), 65535)",
355
  "y": "ceilDiv(numel(shapes.x) / dim(shapes.x, ranks.x - 1), 65535)",
 
363
  "derive": { "strided": "false", "op": "\"logsoftmax\"" },
364
  "bindings": [
365
  "x",
366
+ "rowMax_f32",
367
+ "rowSum_f32",
368
  "y",
369
  {
370
  "name": "params",
 
386
  "id": "strided_3pass_vec4",
387
  "priority": 14,
388
  "when": ["dtypes.T == \"f32\"", "stridedAxisOk", "inner(shapes.x, attrs.axis if attrs.axis >= 0 else attrs.axis + ranks.x) % 4 == 0", "axisDispatchFits", "axisRowScratchFits"],
 
389
  "intermediates": [
390
  { "id": "rowMax", "dtype": "float32", "shape": "[numel(shapes.x) / dim(shapes.x, axis)]" },
391
  { "id": "rowSum", "dtype": "float32", "shape": "[numel(shapes.x) / dim(shapes.x, axis)]" }
 
396
  "name": "LogSoftmax.StridedReduceMaxVec4",
397
  "shader": "softmax-row-stage-strided-vec4.wgsl.jinja",
398
  "derive": { "stage": "\"max\"", "xShape": "shapes.x", "axisSpec": "axis" },
399
+ "bindings": ["x_exp_sum", { "name": "rowMax", "elementType": "vec4<f32>" }],
400
  "dispatch": {
401
  "x": "min(numel(shapes.x) / dim(shapes.x, axis) / 4, 65535)",
402
  "y": "ceilDiv(numel(shapes.x) / dim(shapes.x, axis) / 4, 65535)",
 
409
  "shader": "softmax-row-stage-strided-vec4.wgsl.jinja",
410
  "derive": { "stage": "\"exp_sum\"", "xShape": "shapes.x", "axisSpec": "axis" },
411
  "bindings": [
412
+ "x_exp_sum",
413
  { "name": "rowMax", "buffer": "read-only-storage", "elementType": "vec4<f32>" },
414
  { "arg": "y", "elementType": "vec4<f32>" },
415
+ { "name": "rowSum", "elementType": "vec4<f32>" }
416
  ],
417
  "dispatch": {
418
  "x": "min(numel(shapes.x) / dim(shapes.x, axis) / 4, 65535)",
 
425
  "name": "LogSoftmax.StridedNormalize",
426
  "shader": "softmax-normalize.wgsl.jinja",
427
  "derive": { "strided": "true", "xShape": "shapes.x", "axisSpec": "axis", "op": "\"logsoftmax\"" },
428
+ "bindings": ["x", "rowMax_f32", "rowSum_f32", "y", "params_count"],
429
  "dispatch": {
430
  "x": "min(ceilDiv((numel(shapes.x)), (tunables.WORKGROUP_SIZE)), 65535)",
431
  "y": "ceilDiv(ceilDiv((numel(shapes.x)), (tunables.WORKGROUP_SIZE)), 65535)",
 
437
  {
438
  "id": "strided_online_coop",
439
  "priority": 19,
440
+ "when": ["stridedAxisOk", "f16Ok(dtypes.T)", "stridedCoopDispatchFits"],
441
  "passes": [
442
  {
443
  "id": "main",
 
446
  "derive": {
447
  "xShape": "shapes.x",
448
  "axisSpec": "axis",
 
449
  "tileCols": "stridedCoopTileCols",
450
+ "onlineStreams": "stridedOnlineStreams",
451
+ "op": "\"logsoftmax\""
452
  },
453
  "bindings": ["x", "y"],
454
  "dispatch": {
 
463
  "id": "strided_online_packed4_tail",
464
  "priority": 18,
465
  "when": ["dtypes.T == \"f32\"", "stridedAxisOk", "inner(shapes.x, attrs.axis if attrs.axis >= 0 else attrs.axis + ranks.x) >= 4", "inner(shapes.x, attrs.axis if attrs.axis >= 0 else attrs.axis + ranks.x) % 4 != 0", "dim(shapes.x, axis) < 2048 or axisRows < tunables.STRIDED_CAPACITY_MIN_ROWS", "axisDispatchFits"],
 
466
  "passes": [
467
  {
468
  "id": "main",
 
483
  "priority": 12,
484
  "when": ["stridedAxisOk", "f16Ok(dtypes.T)", "axisDispatchFits", "axisRowScratchFits"],
485
  "demoteWhen": ["dtypes.T == \"f32\" and inner(shapes.x, attrs.axis if attrs.axis >= 0 else attrs.axis + ranks.x) >= 4 and inner(shapes.x, attrs.axis if attrs.axis >= 0 else attrs.axis + ranks.x) % 4 != 0"],
 
486
  "intermediates": [
487
  { "id": "rowMax", "dtype": "float32", "shape": "[numel(shapes.x) / dim(shapes.x, axis)]" },
488
  { "id": "rowSum", "dtype": "float32", "shape": "[numel(shapes.x) / dim(shapes.x, axis)]" }
 
505
  "name": "LogSoftmax.StridedExpSum",
506
  "shader": "softmax-row-stage.wgsl.jinja",
507
  "derive": { "stage": "\"exp_sum\"", "strided": "true", "xShape": "shapes.x", "axisSpec": "axis" },
508
+ "bindings": ["x", "rowMax_f32", "y", "rowSum"],
509
  "dispatch": {
510
  "x": "min(numel(shapes.x) / dim(shapes.x, axis), 65535)",
511
  "y": "ceilDiv(numel(shapes.x) / dim(shapes.x, axis), 65535)",
 
517
  "name": "LogSoftmax.StridedNormalize",
518
  "shader": "softmax-normalize.wgsl.jinja",
519
  "derive": { "strided": "true", "xShape": "shapes.x", "axisSpec": "axis", "op": "\"logsoftmax\"" },
520
+ "bindings": ["x", "rowMax_f32", "rowSum_f32", "y", "params_count"],
521
  "dispatch": {
522
  "x": "min(ceilDiv((numel(shapes.x)), (tunables.WORKGROUP_SIZE)), 65535)",
523
  "y": "ceilDiv(ceilDiv((numel(shapes.x)), (tunables.WORKGROUP_SIZE)), 65535)",
build/webgpu/metadata.json CHANGED
@@ -1,30 +1,30 @@
1
  {
2
  "name": "ai.onnx.LogSoftmax",
3
- "id": "_ai_onnx_logsoftmax_webgpu_eafe4e3",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
- "bench.json": "PypxQO0F40vAAACduE1EoHuJbXFSoUdf/1sxjfO3XYI=",
11
- "manifest.json": "YEgGrgHNsUKssjpYJg/VI7JNOc7ROOzoE7TnwK8uD48=",
12
- "softmax-longrow-normalize.wgsl.jinja": "WUMowCIMomWax9n1OZifCH/q1sfWCKgwU5OdAH6eOMo=",
13
- "softmax-longrow-stats.wgsl.jinja": "prBVBse5ULq+Wl8maEVHnHtV24ispCW8XYS6ys3rzmA=",
14
- "softmax-normalize.wgsl.jinja": "5HX4KpHm5VW4oAq0LfwDIv2h4ZcaW5F6DIw8hYHiwBA=",
15
- "softmax-online-packed-rows.wgsl.jinja": "R05RjrEHTCi8zUwQKccDnrznkHAi3i4Xl1+VAXECEyQ=",
16
- "softmax-online.wgsl.jinja": "t/yk5HbGLSgCjBNT2NbQ93rACba+TWWe0Hc/I4M3YhY=",
17
- "softmax-row-stage-strided-vec4.wgsl.jinja": "bJgInlBtWpRzBoQLGZzAIWQ+crsQ9rt4Sokaq/fozFE=",
18
- "softmax-row-stage.wgsl.jinja": "HyD4uC2ncynJAQwamSNH9orhDje96yEs5IecERo2ivI=",
19
- "softmax-strided-online-coop.wgsl.jinja": "/OpczYeV5thgixotFxCXjWVCQvqO3kEKY0fmCgm0GPw=",
20
- "softmax-strided-packed4-tail.wgsl.jinja": "CUut2x8eExSySbqp4R97Rt6F0GbFJ6b5LOHLKLpWG5Q=",
21
- "softmax-subgroup-rows.wgsl.jinja": "jwyn5Nf1BBfBuaAIKIfJ5tMg16FcOeMW7llo2hy/ktw=",
22
- "test.json": "pLfIl89rWpvYoFMWrF35r5mJTQQ1BkpZPmChvLUF2nQ="
23
  }
24
  },
25
- "provenance": { "kernel": { "sha": "91d990483a174128daf7673f3f37a7c890493ae1", "dirty": false } },
26
  "webgpu": {
27
- "manifestSpec": "2.0",
28
  "variants": {
29
  "longrow_split": ["softmax-longrow-normalize.wgsl.jinja", "softmax-longrow-stats.wgsl.jinja"],
30
  "online_subgroup_rows_vec4": ["softmax-subgroup-rows.wgsl.jinja"],
 
1
  {
2
  "name": "ai.onnx.LogSoftmax",
3
+ "id": "_ai_onnx_logsoftmax_webgpu_4896bd3",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
+ "bench.json": "SvuAkJFufs4pJCvvQl68ccv1olqep5tmHlqxCp4Dd/s=",
11
+ "manifest.json": "agl/2kS1pO7NsCz4/7uItvvoLvoK5flm7PTJ89i8bHw=",
12
+ "softmax-longrow-normalize.wgsl.jinja": "jy4t+YROOPczKJwk1qn7dgbA4TFpslDRX4CLql5Ychk=",
13
+ "softmax-longrow-stats.wgsl.jinja": "S8w3EVvJXQavrn/79+OYm6yMZWERwlyJiZhYF8HkAj0=",
14
+ "softmax-normalize.wgsl.jinja": "MRBOzfP0VMvC2H8uWhMJ3wm5RckrdGxMneOy88Y92b4=",
15
+ "softmax-online-packed-rows.wgsl.jinja": "8SJM2qyv5RnJK/nzoif+8RZBcIIovrtcCVq+tQQ6n0k=",
16
+ "softmax-online.wgsl.jinja": "kspDFq+qGAmhnhKKcOmOWTLOQ2vX6y54NHbUlPnxhHY=",
17
+ "softmax-row-stage-strided-vec4.wgsl.jinja": "zTL9bEcqInxFgFd5V2HamSyPqzPzZyEcuW87QlO4aU4=",
18
+ "softmax-row-stage.wgsl.jinja": "Uv2knZMUyaueKOSxAhn7Vko2N0QajW/vyOJWSNbyux4=",
19
+ "softmax-strided-online-coop.wgsl.jinja": "ZMjzbUYJpr/AI6VDYIoPvqIt2vOadAtKxkfc/eFcvNE=",
20
+ "softmax-strided-packed4-tail.wgsl.jinja": "16AesZJMCqOU7HANkDNZlJdjpkohzbOk/M7pQu/vMNQ=",
21
+ "softmax-subgroup-rows.wgsl.jinja": "l8/ApzifNWPXI4+nQlz+dJ4ZI+t7IeiDqLrRj/XmmQw=",
22
+ "test.json": "Purex4g7SQtnFKtBVaRg6FkZWPLvCWGk/5pQ1aol1iI="
23
  }
24
  },
25
+ "provenance": { "kernel": { "sha": "6fdf6301e2bbcc2f03bf1eaf493b7ad55ef33afc", "dirty": false } },
26
  "webgpu": {
27
+ "manifestSpec": "2.1",
28
  "variants": {
29
  "longrow_split": ["softmax-longrow-normalize.wgsl.jinja", "softmax-longrow-stats.wgsl.jinja"],
30
  "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;
@@ -34,7 +31,6 @@ fn is_nan_f32(value: f32) -> bool {
34
  return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
35
  }
36
 
37
-
38
  @compute @workgroup_size(WG)
39
  fn main(@builtin(workgroup_id) wg: vec3<u32>,
40
  @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;
 
31
  return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
32
  }
33
 
 
34
  @compute @workgroup_size(WG)
35
  fn main(@builtin(workgroup_id) wg: vec3<u32>,
36
  @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,9 +1,13 @@
1
- {% if T == "f16" %}
2
- enable f16;
3
- {% endif %}
 
 
 
 
 
4
  {{ env.wgsl.resourceDeclarations }}
5
  {% if strided %}
6
-
7
  {% set stridedAxisUses = ["row_for_offset"] %}
8
  // Compiled indexing for row-wise reductions over a non-last axis. A row is one
9
  // slice along the reduction axis, indexed as outer * INNER_SIZE + inner.
@@ -42,17 +46,17 @@ const INNER_SIZE_SAFE: u32 = {{ inner_size.value if inner_size.value > 0 else 1
42
  {% if usesAxisStrideSafe %}
43
  const AXIS_STRIDE_SAFE: u32 = {{ (reduce_size * inner_size.value) if (reduce_size * inner_size.value) > 0 else 1 }}u;
44
  {% endif %}
45
-
46
  {% if usesElementOffset %}
 
47
  // Flat offset of element k (along the reduce axis) of `row`.
48
  fn element_offset(row: u32, k: u32) -> u32 {
49
  let outer = row / INNER_SIZE_SAFE;
50
  let inner = row % INNER_SIZE_SAFE;
51
  return outer * REDUCE_SIZE * INNER_SIZE + k * INNER_SIZE + inner;
52
  }
53
-
54
  {% endif %}
55
  {% if usesRowForOffset %}
 
56
  // Row that owns the flat element `offset` (inverse of element_offset over
57
  // the non-axis coordinates).
58
  fn row_for_offset(offset: u32) -> u32 {
@@ -60,7 +64,7 @@ fn row_for_offset(offset: u32) -> u32 {
60
  let inner = offset % INNER_SIZE_SAFE;
61
  return outer * INNER_SIZE_SAFE + inner;
62
  }
63
- {%- endif -%}
64
  {% endif %}
65
  // FLT_MAX, not -inf, as the online (m, d) accumulator init: merges must keep
66
  // `m - m` finite so an empty lane / all--inf row contributes the exact
@@ -88,15 +92,9 @@ fn is_nan_f32(value: f32) -> bool {
88
  return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
89
  }
90
 
91
-
92
  @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
93
  fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
94
- // 2D-folded flat index: gid.y carries the high bits past the
95
- // per-axis dispatch fold width (outputs > 16.7M elements).
96
- let i = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ tunables.WORKGROUP_SIZE }}u;
97
- if (i >= params.count) {
98
- return;
99
- }
100
  // Final element-parallel log-softmax pass. Each element rereads x and writes
101
  // (x - rowMax) - log(rowSum); strided mode changes only the row map.
102
  {% if strided %}
 
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 strided %}
 
11
  {% set stridedAxisUses = ["row_for_offset"] %}
12
  // Compiled indexing for row-wise reductions over a non-last axis. A row is one
13
  // slice along the reduction axis, indexed as outer * INNER_SIZE + inner.
 
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
  {% endif %}
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
 
92
  return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
93
  }
94
 
 
95
  @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
96
  fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
97
+ {{ flat_index_2d(tunables.WORKGROUP_SIZE) }}
 
 
 
 
 
98
  // Final element-parallel log-softmax pass. Each element rereads x and writes
99
  // (x - rowMax) - log(rowSum); strided mode changes only the row map.
100
  {% if strided %}
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);
@@ -218,7 +191,7 @@ fn main(@builtin(workgroup_id) wid: vec3<u32>,
218
  let logD = log(finalD);
219
  {% if useVec4 %}
220
  for (var c = lid.x; c < params.vecCols; c = c + WG) {
221
- let v = vec4<f32>(x[rowBase + c]);
222
  var o = vec4<f32>(
223
  shifted_value(v.x, finalM),
224
  shifted_value(v.y, finalM),
@@ -227,7 +200,7 @@ fn main(@builtin(workgroup_id) wid: vec3<u32>,
227
  if (rowIsNan) {
228
  o = vec4<f32>(nanOut);
229
  }
230
- y[rowBase + c] = vec4<{{ scalar }}>(o);
231
  }
232
  {% else %}
233
  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);
 
191
  let logD = log(finalD);
192
  {% if useVec4 %}
193
  for (var c = lid.x; c < params.vecCols; c = c + WG) {
194
+ let v = {{ read_vec4("c") }};
195
  var o = vec4<f32>(
196
  shifted_value(v.x, finalM),
197
  shifted_value(v.y, finalM),
 
200
  if (rowIsNan) {
201
  o = vec4<f32>(nanOut);
202
  }
203
+ {{ store_vec4("o", "c") }}
204
  }
205
  {% else %}
206
  for (var c = lid.x; c < params.cols; c = c + WG) {
build/webgpu/softmax-row-stage-strided-vec4.wgsl.jinja CHANGED
@@ -46,17 +46,17 @@ const INNER_SIZE_SAFE: u32 = {{ inner_size.value if inner_size.value > 0 else 1
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
-
50
  {% if usesElementOffset %}
 
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
-
58
  {% endif %}
59
  {% if usesRowForOffset %}
 
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,7 +64,8 @@ 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 stage == "max" %}
69
  // max(1u,..) so an empty strided tensor (INNER_SIZE 0) still compiles — the kernel
70
  // returns before any divide (VEC4_ROWS 0 -> guard), this only avoids a const `/0`.
@@ -84,17 +85,19 @@ var<workgroup> partial: array<vec4<f32>, WGS>;
84
  fn {{ reduce_fn }}(value: vec4<f32>, tid: u32) -> vec4<f32> {
85
  partial[tid] = value;
86
  workgroupBarrier();
87
- var stride: u32 = WGS / 2u;
 
 
 
 
 
88
  loop {
89
- if (stride == 0u) { break; }
90
- {% if stage == "max" %}
91
- if (tid < stride) { partial[tid] = max(partial[tid], partial[tid + stride]); }
92
- {% else %}
93
- if (tid < stride) { partial[tid] = partial[tid] + partial[tid + stride]; }
94
- {% endif %}
95
- stride = stride / 2u;
96
  workgroupBarrier();
97
- }
 
98
  return partial[0];
99
  }
100
 
 
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
  {% if stage == "max" %}
70
  // max(1u,..) so an empty strided tensor (INNER_SIZE 0) still compiles — the kernel
71
  // returns before any divide (VEC4_ROWS 0 -> guard), this only avoids a const `/0`.
 
85
  fn {{ reduce_fn }}(value: vec4<f32>, tid: u32) -> vec4<f32> {
86
  partial[tid] = value;
87
  workgroupBarrier();
88
+ {% macro wgsl_tree_fold_stmt(a, op, idx, svar) %}
89
+ {% if op == "max" or op == "min" %}
90
+ {{ a }}[{{ idx }}] = {{ op }}({{ a }}[{{ idx }}], {{ a }}[{{ idx }} + {{ svar }}]);{% else %}
91
+ {{ a }}[{{ idx }}] = {{ a }}[{{ idx }}] {{ "*" if op == "prod" else "+" }} {{ a }}[{{ idx }} + {{ svar }}];{% endif %}{% endmacro %}
92
+ {% macro wgsl_tree_fold(arrays, op="add", idx="lid", wg="WORKGROUP_SIZE", svar="stride", typed=false, form="tail", breakInline=false, bodyInline=false, barrierFirst=false, reuse=false) %}
93
+ var {{ svar }}{{ ": u32 " if typed else " " }}= {{ wg }} / 2u;
94
  loop {
95
+ if ({{ svar }} == 0u) { break; }
96
+ if ({{ idx }} < {{ svar }}) { {{ wgsl_tree_fold_stmt(arrays[0], op, idx, svar) }} }
97
+ {{ svar }} = {{ svar }} / 2u;
 
 
 
 
98
  workgroupBarrier();
99
+ }{% endmacro %}
100
+ {{ wgsl_tree_fold(["partial"], op=stage, idx="tid", wg="WGS", typed=true, form="head", breakInline=true, bodyInline=true) }}
101
  return partial[0];
102
  }
103
 
build/webgpu/softmax-row-stage.wgsl.jinja CHANGED
@@ -1,9 +1,6 @@
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;
@@ -46,17 +43,17 @@ const INNER_SIZE_SAFE: u32 = {{ inner_size.value if inner_size.value > 0 else 1
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
-
50
  {% if usesElementOffset %}
 
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
-
58
  {% endif %}
59
  {% if usesRowForOffset %}
 
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,10 +61,10 @@ 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
  {% endif %}
69
- {% macro wgsl_tree_reduce_f32(name, mode, buffer="partial", wg="WG", trailingBarrier=true) %}
70
- fn {{ name }}(value: f32, tid: u32) -> f32 {
 
71
  {{ buffer }}[tid] = value;
72
  workgroupBarrier();
73
  // Ceil-halving keeps every lane when the workgroup size is not a power of
@@ -93,14 +90,13 @@ fn {{ name }}(value: f32, tid: u32) -> f32 {
93
  // slot 0 here, so the next call's first store must not run until all lanes have read it.
94
  // `trailingBarrier=false` is safe only when the buffer is never written again before kernel exit.
95
  let reduced = {{ buffer }}[0];
96
- {% if trailingBarrier %}
97
  workgroupBarrier();
98
- {% endif %}
99
  return reduced;
100
  }
101
  {% endmacro %}
102
 
103
  var<workgroup> partial: array<f32, WGS>;
 
104
  {% if stage == "max" %}
105
  {{ wgsl_tree_reduce_f32("reduce_max_f32", "max", "partial", "WGS") }}
106
  {% else %}
@@ -108,7 +104,6 @@ var<workgroup> partial: array<f32, WGS>;
108
  {% endif %}
109
  {% set count = "REDUCE_SIZE" if strided else "params.cols" %}
110
  {% set offset_of = "element_offset(row, k)" if strided else "row * params.cols + k" %}
111
-
112
  @compute @workgroup_size(WGS)
113
  fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
114
  {% if strided %}
 
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;
 
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
  {% if usesElementOffset %}
47
+
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
  {% endif %}
55
  {% if usesRowForOffset %}
56
+
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
  let inner = offset % INNER_SIZE_SAFE;
62
  return outer * INNER_SIZE_SAFE + inner;
63
  }
 
64
  {% endif %}
65
+ {% endif %}
66
+ {% macro wgsl_tree_reduce_f32(name, mode, buffer="partial", wg="WG", trailingBarrier=true, valueType="f32") %}
67
+ fn {{ name }}(value: {{ valueType }}, tid: u32) -> {{ valueType }} {
68
  {{ buffer }}[tid] = value;
69
  workgroupBarrier();
70
  // Ceil-halving keeps every lane when the workgroup size is not a power of
 
90
  // slot 0 here, so the next call's first store must not run until all lanes have read it.
91
  // `trailingBarrier=false` is safe only when the buffer is never written again before kernel exit.
92
  let reduced = {{ buffer }}[0];
 
93
  workgroupBarrier();
 
94
  return reduced;
95
  }
96
  {% endmacro %}
97
 
98
  var<workgroup> partial: array<f32, WGS>;
99
+
100
  {% if stage == "max" %}
101
  {{ wgsl_tree_reduce_f32("reduce_max_f32", "max", "partial", "WGS") }}
102
  {% else %}
 
104
  {% endif %}
105
  {% set count = "REDUCE_SIZE" if strided else "params.cols" %}
106
  {% set offset_of = "element_offset(row, k)" if strided else "row * params.cols + k" %}
 
107
  @compute @workgroup_size(WGS)
108
  fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
109
  {% if strided %}
build/webgpu/softmax-strided-online-coop.wgsl.jinja CHANGED
@@ -5,12 +5,7 @@
5
  // Each invocation maintains the manifest-selected number of independent
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 +45,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,7 +63,7 @@ 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
  // FLT_MAX, not -inf, as the online (m, d) accumulator init: merges must keep
73
  // `m - m` finite so an empty lane / all--inf row contributes the exact
74
  // accumulator identity (m, d) = (-FLT_MAX, 0). Operator epilogues interpret
@@ -88,11 +83,7 @@ fn shifted_value(value: f32, maxValue: f32) -> f32 {
88
  let equalFiniteMax = select(false, value == maxValue, is_finite_f32(maxValue));
89
  return select(value - maxValue, 0.0, equalFiniteMax);
90
  }
91
-
92
- const TILE_COLS: u32 = {{ tileCols }}u;
93
- const AXIS_LANES: u32 = {{ axisLanes }}u;
94
- {% set negInit = "-FLT_MAX" if operation == "logsoftmax" else "-F32_MAX" %}
95
-
96
  {% macro update_partial(maxName, sumName, valueName, sumValue) %}
97
  if ({{ valueName }} > {{ maxName }}) {
98
  {{ sumName }} = {{ sumName }} * exp({{ maxName }} - {{ valueName }}) + {{ sumValue }};
@@ -106,6 +97,9 @@ const AXIS_LANES: u32 = {{ axisLanes }}u;
106
  }
107
  {% endmacro %}
108
 
 
 
 
109
  var<workgroup> part_max: array<f32, TILE_COLS * AXIS_LANES>;
110
  var<workgroup> part_sum: array<f32, TILE_COLS * AXIS_LANES>;
111
 
 
5
  // Each invocation maintains the manifest-selected number of independent
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 axisLanes = (256 / tileCols) | int %}
 
 
 
9
  {{ env.wgsl.resourceDeclarations }}
10
  {% set stridedAxisUses = ["STRIDED_ROWS", "element_offset"] %}
11
  // Compiled indexing for row-wise reductions over a non-last axis. A row is one
 
45
  {% if usesAxisStrideSafe %}
46
  const AXIS_STRIDE_SAFE: u32 = {{ (reduce_size * inner_size.value) if (reduce_size * inner_size.value) > 0 else 1 }}u;
47
  {% endif %}
 
48
  {% if usesElementOffset %}
49
+
50
  // Flat offset of element k (along the reduce axis) of `row`.
51
  fn element_offset(row: u32, k: u32) -> u32 {
52
  let outer = row / INNER_SIZE_SAFE;
53
  let inner = row % INNER_SIZE_SAFE;
54
  return outer * REDUCE_SIZE * INNER_SIZE + k * INNER_SIZE + inner;
55
  }
 
56
  {% endif %}
57
  {% if usesRowForOffset %}
58
+
59
  // Row that owns the flat element `offset` (inverse of element_offset over
60
  // the non-axis coordinates).
61
  fn row_for_offset(offset: u32) -> u32 {
 
63
  let inner = offset % INNER_SIZE_SAFE;
64
  return outer * INNER_SIZE_SAFE + inner;
65
  }
66
+ {% endif %}
67
  // FLT_MAX, not -inf, as the online (m, d) accumulator init: merges must keep
68
  // `m - m` finite so an empty lane / all--inf row contributes the exact
69
  // accumulator identity (m, d) = (-FLT_MAX, 0). Operator epilogues interpret
 
83
  let equalFiniteMax = select(false, value == maxValue, is_finite_f32(maxValue));
84
  return select(value - maxValue, 0.0, equalFiniteMax);
85
  }
86
+ {% set negInit = "-FLT_MAX" %}
 
 
 
 
87
  {% macro update_partial(maxName, sumName, valueName, sumValue) %}
88
  if ({{ valueName }} > {{ maxName }}) {
89
  {{ sumName }} = {{ sumName }} * exp({{ maxName }} - {{ valueName }}) + {{ sumValue }};
 
97
  }
98
  {% endmacro %}
99
 
100
+ const TILE_COLS: u32 = {{ tileCols }}u;
101
+ const AXIS_LANES: u32 = {{ axisLanes }}u;
102
+
103
  var<workgroup> part_max: array<f32, TILE_COLS * AXIS_LANES>;
104
  var<workgroup> part_sum: array<f32, TILE_COLS * AXIS_LANES>;
105
 
build/webgpu/softmax-strided-packed4-tail.wgsl.jinja CHANGED
@@ -44,17 +44,17 @@ const INNER_SIZE_SAFE: u32 = {{ inner_size.value if inner_size.value > 0 else 1
44
  {% if usesAxisStrideSafe %}
45
  const AXIS_STRIDE_SAFE: u32 = {{ (reduce_size * inner_size.value) if (reduce_size * inner_size.value) > 0 else 1 }}u;
46
  {% endif %}
47
-
48
  {% if usesElementOffset %}
 
49
  // Flat offset of element k (along the reduce axis) of `row`.
50
  fn element_offset(row: u32, k: u32) -> u32 {
51
  let outer = row / INNER_SIZE_SAFE;
52
  let inner = row % INNER_SIZE_SAFE;
53
  return outer * REDUCE_SIZE * INNER_SIZE + k * INNER_SIZE + inner;
54
  }
55
-
56
  {% endif %}
57
  {% if usesRowForOffset %}
 
58
  // Row that owns the flat element `offset` (inverse of element_offset over
59
  // the non-axis coordinates).
60
  fn row_for_offset(offset: u32) -> u32 {
@@ -62,7 +62,7 @@ fn row_for_offset(offset: u32) -> u32 {
62
  let inner = offset % INNER_SIZE_SAFE;
63
  return outer * INNER_SIZE_SAFE + inner;
64
  }
65
- {%- endif -%}
66
  // FLT_MAX, not -inf, as the online (m, d) accumulator init: merges must keep
67
  // `m - m` finite so an empty lane / all--inf row contributes the exact
68
  // accumulator identity (m, d) = (-FLT_MAX, 0). Operator epilogues interpret
@@ -82,6 +82,7 @@ fn shifted_value(value: f32, maxValue: f32) -> f32 {
82
  let equalFiniteMax = select(false, value == maxValue, is_finite_f32(maxValue));
83
  return select(value - maxValue, 0.0, equalFiniteMax);
84
  }
 
85
  fn exp_shift(value: f32, maxValue: f32) -> f32 {
86
  return exp(shifted_value(value, maxValue));
87
  }
@@ -98,6 +99,9 @@ const PACKED_ROWS: u32 = OUTER_SIZE * PACKED_INNER;
98
 
99
  var<workgroup> partial: array<vec4<f32>, WGS>;
100
 
 
 
 
101
  fn reduce_max4(value: vec4<f32>, tid: u32) -> vec4<f32> {
102
  partial[tid] = value;
103
  workgroupBarrier();
@@ -107,7 +111,9 @@ fn reduce_max4(value: vec4<f32>, tid: u32) -> vec4<f32> {
107
  }
108
  workgroupBarrier();
109
  }
110
- return partial[0];
 
 
111
  }
112
 
113
  fn reduce_add4(value: vec4<f32>, tid: u32) -> vec4<f32> {
@@ -119,7 +125,9 @@ fn reduce_add4(value: vec4<f32>, tid: u32) -> vec4<f32> {
119
  }
120
  workgroupBarrier();
121
  }
122
- return partial[0];
 
 
123
  }
124
 
125
  fn stable_exp(value: f32, maximum: f32) -> f32 {
 
44
  {% if usesAxisStrideSafe %}
45
  const AXIS_STRIDE_SAFE: u32 = {{ (reduce_size * inner_size.value) if (reduce_size * inner_size.value) > 0 else 1 }}u;
46
  {% endif %}
 
47
  {% if usesElementOffset %}
48
+
49
  // Flat offset of element k (along the reduce axis) of `row`.
50
  fn element_offset(row: u32, k: u32) -> u32 {
51
  let outer = row / INNER_SIZE_SAFE;
52
  let inner = row % INNER_SIZE_SAFE;
53
  return outer * REDUCE_SIZE * INNER_SIZE + k * INNER_SIZE + inner;
54
  }
 
55
  {% endif %}
56
  {% if usesRowForOffset %}
57
+
58
  // Row that owns the flat element `offset` (inverse of element_offset over
59
  // the non-axis coordinates).
60
  fn row_for_offset(offset: u32) -> u32 {
 
62
  let inner = offset % INNER_SIZE_SAFE;
63
  return outer * INNER_SIZE_SAFE + inner;
64
  }
65
+ {% endif %}
66
  // FLT_MAX, not -inf, as the online (m, d) accumulator init: merges must keep
67
  // `m - m` finite so an empty lane / all--inf row contributes the exact
68
  // accumulator identity (m, d) = (-FLT_MAX, 0). Operator epilogues interpret
 
82
  let equalFiniteMax = select(false, value == maxValue, is_finite_f32(maxValue));
83
  return select(value - maxValue, 0.0, equalFiniteMax);
84
  }
85
+
86
  fn exp_shift(value: f32, maxValue: f32) -> f32 {
87
  return exp(shifted_value(value, maxValue));
88
  }
 
99
 
100
  var<workgroup> partial: array<vec4<f32>, WGS>;
101
 
102
+ // Both reductions fold in `partial`, so each returns only after every lane has
103
+ // loaded slot 0: without the trailing barrier, a lane that starts the next
104
+ // reduction overwrites slot 0 before slower lanes read the previous result.
105
  fn reduce_max4(value: vec4<f32>, tid: u32) -> vec4<f32> {
106
  partial[tid] = value;
107
  workgroupBarrier();
 
111
  }
112
  workgroupBarrier();
113
  }
114
+ let result = partial[0];
115
+ workgroupBarrier();
116
+ return result;
117
  }
118
 
119
  fn reduce_add4(value: vec4<f32>, tid: u32) -> vec4<f32> {
 
125
  }
126
  workgroupBarrier();
127
  }
128
+ let result = partial[0];
129
+ workgroupBarrier();
130
+ return result;
131
  }
132
 
133
  fn stable_exp(value: f32, maximum: f32) -> f32 {
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
@@ -22,7 +22,7 @@
22
  {
23
  "name": "strided_capacity_large_axis_tail_boundary",
24
  "provenance": {
25
- "notes": "At the large-inner crossover, axis length 2,048 and inner size 6 select the capacity path with a non-multiple-of-four tail."
26
  },
27
  "tunables": { "STRIDED_CAPACITY_MIN_ROWS": 1 },
28
  "attrs": { "axis": 1 },
@@ -73,7 +73,7 @@
73
  {
74
  "name": "packed_rows_tail_axis1_33x32",
75
  "provenance": {
76
- "notes": "One row beyond a full 32-row packed workgroup locks the shared portable multirow kernel's partial-workgroup barrier and output guards."
77
  },
78
  "attrs": { "axis": 1 },
79
  "inputs": {
@@ -102,9 +102,7 @@
102
  },
103
  {
104
  "name": "many_rows_axis1_2048x32",
105
- "provenance": {
106
- "notes": "Many 32-column rows exercise the packed-rows path below the 64-column subgroup-per-row threshold."
107
- },
108
  "attrs": { "axis": 1 },
109
  "inputs": {
110
  "x": {
@@ -712,7 +710,7 @@
712
  {
713
  "name": "strided_rowmax_scratch_over_128mib_f16_capacity_fallback",
714
  "provenance": {
715
- "notes": "With 33,554,433 strided rows, a float32 row-maximum buffer would exceed 128 MiB. Float16 keeps the input within the minimum storage-binding limit and selects the single-pass online path without row scratch."
716
  },
717
  "attrs": { "axis": 0 },
718
  "inputs": {
@@ -790,7 +788,7 @@
790
  {
791
  "name": "online_stream_axis31_f32",
792
  "provenance": {
793
- "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."
794
  },
795
  "attrs": { "axis": 1 },
796
  "inputs": {
@@ -805,7 +803,7 @@
805
  {
806
  "name": "online_stream_axis32_f32",
807
  "provenance": {
808
- "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."
809
  },
810
  "attrs": { "axis": 1 },
811
  "inputs": {
@@ -820,7 +818,7 @@
820
  {
821
  "name": "online_stream_axis33_f32",
822
  "provenance": {
823
- "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."
824
  },
825
  "attrs": { "axis": 1 },
826
  "inputs": {
@@ -835,7 +833,7 @@
835
  {
836
  "name": "online_stream_axis64_f32",
837
  "provenance": {
838
- "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."
839
  },
840
  "attrs": { "axis": 1 },
841
  "inputs": {
@@ -850,7 +848,7 @@
850
  {
851
  "name": "online_stream_axis65_f32",
852
  "provenance": {
853
- "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."
854
  },
855
  "attrs": { "axis": 1 },
856
  "inputs": {
@@ -865,7 +863,7 @@
865
  {
866
  "name": "online_stream_columns_tail_f32",
867
  "provenance": {
868
- "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."
869
  },
870
  "attrs": { "axis": 1 },
871
  "inputs": {
@@ -906,7 +904,7 @@
906
  {
907
  "name": "online_stream_four_way_tail_f32",
908
  "provenance": {
909
- "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."
910
  },
911
  "attrs": { "axis": 1 },
912
  "inputs": {
 
22
  {
23
  "name": "strided_capacity_large_axis_tail_boundary",
24
  "provenance": {
25
+ "notes": "LogSoftmax axis length 2048 is strided by an inner size of 6, which is not a multiple of four, leaving a scalar tail; checks correctness on a non-contiguous reduction axis at this length."
26
  },
27
  "tunables": { "STRIDED_CAPACITY_MIN_ROWS": 1 },
28
  "attrs": { "axis": 1 },
 
73
  {
74
  "name": "packed_rows_tail_axis1_33x32",
75
  "provenance": {
76
+ "notes": "Thirty-three rows leave one row after a full 32-row group; every row must be normalized independently and written once."
77
  },
78
  "attrs": { "axis": 1 },
79
  "inputs": {
 
102
  },
103
  {
104
  "name": "many_rows_axis1_2048x32",
105
+ "provenance": { "notes": "Many 32-column rows exercise short-row LogSoftmax." },
 
 
106
  "attrs": { "axis": 1 },
107
  "inputs": {
108
  "x": {
 
710
  {
711
  "name": "strided_rowmax_scratch_over_128mib_f16_capacity_fallback",
712
  "provenance": {
713
+ "notes": "33,554,433 independent strided float16 rows exceed 128 MiB of float32 per-row statistics; the output must still be correct within the minimum storage-binding limit."
714
  },
715
  "attrs": { "axis": 0 },
716
  "inputs": {
 
788
  {
789
  "name": "online_stream_axis31_f32",
790
  "provenance": {
791
+ "notes": "Exercises the per-lane stream-count boundary, reduction tail, or column tail. f16 comparison includes its storage rounding."
792
  },
793
  "attrs": { "axis": 1 },
794
  "inputs": {
 
803
  {
804
  "name": "online_stream_axis32_f32",
805
  "provenance": {
806
+ "notes": "Exercises the per-lane stream-count boundary, reduction tail, or column tail. f16 comparison includes its storage rounding."
807
  },
808
  "attrs": { "axis": 1 },
809
  "inputs": {
 
818
  {
819
  "name": "online_stream_axis33_f32",
820
  "provenance": {
821
+ "notes": "Exercises the per-lane stream-count boundary, reduction tail, or column tail. f16 comparison includes its storage rounding."
822
  },
823
  "attrs": { "axis": 1 },
824
  "inputs": {
 
833
  {
834
  "name": "online_stream_axis64_f32",
835
  "provenance": {
836
+ "notes": "Exercises the per-lane stream-count boundary, reduction tail, or column tail. f16 comparison includes its storage rounding."
837
  },
838
  "attrs": { "axis": 1 },
839
  "inputs": {
 
848
  {
849
  "name": "online_stream_axis65_f32",
850
  "provenance": {
851
+ "notes": "Exercises the per-lane stream-count boundary, reduction tail, or column tail. f16 comparison includes its storage rounding."
852
  },
853
  "attrs": { "axis": 1 },
854
  "inputs": {
 
863
  {
864
  "name": "online_stream_columns_tail_f32",
865
  "provenance": {
866
+ "notes": "Exercises the per-lane stream-count boundary, reduction tail, or column tail. f16 comparison includes its storage rounding."
867
  },
868
  "attrs": { "axis": 1 },
869
  "inputs": {
 
904
  {
905
  "name": "online_stream_four_way_tail_f32",
906
  "provenance": {
907
+ "notes": "Exercises the per-lane stream-count boundary, reduction tail, or column tail. f16 comparison includes its storage rounding."
908
  },
909
  "attrs": { "axis": 1 },
910
  "inputs": {