Xenova HF Staff commited on
Commit
66f0d7a
·
verified ·
1 Parent(s): 365cf2a

sync 6fdf6301e2bb

Browse files
README.md CHANGED
@@ -44,20 +44,34 @@ Attributes and default values (overridable per request):
44
  | --- | --- |
45
  | `T` | `float32`, `float16` |
46
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  ## Files
48
 
49
  - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, per-variant templates, provenance)
50
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
51
  - [`test.json`](build/webgpu/test.json) — correctness cases
52
- - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
53
  - [`bias-softmax-longrow-normalize.wgsl.jinja`](build/webgpu/bias-softmax-longrow-normalize.wgsl.jinja)
54
- - [`bias-softmax-longrow-stats.wgsl.jinja`](build/webgpu/bias-softmax-longrow-stats.wgsl.jinja)
55
  - [`bias-softmax.wgsl.jinja`](build/webgpu/bias-softmax.wgsl.jinja)
 
 
 
56
 
57
  ## Use with `@huggingface/kernels`
58
 
59
  ```sh
60
- npm install --save-exact @huggingface/kernels@0.0.1-preview.2
61
  ```
62
 
63
  Required output shapes and logical data types are inferred from the supplied inputs and attributes; result tensors are allocated automatically.
 
44
  | --- | --- |
45
  | `T` | `float32`, `float16` |
46
 
47
+ ## Implementation variants
48
+
49
+ One implementation is selected per call from the device capabilities, the request shapes and the dtypes; these notes say what each one covers.
50
+
51
+ - `online_subgroup_rows_vec4` — Subgroup-per-row fused bias and softmax with register-resident vec4 values, f32 addition and reduction, and the existing zero-output convention for fully masked rows.
52
+ - `online_workgroup_vec4` — Vectorized workgroup online bias-softmax with f32 addition and reduction; portable fallback below the register-resident subgroup-row path.
53
+ - `online_workgroup_scalar_vec4` — Logical vec4 online reduction with scalar storage for unaligned row strides. Invalid tail components contribute zero denominator and are never stored.
54
+
55
+ ## Device requirements
56
+
57
+ Some implementation variants require `subgroups`. These are route-specific capabilities, not package-wide requirements; availability also depends on the request shape and dtype.
58
+
59
  ## Files
60
 
61
  - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, per-variant templates, provenance)
62
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
63
  - [`test.json`](build/webgpu/test.json) — correctness cases
64
+ - [`bench.json`](build/webgpu/bench.json) — benchmark cases
65
  - [`bias-softmax-longrow-normalize.wgsl.jinja`](build/webgpu/bias-softmax-longrow-normalize.wgsl.jinja)
 
66
  - [`bias-softmax.wgsl.jinja`](build/webgpu/bias-softmax.wgsl.jinja)
67
+ - [`softmax-longrow-stats.wgsl.jinja`](build/webgpu/softmax-longrow-stats.wgsl.jinja)
68
+ - [`softmax-online.wgsl.jinja`](build/webgpu/softmax-online.wgsl.jinja)
69
+ - [`softmax-subgroup-rows.wgsl.jinja`](build/webgpu/softmax-subgroup-rows.wgsl.jinja)
70
 
71
  ## Use with `@huggingface/kernels`
72
 
73
  ```sh
74
+ npm install --save-exact @huggingface/kernels@0.0.1-preview.3
75
  ```
76
 
77
  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
@@ -1,5 +1,9 @@
1
  {
2
- "tunableSpace": { "WORKGROUP_SIZE": [64, 128, 256], "BLOCK_COLS": [1024, 2048, 4096, 8192] },
 
 
 
 
3
  "cases": [
4
  {
5
  "name": "biassoftmax-f32-attn-32x512x512",
@@ -67,6 +71,474 @@
67
  },
68
  "outputs": { "output": { "shape": [262144, 2], "dtype": "float32" } },
69
  "bench": { "metrics": [{ "type": "bandwidth", "value": "(262144 * 2 * 2 + 2) * dtypeBytes(args.dtype)" }] }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  }
71
  ]
72
  }
 
1
  {
2
+ "tunableSpace": {
3
+ "WORKGROUP_SIZE": [64, 128, 256],
4
+ "BLOCK_COLS": [1024, 2048, 4096, 8192],
5
+ "ONLINE_WORKGROUP_SIZE": [32, 64, 128, 256]
6
+ },
7
  "cases": [
8
  {
9
  "name": "biassoftmax-f32-attn-32x512x512",
 
71
  },
72
  "outputs": { "output": { "shape": [262144, 2], "dtype": "float32" } },
73
  "bench": { "metrics": [{ "type": "bandwidth", "value": "(262144 * 2 * 2 + 2) * dtypeBytes(args.dtype)" }] }
74
+ },
75
+ {
76
+ "name": "reuse_float32_r4096_c513_bias16_wg256_inner0",
77
+ "preset": "stress",
78
+ "vars": { "dtype": "float32" },
79
+ "attrs": { "axis": 1, "is_inner_broadcast": 0 },
80
+ "tunables": { "WORKGROUP_SIZE": 256 },
81
+ "inputs": {
82
+ "data": { "shape": [4096, 513], "dtype": "float32", "dist": "normal", "seed": 601, "scale": 2 },
83
+ "bias": { "shape": [16, 513], "dtype": "float32", "dist": "normal", "seed": 602, "scale": 1 }
84
+ },
85
+ "outputs": { "output": { "shape": [4096, 513], "dtype": "float32" } },
86
+ "bench": { "metrics": [{ "type": "bandwidth", "value": "(4096 * 513 * 2 + 16 * 513) * dtypeBytes(args.dtype)" }] }
87
+ },
88
+ {
89
+ "name": "reuse_float32_r1024_c4095_bias16_wg256_inner0",
90
+ "preset": "stress",
91
+ "vars": { "dtype": "float32" },
92
+ "attrs": { "axis": 1, "is_inner_broadcast": 0 },
93
+ "tunables": { "WORKGROUP_SIZE": 256 },
94
+ "inputs": {
95
+ "data": { "shape": [1024, 4095], "dtype": "float32", "dist": "normal", "seed": 601, "scale": 2 },
96
+ "bias": { "shape": [16, 4095], "dtype": "float32", "dist": "normal", "seed": 602, "scale": 1 }
97
+ },
98
+ "outputs": { "output": { "shape": [1024, 4095], "dtype": "float32" } },
99
+ "bench": {
100
+ "metrics": [{ "type": "bandwidth", "value": "(1024 * 4095 * 2 + 16 * 4095) * dtypeBytes(args.dtype)" }]
101
+ }
102
+ },
103
+ {
104
+ "name": "reuse_float16_r4096_c513_bias16_wg256_inner0",
105
+ "preset": "stress",
106
+ "vars": { "dtype": "float16" },
107
+ "attrs": { "axis": 1, "is_inner_broadcast": 0 },
108
+ "tunables": { "WORKGROUP_SIZE": 256 },
109
+ "inputs": {
110
+ "data": { "shape": [4096, 513], "dtype": "float16", "dist": "normal", "seed": 601, "scale": 2 },
111
+ "bias": { "shape": [16, 513], "dtype": "float16", "dist": "normal", "seed": 602, "scale": 1 }
112
+ },
113
+ "outputs": { "output": { "shape": [4096, 513], "dtype": "float16" } },
114
+ "bench": { "metrics": [{ "type": "bandwidth", "value": "(4096 * 513 * 2 + 16 * 513) * dtypeBytes(args.dtype)" }] }
115
+ },
116
+ {
117
+ "name": "reuse_float16_r1024_c4095_bias16_wg256_inner0",
118
+ "preset": "stress",
119
+ "vars": { "dtype": "float16" },
120
+ "attrs": { "axis": 1, "is_inner_broadcast": 0 },
121
+ "tunables": { "WORKGROUP_SIZE": 256 },
122
+ "inputs": {
123
+ "data": { "shape": [1024, 4095], "dtype": "float16", "dist": "normal", "seed": 601, "scale": 2 },
124
+ "bias": { "shape": [16, 4095], "dtype": "float16", "dist": "normal", "seed": 602, "scale": 1 }
125
+ },
126
+ "outputs": { "output": { "shape": [1024, 4095], "dtype": "float16" } },
127
+ "bench": {
128
+ "metrics": [{ "type": "bandwidth", "value": "(1024 * 4095 * 2 + 16 * 4095) * dtypeBytes(args.dtype)" }]
129
+ }
130
+ },
131
+ {
132
+ "name": "long_float32_r4_c65537_bias2_wg64_inner1",
133
+ "preset": "stress",
134
+ "vars": { "dtype": "float32" },
135
+ "attrs": { "axis": 1, "is_inner_broadcast": 1 },
136
+ "tunables": { "WORKGROUP_SIZE": 64 },
137
+ "inputs": {
138
+ "data": { "shape": [4, 65537], "dtype": "float32", "dist": "normal", "seed": 611, "scale": 2 },
139
+ "bias": { "shape": [2, 65537], "dtype": "float32", "dist": "normal", "seed": 612, "scale": 1 }
140
+ },
141
+ "outputs": { "output": { "shape": [4, 65537], "dtype": "float32" } },
142
+ "bench": { "metrics": [{ "type": "bandwidth", "value": "(4 * 65537 * 2 + 2 * 65537) * dtypeBytes(args.dtype)" }] }
143
+ },
144
+ {
145
+ "name": "long_float32_r8_c131073_bias4_wg256_inner0",
146
+ "preset": "stress",
147
+ "vars": { "dtype": "float32" },
148
+ "attrs": { "axis": 1, "is_inner_broadcast": 0 },
149
+ "tunables": { "WORKGROUP_SIZE": 256 },
150
+ "inputs": {
151
+ "data": { "shape": [8, 131073], "dtype": "float32", "dist": "normal", "seed": 611, "scale": 2 },
152
+ "bias": { "shape": [4, 131073], "dtype": "float32", "dist": "normal", "seed": 612, "scale": 1 }
153
+ },
154
+ "outputs": { "output": { "shape": [8, 131073], "dtype": "float32" } },
155
+ "bench": {
156
+ "metrics": [{ "type": "bandwidth", "value": "(8 * 131073 * 2 + 4 * 131073) * dtypeBytes(args.dtype)" }]
157
+ }
158
+ },
159
+ {
160
+ "name": "long_float16_r4_c65537_bias2_wg64_inner1",
161
+ "preset": "stress",
162
+ "vars": { "dtype": "float16" },
163
+ "attrs": { "axis": 1, "is_inner_broadcast": 1 },
164
+ "tunables": { "WORKGROUP_SIZE": 64 },
165
+ "inputs": {
166
+ "data": { "shape": [4, 65537], "dtype": "float16", "dist": "normal", "seed": 611, "scale": 2 },
167
+ "bias": { "shape": [2, 65537], "dtype": "float16", "dist": "normal", "seed": 612, "scale": 1 }
168
+ },
169
+ "outputs": { "output": { "shape": [4, 65537], "dtype": "float16" } },
170
+ "bench": { "metrics": [{ "type": "bandwidth", "value": "(4 * 65537 * 2 + 2 * 65537) * dtypeBytes(args.dtype)" }] }
171
+ },
172
+ {
173
+ "name": "long_float16_r8_c131073_bias4_wg256_inner0",
174
+ "preset": "stress",
175
+ "vars": { "dtype": "float16" },
176
+ "attrs": { "axis": 1, "is_inner_broadcast": 0 },
177
+ "tunables": { "WORKGROUP_SIZE": 256 },
178
+ "inputs": {
179
+ "data": { "shape": [8, 131073], "dtype": "float16", "dist": "normal", "seed": 611, "scale": 2 },
180
+ "bias": { "shape": [4, 131073], "dtype": "float16", "dist": "normal", "seed": 612, "scale": 1 }
181
+ },
182
+ "outputs": { "output": { "shape": [8, 131073], "dtype": "float16" } },
183
+ "bench": {
184
+ "metrics": [{ "type": "bandwidth", "value": "(8 * 131073 * 2 + 4 * 131073) * dtypeBytes(args.dtype)" }]
185
+ }
186
+ },
187
+ {
188
+ "name": "subgroup-neighbor-float32-r64-c64-bias2-inner0",
189
+ "preset": "stress",
190
+ "attrs": { "axis": 1, "is_inner_broadcast": 0 },
191
+ "inputs": {
192
+ "data": { "shape": [64, 64], "dtype": "float32", "dist": "normal", "seed": 761, "scale": 2 },
193
+ "bias": { "shape": [2, 64], "dtype": "float32", "dist": "normal", "seed": 762, "scale": 1 }
194
+ },
195
+ "outputs": { "output": { "shape": [64, 64], "dtype": "float32" } },
196
+ "vars": { "dtype": "float32", "rows": 64, "cols": 64, "biasRows": 2 },
197
+ "bench": {
198
+ "metrics": [
199
+ {
200
+ "type": "bandwidth",
201
+ "value": "(2 * args.rows * args.cols + args.biasRows * args.cols) * dtypeBytes(args.dtype)"
202
+ }
203
+ ]
204
+ }
205
+ },
206
+ {
207
+ "name": "subgroup-neighbor-float32-r65-c256-bias5-inner1",
208
+ "preset": "stress",
209
+ "attrs": { "axis": 1, "is_inner_broadcast": 1 },
210
+ "inputs": {
211
+ "data": { "shape": [65, 256], "dtype": "float32", "dist": "normal", "seed": 761, "scale": 2 },
212
+ "bias": { "shape": [5, 256], "dtype": "float32", "dist": "normal", "seed": 762, "scale": 1 }
213
+ },
214
+ "outputs": { "output": { "shape": [65, 256], "dtype": "float32" } },
215
+ "vars": { "dtype": "float32", "rows": 65, "cols": 256, "biasRows": 5 },
216
+ "bench": {
217
+ "metrics": [
218
+ {
219
+ "type": "bandwidth",
220
+ "value": "(2 * args.rows * args.cols + args.biasRows * args.cols) * dtypeBytes(args.dtype)"
221
+ }
222
+ ]
223
+ }
224
+ },
225
+ {
226
+ "name": "subgroup-neighbor-float32-r2048-c128-bias16-inner0",
227
+ "preset": "stress",
228
+ "attrs": { "axis": 1, "is_inner_broadcast": 0 },
229
+ "inputs": {
230
+ "data": { "shape": [2048, 128], "dtype": "float32", "dist": "normal", "seed": 761, "scale": 2 },
231
+ "bias": { "shape": [16, 128], "dtype": "float32", "dist": "normal", "seed": 762, "scale": 1 }
232
+ },
233
+ "outputs": { "output": { "shape": [2048, 128], "dtype": "float32" } },
234
+ "vars": { "dtype": "float32", "rows": 2048, "cols": 128, "biasRows": 16 },
235
+ "bench": {
236
+ "metrics": [
237
+ {
238
+ "type": "bandwidth",
239
+ "value": "(2 * args.rows * args.cols + args.biasRows * args.cols) * dtypeBytes(args.dtype)"
240
+ }
241
+ ]
242
+ }
243
+ },
244
+ {
245
+ "name": "subgroup-neighbor-float32-r512-c1024-bias8-inner1",
246
+ "preset": "stress",
247
+ "attrs": { "axis": 1, "is_inner_broadcast": 1 },
248
+ "inputs": {
249
+ "data": { "shape": [512, 1024], "dtype": "float32", "dist": "normal", "seed": 761, "scale": 2 },
250
+ "bias": { "shape": [8, 1024], "dtype": "float32", "dist": "normal", "seed": 762, "scale": 1 }
251
+ },
252
+ "outputs": { "output": { "shape": [512, 1024], "dtype": "float32" } },
253
+ "vars": { "dtype": "float32", "rows": 512, "cols": 1024, "biasRows": 8 },
254
+ "bench": {
255
+ "metrics": [
256
+ {
257
+ "type": "bandwidth",
258
+ "value": "(2 * args.rows * args.cols + args.biasRows * args.cols) * dtypeBytes(args.dtype)"
259
+ }
260
+ ]
261
+ }
262
+ },
263
+ {
264
+ "name": "subgroup-neighbor-float32-r128-c1028-bias4-inner0",
265
+ "preset": "stress",
266
+ "attrs": { "axis": 1, "is_inner_broadcast": 0 },
267
+ "inputs": {
268
+ "data": { "shape": [128, 1028], "dtype": "float32", "dist": "normal", "seed": 761, "scale": 2 },
269
+ "bias": { "shape": [4, 1028], "dtype": "float32", "dist": "normal", "seed": 762, "scale": 1 }
270
+ },
271
+ "outputs": { "output": { "shape": [128, 1028], "dtype": "float32" } },
272
+ "vars": { "dtype": "float32", "rows": 128, "cols": 1028, "biasRows": 4 },
273
+ "bench": {
274
+ "metrics": [
275
+ {
276
+ "type": "bandwidth",
277
+ "value": "(2 * args.rows * args.cols + args.biasRows * args.cols) * dtypeBytes(args.dtype)"
278
+ }
279
+ ]
280
+ }
281
+ },
282
+ {
283
+ "name": "subgroup-neighbor-float32-r63-c512-bias3-inner1",
284
+ "preset": "stress",
285
+ "attrs": { "axis": 1, "is_inner_broadcast": 1 },
286
+ "inputs": {
287
+ "data": { "shape": [63, 512], "dtype": "float32", "dist": "normal", "seed": 761, "scale": 2 },
288
+ "bias": { "shape": [3, 512], "dtype": "float32", "dist": "normal", "seed": 762, "scale": 1 }
289
+ },
290
+ "outputs": { "output": { "shape": [63, 512], "dtype": "float32" } },
291
+ "vars": { "dtype": "float32", "rows": 63, "cols": 512, "biasRows": 3 },
292
+ "bench": {
293
+ "metrics": [
294
+ {
295
+ "type": "bandwidth",
296
+ "value": "(2 * args.rows * args.cols + args.biasRows * args.cols) * dtypeBytes(args.dtype)"
297
+ }
298
+ ]
299
+ }
300
+ },
301
+ {
302
+ "name": "subgroup-neighbor-float16-r64-c64-bias2-inner0",
303
+ "preset": "stress",
304
+ "attrs": { "axis": 1, "is_inner_broadcast": 0 },
305
+ "inputs": {
306
+ "data": { "shape": [64, 64], "dtype": "float16", "dist": "normal", "seed": 761, "scale": 2 },
307
+ "bias": { "shape": [2, 64], "dtype": "float16", "dist": "normal", "seed": 762, "scale": 1 }
308
+ },
309
+ "outputs": { "output": { "shape": [64, 64], "dtype": "float16" } },
310
+ "vars": { "dtype": "float16", "rows": 64, "cols": 64, "biasRows": 2 },
311
+ "bench": {
312
+ "metrics": [
313
+ {
314
+ "type": "bandwidth",
315
+ "value": "(2 * args.rows * args.cols + args.biasRows * args.cols) * dtypeBytes(args.dtype)"
316
+ }
317
+ ]
318
+ }
319
+ },
320
+ {
321
+ "name": "subgroup-neighbor-float16-r65-c256-bias5-inner1",
322
+ "preset": "stress",
323
+ "attrs": { "axis": 1, "is_inner_broadcast": 1 },
324
+ "inputs": {
325
+ "data": { "shape": [65, 256], "dtype": "float16", "dist": "normal", "seed": 761, "scale": 2 },
326
+ "bias": { "shape": [5, 256], "dtype": "float16", "dist": "normal", "seed": 762, "scale": 1 }
327
+ },
328
+ "outputs": { "output": { "shape": [65, 256], "dtype": "float16" } },
329
+ "vars": { "dtype": "float16", "rows": 65, "cols": 256, "biasRows": 5 },
330
+ "bench": {
331
+ "metrics": [
332
+ {
333
+ "type": "bandwidth",
334
+ "value": "(2 * args.rows * args.cols + args.biasRows * args.cols) * dtypeBytes(args.dtype)"
335
+ }
336
+ ]
337
+ }
338
+ },
339
+ {
340
+ "name": "subgroup-neighbor-float16-r2048-c128-bias16-inner0",
341
+ "preset": "stress",
342
+ "attrs": { "axis": 1, "is_inner_broadcast": 0 },
343
+ "inputs": {
344
+ "data": { "shape": [2048, 128], "dtype": "float16", "dist": "normal", "seed": 761, "scale": 2 },
345
+ "bias": { "shape": [16, 128], "dtype": "float16", "dist": "normal", "seed": 762, "scale": 1 }
346
+ },
347
+ "outputs": { "output": { "shape": [2048, 128], "dtype": "float16" } },
348
+ "vars": { "dtype": "float16", "rows": 2048, "cols": 128, "biasRows": 16 },
349
+ "bench": {
350
+ "metrics": [
351
+ {
352
+ "type": "bandwidth",
353
+ "value": "(2 * args.rows * args.cols + args.biasRows * args.cols) * dtypeBytes(args.dtype)"
354
+ }
355
+ ]
356
+ }
357
+ },
358
+ {
359
+ "name": "subgroup-neighbor-float16-r512-c1024-bias8-inner1",
360
+ "preset": "stress",
361
+ "attrs": { "axis": 1, "is_inner_broadcast": 1 },
362
+ "inputs": {
363
+ "data": { "shape": [512, 1024], "dtype": "float16", "dist": "normal", "seed": 761, "scale": 2 },
364
+ "bias": { "shape": [8, 1024], "dtype": "float16", "dist": "normal", "seed": 762, "scale": 1 }
365
+ },
366
+ "outputs": { "output": { "shape": [512, 1024], "dtype": "float16" } },
367
+ "vars": { "dtype": "float16", "rows": 512, "cols": 1024, "biasRows": 8 },
368
+ "bench": {
369
+ "metrics": [
370
+ {
371
+ "type": "bandwidth",
372
+ "value": "(2 * args.rows * args.cols + args.biasRows * args.cols) * dtypeBytes(args.dtype)"
373
+ }
374
+ ]
375
+ }
376
+ },
377
+ {
378
+ "name": "subgroup-neighbor-float16-r128-c1028-bias4-inner0",
379
+ "preset": "stress",
380
+ "attrs": { "axis": 1, "is_inner_broadcast": 0 },
381
+ "inputs": {
382
+ "data": { "shape": [128, 1028], "dtype": "float16", "dist": "normal", "seed": 761, "scale": 2 },
383
+ "bias": { "shape": [4, 1028], "dtype": "float16", "dist": "normal", "seed": 762, "scale": 1 }
384
+ },
385
+ "outputs": { "output": { "shape": [128, 1028], "dtype": "float16" } },
386
+ "vars": { "dtype": "float16", "rows": 128, "cols": 1028, "biasRows": 4 },
387
+ "bench": {
388
+ "metrics": [
389
+ {
390
+ "type": "bandwidth",
391
+ "value": "(2 * args.rows * args.cols + args.biasRows * args.cols) * dtypeBytes(args.dtype)"
392
+ }
393
+ ]
394
+ }
395
+ },
396
+ {
397
+ "name": "subgroup-neighbor-float16-r63-c512-bias3-inner1",
398
+ "preset": "stress",
399
+ "attrs": { "axis": 1, "is_inner_broadcast": 1 },
400
+ "inputs": {
401
+ "data": { "shape": [63, 512], "dtype": "float16", "dist": "normal", "seed": 761, "scale": 2 },
402
+ "bias": { "shape": [3, 512], "dtype": "float16", "dist": "normal", "seed": 762, "scale": 1 }
403
+ },
404
+ "outputs": { "output": { "shape": [63, 512], "dtype": "float16" } },
405
+ "vars": { "dtype": "float16", "rows": 63, "cols": 512, "biasRows": 3 },
406
+ "bench": {
407
+ "metrics": [
408
+ {
409
+ "type": "bandwidth",
410
+ "value": "(2 * args.rows * args.cols + args.biasRows * args.cols) * dtypeBytes(args.dtype)"
411
+ }
412
+ ]
413
+ }
414
+ },
415
+ {
416
+ "name": "workgroup-neighbor-float32-r65-c33-bias5-inner1",
417
+ "preset": "stress",
418
+ "attrs": { "axis": 1, "is_inner_broadcast": 1 },
419
+ "inputs": {
420
+ "data": { "dtype": "float32", "shape": [65, 33], "dist": "normal", "seed": 701, "scale": 2 },
421
+ "bias": { "dtype": "float32", "shape": [5, 33], "dist": "normal", "seed": 702, "scale": 1 }
422
+ },
423
+ "outputs": { "output": { "dtype": "float32", "shape": [65, 33] } },
424
+ "bench": {
425
+ "metrics": [
426
+ { "type": "bandwidth", "value": "(args.rows * 2 + args.biasRows) * args.cols * dtypeBytes(args.dtype)" }
427
+ ]
428
+ },
429
+ "vars": { "dtype": "float32", "rows": 65, "cols": 33, "biasRows": 5 }
430
+ },
431
+ {
432
+ "name": "workgroup-neighbor-float32-r512-c257-bias8-inner0",
433
+ "preset": "stress",
434
+ "attrs": { "axis": 1, "is_inner_broadcast": 0 },
435
+ "inputs": {
436
+ "data": { "dtype": "float32", "shape": [512, 257], "dist": "normal", "seed": 701, "scale": 2 },
437
+ "bias": { "dtype": "float32", "shape": [8, 257], "dist": "normal", "seed": 702, "scale": 1 }
438
+ },
439
+ "outputs": { "output": { "dtype": "float32", "shape": [512, 257] } },
440
+ "bench": {
441
+ "metrics": [
442
+ { "type": "bandwidth", "value": "(args.rows * 2 + args.biasRows) * args.cols * dtypeBytes(args.dtype)" }
443
+ ]
444
+ },
445
+ "vars": { "dtype": "float32", "rows": 512, "cols": 257, "biasRows": 8 }
446
+ },
447
+ {
448
+ "name": "workgroup-neighbor-float32-r128-c1025-bias4-inner1",
449
+ "preset": "stress",
450
+ "attrs": { "axis": 1, "is_inner_broadcast": 1 },
451
+ "inputs": {
452
+ "data": { "dtype": "float32", "shape": [128, 1025], "dist": "normal", "seed": 701, "scale": 2 },
453
+ "bias": { "dtype": "float32", "shape": [4, 1025], "dist": "normal", "seed": 702, "scale": 1 }
454
+ },
455
+ "outputs": { "output": { "dtype": "float32", "shape": [128, 1025] } },
456
+ "bench": {
457
+ "metrics": [
458
+ { "type": "bandwidth", "value": "(args.rows * 2 + args.biasRows) * args.cols * dtypeBytes(args.dtype)" }
459
+ ]
460
+ },
461
+ "vars": { "dtype": "float32", "rows": 128, "cols": 1025, "biasRows": 4 }
462
+ },
463
+ {
464
+ "name": "workgroup-neighbor-float32-r64-c4097-bias2-inner0",
465
+ "preset": "stress",
466
+ "attrs": { "axis": 1, "is_inner_broadcast": 0 },
467
+ "inputs": {
468
+ "data": { "dtype": "float32", "shape": [64, 4097], "dist": "normal", "seed": 701, "scale": 2 },
469
+ "bias": { "dtype": "float32", "shape": [2, 4097], "dist": "normal", "seed": 702, "scale": 1 }
470
+ },
471
+ "outputs": { "output": { "dtype": "float32", "shape": [64, 4097] } },
472
+ "bench": {
473
+ "metrics": [
474
+ { "type": "bandwidth", "value": "(args.rows * 2 + args.biasRows) * args.cols * dtypeBytes(args.dtype)" }
475
+ ]
476
+ },
477
+ "vars": { "dtype": "float32", "rows": 64, "cols": 4097, "biasRows": 2 }
478
+ },
479
+ {
480
+ "name": "workgroup-neighbor-float16-r65-c33-bias5-inner1",
481
+ "preset": "stress",
482
+ "attrs": { "axis": 1, "is_inner_broadcast": 1 },
483
+ "inputs": {
484
+ "data": { "dtype": "float16", "shape": [65, 33], "dist": "normal", "seed": 701, "scale": 2 },
485
+ "bias": { "dtype": "float16", "shape": [5, 33], "dist": "normal", "seed": 702, "scale": 1 }
486
+ },
487
+ "outputs": { "output": { "dtype": "float16", "shape": [65, 33] } },
488
+ "bench": {
489
+ "metrics": [
490
+ { "type": "bandwidth", "value": "(args.rows * 2 + args.biasRows) * args.cols * dtypeBytes(args.dtype)" }
491
+ ]
492
+ },
493
+ "vars": { "dtype": "float16", "rows": 65, "cols": 33, "biasRows": 5 }
494
+ },
495
+ {
496
+ "name": "workgroup-neighbor-float16-r512-c257-bias8-inner0",
497
+ "preset": "stress",
498
+ "attrs": { "axis": 1, "is_inner_broadcast": 0 },
499
+ "inputs": {
500
+ "data": { "dtype": "float16", "shape": [512, 257], "dist": "normal", "seed": 701, "scale": 2 },
501
+ "bias": { "dtype": "float16", "shape": [8, 257], "dist": "normal", "seed": 702, "scale": 1 }
502
+ },
503
+ "outputs": { "output": { "dtype": "float16", "shape": [512, 257] } },
504
+ "bench": {
505
+ "metrics": [
506
+ { "type": "bandwidth", "value": "(args.rows * 2 + args.biasRows) * args.cols * dtypeBytes(args.dtype)" }
507
+ ]
508
+ },
509
+ "vars": { "dtype": "float16", "rows": 512, "cols": 257, "biasRows": 8 }
510
+ },
511
+ {
512
+ "name": "workgroup-neighbor-float16-r128-c1025-bias4-inner1",
513
+ "preset": "stress",
514
+ "attrs": { "axis": 1, "is_inner_broadcast": 1 },
515
+ "inputs": {
516
+ "data": { "dtype": "float16", "shape": [128, 1025], "dist": "normal", "seed": 701, "scale": 2 },
517
+ "bias": { "dtype": "float16", "shape": [4, 1025], "dist": "normal", "seed": 702, "scale": 1 }
518
+ },
519
+ "outputs": { "output": { "dtype": "float16", "shape": [128, 1025] } },
520
+ "bench": {
521
+ "metrics": [
522
+ { "type": "bandwidth", "value": "(args.rows * 2 + args.biasRows) * args.cols * dtypeBytes(args.dtype)" }
523
+ ]
524
+ },
525
+ "vars": { "dtype": "float16", "rows": 128, "cols": 1025, "biasRows": 4 }
526
+ },
527
+ {
528
+ "name": "workgroup-neighbor-float16-r64-c4097-bias2-inner0",
529
+ "preset": "stress",
530
+ "attrs": { "axis": 1, "is_inner_broadcast": 0 },
531
+ "inputs": {
532
+ "data": { "dtype": "float16", "shape": [64, 4097], "dist": "normal", "seed": 701, "scale": 2 },
533
+ "bias": { "dtype": "float16", "shape": [2, 4097], "dist": "normal", "seed": 702, "scale": 1 }
534
+ },
535
+ "outputs": { "output": { "dtype": "float16", "shape": [64, 4097] } },
536
+ "bench": {
537
+ "metrics": [
538
+ { "type": "bandwidth", "value": "(args.rows * 2 + args.biasRows) * args.cols * dtypeBytes(args.dtype)" }
539
+ ]
540
+ },
541
+ "vars": { "dtype": "float16", "rows": 64, "cols": 4097, "biasRows": 2 }
542
  }
543
  ]
544
  }
build/webgpu/bias-softmax-longrow-normalize.wgsl.jinja CHANGED
@@ -4,13 +4,12 @@
4
 
5
  const WG: u32 = {{ tunables.WORKGROUP_SIZE }}u;
6
  const BLOCK_COLS: u32 = {{ tunables.BLOCK_COLS }}u;
7
- fn bias_index(index: u32) -> u32 {
8
- let row = index / params.blockSize;
9
- let column = index % params.blockSize;
10
  {% if isInnerBroadcast %}
11
- return (row / {{ innerRepeat }}u) * params.blockSize + column;
12
  {% else %}
13
- return (row % {{ biasBlockCountSpec }}u) * params.blockSize + column;
14
  {% endif %}
15
  }
16
  // FLT_MAX, not -inf, as the online (m, d) accumulator init: merges must keep
@@ -32,6 +31,7 @@ fn shifted_value(value: f32, maxValue: f32) -> f32 {
32
  let equalFiniteMax = select(false, value == maxValue, is_finite_f32(maxValue));
33
  return select(value - maxValue, 0.0, equalFiniteMax);
34
  }
 
35
  fn exp_shift(value: f32, maxValue: f32) -> f32 {
36
  return exp(shifted_value(value, maxValue));
37
  }
@@ -42,6 +42,7 @@ fn main(@builtin(workgroup_id) wid: vec3<u32>,
42
  let block = wid.x;
43
  let row = wid.y;
44
  let rowBase = row * params.blockSize;
 
45
  let colStart = block * BLOCK_COLS;
46
  let colEnd = min(params.blockSize, colStart + BLOCK_COLS);
47
  let m = rowMax[row];
@@ -50,7 +51,7 @@ fn main(@builtin(workgroup_id) wid: vec3<u32>,
50
 
51
  for (var col = colStart + lid.x; col < colEnd; col += WG) {
52
  let i = rowBase + col;
53
- let v = f32(data[i]) + f32(bias[bias_index(i)]);
54
  output[i] = {{ scalar }}(exp_shift(v, m) * inv);
55
  }
56
  }
 
4
 
5
  const WG: u32 = {{ tunables.WORKGROUP_SIZE }}u;
6
  const BLOCK_COLS: u32 = {{ tunables.BLOCK_COLS }}u;
7
+ // Flattened bias-row address shared by packed, adaptive, and split rows.
8
+ fn bias_base(row: u32) -> u32 {
 
9
  {% if isInnerBroadcast %}
10
+ return (row / {{ innerRepeat }}u) * params.blockSize;
11
  {% else %}
12
+ return (row % {{ biasBlockCountSpec }}u) * params.blockSize;
13
  {% endif %}
14
  }
15
  // FLT_MAX, not -inf, as the online (m, d) accumulator init: merges must keep
 
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
  let block = wid.x;
43
  let row = wid.y;
44
  let rowBase = row * params.blockSize;
45
+ let biasBase = bias_base(row);
46
  let colStart = block * BLOCK_COLS;
47
  let colEnd = min(params.blockSize, colStart + BLOCK_COLS);
48
  let m = rowMax[row];
 
51
 
52
  for (var col = colStart + lid.x; col < colEnd; col += WG) {
53
  let i = rowBase + col;
54
+ let v = f32(data[i]) + f32(bias[biasBase + col]);
55
  output[i] = {{ scalar }}(exp_shift(v, m) * inv);
56
  }
57
  }
build/webgpu/bias-softmax.wgsl.jinja CHANGED
@@ -27,6 +27,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 +38,6 @@ fn exp_shift(value: f32, maxValue: f32) -> f32 {
37
  // Both the subgroup and portable barrier-tree engines return the same merged
38
  // pair to every invocation. Repeated merges require a workgroup barrier between
39
  // calls before their shared partial storage is reused.
40
- {% set combineSubgroups = combineSubgroups is defined and combineSubgroups %}
41
  {% if combineSubgroups %}
42
  // Cross-subgroup merge that assumes nothing about which invocations share a
43
  // subgroup or how many subgroups there are: each subgroup's elected lane
@@ -90,41 +90,9 @@ fn combine_partials(m: f32, d: f32, lidx: u32, sgSize: u32) -> vec2<f32> {
90
  return combinedMD;
91
  }
92
  {% else %}
93
- {% set mdStreamed = mdStreams is defined %}
94
- {% set mdStreams = mdStreams if mdStreams is defined else 1 %}
95
- {% set mdExtent = "WG" if mdStreams == 1 else "WG * " ~ mdStreams ~ "u" %}
96
  var<workgroup> partialM: array<f32, {{ mdExtent }}>;
97
  var<workgroup> partialD: array<f32, {{ mdExtent }}>;
98
- {% if mdStreamed %}
99
-
100
- // In-place fold of {{ mdStreams }} streams. Input partials occupy
101
- // partialM/partialD; stream s returns its merged pair in slot s * WG.
102
- fn combine_partials_streams(lidx: u32) {
103
- workgroupBarrier();
104
- var stride = WG / 2u;
105
- loop {
106
- if (stride == 0u) {
107
- break;
108
- }
109
- if (lidx < stride) {
110
- {% for s in range(mdStreams) %}
111
- {
112
- let slot = {{ s }}u * WG + lidx;
113
- let m1 = partialM[slot];
114
- let d1 = partialD[slot];
115
- let m2 = partialM[slot + stride];
116
- let d2 = partialD[slot + stride];
117
- let mNew = max(m1, m2);
118
- partialD[slot] = d1 * exp_shift(m1, mNew) + d2 * exp_shift(m2, mNew);
119
- partialM[slot] = mNew;
120
- }
121
- {% endfor %}
122
- }
123
- workgroupBarrier();
124
- stride = stride / 2u;
125
- }
126
- }
127
- {% else %}
128
 
129
  fn combine_partials(m: f32, d: f32, lidx: u32) -> vec2<f32> {
130
  partialM[lidx] = m;
@@ -154,25 +122,22 @@ fn combine_partials(m: f32, d: f32, lidx: u32) -> vec2<f32> {
154
  return merged;
155
  }
156
  {% endif %}
157
- {% endif %}
158
-
159
 
160
  {% endif %}
161
- fn bias_index(index: u32) -> u32 {
162
- let row = index / params.blockSize;
163
- let column = index % params.blockSize;
164
  {% if isInnerBroadcast %}
165
- return (row / {{ innerRepeat }}u) * params.blockSize + column;
166
  {% else %}
167
- return (row % {{ biasBlockCountSpec }}u) * params.blockSize + column;
168
  {% endif %}
169
  }
170
 
171
  @compute @workgroup_size(WG)
172
  fn main(@builtin(workgroup_id) wid: vec3<u32>,
173
- {%- if packedRows %}
174
  @builtin(num_workgroups) nwg: vec3<u32>,
175
- {%- endif %}
176
  @builtin(local_invocation_id) lid: vec3<u32>{% if combineSubgroups and not packedRows %},
177
  @builtin(subgroup_size) sgSize: u32{% endif %}) {
178
  {% if packedRows %}
@@ -180,11 +145,12 @@ fn main(@builtin(workgroup_id) wid: vec3<u32>,
180
  // one-dimensional and bounded even for very large batch counts.
181
  for (var row = wid.x * WG + lid.x; row < params.batchCount; row += nwg.x * WG) {
182
  let base = row * params.blockSize;
 
183
  {% if blockSize == 2 %}
184
  let i0 = base;
185
  let i1 = base + 1u;
186
- let v0 = f32(data[i0]) + f32(bias[bias_index(i0)]);
187
- let v1 = f32(data[i1]) + f32(bias[bias_index(i1)]);
188
 
189
  // Compute a two-element row as a logistic pair, choosing the non-positive
190
  // exponent branch for stability. Non-finite rows use the reduction below.
@@ -221,7 +187,7 @@ fn main(@builtin(workgroup_id) wid: vec3<u32>,
221
  var denom = 0.0;
222
  for (var dd = 0u; dd < params.blockSize; dd += 1u) {
223
  let i = base + dd;
224
- let v = f32(data[i]) + f32(bias[bias_index(i)]);
225
  values[dd] = v;
226
  let new_m = max(m, v);
227
  denom = denom * exp_shift(m, new_m) + exp_shift(v, new_m);
@@ -243,6 +209,7 @@ fn main(@builtin(workgroup_id) wid: vec3<u32>,
243
  return;
244
  }
245
  let base = row * params.blockSize;
 
246
  let tid = lid.x;
247
 
248
  // Single online (m, d) stats pass over (data + bias), then one cooperative
@@ -251,7 +218,7 @@ fn main(@builtin(workgroup_id) wid: vec3<u32>,
251
  var denom = 0.0;
252
  for (var dd = tid; dd < params.blockSize; dd = dd + WG) {
253
  let i = base + dd;
254
- let v = f32(data[i]) + f32(bias[bias_index(i)]);
255
  let new_m = max(m, v);
256
  denom = denom * exp_shift(m, new_m) + exp_shift(v, new_m);
257
  m = new_m;
@@ -266,7 +233,7 @@ fn main(@builtin(workgroup_id) wid: vec3<u32>,
266
 
267
  for (var dd = tid; dd < params.blockSize; dd = dd + WG) {
268
  let i = base + dd;
269
- let v = f32(data[i]) + f32(bias[bias_index(i)]);
270
  output[i] = {{ scalar }}(exp_shift(v, rowMax) * inv);
271
  }
272
  {% endif %}
 
27
  let equalFiniteMax = select(false, value == maxValue, is_finite_f32(maxValue));
28
  return select(value - maxValue, 0.0, equalFiniteMax);
29
  }
30
+
31
  fn exp_shift(value: f32, maxValue: f32) -> f32 {
32
  return exp(shifted_value(value, maxValue));
33
  }
 
38
  // Both the subgroup and portable barrier-tree engines return the same merged
39
  // pair to every invocation. Repeated merges require a workgroup barrier between
40
  // calls before their shared partial storage is reused.
 
41
  {% if combineSubgroups %}
42
  // Cross-subgroup merge that assumes nothing about which invocations share a
43
  // subgroup or how many subgroups there are: each subgroup's elected lane
 
90
  return combinedMD;
91
  }
92
  {% else %}
93
+ {% set mdExtent = "WG" %}
 
 
94
  var<workgroup> partialM: array<f32, {{ mdExtent }}>;
95
  var<workgroup> partialD: array<f32, {{ mdExtent }}>;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
  fn combine_partials(m: f32, d: f32, lidx: u32) -> vec2<f32> {
98
  partialM[lidx] = m;
 
122
  return merged;
123
  }
124
  {% endif %}
 
 
125
 
126
  {% endif %}
127
+ // Flattened bias-row address shared by packed, adaptive, and split rows.
128
+ fn bias_base(row: u32) -> u32 {
 
129
  {% if isInnerBroadcast %}
130
+ return (row / {{ innerRepeat }}u) * params.blockSize;
131
  {% else %}
132
+ return (row % {{ biasBlockCountSpec }}u) * params.blockSize;
133
  {% endif %}
134
  }
135
 
136
  @compute @workgroup_size(WG)
137
  fn main(@builtin(workgroup_id) wid: vec3<u32>,
138
+ {% if packedRows %}
139
  @builtin(num_workgroups) nwg: vec3<u32>,
140
+ {% endif %}
141
  @builtin(local_invocation_id) lid: vec3<u32>{% if combineSubgroups and not packedRows %},
142
  @builtin(subgroup_size) sgSize: u32{% endif %}) {
143
  {% if packedRows %}
 
145
  // one-dimensional and bounded even for very large batch counts.
146
  for (var row = wid.x * WG + lid.x; row < params.batchCount; row += nwg.x * WG) {
147
  let base = row * params.blockSize;
148
+ let biasBase = bias_base(row);
149
  {% if blockSize == 2 %}
150
  let i0 = base;
151
  let i1 = base + 1u;
152
+ let v0 = f32(data[i0]) + f32(bias[biasBase]);
153
+ let v1 = f32(data[i1]) + f32(bias[biasBase + 1u]);
154
 
155
  // Compute a two-element row as a logistic pair, choosing the non-positive
156
  // exponent branch for stability. Non-finite rows use the reduction below.
 
187
  var denom = 0.0;
188
  for (var dd = 0u; dd < params.blockSize; dd += 1u) {
189
  let i = base + dd;
190
+ let v = f32(data[i]) + f32(bias[biasBase + dd]);
191
  values[dd] = v;
192
  let new_m = max(m, v);
193
  denom = denom * exp_shift(m, new_m) + exp_shift(v, new_m);
 
209
  return;
210
  }
211
  let base = row * params.blockSize;
212
+ let biasBase = bias_base(row);
213
  let tid = lid.x;
214
 
215
  // Single online (m, d) stats pass over (data + bias), then one cooperative
 
218
  var denom = 0.0;
219
  for (var dd = tid; dd < params.blockSize; dd = dd + WG) {
220
  let i = base + dd;
221
+ let v = f32(data[i]) + f32(bias[biasBase + dd]);
222
  let new_m = max(m, v);
223
  denom = denom * exp_shift(m, new_m) + exp_shift(v, new_m);
224
  m = new_m;
 
233
 
234
  for (var dd = tid; dd < params.blockSize; dd = dd + WG) {
235
  let i = base + dd;
236
+ let v = f32(data[i]) + f32(bias[biasBase + dd]);
237
  output[i] = {{ scalar }}(exp_shift(v, rowMax) * inv);
238
  }
239
  {% endif %}
build/webgpu/manifest.json CHANGED
@@ -7,26 +7,149 @@
7
  "attributes": { "axis": { "default": 1 }, "is_inner_broadcast": {} },
8
  "attributeConstraints": { "is_inner_broadcast": { "required": true } },
9
  "typeConstraints": { "T": ["float32", "float16"] },
10
- "tunables": { "WORKGROUP_SIZE": { "default": 256 }, "BLOCK_COLS": { "default": 2048 } },
 
 
 
 
11
  "derive": {
12
  "axisNorm": "attrs.axis if attrs.axis >= 0 else attrs.axis + ranks.data",
13
  "batchCount": "outer(shapes.data, axisNorm)",
14
  "blockSize": "dim(shapes.data, axisNorm) * inner(shapes.data, axisNorm)",
15
  "biasBlockCount": "numel(shapes.bias) / max(1, blockSize)",
16
- "biasContract": "(numel(shapes.data) == 0 and numel(shapes.bias) == 0) or (blockSize > 0 and biasBlockCount > 0 and numel(shapes.bias) % blockSize == 0 and biasBlockCount <= batchCount and batchCount % biasBlockCount == 0)"
 
 
 
 
 
 
17
  },
18
  "when": ["numel(shapes.data) == numel(shapes.output)", "ranks.data >= 1", "attrs.axis + ranks.data >= 0", "attrs.axis < ranks.data", "biasContract", "f16Ok(dtypes.T)"],
19
  "bindings": {
20
- "data": { "buffer": "read-only-storage", "elementType": "$scalar" },
21
- "bias": { "buffer": "read-only-storage", "elementType": "$scalar" },
22
- "output": { "buffer": "storage", "elementType": "$scalar" }
23
  },
24
  "variants": [
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  {
26
  "id": "longrow_split",
27
  "priority": 40,
28
  "when": ["blockSize >= 65536", "batchCount > 0", "batchCount <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535)", "ceilDiv(blockSize, tunables.BLOCK_COLS) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535)"],
29
- "derive": { "scalar": "dtypes.T", "usesF16": "dtypes.T == \"f16\"", "combineSubgroups": false },
30
  "intermediates": [
31
  {
32
  "id": "blockMax",
@@ -45,9 +168,10 @@
45
  {
46
  "id": "block_stats",
47
  "name": "BiasSoftmax.LongRowBlockStats",
48
- "shader": "bias-softmax-longrow-stats.wgsl.jinja",
49
  "derive": {
50
  "stage": "\"block\"",
 
51
  "isInnerBroadcast": "attrs.is_inner_broadcast != 0",
52
  "biasBlockCountSpec": "max(1, biasBlockCount)",
53
  "innerRepeat": "max(1, batchCount / max(1, biasBlockCount))"
@@ -55,8 +179,8 @@
55
  "bindings": [
56
  "data",
57
  "bias",
58
- { "name": "blockMax", "buffer": "storage", "elementType": "f32" },
59
- { "name": "blockSum", "buffer": "storage", "elementType": "f32" },
60
  {
61
  "name": "params",
62
  "struct": [
@@ -70,13 +194,13 @@
70
  {
71
  "id": "row_stats",
72
  "name": "BiasSoftmax.LongRowStats",
73
- "shader": "bias-softmax-longrow-stats.wgsl.jinja",
74
  "derive": { "stage": "\"row\"" },
75
  "bindings": [
76
  { "name": "blockMax", "buffer": "read-only-storage", "elementType": "f32" },
77
  { "name": "blockSum", "buffer": "read-only-storage", "elementType": "f32" },
78
- { "name": "rowMax", "buffer": "storage", "elementType": "f32" },
79
- { "name": "rowSum", "buffer": "storage", "elementType": "f32" },
80
  {
81
  "name": "params",
82
  "struct": [{ "name": "blocks", "type": "u32", "value": "ceilDiv(blockSize, tunables.BLOCK_COLS)" }]
@@ -109,12 +233,7 @@
109
  "id": "packed_rows",
110
  "priority": 30,
111
  "when": ["blockSize > 0", "blockSize <= 8", "batchCount >= 64"],
112
- "derive": {
113
- "scalar": "dtypes.T",
114
- "usesF16": "dtypes.T == \"f16\"",
115
- "combineSubgroups": false,
116
- "packedRows": true
117
- },
118
  "passes": [
119
  {
120
  "id": "main",
@@ -149,12 +268,7 @@
149
  "id": "adaptive_row",
150
  "priority": 10,
151
  "when": ["numel(shapes.data) >= 0"],
152
- "derive": {
153
- "packedRows": false,
154
- "scalar": "dtypes.T",
155
- "usesF16": "dtypes.T == \"f16\"",
156
- "combineSubgroups": "device.features.has(\"subgroups\")"
157
- },
158
  "passes": [
159
  {
160
  "id": "main",
@@ -177,8 +291,7 @@
177
  ]
178
  }
179
  ],
180
- "dispatch": { "x": "min(batchCount, 65535)", "y": "ceilDiv(batchCount, 65535)", "z": 1 },
181
- "subgroupCollectivesWidth": "portable"
182
  }
183
  ]
184
  }
 
7
  "attributes": { "axis": { "default": 1 }, "is_inner_broadcast": {} },
8
  "attributeConstraints": { "is_inner_broadcast": { "required": true } },
9
  "typeConstraints": { "T": ["float32", "float16"] },
10
+ "tunables": {
11
+ "WORKGROUP_SIZE": { "default": 256 },
12
+ "BLOCK_COLS": { "default": 2048 },
13
+ "ONLINE_WORKGROUP_SIZE": { "default": 64 }
14
+ },
15
  "derive": {
16
  "axisNorm": "attrs.axis if attrs.axis >= 0 else attrs.axis + ranks.data",
17
  "batchCount": "outer(shapes.data, axisNorm)",
18
  "blockSize": "dim(shapes.data, axisNorm) * inner(shapes.data, axisNorm)",
19
  "biasBlockCount": "numel(shapes.bias) / max(1, blockSize)",
20
+ "biasContract": "(numel(shapes.data) == 0 and numel(shapes.bias) == 0) or (blockSize > 0 and biasBlockCount > 0 and numel(shapes.bias) % blockSize == 0 and biasBlockCount <= batchCount and batchCount % biasBlockCount == 0)",
21
+ "deviceWorkgroupCap": "min(device.limits.maxComputeInvocationsPerWorkgroup, device.limits.maxComputeWorkgroupSizeX)",
22
+ "subgroupRowsWorkgroup": "min(tunables.WORKGROUP_SIZE, deviceWorkgroupCap)",
23
+ "subgroupRowsLaneVecs": "ceilDiv(blockSize / 4, device.adapterInfo.subgroupMinSize) if (device.features.has(\"subgroups\") and has(device.adapterInfo, \"subgroupMinSize\") and device.adapterInfo.subgroupMinSize > 0) else 0",
24
+ "biasBaseWorkgroup": "min(tunables.ONLINE_WORKGROUP_SIZE, deviceWorkgroupCap)",
25
+ "biasMaxWorkgroup": "min(tunables.WORKGROUP_SIZE, deviceWorkgroupCap)",
26
+ "biasWorkgroupSize": "min(biasMaxWorkgroup, pow2ceil(ceilDiv(blockSize, 4)), max(biasBaseWorkgroup, pow2ceil(ceilDiv(biasBaseWorkgroup * biasMaxWorkgroup, max(1, batchCount)))))"
27
  },
28
  "when": ["numel(shapes.data) == numel(shapes.output)", "ranks.data >= 1", "attrs.axis + ranks.data >= 0", "attrs.axis < ranks.data", "biasContract", "f16Ok(dtypes.T)"],
29
  "bindings": {
30
+ "data": { "elementType": "$scalar" },
31
+ "bias": { "elementType": "$scalar" },
32
+ "output": { "elementType": "$scalar" }
33
  },
34
  "variants": [
35
+ {
36
+ "id": "online_subgroup_rows_vec4",
37
+ "priority": 20,
38
+ "when": ["blockSize % 4 == 0", "has(device.adapterInfo, \"subgroupMinSize\")", "device.adapterInfo.subgroupMinSize >= 16", "blockSize >= 64", "subgroupRowsLaneVecs >= 1", "subgroupRowsLaneVecs <= 8", "batchCount >= 64", "has(device.adapterInfo, \"subgroupMaxSize\")", "device.adapterInfo.subgroupMaxSize <= subgroupRowsWorkgroup", "pow2ceil(subgroupRowsWorkgroup) == subgroupRowsWorkgroup", "ceilDiv(ceilDiv(batchCount, subgroupRowsWorkgroup / device.adapterInfo.subgroupMaxSize), min(device.limits.maxComputeWorkgroupsPerDimension, 65535)) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535)"],
39
+ "requires": { "features": ["subgroups"] },
40
+ "derive": {
41
+ "scalar": "dtypes.T",
42
+ "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
43
+ "workgroupSize": "subgroupRowsWorkgroup",
44
+ "vecsPerLane": "subgroupRowsLaneVecs",
45
+ "isInnerBroadcast": "attrs.is_inner_broadcast != 0",
46
+ "biasBlockCountSpec": "max(1, biasBlockCount)",
47
+ "innerRepeat": "max(1, batchCount / max(1, biasBlockCount))"
48
+ },
49
+ "passes": [
50
+ {
51
+ "id": "main",
52
+ "name": "BiasSoftmax.SubgroupRowsVec4",
53
+ "shader": "softmax-subgroup-rows.wgsl.jinja",
54
+ "derive": { "op": "\"biassoftmax\"" },
55
+ "bindings": [
56
+ { "arg": "data", "name": "x", "elementType": "$vectorScalar" },
57
+ { "arg": "bias", "name": "bias", "elementType": "$vectorScalar" },
58
+ { "arg": "output", "name": "y", "elementType": "$vectorScalar" },
59
+ {
60
+ "name": "params",
61
+ "struct": [
62
+ { "name": "rows", "type": "u32", "value": "batchCount" },
63
+ { "name": "vecCols", "type": "u32", "value": "blockSize / 4" }
64
+ ]
65
+ }
66
+ ],
67
+ "dispatch": {
68
+ "x": "min(ceilDiv(batchCount, subgroupRowsWorkgroup / device.adapterInfo.subgroupMaxSize), 65535)",
69
+ "y": "ceilDiv(ceilDiv(batchCount, subgroupRowsWorkgroup / device.adapterInfo.subgroupMaxSize), 65535)",
70
+ "z": 1
71
+ }
72
+ }
73
+ ]
74
+ },
75
+ {
76
+ "id": "online_workgroup_vec4",
77
+ "priority": 15,
78
+ "when": ["blockSize % 4 == 0", "blockSize > 32", "blockSize < 65536", "pow2ceil(biasWorkgroupSize) == biasWorkgroupSize", "ceilDiv(batchCount, min(device.limits.maxComputeWorkgroupsPerDimension, 65535)) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535)"],
79
+ "derive": {
80
+ "scalar": "dtypes.T",
81
+ "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
82
+ "workgroupSize": "biasWorkgroupSize",
83
+ "combineSubgroups": false,
84
+ "isInnerBroadcast": "attrs.is_inner_broadcast != 0",
85
+ "biasBlockCountSpec": "max(1, biasBlockCount)",
86
+ "innerRepeat": "max(1, batchCount / max(1, biasBlockCount))",
87
+ "scalarVec4": false
88
+ },
89
+ "passes": [
90
+ {
91
+ "id": "main",
92
+ "name": "BiasSoftmax.WorkgroupVec4",
93
+ "shader": "softmax-online.wgsl.jinja",
94
+ "derive": { "op": "\"biassoftmax\"", "useVec4": true },
95
+ "bindings": [
96
+ { "arg": "data", "name": "x", "elementType": "$vectorScalar" },
97
+ { "arg": "bias", "name": "bias", "elementType": "$vectorScalar" },
98
+ { "arg": "output", "name": "y", "elementType": "$vectorScalar" },
99
+ {
100
+ "name": "params",
101
+ "struct": [
102
+ { "name": "rows", "type": "u32", "value": "batchCount" },
103
+ { "name": "vecCols", "type": "u32", "value": "blockSize / 4" }
104
+ ]
105
+ }
106
+ ],
107
+ "dispatch": { "x": "min(batchCount, 65535)", "y": "ceilDiv(batchCount, 65535)", "z": 1 }
108
+ }
109
+ ]
110
+ },
111
+ {
112
+ "id": "online_workgroup_scalar_vec4",
113
+ "priority": 15,
114
+ "when": ["blockSize % 4 != 0", "blockSize > 32", "blockSize < 65536", "pow2ceil(biasWorkgroupSize) == biasWorkgroupSize", "ceilDiv(batchCount, min(device.limits.maxComputeWorkgroupsPerDimension, 65535)) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535)"],
115
+ "derive": {
116
+ "scalar": "dtypes.T",
117
+ "vectorScalar": "\"vec4<\" ~ dtypes.T ~ \">\"",
118
+ "workgroupSize": "biasWorkgroupSize",
119
+ "combineSubgroups": false,
120
+ "isInnerBroadcast": "attrs.is_inner_broadcast != 0",
121
+ "biasBlockCountSpec": "max(1, biasBlockCount)",
122
+ "innerRepeat": "max(1, batchCount / max(1, biasBlockCount))",
123
+ "scalarVec4": true
124
+ },
125
+ "passes": [
126
+ {
127
+ "id": "main",
128
+ "name": "BiasSoftmax.WorkgroupScalarVec4",
129
+ "shader": "softmax-online.wgsl.jinja",
130
+ "derive": { "op": "\"biassoftmax\"", "useVec4": true },
131
+ "bindings": [
132
+ { "arg": "data", "name": "x", "elementType": "$scalar" },
133
+ { "arg": "bias", "name": "bias", "elementType": "$scalar" },
134
+ { "arg": "output", "name": "y", "elementType": "$scalar" },
135
+ {
136
+ "name": "params",
137
+ "struct": [
138
+ { "name": "rows", "type": "u32", "value": "batchCount" },
139
+ { "name": "vecCols", "type": "u32", "value": "ceilDiv(blockSize, 4)" },
140
+ { "name": "cols", "type": "u32", "value": "blockSize" }
141
+ ]
142
+ }
143
+ ],
144
+ "dispatch": { "x": "min(batchCount, 65535)", "y": "ceilDiv(batchCount, 65535)", "z": 1 }
145
+ }
146
+ ]
147
+ },
148
  {
149
  "id": "longrow_split",
150
  "priority": 40,
151
  "when": ["blockSize >= 65536", "batchCount > 0", "batchCount <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535)", "ceilDiv(blockSize, tunables.BLOCK_COLS) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535)"],
152
+ "derive": { "scalar": "dtypes.T", "combineSubgroups": false },
153
  "intermediates": [
154
  {
155
  "id": "blockMax",
 
168
  {
169
  "id": "block_stats",
170
  "name": "BiasSoftmax.LongRowBlockStats",
171
+ "shader": "softmax-longrow-stats.wgsl.jinja",
172
  "derive": {
173
  "stage": "\"block\"",
174
+ "biasRow": true,
175
  "isInnerBroadcast": "attrs.is_inner_broadcast != 0",
176
  "biasBlockCountSpec": "max(1, biasBlockCount)",
177
  "innerRepeat": "max(1, batchCount / max(1, biasBlockCount))"
 
179
  "bindings": [
180
  "data",
181
  "bias",
182
+ { "name": "blockMax", "elementType": "f32" },
183
+ { "name": "blockSum", "elementType": "f32" },
184
  {
185
  "name": "params",
186
  "struct": [
 
194
  {
195
  "id": "row_stats",
196
  "name": "BiasSoftmax.LongRowStats",
197
+ "shader": "softmax-longrow-stats.wgsl.jinja",
198
  "derive": { "stage": "\"row\"" },
199
  "bindings": [
200
  { "name": "blockMax", "buffer": "read-only-storage", "elementType": "f32" },
201
  { "name": "blockSum", "buffer": "read-only-storage", "elementType": "f32" },
202
+ { "name": "rowMax", "elementType": "f32" },
203
+ { "name": "rowSum", "elementType": "f32" },
204
  {
205
  "name": "params",
206
  "struct": [{ "name": "blocks", "type": "u32", "value": "ceilDiv(blockSize, tunables.BLOCK_COLS)" }]
 
233
  "id": "packed_rows",
234
  "priority": 30,
235
  "when": ["blockSize > 0", "blockSize <= 8", "batchCount >= 64"],
236
+ "derive": { "scalar": "dtypes.T", "combineSubgroups": false, "packedRows": true },
 
 
 
 
 
237
  "passes": [
238
  {
239
  "id": "main",
 
268
  "id": "adaptive_row",
269
  "priority": 10,
270
  "when": ["numel(shapes.data) >= 0"],
271
+ "derive": { "packedRows": false, "scalar": "dtypes.T", "combineSubgroups": "device.features.has(\"subgroups\")" },
 
 
 
 
 
272
  "passes": [
273
  {
274
  "id": "main",
 
291
  ]
292
  }
293
  ],
294
+ "dispatch": { "x": "min(batchCount, 65535)", "y": "ceilDiv(batchCount, 65535)", "z": 1 }
 
295
  }
296
  ]
297
  }
build/webgpu/metadata.json CHANGED
@@ -1,25 +1,30 @@
1
  {
2
  "name": "com.microsoft.BiasSoftmax",
3
- "id": "_com_microsoft_biassoftmax_webgpu_cbcd64a",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
- "bench.json": "rgHjgRMHX0A/pGpGh4i7pvOQ59E3cUmsKVhIyuZBnvY=",
11
- "bias-softmax-longrow-normalize.wgsl.jinja": "lX2Z/FpHVrFdQTOlt+JKYwA7eZAokjLCgluP7kssTkc=",
12
- "bias-softmax-longrow-stats.wgsl.jinja": "SXOFecNckSGTdUceUizrqmBaFAeByEkUA0bei53gRw0=",
13
- "bias-softmax.wgsl.jinja": "CR9eMAgk196zS8W9yMWQTLlqDcvrrzVdvHMe9NjPnO0=",
14
- "manifest.json": "Byu1oYp1SQbvVzBg59JbyQIwhIJTedDlg9UrOMbePEU=",
15
- "test.json": "B+sdyYMUEL3T1qIYkK3plwl3nTChHF7KfEa6LDITSdE="
 
 
16
  }
17
  },
18
- "provenance": { "kernel": { "sha": "91d990483a174128daf7673f3f37a7c890493ae1", "dirty": false } },
19
  "webgpu": {
20
- "manifestSpec": "2.0",
21
  "variants": {
22
- "longrow_split": ["bias-softmax-longrow-normalize.wgsl.jinja", "bias-softmax-longrow-stats.wgsl.jinja"],
 
 
 
23
  "packed_rows": ["bias-softmax.wgsl.jinja"],
24
  "adaptive_row": ["bias-softmax.wgsl.jinja"]
25
  }
 
1
  {
2
  "name": "com.microsoft.BiasSoftmax",
3
+ "id": "_com_microsoft_biassoftmax_webgpu_645de3f",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
+ "bench.json": "ULVbXINWou5wlT+4e2N9gtCE7nF8AjENyJAZwBBGwP4=",
11
+ "bias-softmax-longrow-normalize.wgsl.jinja": "C4RjAERxuPnTg7GKB286RYMPx5HWXFd9LqKrUcAdtYs=",
12
+ "bias-softmax.wgsl.jinja": "ptSv4JQ/WGXTqUKNqFowI6g64po1WtcXEP0sjM2LHEk=",
13
+ "manifest.json": "P0QfnLqsi08AxrTCYJkPTTQ0nFFh24vGS/7rwAKoOMw=",
14
+ "softmax-longrow-stats.wgsl.jinja": "oj7lwGMXQNTAI9NWK3/PgXwLT2R3vtTEmh6HUw4BtLE=",
15
+ "softmax-online.wgsl.jinja": "N/W8ChLz0EbG2y9O85tk1rmZwc6CpvlQb5lwxgfvWks=",
16
+ "softmax-subgroup-rows.wgsl.jinja": "S9jb9PzuCJjMBpOcVe2hWmSupF9Nia9AA5OyaIlTwSk=",
17
+ "test.json": "Do/oRO/PgM8zp16pyq7EVawVJu1VThzud/P5lPfX+I8="
18
  }
19
  },
20
+ "provenance": { "kernel": { "sha": "6fdf6301e2bbcc2f03bf1eaf493b7ad55ef33afc", "dirty": false } },
21
  "webgpu": {
22
+ "manifestSpec": "2.1",
23
  "variants": {
24
+ "online_subgroup_rows_vec4": ["softmax-subgroup-rows.wgsl.jinja"],
25
+ "online_workgroup_vec4": ["softmax-online.wgsl.jinja"],
26
+ "online_workgroup_scalar_vec4": ["softmax-online.wgsl.jinja"],
27
+ "longrow_split": ["bias-softmax-longrow-normalize.wgsl.jinja", "softmax-longrow-stats.wgsl.jinja"],
28
  "packed_rows": ["bias-softmax.wgsl.jinja"],
29
  "adaptive_row": ["bias-softmax.wgsl.jinja"]
30
  }
build/webgpu/{bias-softmax-longrow-stats.wgsl.jinja → softmax-longrow-stats.wgsl.jinja} RENAMED
@@ -1,24 +1,29 @@
1
- // Split statistics for very long contiguous BiasSoftmax rows. The block stage
2
- // computes independent online (maximum, sum-exp) pairs over BLOCK_COLS. The row
3
- // stage deterministically folds them into one result per row.
4
- {% if stage == "block" and usesF16 %}
5
- enable f16;
6
- {% endif %}
 
 
 
 
7
  {{ env.wgsl.resourceDeclarations }}
8
 
9
  const WG: u32 = {{ tunables.WORKGROUP_SIZE }}u;
10
  {% if stage == "block" %}
11
  const BLOCK_COLS: u32 = {{ tunables.BLOCK_COLS }}u;
12
- fn bias_index(index: u32) -> u32 {
13
- let row = index / params.blockSize;
14
- let column = index % params.blockSize;
15
- {% if isInnerBroadcast | default(false) %}
16
- return (row / {{ innerRepeat | default(1) }}u) * params.blockSize + column;
17
  {% else %}
18
- return (row % {{ biasBlockCountSpec | default(1) }}u) * params.blockSize + column;
19
  {% endif %}
20
  }
21
  {% endif %}
 
22
  // FLT_MAX, not -inf, as the online (m, d) accumulator init: merges must keep
23
  // `m - m` finite so an empty lane / all--inf row contributes the exact
24
  // accumulator identity (m, d) = (-FLT_MAX, 0). Operator epilogues interpret
@@ -38,6 +43,7 @@ fn shifted_value(value: f32, maxValue: f32) -> f32 {
38
  let equalFiniteMax = select(false, value == maxValue, is_finite_f32(maxValue));
39
  return select(value - maxValue, 0.0, equalFiniteMax);
40
  }
 
41
  fn exp_shift(value: f32, maxValue: f32) -> f32 {
42
  return exp(shifted_value(value, maxValue));
43
  }
@@ -47,94 +53,9 @@ fn exp_shift(value: f32, maxValue: f32) -> f32 {
47
  // Both the subgroup and portable barrier-tree engines return the same merged
48
  // pair to every invocation. Repeated merges require a workgroup barrier between
49
  // calls before their shared partial storage is reused.
50
- {% set combineSubgroups = combineSubgroups is defined and combineSubgroups %}
51
- {% if combineSubgroups %}
52
- // Cross-subgroup merge that assumes nothing about which invocations share a
53
- // subgroup or how many subgroups there are: each subgroup's elected lane
54
- // publishes the subgroup pair in the slot at its OWN invocation index and sets
55
- // that index's bit in a workgroup bitmask; thread 0 then folds exactly the
56
- // published slots, in ascending index order (the online (m, d) merge is not
57
- // float-associative, so the order is fixed), and clears the mask for the next
58
- // call as it reads it. Workgroup memory starts zeroed, so the mask needs no
59
- // setup. Same three collectives as a single-subgroup reduce, two barriers.
60
- var<workgroup> partialM: array<f32, WG>;
61
- var<workgroup> partialD: array<f32, WG>;
62
- var<workgroup> leaderMask: array<atomic<u32>, (WG + 31u) / 32u>;
63
- var<workgroup> combinedMD: vec2<f32>;
64
-
65
- // When the whole workgroup is one subgroup the subgroup reduce already covers
66
- // it (no barriers, no shared state). `subgroup_size` is the size of the current
67
- // subgroup and uniform, so the test is exact and may guard the barriers below.
68
- fn combine_partials(m: f32, d: f32, lidx: u32, sgSize: u32) -> vec2<f32> {
69
- let sgM = subgroupMax(m);
70
- // A lane with no elements contributes d == 0 (exact identity). A +inf
71
- // element made exp(inf - inf) = NaN stick in that lane's d; a NaN element
72
- // landed in d via exp(NaN); both survive the merge and are detected by the
73
- // code after the reduction.
74
- let sgD = subgroupAdd(d * exp_shift(m, sgM));
75
- if (sgSize == WG) {
76
- return vec2<f32>(sgM, sgD);
77
- }
78
- if (subgroupElect()) {
79
- partialM[lidx] = sgM;
80
- partialD[lidx] = sgD;
81
- atomicOr(&leaderMask[lidx / 32u], 1u << (lidx % 32u));
82
- }
83
- workgroupBarrier();
84
- if (lidx == 0u) {
85
- var accM = -FLT_MAX;
86
- var accD = 0.0;
87
- for (var w = 0u; w < (WG + 31u) / 32u; w = w + 1u) {
88
- var bits = atomicExchange(&leaderMask[w], 0u);
89
- while (bits != 0u) {
90
- let slot = w * 32u + firstTrailingBit(bits);
91
- bits = bits & (bits - 1u);
92
- let mNew = max(accM, partialM[slot]);
93
- accD = accD * exp_shift(accM, mNew) + partialD[slot] * exp_shift(partialM[slot], mNew);
94
- accM = mNew;
95
- }
96
- }
97
- combinedMD = vec2<f32>(accM, accD);
98
- }
99
- workgroupBarrier();
100
- return combinedMD;
101
- }
102
- {% else %}
103
- {% set mdStreamed = mdStreams is defined %}
104
- {% set mdStreams = mdStreams if mdStreams is defined else 1 %}
105
- {% set mdExtent = "WG" if mdStreams == 1 else "WG * " ~ mdStreams ~ "u" %}
106
  var<workgroup> partialM: array<f32, {{ mdExtent }}>;
107
  var<workgroup> partialD: array<f32, {{ mdExtent }}>;
108
- {% if mdStreamed %}
109
-
110
- // In-place fold of {{ mdStreams }} streams. Input partials occupy
111
- // partialM/partialD; stream s returns its merged pair in slot s * WG.
112
- fn combine_partials_streams(lidx: u32) {
113
- workgroupBarrier();
114
- var stride = WG / 2u;
115
- loop {
116
- if (stride == 0u) {
117
- break;
118
- }
119
- if (lidx < stride) {
120
- {% for s in range(mdStreams) %}
121
- {
122
- let slot = {{ s }}u * WG + lidx;
123
- let m1 = partialM[slot];
124
- let d1 = partialD[slot];
125
- let m2 = partialM[slot + stride];
126
- let d2 = partialD[slot + stride];
127
- let mNew = max(m1, m2);
128
- partialD[slot] = d1 * exp_shift(m1, mNew) + d2 * exp_shift(m2, mNew);
129
- partialM[slot] = mNew;
130
- }
131
- {% endfor %}
132
- }
133
- workgroupBarrier();
134
- stride = stride / 2u;
135
- }
136
- }
137
- {% else %}
138
 
139
  fn combine_partials(m: f32, d: f32, lidx: u32) -> vec2<f32> {
140
  partialM[lidx] = m;
@@ -163,36 +84,35 @@ fn combine_partials(m: f32, d: f32, lidx: u32) -> vec2<f32> {
163
  workgroupBarrier();
164
  return merged;
165
  }
166
- {% endif %}
167
- {% endif %}
168
-
169
 
170
  @compute @workgroup_size(WG)
171
- fn main(@builtin(workgroup_id) wid: vec3<u32>,
172
  @builtin(local_invocation_id) lid: vec3<u32>) {
173
  {% if stage == "block" %}
174
- let block = wid.x;
175
- let row = wid.y;
176
- let rowBase = row * params.blockSize;
 
 
 
177
  let colStart = block * BLOCK_COLS;
178
- let colEnd = min(params.blockSize, colStart + BLOCK_COLS);
179
 
180
  var m = -FLT_MAX;
181
  var d = 0.0;
182
- for (var col = colStart + lid.x; col < colEnd; col += WG) {
183
- let i = rowBase + col;
184
- let v = f32(data[i]) + f32(bias[bias_index(i)]);
185
  let mNew = max(m, v);
186
  d = d * exp_shift(m, mNew) + exp_shift(v, mNew);
187
  m = mNew;
188
  }
189
  {% else %}
190
- let row = wid.x;
191
  let base = row * params.blocks;
192
 
193
  var m = -FLT_MAX;
194
  var d = 0.0;
195
- for (var block = lid.x; block < params.blocks; block += WG) {
196
  let bm = blockMax[base + block];
197
  let bd = blockSum[base + block];
198
  let mNew = max(m, bm);
 
1
+ // Online-softmax (maximum, sum-exp) statistics for very long contiguous
2
+ // last-axis rows. The statistics are the same for softmax and log-softmax.
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 = biasRow | default(false) %}
7
+ {% set isInnerBroadcast = isInnerBroadcast if isInnerBroadcast is defined else false %}
8
+ {% set innerRepeat = innerRepeat if innerRepeat is defined else 1 %}
9
+ {% set biasBlockCountSpec = biasBlockCountSpec if biasBlockCountSpec is defined else 1 %}
10
+ {% set cols = "params.blockSize" if biasRow else "params.cols" %}
11
  {{ env.wgsl.resourceDeclarations }}
12
 
13
  const WG: u32 = {{ tunables.WORKGROUP_SIZE }}u;
14
  {% if stage == "block" %}
15
  const BLOCK_COLS: u32 = {{ tunables.BLOCK_COLS }}u;
16
+ {% if biasRow %}
17
+ // Flattened bias-row address shared by packed, adaptive, and split rows.
18
+ fn bias_base(row: u32) -> u32 {
19
+ {% if isInnerBroadcast %}
20
+ return (row / {{ innerRepeat }}u) * params.blockSize;
21
  {% else %}
22
+ return (row % {{ biasBlockCountSpec }}u) * params.blockSize;
23
  {% endif %}
24
  }
25
  {% endif %}
26
+ {% endif %}
27
  // FLT_MAX, not -inf, as the online (m, d) accumulator init: merges must keep
28
  // `m - m` finite so an empty lane / all--inf row contributes the exact
29
  // accumulator identity (m, d) = (-FLT_MAX, 0). Operator epilogues interpret
 
43
  let equalFiniteMax = select(false, value == maxValue, is_finite_f32(maxValue));
44
  return select(value - maxValue, 0.0, equalFiniteMax);
45
  }
46
+
47
  fn exp_shift(value: f32, maxValue: f32) -> f32 {
48
  return exp(shifted_value(value, maxValue));
49
  }
 
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
+ {% set mdExtent = "WG" %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  var<workgroup> partialM: array<f32, {{ mdExtent }}>;
58
  var<workgroup> partialD: array<f32, {{ mdExtent }}>;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
  fn combine_partials(m: f32, d: f32, lidx: u32) -> vec2<f32> {
61
  partialM[lidx] = m;
 
84
  workgroupBarrier();
85
  return merged;
86
  }
 
 
 
87
 
88
  @compute @workgroup_size(WG)
89
+ fn main(@builtin(workgroup_id) wg: vec3<u32>,
90
  @builtin(local_invocation_id) lid: vec3<u32>) {
91
  {% if stage == "block" %}
92
+ let block = wg.x;
93
+ let row = wg.y;
94
+ let rowBase = row * {{ cols }};
95
+ {% if biasRow %}
96
+ let biasBase = bias_base(row);
97
+ {% endif %}
98
  let colStart = block * BLOCK_COLS;
99
+ let colEnd = min({{ cols }}, colStart + BLOCK_COLS);
100
 
101
  var m = -FLT_MAX;
102
  var d = 0.0;
103
+ for (var col = colStart + lid.x; col < colEnd; col = col + WG) {
104
+ let v = {{ "f32(data[rowBase + col]) + f32(bias[biasBase + col])" if biasRow else "f32(x[rowBase + col])" }};
 
105
  let mNew = max(m, v);
106
  d = d * exp_shift(m, mNew) + exp_shift(v, mNew);
107
  m = mNew;
108
  }
109
  {% else %}
110
+ let row = wg.x;
111
  let base = row * params.blocks;
112
 
113
  var m = -FLT_MAX;
114
  var d = 0.0;
115
+ for (var block = lid.x; block < params.blocks; block = block + WG) {
116
  let bm = blockMax[base + block];
117
  let bd = blockSum[base + block];
118
  let mNew = max(m, bm);
build/webgpu/softmax-online.wgsl.jinja ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ env.wgsl.resourceDeclarations }}
2
+
3
+ // Workgroup-level online normalization over one contiguous last-axis row per
4
+ // workgroup. Each thread keeps an online (maximum, denominator) pair over its
5
+ // strided chunk. The workgroup merges those pairs, then a fused second sweep
6
+ // writes the normalized result. Vectorized rows use native vec4 storage when
7
+ // aligned, or guarded scalar storage at unaligned row strides. Both paths retain
8
+ // f32 bias and reduction math.
9
+ //
10
+ // `workgroupSize` fixes the merge-tree width for this specialization.
11
+ // `scalarVec4` selects scalar storage for fused bias rows only.
12
+ {% macro read_vec4(c) %}
13
+ {% if scalarVec4 %}load_word(rowBase, biasBase, {{ c }}){% else %}vec4<f32>(x[rowBase + {{ c }}]) + vec4<f32>(bias[biasBase + {{ c }}]){% endif %}
14
+ {% endmacro %}
15
+ {% macro store_vec4(value, c) %}
16
+ {% if scalarVec4 %}
17
+ {% for i in range(4) %}
18
+ if ({{ c }} * 4u + {{ i }}u < params.cols) {
19
+ y[rowBase + {{ c }} * 4u + {{ i }}u] = {{ scalar }}({{ value }}[{{ i }}]);
20
+ }
21
+ {% endfor %}
22
+ {% else %}
23
+ y[rowBase + {{ c }}] = vec4<{{ scalar }}>({{ value }});
24
+ {% endif %}
25
+ {% endmacro %}
26
+ const WG: u32 = {{ workgroupSize }}u;
27
+ // FLT_MAX, not -inf, as the online (m, d) accumulator init: merges must keep
28
+ // `m - m` finite so an empty lane / all--inf row contributes the exact
29
+ // accumulator identity (m, d) = (-FLT_MAX, 0). Operator epilogues interpret
30
+ // a zero final denominator according to their public semantics. Using -inf
31
+ // here changes +inf-row behavior.
32
+ const FLT_MAX: f32 = 3.4028234663852886e38;
33
+
34
+ fn is_finite_f32(value: f32) -> bool {
35
+ return select(false, value <= FLT_MAX, value >= -FLT_MAX);
36
+ }
37
+
38
+ // x - m that is exactly 0 when x equals a finite m, so exp(shifted) == 1
39
+ // exactly at the row max. `x - x` on an infinite max is a legal fast-math
40
+ // fold to 0, which would silently turn +inf rows finite — the explicit
41
+ // equality test keeps the NaN propagation of the serial kernels.
42
+ fn shifted_value(value: f32, maxValue: f32) -> f32 {
43
+ let equalFiniteMax = select(false, value == maxValue, is_finite_f32(maxValue));
44
+ return select(value - maxValue, 0.0, equalFiniteMax);
45
+ }
46
+
47
+ fn exp_shift(value: f32, maxValue: f32) -> f32 {
48
+ return exp(shifted_value(value, maxValue));
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
+ {% set mdExtent = "WG" %}
57
+ var<workgroup> partialM: array<f32, {{ mdExtent }}>;
58
+ var<workgroup> partialD: array<f32, {{ mdExtent }}>;
59
+
60
+ fn combine_partials(m: f32, d: f32, lidx: u32) -> vec2<f32> {
61
+ partialM[lidx] = m;
62
+ partialD[lidx] = d;
63
+ workgroupBarrier();
64
+ var stride = WG / 2u;
65
+ loop {
66
+ if (stride == 0u) {
67
+ break;
68
+ }
69
+ if (lidx < stride) {
70
+ let m1 = partialM[lidx];
71
+ let d1 = partialD[lidx];
72
+ let m2 = partialM[lidx + stride];
73
+ let d2 = partialD[lidx + stride];
74
+ let mNew = max(m1, m2);
75
+ partialD[lidx] = d1 * exp_shift(m1, mNew) + d2 * exp_shift(m2, mNew);
76
+ partialM[lidx] = mNew;
77
+ }
78
+ workgroupBarrier();
79
+ stride = stride / 2u;
80
+ }
81
+ let merged = vec2<f32>(partialM[0], partialD[0]);
82
+ // Trailing barrier so back-to-back calls cannot race a next call's partial
83
+ // stores against this call's reads of slot 0.
84
+ workgroupBarrier();
85
+ return merged;
86
+ }
87
+ {% if useVec4 and scalarVec4 %}
88
+ fn load_word(rowBase: u32, biasBase: u32, c: u32) -> vec4<f32> {
89
+ let col = c * 4u;
90
+ if (col + 3u < params.cols) {
91
+ return vec4<f32>({% for i in range(4) %}f32(x[rowBase + col + {{ i }}u]){% if not loop.last %}, {% endif %}{% endfor %}) + vec4<f32>({% for i in range(4) %}f32(bias[biasBase + col + {{ i }}u]){% if not loop.last %}, {% endif %}{% endfor %});
92
+ }
93
+ var v = vec4<f32>(-FLT_MAX);
94
+ {% for i in range(4) %}
95
+ if (col + {{ i }}u < params.cols) {
96
+ v[{{ i }}] = f32(x[rowBase + col + {{ i }}u]) + f32(bias[biasBase + col + {{ i }}u]);
97
+ }
98
+ {% endfor %}
99
+ return v;
100
+ }
101
+ {% endif %}
102
+
103
+ @compute @workgroup_size({{ workgroupSize }})
104
+ fn main(@builtin(workgroup_id) wid: vec3<u32>,
105
+ @builtin(local_invocation_id) lid: vec3<u32>) {
106
+ // 2D-folded row index: wid.y carries the high bits past the
107
+ // per-axis dispatch fold width (rows > the per-axis dispatch fold width).
108
+ let row = wid.x + wid.y * {{ DISPATCH_FOLD_WIDTH }}u;
109
+ if (row >= params.rows) {
110
+ return;
111
+ }
112
+ let rowBase = row * params.{% if scalarVec4 %}cols{% else %}vecCols{% endif %};
113
+ {% if isInnerBroadcast %}
114
+ let biasBase = (row / {{ innerRepeat }}u) * params.{% if scalarVec4 %}cols{% else %}vecCols{% endif %};
115
+ {% else %}
116
+ let biasBase = (row % {{ biasBlockCountSpec }}u) * params.{% if scalarVec4 %}cols{% else %}vecCols{% endif %};
117
+ {% endif %}
118
+
119
+ // Thread-local online (m, d) over a strided chunk; f32 accumulation.
120
+ var m = -FLT_MAX;
121
+ var d = 0.0;
122
+ for (var c = lid.x; c < params.vecCols; c = c + WG) {
123
+ let v = {{ read_vec4("c") }};
124
+ let mNew = max(m, max(max(v.x, v.y), max(v.z, v.w)));
125
+ {% if scalarVec4 %}
126
+ // Padding is an identity even when valid values equal -FLT_MAX.
127
+ // Its exponential must contribute zero, not exp(0).
128
+ let valid = vec4<u32>(c * 4u) + vec4<u32>(0u, 1u, 2u, 3u) < vec4<u32>(params.cols);
129
+ let terms = vec4<f32>(exp_shift(v.x, mNew), exp_shift(v.y, mNew), exp_shift(v.z, mNew), exp_shift(v.w, mNew));
130
+ let masked = select(vec4<f32>(0.0), terms, valid);
131
+ d = d * exp_shift(m, mNew) + masked.x + masked.y + masked.z + masked.w;
132
+ {% else %}
133
+ d = d * exp_shift(m, mNew)
134
+ + exp_shift(v.x, mNew) + exp_shift(v.y, mNew) + exp_shift(v.z, mNew) + exp_shift(v.w, mNew);
135
+ {% endif %}
136
+ m = mNew;
137
+ }
138
+
139
+ let rowMD = combine_partials(m, d, lid.x);
140
+ let finalM = rowMD.x;
141
+ let finalD = rowMD.y;
142
+
143
+ let inv = select(0.0, 1.0 / finalD, finalD > 0.0);
144
+ for (var c = lid.x; c < params.vecCols; c += WG) {
145
+ let v = {{ read_vec4("c") }};
146
+ let o = vec4<f32>(exp_shift(v.x, finalM), exp_shift(v.y, finalM), exp_shift(v.z, finalM), exp_shift(v.w, finalM)) * inv;
147
+ {{ store_vec4("o", "c") }}
148
+ }
149
+ }
build/webgpu/softmax-subgroup-rows.wgsl.jinja ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ enable subgroups;
2
+ {{ env.wgsl.resourceDeclarations }}
3
+
4
+ // Subgroup-per-row online normalization over a contiguous last axis of
5
+ // moderate width. One SUBGROUP owns each row and a workgroup carries as many
6
+ // rows as it has subgroups, so the (max, denom) merge is one pair of subgroup
7
+ // collectives: no workgroup memory, no barriers. Each lane keeps its vec4 words
8
+ // in registers between the statistics sweep and the normalizing store, so the
9
+ // row is read from memory once. Each lane forms an online (maximum,
10
+ // denominator) pair, and subgroup collectives merge those pairs.
11
+ const WG: u32 = {{ workgroupSize }}u;
12
+ // FLT_MAX, not -inf, as the online (m, d) accumulator init: merges must keep
13
+ // `m - m` finite so an empty lane / all--inf row contributes the exact
14
+ // accumulator identity (m, d) = (-FLT_MAX, 0). Operator epilogues interpret
15
+ // a zero final denominator according to their public semantics. Using -inf
16
+ // here changes +inf-row behavior.
17
+ const FLT_MAX: f32 = 3.4028234663852886e38;
18
+
19
+ fn is_finite_f32(value: f32) -> bool {
20
+ return select(false, value <= FLT_MAX, value >= -FLT_MAX);
21
+ }
22
+
23
+ // x - m that is exactly 0 when x equals a finite m, so exp(shifted) == 1
24
+ // exactly at the row max. `x - x` on an infinite max is a legal fast-math
25
+ // fold to 0, which would silently turn +inf rows finite — the explicit
26
+ // equality test keeps the NaN propagation of the serial kernels.
27
+ 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
+
32
+ fn exp_shift(value: f32, maxValue: f32) -> f32 {
33
+ return exp(shifted_value(value, maxValue));
34
+ }
35
+ var<workgroup> sgLaneClaims: atomic<u32>;
36
+
37
+ struct SubgroupLogicalLanes {
38
+ ltid: u32,
39
+ ord: u32,
40
+ rank: u32,
41
+ count: u32,
42
+ }
43
+
44
+ fn subgroup_logical_lanes(rank: u32, count: u32) -> SubgroupLogicalLanes {
45
+ // Every lane performs the atomic so no collective follows a lane guard: a
46
+ // subgroup op after a closed `if (rank == 0u)` block is a reconvergence
47
+ // hazard, since every subgroup operation must be reached in uniform control
48
+ // flow. Only the lane-0 lane adds its subgroup's claim, every other lane
49
+ // adds 0 and discards its snapshot. Lane 0 is the lowest lane, which is the
50
+ // lane `subgroupBroadcastFirst` reads.
51
+ let ticket = atomicAdd(&sgLaneClaims, select(0u, count | (1u << 16u), rank == 0u));
52
+ let claim = subgroupBroadcastFirst(ticket);
53
+ return SubgroupLogicalLanes((claim & 0xffffu) + rank, claim >> 16u, rank, count);
54
+ }
55
+
56
+ @compute @workgroup_size({{ workgroupSize }})
57
+ fn main(@builtin(workgroup_id) wid: vec3<u32>,
58
+ @builtin(subgroup_invocation_id) sgLaneId: u32,
59
+ @builtin(subgroup_size) sgSize: u32) {
60
+ // Rows tile the folded workgroup grid; the lanes of one subgroup share a row.
61
+ // Which subgroup owns which row, and which lane holds which columns, come from
62
+ // the logical lane coordinates, so no assumption is made about which
63
+ // invocations share a subgroup.
64
+ let L = subgroup_logical_lanes(sgLaneId, sgSize);
65
+ let sgLane = L.rank;
66
+ let rowsPerWorkgroup = WG / sgSize;
67
+ let row = (wid.x + wid.y * {{ DISPATCH_FOLD_WIDTH }}u) * rowsPerWorkgroup + L.ord;
68
+ // A subgroup past the last row runs the collectives on an empty row and
69
+ // stores nothing, so every collective stays in uniform control flow.
70
+ let rowValid = row < params.rows;
71
+ let vecLimit = select(0u, params.vecCols, rowValid);
72
+ let rowBase = row * params.vecCols;
73
+ {% if isInnerBroadcast %}
74
+ let biasBase = (row / {{ innerRepeat }}u) * params.vecCols;
75
+ {% else %}
76
+ let biasBase = (row % {{ biasBlockCountSpec }}u) * params.vecCols;
77
+ {% endif %}
78
+
79
+ // Lane-local online (m, d) over the lane's strided words; f32 accumulation.
80
+ var m = -FLT_MAX;
81
+ var d = 0.0;
82
+ {% for i in range(vecsPerLane) %}
83
+ var v{{ i }} = vec4<f32>(0.0);
84
+ let c{{ i }} = sgLane + {{ i }}u * sgSize;
85
+ if (c{{ i }} < vecLimit) {
86
+ v{{ i }} = vec4<f32>(x[rowBase + c{{ i }}]) + vec4<f32>(bias[biasBase + c{{ i }}]);
87
+ let mNew{{ i }} = max(m, max(max(v{{ i }}.x, v{{ i }}.y), max(v{{ i }}.z, v{{ i }}.w)));
88
+ d = d * exp_shift(m, mNew{{ i }})
89
+ + exp_shift(v{{ i }}.x, mNew{{ i }}) + exp_shift(v{{ i }}.y, mNew{{ i }})
90
+ + exp_shift(v{{ i }}.z, mNew{{ i }}) + exp_shift(v{{ i }}.w, mNew{{ i }});
91
+ m = mNew{{ i }};
92
+ }
93
+ {% endfor %}
94
+
95
+ // Subgroup merge: a lane with no words contributes d == 0 (exact identity).
96
+ let finalM = subgroupMax(m);
97
+ let finalD = subgroupAdd(d * exp_shift(m, finalM));
98
+
99
+ // BiasSoftmax treats an entirely additive-masked row as zero.
100
+ let inv = select(0.0, 1.0 / finalD, finalD > 0.0);
101
+ {% for i in range(vecsPerLane) %}
102
+ if (c{{ i }} < vecLimit) {
103
+ let o{{ i }} = vec4<f32>(
104
+ exp_shift(v{{ i }}.x, finalM), exp_shift(v{{ i }}.y, finalM),
105
+ exp_shift(v{{ i }}.z, finalM), exp_shift(v{{ i }}.w, finalM)) * inv;
106
+ y[rowBase + c{{ i }}] = vec4<{{ scalar }}>(o{{ i }});
107
+ }
108
+ {% endfor %}
109
+ }
build/webgpu/test.json CHANGED
@@ -1,4 +1,8 @@
1
  {
 
 
 
 
2
  "cases": [
3
  {
4
  "name": "inner_broadcast_consecutive_row_groups",
@@ -461,6 +465,442 @@
461
  }
462
  },
463
  "outputs": { "output": { "dtype": "float32", "shape": [1, 2, 1, 2, 1, 2, 2, 2], "tolerance": 0.000002 } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
464
  }
465
  ]
466
  }
 
1
  {
2
+ "fixtureArrays": {
3
+ "subgroup_rows_float32_nonfinite_bias_input_bias": [0.25, 0.25, 0.25, "Infinity", 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, "NaN", 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25],
4
+ "ragged_float32_inner_broadcast_nonfinite_tail_input_bias": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, "NaN", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity"]
5
+ },
6
  "cases": [
7
  {
8
  "name": "inner_broadcast_consecutive_row_groups",
 
465
  }
466
  },
467
  "outputs": { "output": { "dtype": "float32", "shape": [1, 2, 1, 2, 1, 2, 2, 2], "tolerance": 0.000002 } }
468
+ },
469
+ {
470
+ "name": "bias_row_tail_float32_6x513_inner1_wg256",
471
+ "provenance": {
472
+ "notes": "Odd suffix lengths cross invocation and block boundaries. Distinct bias rows distinguish cyclic reuse from consecutive row groups."
473
+ },
474
+ "attrs": { "axis": 1, "is_inner_broadcast": 1 },
475
+ "tunables": { "WORKGROUP_SIZE": 256 },
476
+ "inputs": {
477
+ "data": {
478
+ "dtype": "float32",
479
+ "shape": [6, 513],
480
+ "data": { "kind": "fillFloat32", "scale": 0.5, "sinStep": 0.017, "cosStep": 0.031 }
481
+ },
482
+ "bias": {
483
+ "dtype": "float32",
484
+ "shape": [2, 513],
485
+ "data": { "kind": "fillFloat32", "scale": 0.25, "sinStep": 0.037, "cosStep": 0.053 }
486
+ }
487
+ },
488
+ "outputs": { "output": { "dtype": "float32", "shape": [6, 513], "tolerance": 0.000001 } }
489
+ },
490
+ {
491
+ "name": "bias_row_tail_float32_6x4095_inner0_wg256",
492
+ "provenance": {
493
+ "notes": "Odd suffix lengths cross invocation and block boundaries. Distinct bias rows distinguish cyclic reuse from consecutive row groups."
494
+ },
495
+ "attrs": { "axis": 1, "is_inner_broadcast": 0 },
496
+ "tunables": { "WORKGROUP_SIZE": 256 },
497
+ "inputs": {
498
+ "data": {
499
+ "dtype": "float32",
500
+ "shape": [6, 4095],
501
+ "data": { "kind": "fillFloat32", "scale": 0.5, "sinStep": 0.017, "cosStep": 0.031 }
502
+ },
503
+ "bias": {
504
+ "dtype": "float32",
505
+ "shape": [3, 4095],
506
+ "data": { "kind": "fillFloat32", "scale": 0.25, "sinStep": 0.037, "cosStep": 0.053 }
507
+ }
508
+ },
509
+ "outputs": { "output": { "dtype": "float32", "shape": [6, 4095], "tolerance": 1e-7 } }
510
+ },
511
+ {
512
+ "name": "bias_row_tail_float32_4x65537_inner1_wg64",
513
+ "provenance": {
514
+ "notes": "Odd suffix lengths cross invocation and block boundaries. Distinct bias rows distinguish cyclic reuse from consecutive row groups."
515
+ },
516
+ "attrs": { "axis": 1, "is_inner_broadcast": 1 },
517
+ "tunables": { "WORKGROUP_SIZE": 64 },
518
+ "inputs": {
519
+ "data": {
520
+ "dtype": "float32",
521
+ "shape": [4, 65537],
522
+ "data": { "kind": "fillFloat32", "scale": 0.5, "sinStep": 0.017, "cosStep": 0.031 }
523
+ },
524
+ "bias": {
525
+ "dtype": "float32",
526
+ "shape": [2, 65537],
527
+ "data": { "kind": "fillFloat32", "scale": 0.25, "sinStep": 0.037, "cosStep": 0.053 }
528
+ }
529
+ },
530
+ "outputs": { "output": { "dtype": "float32", "shape": [4, 65537], "tolerance": 1e-8 } }
531
+ },
532
+ {
533
+ "name": "bias_row_tail_float32_4x131073_inner0_wg256",
534
+ "provenance": {
535
+ "notes": "Odd suffix lengths cross invocation and block boundaries. Distinct bias rows distinguish cyclic reuse from consecutive row groups."
536
+ },
537
+ "attrs": { "axis": 1, "is_inner_broadcast": 0 },
538
+ "tunables": { "WORKGROUP_SIZE": 256 },
539
+ "inputs": {
540
+ "data": {
541
+ "dtype": "float32",
542
+ "shape": [4, 131073],
543
+ "data": { "kind": "fillFloat32", "scale": 0.5, "sinStep": 0.017, "cosStep": 0.031 }
544
+ },
545
+ "bias": {
546
+ "dtype": "float32",
547
+ "shape": [2, 131073],
548
+ "data": { "kind": "fillFloat32", "scale": 0.25, "sinStep": 0.037, "cosStep": 0.053 }
549
+ }
550
+ },
551
+ "outputs": { "output": { "dtype": "float32", "shape": [4, 131073], "tolerance": 1e-8 } }
552
+ },
553
+ {
554
+ "name": "bias_row_tail_float16_6x513_inner1_wg256",
555
+ "provenance": {
556
+ "notes": "Odd suffix lengths cross invocation and block boundaries. Distinct bias rows distinguish cyclic reuse from consecutive row groups."
557
+ },
558
+ "attrs": { "axis": 1, "is_inner_broadcast": 1 },
559
+ "tunables": { "WORKGROUP_SIZE": 256 },
560
+ "inputs": {
561
+ "data": {
562
+ "dtype": "float16",
563
+ "shape": [6, 513],
564
+ "data": { "kind": "fillFloat32", "scale": 0.5, "sinStep": 0.017, "cosStep": 0.031 }
565
+ },
566
+ "bias": {
567
+ "dtype": "float16",
568
+ "shape": [2, 513],
569
+ "data": { "kind": "fillFloat32", "scale": 0.25, "sinStep": 0.037, "cosStep": 0.053 }
570
+ }
571
+ },
572
+ "outputs": { "output": { "dtype": "float16", "shape": [6, 513], "tolerance": 0.00001 } }
573
+ },
574
+ {
575
+ "name": "bias_row_tail_float16_6x4095_inner0_wg256",
576
+ "provenance": {
577
+ "notes": "Odd suffix lengths cross invocation and block boundaries. Distinct bias rows distinguish cyclic reuse from consecutive row groups."
578
+ },
579
+ "attrs": { "axis": 1, "is_inner_broadcast": 0 },
580
+ "tunables": { "WORKGROUP_SIZE": 256 },
581
+ "inputs": {
582
+ "data": {
583
+ "dtype": "float16",
584
+ "shape": [6, 4095],
585
+ "data": { "kind": "fillFloat32", "scale": 0.5, "sinStep": 0.017, "cosStep": 0.031 }
586
+ },
587
+ "bias": {
588
+ "dtype": "float16",
589
+ "shape": [3, 4095],
590
+ "data": { "kind": "fillFloat32", "scale": 0.25, "sinStep": 0.037, "cosStep": 0.053 }
591
+ }
592
+ },
593
+ "outputs": { "output": { "dtype": "float16", "shape": [6, 4095], "tolerance": 0.000001 } }
594
+ },
595
+ {
596
+ "name": "bias_row_tail_float16_4x65537_inner1_wg64",
597
+ "provenance": {
598
+ "notes": "Odd suffix lengths cross invocation and block boundaries. Distinct bias rows distinguish cyclic reuse from consecutive row groups."
599
+ },
600
+ "attrs": { "axis": 1, "is_inner_broadcast": 1 },
601
+ "tunables": { "WORKGROUP_SIZE": 64 },
602
+ "inputs": {
603
+ "data": {
604
+ "dtype": "float16",
605
+ "shape": [4, 65537],
606
+ "data": { "kind": "fillFloat32", "scale": 0.5, "sinStep": 0.017, "cosStep": 0.031 }
607
+ },
608
+ "bias": {
609
+ "dtype": "float16",
610
+ "shape": [2, 65537],
611
+ "data": { "kind": "fillFloat32", "scale": 0.25, "sinStep": 0.037, "cosStep": 0.053 }
612
+ }
613
+ },
614
+ "outputs": { "output": { "dtype": "float16", "shape": [4, 65537], "tolerance": 1e-7 } }
615
+ },
616
+ {
617
+ "name": "bias_row_tail_float16_4x131073_inner0_wg256",
618
+ "provenance": {
619
+ "notes": "Odd suffix lengths cross invocation and block boundaries. Distinct bias rows distinguish cyclic reuse from consecutive row groups."
620
+ },
621
+ "attrs": { "axis": 1, "is_inner_broadcast": 0 },
622
+ "tunables": { "WORKGROUP_SIZE": 256 },
623
+ "inputs": {
624
+ "data": {
625
+ "dtype": "float16",
626
+ "shape": [4, 131073],
627
+ "data": { "kind": "fillFloat32", "scale": 0.5, "sinStep": 0.017, "cosStep": 0.031 }
628
+ },
629
+ "bias": {
630
+ "dtype": "float16",
631
+ "shape": [2, 131073],
632
+ "data": { "kind": "fillFloat32", "scale": 0.25, "sinStep": 0.037, "cosStep": 0.053 }
633
+ }
634
+ },
635
+ "outputs": { "output": { "dtype": "float16", "shape": [4, 131073], "tolerance": 1e-7 } }
636
+ },
637
+ {
638
+ "name": "subgroup_rows_float32_65x256_bias5_inner1",
639
+ "provenance": {
640
+ "notes": "Subgroup-row route boundary: row tail or register budget boundary with flattened bias-row reuse. Values are added and accumulated in float32 before the output cast."
641
+ },
642
+ "attrs": { "axis": 1, "is_inner_broadcast": 1 },
643
+ "inputs": {
644
+ "data": {
645
+ "dtype": "float32",
646
+ "shape": [65, 256],
647
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31, "scale": 2.0 }
648
+ },
649
+ "bias": {
650
+ "dtype": "float32",
651
+ "shape": [5, 256],
652
+ "data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.23, "scale": 1.0 }
653
+ }
654
+ },
655
+ "outputs": { "output": { "dtype": "float32", "shape": [65, 256], "tolerance": 0.00001, "relTolerance": 0.0001 } }
656
+ },
657
+ {
658
+ "name": "subgroup_rows_float32_64x1024_bias4_inner0",
659
+ "provenance": {
660
+ "notes": "Subgroup-row route boundary: row tail or register budget boundary with flattened bias-row reuse. Values are added and accumulated in float32 before the output cast."
661
+ },
662
+ "attrs": { "axis": 1, "is_inner_broadcast": 0 },
663
+ "inputs": {
664
+ "data": {
665
+ "dtype": "float32",
666
+ "shape": [64, 1024],
667
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31, "scale": 2.0 }
668
+ },
669
+ "bias": {
670
+ "dtype": "float32",
671
+ "shape": [4, 1024],
672
+ "data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.23, "scale": 1.0 }
673
+ }
674
+ },
675
+ "outputs": { "output": { "dtype": "float32", "shape": [64, 1024], "tolerance": 0.00001, "relTolerance": 0.0001 } }
676
+ },
677
+ {
678
+ "name": "subgroup_rows_float32_nonfinite_bias",
679
+ "attrs": { "axis": 1, "is_inner_broadcast": 1 },
680
+ "inputs": {
681
+ "data": { "dtype": "float32", "shape": [64, 64], "data": { "kind": "constant", "value": 0.5 } },
682
+ "bias": {
683
+ "dtype": "float32",
684
+ "shape": [4, 64],
685
+ "data": {
686
+ "kind": "values",
687
+ "values": { "$ref": "#/fixtureArrays/subgroup_rows_float32_nonfinite_bias_input_bias" }
688
+ }
689
+ }
690
+ },
691
+ "outputs": {
692
+ "output": {
693
+ "dtype": "float32",
694
+ "shape": [64, 64],
695
+ "allowNaN": true,
696
+ "tolerance": 0.00001,
697
+ "relTolerance": 0.0001
698
+ }
699
+ },
700
+ "provenance": {
701
+ "notes": "Subgroup-row bias fusion must retain the operator semantics for positive infinity, a NaN bias entry, and fully negative-infinity masked rows; this fixture checks their positions as well as finite neighboring rows."
702
+ }
703
+ },
704
+ {
705
+ "name": "subgroup_rows_float16_65x256_bias5_inner1",
706
+ "provenance": {
707
+ "notes": "Subgroup-row route boundary: row tail or register budget boundary with flattened bias-row reuse. Values are added and accumulated in float32 before the output cast."
708
+ },
709
+ "attrs": { "axis": 1, "is_inner_broadcast": 1 },
710
+ "inputs": {
711
+ "data": {
712
+ "dtype": "float16",
713
+ "shape": [65, 256],
714
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31, "scale": 2.0 }
715
+ },
716
+ "bias": {
717
+ "dtype": "float16",
718
+ "shape": [5, 256],
719
+ "data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.23, "scale": 1.0 }
720
+ }
721
+ },
722
+ "outputs": { "output": { "dtype": "float16", "shape": [65, 256], "tolerance": 0.001, "relTolerance": 0.002 } }
723
+ },
724
+ {
725
+ "name": "subgroup_rows_float16_64x1024_bias4_inner0",
726
+ "provenance": {
727
+ "notes": "Subgroup-row route boundary: row tail or register budget boundary with flattened bias-row reuse. Values are added and accumulated in float32 before the output cast."
728
+ },
729
+ "attrs": { "axis": 1, "is_inner_broadcast": 0 },
730
+ "inputs": {
731
+ "data": {
732
+ "dtype": "float16",
733
+ "shape": [64, 1024],
734
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31, "scale": 2.0 }
735
+ },
736
+ "bias": {
737
+ "dtype": "float16",
738
+ "shape": [4, 1024],
739
+ "data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.23, "scale": 1.0 }
740
+ }
741
+ },
742
+ "outputs": { "output": { "dtype": "float16", "shape": [64, 1024], "tolerance": 0.001, "relTolerance": 0.002 } }
743
+ },
744
+ {
745
+ "name": "subgroup_rows_float16_nonfinite_bias",
746
+ "attrs": { "axis": 1, "is_inner_broadcast": 0 },
747
+ "inputs": {
748
+ "data": { "dtype": "float16", "shape": [64, 64], "data": { "kind": "constant", "value": 0.5 } },
749
+ "bias": {
750
+ "dtype": "float16",
751
+ "shape": [4, 64],
752
+ "data": {
753
+ "kind": "values",
754
+ "values": { "$ref": "#/fixtureArrays/subgroup_rows_float32_nonfinite_bias_input_bias" }
755
+ }
756
+ }
757
+ },
758
+ "outputs": {
759
+ "output": { "dtype": "float16", "shape": [64, 64], "allowNaN": true, "tolerance": 0.001, "relTolerance": 0.002 }
760
+ },
761
+ "provenance": {
762
+ "notes": "Subgroup-row bias fusion must retain the operator semantics for positive infinity, a NaN bias entry, and fully negative-infinity masked rows; this fixture checks their positions as well as finite neighboring rows."
763
+ }
764
+ },
765
+ {
766
+ "name": "ragged_float32_negative_limit_cols33",
767
+ "attrs": { "axis": 1, "is_inner_broadcast": 1 },
768
+ "inputs": {
769
+ "data": {
770
+ "dtype": "float32",
771
+ "shape": [3, 33],
772
+ "data": { "kind": "constant", "value": -3.4028234663852886e+38 }
773
+ },
774
+ "bias": { "dtype": "float32", "shape": [1, 33], "data": { "kind": "constant", "value": 0.0 } }
775
+ },
776
+ "outputs": { "output": { "dtype": "float32", "shape": [3, 33], "tolerance": 1e-7, "relTolerance": 0.00001 } },
777
+ "provenance": {
778
+ "notes": "Invalid logical-vec4 tail components must contribute exactly zero denominator, including when valid data equals the negative finite-limit sentinel."
779
+ }
780
+ },
781
+ {
782
+ "name": "ragged_float32_negative_limit_cols65",
783
+ "attrs": { "axis": 1, "is_inner_broadcast": 1 },
784
+ "inputs": {
785
+ "data": {
786
+ "dtype": "float32",
787
+ "shape": [3, 65],
788
+ "data": { "kind": "constant", "value": -3.4028234663852886e+38 }
789
+ },
790
+ "bias": { "dtype": "float32", "shape": [1, 65], "data": { "kind": "constant", "value": 0.0 } }
791
+ },
792
+ "outputs": { "output": { "dtype": "float32", "shape": [3, 65], "tolerance": 1e-7, "relTolerance": 0.00001 } },
793
+ "provenance": {
794
+ "notes": "Invalid logical-vec4 tail components must contribute exactly zero denominator, including when valid data equals the negative finite-limit sentinel."
795
+ }
796
+ },
797
+ {
798
+ "name": "ragged_float32_negative_limit_cols67",
799
+ "attrs": { "axis": 1, "is_inner_broadcast": 1 },
800
+ "inputs": {
801
+ "data": {
802
+ "dtype": "float32",
803
+ "shape": [3, 67],
804
+ "data": { "kind": "constant", "value": -3.4028234663852886e+38 }
805
+ },
806
+ "bias": { "dtype": "float32", "shape": [1, 67], "data": { "kind": "constant", "value": 0.0 } }
807
+ },
808
+ "outputs": { "output": { "dtype": "float32", "shape": [3, 67], "tolerance": 1e-7, "relTolerance": 0.00001 } },
809
+ "provenance": {
810
+ "notes": "Invalid logical-vec4 tail components must contribute exactly zero denominator, including when valid data equals the negative finite-limit sentinel."
811
+ }
812
+ },
813
+ {
814
+ "name": "ragged_float32_inner_broadcast_nonfinite_tail",
815
+ "attrs": { "axis": 1, "is_inner_broadcast": 1 },
816
+ "inputs": {
817
+ "data": {
818
+ "dtype": "float32",
819
+ "shape": [6, 67],
820
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31, "scale": 2.0 }
821
+ },
822
+ "bias": {
823
+ "dtype": "float32",
824
+ "shape": [2, 67],
825
+ "data": {
826
+ "kind": "values",
827
+ "values": { "$ref": "#/fixtureArrays/ragged_float32_inner_broadcast_nonfinite_tail_input_bias" }
828
+ }
829
+ }
830
+ },
831
+ "outputs": {
832
+ "output": { "dtype": "float32", "shape": [6, 67], "tolerance": 1e-7, "relTolerance": 0.00001, "allowNaN": true }
833
+ },
834
+ "provenance": {
835
+ "notes": "Consecutive groups reuse each ragged bias row: first group has a NaN in the last valid component, second group is fully additive-masked. Check exact NaN positions and zero rows."
836
+ }
837
+ },
838
+ {
839
+ "name": "ragged_float16_negative_limit_cols33",
840
+ "attrs": { "axis": 1, "is_inner_broadcast": 1 },
841
+ "inputs": {
842
+ "data": { "dtype": "float16", "shape": [3, 33], "data": { "kind": "constant", "value": -65504.0 } },
843
+ "bias": { "dtype": "float16", "shape": [1, 33], "data": { "kind": "constant", "value": 0.0 } }
844
+ },
845
+ "outputs": { "output": { "dtype": "float16", "shape": [3, 33], "tolerance": 0.00001, "relTolerance": 0.001 } },
846
+ "provenance": {
847
+ "notes": "Invalid logical-vec4 tail components must contribute exactly zero denominator, including when valid data equals the negative finite-limit sentinel."
848
+ }
849
+ },
850
+ {
851
+ "name": "ragged_float16_negative_limit_cols65",
852
+ "attrs": { "axis": 1, "is_inner_broadcast": 1 },
853
+ "inputs": {
854
+ "data": { "dtype": "float16", "shape": [3, 65], "data": { "kind": "constant", "value": -65504.0 } },
855
+ "bias": { "dtype": "float16", "shape": [1, 65], "data": { "kind": "constant", "value": 0.0 } }
856
+ },
857
+ "outputs": { "output": { "dtype": "float16", "shape": [3, 65], "tolerance": 0.00001, "relTolerance": 0.001 } },
858
+ "provenance": {
859
+ "notes": "Invalid logical-vec4 tail components must contribute exactly zero denominator, including when valid data equals the negative finite-limit sentinel."
860
+ }
861
+ },
862
+ {
863
+ "name": "ragged_float16_negative_limit_cols67",
864
+ "attrs": { "axis": 1, "is_inner_broadcast": 1 },
865
+ "inputs": {
866
+ "data": { "dtype": "float16", "shape": [3, 67], "data": { "kind": "constant", "value": -65504.0 } },
867
+ "bias": { "dtype": "float16", "shape": [1, 67], "data": { "kind": "constant", "value": 0.0 } }
868
+ },
869
+ "outputs": { "output": { "dtype": "float16", "shape": [3, 67], "tolerance": 0.00001, "relTolerance": 0.001 } },
870
+ "provenance": {
871
+ "notes": "Invalid logical-vec4 tail components must contribute exactly zero denominator, including when valid data equals the negative finite-limit sentinel."
872
+ }
873
+ },
874
+ {
875
+ "name": "ragged_float16_inner_broadcast_nonfinite_tail",
876
+ "attrs": { "axis": 1, "is_inner_broadcast": 1 },
877
+ "inputs": {
878
+ "data": {
879
+ "dtype": "float16",
880
+ "shape": [6, 67],
881
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31, "scale": 2.0 }
882
+ },
883
+ "bias": {
884
+ "dtype": "float16",
885
+ "shape": [2, 67],
886
+ "data": {
887
+ "kind": "values",
888
+ "values": { "$ref": "#/fixtureArrays/ragged_float32_inner_broadcast_nonfinite_tail_input_bias" }
889
+ }
890
+ }
891
+ },
892
+ "outputs": {
893
+ "output": {
894
+ "dtype": "float16",
895
+ "shape": [6, 67],
896
+ "tolerance": 0.00001,
897
+ "relTolerance": 0.001,
898
+ "allowNaN": true
899
+ }
900
+ },
901
+ "provenance": {
902
+ "notes": "Consecutive groups reuse each ragged bias row: first group has a NaN in the last valid component, second group is fully additive-masked. Check exact NaN positions and zero rows."
903
+ }
904
  }
905
  ]
906
  }