Xenova HF Staff commited on
Commit
87e48ce
·
verified ·
1 Parent(s): 0ef25e5

sync 2e7068faf55e

Browse files
README.md CHANGED
@@ -1,3 +1,76 @@
1
  ---
 
2
  license: apache-2.0
 
 
 
 
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ library_name: kernels
3
  license: apache-2.0
4
+ tags:
5
+ - kernel
6
+ - webgpu
7
+ - wgsl
8
  ---
9
+ # com.microsoft.BiasSoftmax
10
+
11
+ `com.microsoft` · ONNX Runtime contrib operator · contrib since_version 1
12
+
13
+ ## Description
14
+
15
+ Computes `softmax(data + bias)` over the flattened suffix beginning at `axis`. The required `is_inner_broadcast` attribute selects how bias rows are reused: consecutive groups for inner broadcast or cyclic groups for outer broadcast. This specializes the `softmax(scores + additive_mask)` pattern used by transformer attention. Float16 and float32 are supported; the schema's double type is not.
16
+
17
+ See the [ONNX Runtime `BiasSoftmax` contrib-operator spec](https://github.com/microsoft/onnxruntime/blob/main/docs/ContribOperators.md#com.microsoft.BiasSoftmax) for the reference semantics.
18
+
19
+ ## Inputs
20
+
21
+ | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
22
+ | --- | --- | --- | --- | --- | --- | --- |
23
+ | `data` | `data` | `T` | — | — | The input data tensor. | required |
24
+ | `bias` | `bias` | `T` | — | — | The bias (or additive mask) tensor. Its element count must be an integral number of flattened softmax rows and that row count must divide the data row count. | required |
25
+
26
+ ## Outputs
27
+
28
+ | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
29
+ | --- | --- | --- | --- | --- | --- | --- |
30
+ | `output` | `output` | `T` | same as `data` | same as `data` | The output tensor; same shape as data. | required |
31
+
32
+ ## Attributes
33
+
34
+ Attributes and default values (overridable per request):
35
+
36
+ | Attribute | Default | Description |
37
+ | --- | --- | --- |
38
+ | `axis` | `1` | The axis from which softmax is applied; dimensions from `axis` onward are included in the softmax reduction. |
39
+ | `is_inner_broadcast` | — | When 1, bias is broadcast across dimensions from `broadcast_axis` to `axis-1`; when 0, bias is broadcast across dimensions 0 to `broadcast_axis-1`. |
40
+
41
+ ## Type constraints
42
+
43
+ | Variable | Allowed dtypes |
44
+ | --- | --- |
45
+ | `T` | `float32`, `float16` |
46
+
47
+ ## Files
48
+
49
+ - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, 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
+ The loader derives every required output's shape and logical dtype from the manifest contract and this call.
60
+ It then allocates the result tensors automatically.
61
+
62
+ The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
63
+
64
+ Replace each `*Data` placeholder with a typed array containing the corresponding input data.
65
+
66
+ ```js
67
+ import { getKernel } from "@huggingface/kernels";
68
+
69
+ const kernel = await getKernel("webgpu-kernels/com.microsoft.BiasSoftmax", { version: 1 });
70
+ const { output } = await kernel({
71
+ data: { data: dataData, shape: [1, 2, 2] },
72
+ bias: { data: biasData, shape: [1, 2, 2] },
73
+ }, {
74
+ attrs: { is_inner_broadcast: 1 },
75
+ });
76
+ ```
build/webgpu/bench.json ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "op": "com.microsoft.BiasSoftmax",
3
+ "tunableSpace": { "WORKGROUP_SIZE": [64, 128, 256], "BLOCK_COLS": [1024, 2048, 4096, 8192] },
4
+ "cases": [
5
+ {
6
+ "name": "biassoftmax-f32-attn-32x512x512",
7
+ "preset": "smoke",
8
+ "vars": { "dtype": "float32" },
9
+ "attrs": { "axis": 2, "is_inner_broadcast": 0 },
10
+ "inputs": {
11
+ "data": { "shape": [32, 512, 512], "dtype": "float32", "dist": "normal", "seed": 510, "scale": 2 },
12
+ "bias": { "shape": [1, 512, 512], "dtype": "float32", "dist": "normal", "seed": 511, "scale": 1 }
13
+ },
14
+ "outputs": { "output": { "shape": [32, 512, 512], "dtype": "float32" } },
15
+ "bench": {
16
+ "primary": true,
17
+ "metrics": [{ "type": "bandwidth", "value": "(32 * 512 * 512 * 2 + 512 * 512) * dtypeBytes(args.dtype)" }]
18
+ }
19
+ },
20
+ {
21
+ "name": "biassoftmax-f32-suffix-64x16x256",
22
+ "preset": "stress",
23
+ "vars": { "dtype": "float32" },
24
+ "attrs": { "axis": 1, "is_inner_broadcast": 0 },
25
+ "inputs": {
26
+ "data": { "shape": [64, 16, 256], "dtype": "float32", "dist": "normal", "seed": 512, "scale": 2 },
27
+ "bias": { "shape": [1, 16, 256], "dtype": "float32", "dist": "normal", "seed": 513, "scale": 1 }
28
+ },
29
+ "outputs": { "output": { "shape": [64, 16, 256], "dtype": "float32" } },
30
+ "bench": {
31
+ "metrics": [{ "type": "bandwidth", "value": "(64 * 16 * 256 * 2 + 16 * 256) * dtypeBytes(args.dtype)" }]
32
+ }
33
+ },
34
+ {
35
+ "name": "biassoftmax-f16-attn-32x512x512",
36
+ "preset": "model",
37
+ "vars": { "dtype": "float16" },
38
+ "attrs": { "axis": 2, "is_inner_broadcast": 0 },
39
+ "inputs": {
40
+ "data": { "shape": [32, 512, 512], "dtype": "float16", "dist": "normal", "seed": 514, "scale": 2 },
41
+ "bias": { "shape": [1, 512, 512], "dtype": "float16", "dist": "normal", "seed": 515, "scale": 1 }
42
+ },
43
+ "outputs": { "output": { "shape": [32, 512, 512], "dtype": "float16" } },
44
+ "bench": {
45
+ "metrics": [{ "type": "bandwidth", "value": "(32 * 512 * 512 * 2 + 512 * 512) * dtypeBytes(args.dtype)" }]
46
+ }
47
+ },
48
+ {
49
+ "name": "biassoftmax-f32-lowocc-8x524288-axis1",
50
+ "preset": "stress",
51
+ "vars": { "dtype": "float32" },
52
+ "attrs": { "axis": 1, "is_inner_broadcast": 0 },
53
+ "inputs": {
54
+ "data": { "shape": [8, 524288], "dtype": "float32", "dist": "normal", "seed": 530, "scale": 2 },
55
+ "bias": { "shape": [1, 524288], "dtype": "float32", "dist": "normal", "seed": 531, "scale": 1 }
56
+ },
57
+ "outputs": { "output": { "shape": [8, 524288], "dtype": "float32" } },
58
+ "bench": { "metrics": [{ "type": "bandwidth", "value": "(8 * 524288 * 2 + 524288) * dtypeBytes(args.dtype)" }] }
59
+ },
60
+ {
61
+ "name": "biassoftmax-f32-launchbound-262144x2-axis1",
62
+ "preset": "stress",
63
+ "vars": { "dtype": "float32" },
64
+ "attrs": { "axis": 1, "is_inner_broadcast": 0 },
65
+ "inputs": {
66
+ "data": { "shape": [262144, 2], "dtype": "float32", "dist": "normal", "seed": 532, "scale": 2 },
67
+ "bias": { "shape": [1, 2], "dtype": "float32", "dist": "normal", "seed": 533, "scale": 1 }
68
+ },
69
+ "outputs": { "output": { "shape": [262144, 2], "dtype": "float32" } },
70
+ "bench": { "metrics": [{ "type": "bandwidth", "value": "(262144 * 2 * 2 + 2) * dtypeBytes(args.dtype)" }] }
71
+ }
72
+ ]
73
+ }
build/webgpu/bias-softmax-longrow-normalize.wgsl.jinja ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Parallel normalization for split long rows. One workgroup handles each
2
+ // BLOCK_COLS slice and reuses the finished per-row statistics.
3
+ {% if usesF16 %}
4
+ enable f16;
5
+ {% endif %}
6
+ {{ env.wgsl.resourceDeclarations }}
7
+
8
+ const WG: u32 = {{ tunables.WORKGROUP_SIZE }}u;
9
+ const BLOCK_COLS: u32 = {{ tunables.BLOCK_COLS }}u;
10
+ fn bias_index(index: u32) -> u32 {
11
+ let row = index / params.blockSize;
12
+ let column = index % params.blockSize;
13
+ {% if source.isInnerBroadcast %}
14
+ return (row / {{ source.innerRepeat }}u) * params.blockSize + column;
15
+ {% else %}
16
+ return (row % {{ source.biasBlockCount }}u) * params.blockSize + column;
17
+ {% endif %}
18
+ }
19
+ // FLT_MAX, not -inf, as the online (m, d) accumulator init: merges must keep
20
+ // `m - m` finite so an empty lane / all--inf row contributes the exact
21
+ // accumulator identity (m, d) = (-FLT_MAX, 0). Operator epilogues interpret
22
+ // a zero final denominator according to their public semantics. Using -inf
23
+ // here changes +inf-row behavior.
24
+ const FLT_MAX: f32 = 3.4028234663852886e38;
25
+
26
+ fn is_finite_f32(value: f32) -> bool {
27
+ return select(false, value <= FLT_MAX, value >= -FLT_MAX);
28
+ }
29
+
30
+ // x - m that is exactly 0 when x equals a finite m, so exp(shifted) == 1
31
+ // exactly at the row max. `x - x` on an infinite max is a legal fast-math
32
+ // fold to 0, which would silently turn +inf rows finite — the explicit
33
+ // equality test keeps the NaN propagation of the serial kernels.
34
+ fn shifted_value(value: f32, maxValue: f32) -> f32 {
35
+ let equalFiniteMax = select(false, value == maxValue, is_finite_f32(maxValue));
36
+ return select(value - maxValue, 0.0, equalFiniteMax);
37
+ }
38
+ fn exp_shift(value: f32, maxValue: f32) -> f32 {
39
+ return exp(shifted_value(value, maxValue));
40
+ }
41
+
42
+ @compute @workgroup_size(WG)
43
+ fn main(@builtin(workgroup_id) wid: vec3<u32>,
44
+ @builtin(local_invocation_id) lid: vec3<u32>) {
45
+ let block = wid.x;
46
+ let row = wid.y;
47
+ let rowBase = row * params.blockSize;
48
+ let colStart = block * BLOCK_COLS;
49
+ let colEnd = min(params.blockSize, colStart + BLOCK_COLS);
50
+ let m = rowMax[row];
51
+ let d = rowSum[row];
52
+ let inv = select(0.0, 1.0 / d, d > 0.0);
53
+
54
+ for (var col = colStart + lid.x; col < colEnd; col += WG) {
55
+ let i = rowBase + col;
56
+ let v = f32(data[i]) + f32(bias[bias_index(i)]);
57
+ output[i] = {{ scalar }}(exp_shift(v, m) * inv);
58
+ }
59
+ }
build/webgpu/bias-softmax-longrow-stats.wgsl.jinja ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 source.stage == "block" and usesF16 %}
5
+ enable f16;
6
+ {% endif %}
7
+ {{ env.wgsl.resourceDeclarations }}
8
+
9
+ const WG: u32 = {{ tunables.WORKGROUP_SIZE }}u;
10
+ {% if source.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 source.isInnerBroadcast | default(false) %}
16
+ return (row / {{ source.innerRepeat | default(1) }}u) * params.blockSize + column;
17
+ {% else %}
18
+ return (row % {{ source.biasBlockCount | 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
25
+ // a zero final denominator according to their public semantics. Using -inf
26
+ // here changes +inf-row behavior.
27
+ const FLT_MAX: f32 = 3.4028234663852886e38;
28
+
29
+ fn is_finite_f32(value: f32) -> bool {
30
+ return select(false, value <= FLT_MAX, value >= -FLT_MAX);
31
+ }
32
+
33
+ // x - m that is exactly 0 when x equals a finite m, so exp(shifted) == 1
34
+ // exactly at the row max. `x - x` on an infinite max is a legal fast-math
35
+ // fold to 0, which would silently turn +inf rows finite — the explicit
36
+ // equality test keeps the NaN propagation of the serial kernels.
37
+ 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
+ }
44
+ // Workgroup-cooperative merge of per-thread online-softmax (m, d) partials:
45
+ // mNew = max(m1, m2)
46
+ // dNew = d1 * exp(m1 - mNew) + d2 * exp(m2 - mNew)
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
+ // Per-subgroup partials are published into a deterministic slot: the subgroup's
53
+ // ordinal index within the workgroup (lidx / sgSize). The online (m, d) merge
54
+ // is not float-associative, so thread 0 must fold partials in a fixed order.
55
+ // Subgroups partition a workgroup into contiguous ordinal ranges on supported
56
+ // backends, so the ordinal slot is unique per subgroup and every slot in
57
+ // [0, subgroupCount) is written (each subgroup elects one leader).
58
+ // Sized for the worst case of one partial per invocation.
59
+ var<workgroup> partialM: array<f32, WG>;
60
+ var<workgroup> partialD: array<f32, WG>;
61
+ var<workgroup> combinedMD: vec2<f32>;
62
+
63
+ // When the whole workgroup is one subgroup the subgroup reduce already covers
64
+ // it (no barriers, no shared state); otherwise subgroup leaders publish
65
+ // partials through shared memory and thread 0 folds them in ordinal order.
66
+ fn combine_partials(m: f32, d: f32, lidx: u32, sgSize: u32) -> vec2<f32> {
67
+ let sgM = subgroupMax(m);
68
+ // A lane with no elements contributes d == 0 (exact identity). A +inf
69
+ // element made exp(inf - inf) = NaN stick in that lane's d; a NaN element
70
+ // landed in d via exp(NaN); both survive the merge and are detected by the
71
+ // code after the reduction.
72
+ let sgD = subgroupAdd(d * exp_shift(m, sgM));
73
+ if (sgSize == WG) {
74
+ return vec2<f32>(sgM, sgD);
75
+ }
76
+ let subgroupCount = (WG + sgSize - 1u) / sgSize;
77
+ // Pre-seed every fold slot with the (max, denom) identity. The fold below reads a
78
+ // fixed subgroupCount slots in ordinal order (for determinism), but a slot whose
79
+ // subgroup elects no leader this call — e.g. a fully out-of-window key tile in the
80
+ // flash-attention loop that re-uses this shared memory each iteration — would
81
+ // otherwise read stale shared memory. Identity makes such a slot a no-op.
82
+ // (max identity = -FLT_MAX, denom identity = 0.)
83
+ if (lidx < subgroupCount) {
84
+ partialM[lidx] = -FLT_MAX;
85
+ partialD[lidx] = 0.0;
86
+ }
87
+ workgroupBarrier();
88
+ if (subgroupElect()) {
89
+ let slot = lidx / sgSize;
90
+ partialM[slot] = sgM;
91
+ partialD[slot] = sgD;
92
+ }
93
+ workgroupBarrier();
94
+ if (lidx == 0u) {
95
+ var accM = -FLT_MAX;
96
+ var accD = 0.0;
97
+ for (var i = 0u; i < subgroupCount; i = i + 1u) {
98
+ let mNew = max(accM, partialM[i]);
99
+ accD = accD * exp_shift(accM, mNew) + partialD[i] * exp_shift(partialM[i], mNew);
100
+ accM = mNew;
101
+ }
102
+ combinedMD = vec2<f32>(accM, accD);
103
+ }
104
+ workgroupBarrier();
105
+ return combinedMD;
106
+ }
107
+ {% else %}
108
+ {% set mdStreamed = mdStreams is defined %}
109
+ {% set mdStreams = mdStreams if mdStreams is defined else 1 %}
110
+ {% set mdExtent = "WG" if mdStreams == 1 else "WG * " ~ mdStreams ~ "u" %}
111
+ var<workgroup> partialM: array<f32, {{ mdExtent }}>;
112
+ var<workgroup> partialD: array<f32, {{ mdExtent }}>;
113
+ {% if mdStreamed %}
114
+
115
+ // In-place fold of {{ mdStreams }} streams. The caller stores its per-thread
116
+ // partials into partialM/partialD first and reads the merged pair of stream s
117
+ // from slot s * WG afterwards.
118
+ fn combine_partials_streams(lidx: u32) {
119
+ workgroupBarrier();
120
+ var stride = WG / 2u;
121
+ loop {
122
+ if (stride == 0u) {
123
+ break;
124
+ }
125
+ if (lidx < stride) {
126
+ {% for s in range(mdStreams) %}
127
+ {
128
+ let slot = {{ s }}u * WG + lidx;
129
+ let m1 = partialM[slot];
130
+ let d1 = partialD[slot];
131
+ let m2 = partialM[slot + stride];
132
+ let d2 = partialD[slot + stride];
133
+ let mNew = max(m1, m2);
134
+ partialD[slot] = d1 * exp_shift(m1, mNew) + d2 * exp_shift(m2, mNew);
135
+ partialM[slot] = mNew;
136
+ }
137
+ {% endfor %}
138
+ }
139
+ workgroupBarrier();
140
+ stride = stride / 2u;
141
+ }
142
+ }
143
+ {% else %}
144
+
145
+ fn combine_partials(m: f32, d: f32, lidx: u32) -> vec2<f32> {
146
+ partialM[lidx] = m;
147
+ partialD[lidx] = d;
148
+ workgroupBarrier();
149
+ var stride = WG / 2u;
150
+ loop {
151
+ if (stride == 0u) {
152
+ break;
153
+ }
154
+ if (lidx < stride) {
155
+ let m1 = partialM[lidx];
156
+ let d1 = partialD[lidx];
157
+ let m2 = partialM[lidx + stride];
158
+ let d2 = partialD[lidx + stride];
159
+ let mNew = max(m1, m2);
160
+ partialD[lidx] = d1 * exp_shift(m1, mNew) + d2 * exp_shift(m2, mNew);
161
+ partialM[lidx] = mNew;
162
+ }
163
+ workgroupBarrier();
164
+ stride = stride / 2u;
165
+ }
166
+ let merged = vec2<f32>(partialM[0], partialD[0]);
167
+ // Trailing barrier so back-to-back calls cannot race a next call's partial
168
+ // stores against this call's reads of slot 0.
169
+ workgroupBarrier();
170
+ return merged;
171
+ }
172
+ {% endif %}
173
+ {% endif %}
174
+
175
+
176
+ @compute @workgroup_size(WG)
177
+ fn main(@builtin(workgroup_id) wid: vec3<u32>,
178
+ @builtin(local_invocation_id) lid: vec3<u32>) {
179
+ {% if source.stage == "block" %}
180
+ let block = wid.x;
181
+ let row = wid.y;
182
+ let rowBase = row * params.blockSize;
183
+ let colStart = block * BLOCK_COLS;
184
+ let colEnd = min(params.blockSize, colStart + BLOCK_COLS);
185
+
186
+ var m = -FLT_MAX;
187
+ var d = 0.0;
188
+ for (var col = colStart + lid.x; col < colEnd; col += WG) {
189
+ let i = rowBase + col;
190
+ let v = f32(data[i]) + f32(bias[bias_index(i)]);
191
+ let mNew = max(m, v);
192
+ d = d * exp_shift(m, mNew) + exp_shift(v, mNew);
193
+ m = mNew;
194
+ }
195
+ {% else %}
196
+ let row = wid.x;
197
+ let base = row * params.blocks;
198
+
199
+ var m = -FLT_MAX;
200
+ var d = 0.0;
201
+ for (var block = lid.x; block < params.blocks; block += WG) {
202
+ let bm = blockMax[base + block];
203
+ let bd = blockSum[base + block];
204
+ let mNew = max(m, bm);
205
+ d = d * exp_shift(m, mNew) + bd * exp_shift(bm, mNew);
206
+ m = mNew;
207
+ }
208
+ {% endif %}
209
+
210
+ let merged = combine_partials(m, d, lid.x);
211
+ if (lid.x == 0u) {
212
+ {% if source.stage == "block" %}
213
+ let out = row * params.blocks + block;
214
+ blockMax[out] = merged.x;
215
+ blockSum[out] = merged.y;
216
+ {% else %}
217
+ rowMax[row] = merged.x;
218
+ rowSum[row] = merged.y;
219
+ {% endif %}
220
+ }
221
+ }
build/webgpu/bias-softmax.wgsl.jinja ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // BiasSoftmax computes softmax(data + bias) over suffix axes [axis, rank), not
2
+ // only the last axis. A workgroup normally owns one softmax block; short rows
3
+ // can instead be packed with one invocation per row. Inner broadcast reuses
4
+ // one bias row across consecutive data rows; outer broadcast cycles bias rows.
5
+ {% if usesF16 %}
6
+ enable f16;
7
+ {% endif %}
8
+ {% if combineSubgroups %}
9
+ enable subgroups;
10
+ {% endif %}
11
+ {{ env.wgsl.resourceDeclarations }}
12
+
13
+ const WG: u32 = {{ tunables.WORKGROUP_SIZE }}u;
14
+ // FLT_MAX, not -inf, as the online (m, d) accumulator init: merges must keep
15
+ // `m - m` finite so an empty lane / all--inf row contributes the exact
16
+ // accumulator identity (m, d) = (-FLT_MAX, 0). Operator epilogues interpret
17
+ // a zero final denominator according to their public semantics. Using -inf
18
+ // here changes +inf-row behavior.
19
+ const FLT_MAX: f32 = 3.4028234663852886e38;
20
+
21
+ fn is_finite_f32(value: f32) -> bool {
22
+ return select(false, value <= FLT_MAX, value >= -FLT_MAX);
23
+ }
24
+
25
+ // x - m that is exactly 0 when x equals a finite m, so exp(shifted) == 1
26
+ // exactly at the row max. `x - x` on an infinite max is a legal fast-math
27
+ // fold to 0, which would silently turn +inf rows finite — the explicit
28
+ // equality test keeps the NaN propagation of the serial kernels.
29
+ fn shifted_value(value: f32, maxValue: f32) -> f32 {
30
+ let equalFiniteMax = select(false, value == maxValue, is_finite_f32(maxValue));
31
+ return select(value - maxValue, 0.0, equalFiniteMax);
32
+ }
33
+ fn exp_shift(value: f32, maxValue: f32) -> f32 {
34
+ return exp(shifted_value(value, maxValue));
35
+ }
36
+ {% if not packedRows %}
37
+ // Workgroup-cooperative merge of per-thread online-softmax (m, d) partials:
38
+ // mNew = max(m1, m2)
39
+ // dNew = d1 * exp(m1 - mNew) + d2 * exp(m2 - mNew)
40
+ // Both the subgroup and portable barrier-tree engines return the same merged
41
+ // pair to every invocation. Repeated merges require a workgroup barrier between
42
+ // calls before their shared partial storage is reused.
43
+ {% set combineSubgroups = combineSubgroups is defined and combineSubgroups %}
44
+ {% if combineSubgroups %}
45
+ // Per-subgroup partials are published into a deterministic slot: the subgroup's
46
+ // ordinal index within the workgroup (lidx / sgSize). The online (m, d) merge
47
+ // is not float-associative, so thread 0 must fold partials in a fixed order.
48
+ // Subgroups partition a workgroup into contiguous ordinal ranges on supported
49
+ // backends, so the ordinal slot is unique per subgroup and every slot in
50
+ // [0, subgroupCount) is written (each subgroup elects one leader).
51
+ // Sized for the worst case of one partial per invocation.
52
+ var<workgroup> partialM: array<f32, WG>;
53
+ var<workgroup> partialD: array<f32, WG>;
54
+ var<workgroup> combinedMD: vec2<f32>;
55
+
56
+ // When the whole workgroup is one subgroup the subgroup reduce already covers
57
+ // it (no barriers, no shared state); otherwise subgroup leaders publish
58
+ // partials through shared memory and thread 0 folds them in ordinal order.
59
+ fn combine_partials(m: f32, d: f32, lidx: u32, sgSize: u32) -> vec2<f32> {
60
+ let sgM = subgroupMax(m);
61
+ // A lane with no elements contributes d == 0 (exact identity). A +inf
62
+ // element made exp(inf - inf) = NaN stick in that lane's d; a NaN element
63
+ // landed in d via exp(NaN); both survive the merge and are detected by the
64
+ // code after the reduction.
65
+ let sgD = subgroupAdd(d * exp_shift(m, sgM));
66
+ if (sgSize == WG) {
67
+ return vec2<f32>(sgM, sgD);
68
+ }
69
+ let subgroupCount = (WG + sgSize - 1u) / sgSize;
70
+ // Pre-seed every fold slot with the (max, denom) identity. The fold below reads a
71
+ // fixed subgroupCount slots in ordinal order (for determinism), but a slot whose
72
+ // subgroup elects no leader this call — e.g. a fully out-of-window key tile in the
73
+ // flash-attention loop that re-uses this shared memory each iteration — would
74
+ // otherwise read stale shared memory. Identity makes such a slot a no-op.
75
+ // (max identity = -FLT_MAX, denom identity = 0.)
76
+ if (lidx < subgroupCount) {
77
+ partialM[lidx] = -FLT_MAX;
78
+ partialD[lidx] = 0.0;
79
+ }
80
+ workgroupBarrier();
81
+ if (subgroupElect()) {
82
+ let slot = lidx / sgSize;
83
+ partialM[slot] = sgM;
84
+ partialD[slot] = sgD;
85
+ }
86
+ workgroupBarrier();
87
+ if (lidx == 0u) {
88
+ var accM = -FLT_MAX;
89
+ var accD = 0.0;
90
+ for (var i = 0u; i < subgroupCount; i = i + 1u) {
91
+ let mNew = max(accM, partialM[i]);
92
+ accD = accD * exp_shift(accM, mNew) + partialD[i] * exp_shift(partialM[i], mNew);
93
+ accM = mNew;
94
+ }
95
+ combinedMD = vec2<f32>(accM, accD);
96
+ }
97
+ workgroupBarrier();
98
+ return combinedMD;
99
+ }
100
+ {% else %}
101
+ {% set mdStreamed = mdStreams is defined %}
102
+ {% set mdStreams = mdStreams if mdStreams is defined else 1 %}
103
+ {% set mdExtent = "WG" if mdStreams == 1 else "WG * " ~ mdStreams ~ "u" %}
104
+ var<workgroup> partialM: array<f32, {{ mdExtent }}>;
105
+ var<workgroup> partialD: array<f32, {{ mdExtent }}>;
106
+ {% if mdStreamed %}
107
+
108
+ // In-place fold of {{ mdStreams }} streams. The caller stores its per-thread
109
+ // partials into partialM/partialD first and reads the merged pair of stream s
110
+ // from slot s * WG afterwards.
111
+ fn combine_partials_streams(lidx: u32) {
112
+ workgroupBarrier();
113
+ var stride = WG / 2u;
114
+ loop {
115
+ if (stride == 0u) {
116
+ break;
117
+ }
118
+ if (lidx < stride) {
119
+ {% for s in range(mdStreams) %}
120
+ {
121
+ let slot = {{ s }}u * WG + lidx;
122
+ let m1 = partialM[slot];
123
+ let d1 = partialD[slot];
124
+ let m2 = partialM[slot + stride];
125
+ let d2 = partialD[slot + stride];
126
+ let mNew = max(m1, m2);
127
+ partialD[slot] = d1 * exp_shift(m1, mNew) + d2 * exp_shift(m2, mNew);
128
+ partialM[slot] = mNew;
129
+ }
130
+ {% endfor %}
131
+ }
132
+ workgroupBarrier();
133
+ stride = stride / 2u;
134
+ }
135
+ }
136
+ {% else %}
137
+
138
+ fn combine_partials(m: f32, d: f32, lidx: u32) -> vec2<f32> {
139
+ partialM[lidx] = m;
140
+ partialD[lidx] = d;
141
+ workgroupBarrier();
142
+ var stride = WG / 2u;
143
+ loop {
144
+ if (stride == 0u) {
145
+ break;
146
+ }
147
+ if (lidx < stride) {
148
+ let m1 = partialM[lidx];
149
+ let d1 = partialD[lidx];
150
+ let m2 = partialM[lidx + stride];
151
+ let d2 = partialD[lidx + stride];
152
+ let mNew = max(m1, m2);
153
+ partialD[lidx] = d1 * exp_shift(m1, mNew) + d2 * exp_shift(m2, mNew);
154
+ partialM[lidx] = mNew;
155
+ }
156
+ workgroupBarrier();
157
+ stride = stride / 2u;
158
+ }
159
+ let merged = vec2<f32>(partialM[0], partialD[0]);
160
+ // Trailing barrier so back-to-back calls cannot race a next call's partial
161
+ // stores against this call's reads of slot 0.
162
+ workgroupBarrier();
163
+ return merged;
164
+ }
165
+ {% endif %}
166
+ {% endif %}
167
+
168
+
169
+ {% endif %}
170
+ fn bias_index(index: u32) -> u32 {
171
+ let row = index / params.blockSize;
172
+ let column = index % params.blockSize;
173
+ {% if source.isInnerBroadcast %}
174
+ return (row / {{ source.innerRepeat }}u) * params.blockSize + column;
175
+ {% else %}
176
+ return (row % {{ source.biasBlockCount }}u) * params.blockSize + column;
177
+ {% endif %}
178
+ }
179
+
180
+ @compute @workgroup_size(WG)
181
+ fn main(@builtin(workgroup_id) wid: vec3<u32>,
182
+ @builtin(num_workgroups) nwg: vec3<u32>,
183
+ @builtin(local_invocation_id) lid: vec3<u32>{% if combineSubgroups and not packedRows %},
184
+ @builtin(subgroup_size) sgSize: u32{% endif %}) {
185
+ {% if packedRows %}
186
+ // One invocation owns a complete short row. Grid-striding keeps the dispatch
187
+ // one-dimensional and bounded even for very large batch counts.
188
+ for (var row = wid.x * WG + lid.x; row < params.batchCount; row += nwg.x * WG) {
189
+ let base = row * params.blockSize;
190
+ {% if source.blockSize == 2 %}
191
+ let i0 = base;
192
+ let i1 = base + 1u;
193
+ let v0 = f32(data[i0]) + f32(bias[bias_index(i0)]);
194
+ let v1 = f32(data[i1]) + f32(bias[bias_index(i1)]);
195
+
196
+ // The overwhelmingly common two-element case is a logistic pair. Choose
197
+ // the non-positive exponent branch for stability and use one exp total.
198
+ // Non-finite values retain the generic kernel's established semantics.
199
+ if (is_finite_f32(v0) && is_finite_f32(v1)) {
200
+ if (v0 >= v1) {
201
+ let e = exp(v1 - v0);
202
+ let inv = 1.0 / (1.0 + e);
203
+ output[i0] = {{ scalar }}(inv);
204
+ output[i1] = {{ scalar }}(e * inv);
205
+ } else {
206
+ let e = exp(v0 - v1);
207
+ let inv = 1.0 / (1.0 + e);
208
+ output[i0] = {{ scalar }}(e * inv);
209
+ output[i1] = {{ scalar }}(inv);
210
+ }
211
+ } else {
212
+ var m = -FLT_MAX;
213
+ var denom = 0.0;
214
+ let newM0 = max(m, v0);
215
+ denom = denom * exp_shift(m, newM0) + exp_shift(v0, newM0);
216
+ m = newM0;
217
+ let newM1 = max(m, v1);
218
+ denom = denom * exp_shift(m, newM1) + exp_shift(v1, newM1);
219
+ m = newM1;
220
+ let inv = select(0.0, 1.0 / denom, denom > 0.0);
221
+ output[i0] = {{ scalar }}(exp_shift(v0, m) * inv);
222
+ output[i1] = {{ scalar }}(exp_shift(v1, m) * inv);
223
+ }
224
+ {% else %}
225
+ // Short rows fit in private registers, so the normalization sweep does not
226
+ // reread data or bias from storage.
227
+ var values: array<f32, {{ source.blockSize }}>;
228
+ var m = -FLT_MAX;
229
+ var denom = 0.0;
230
+ for (var dd = 0u; dd < params.blockSize; dd += 1u) {
231
+ let i = base + dd;
232
+ let v = f32(data[i]) + f32(bias[bias_index(i)]);
233
+ values[dd] = v;
234
+ let new_m = max(m, v);
235
+ denom = denom * exp_shift(m, new_m) + exp_shift(v, new_m);
236
+ m = new_m;
237
+ }
238
+ let inv = select(0.0, 1.0 / denom, denom > 0.0);
239
+ for (var dd = 0u; dd < params.blockSize; dd += 1u) {
240
+ let i = base + dd;
241
+ output[i] = {{ scalar }}(exp_shift(values[dd], m) * inv);
242
+ }
243
+ {% endif %}
244
+ }
245
+ {% else %}
246
+ // 2D-folded row index: wid.y carries the high bits past the maxComputeWorkgroupsPerDimension
247
+ // workgroup-per-dimension dispatch limit. Reduces to wid.x when nwg.y == 1;
248
+ // the row >= params.batchCount guard drops the over-dispatched tail.
249
+ let row = wid.x + wid.y * nwg.x;
250
+ if (row >= params.batchCount) {
251
+ return;
252
+ }
253
+ let base = row * params.blockSize;
254
+ let tid = lid.x;
255
+
256
+ // Single online (m, d) stats pass over (data + bias), then one cooperative
257
+ // combine. The row is read once for stats and once for normalization.
258
+ var m = -FLT_MAX;
259
+ var denom = 0.0;
260
+ for (var dd = tid; dd < params.blockSize; dd = dd + WG) {
261
+ let i = base + dd;
262
+ let v = f32(data[i]) + f32(bias[bias_index(i)]);
263
+ let new_m = max(m, v);
264
+ denom = denom * exp_shift(m, new_m) + exp_shift(v, new_m);
265
+ m = new_m;
266
+ }
267
+ {% if combineSubgroups %}
268
+ let md = combine_partials(m, denom, tid, sgSize);
269
+ {% else %}
270
+ let md = combine_partials(m, denom, tid);
271
+ {% endif %}
272
+ let rowMax = md.x;
273
+ let inv = select(0.0, 1.0 / md.y, md.y > 0.0);
274
+
275
+ for (var dd = tid; dd < params.blockSize; dd = dd + WG) {
276
+ let i = base + dd;
277
+ let v = f32(data[i]) + f32(bias[bias_index(i)]);
278
+ output[i] = {{ scalar }}(exp_shift(v, rowMax) * inv);
279
+ }
280
+ {% endif %}
281
+ }
build/webgpu/manifest.json ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "domain": "com.microsoft",
3
+ "name": "BiasSoftmax",
4
+ "sinceVersion": 1,
5
+ "description": "Computes `softmax(data + bias)` over the flattened suffix beginning at `axis`. The required `is_inner_broadcast` attribute selects how bias rows are reused: consecutive groups for inner broadcast or cyclic groups for outer broadcast. This specializes the `softmax(scores + additive_mask)` pattern used by transformer attention. Float16 and float32 are supported; the schema's double type is not.",
6
+ "inputs": [
7
+ { "role": "data", "dtype": "T", "description": "The input data tensor." },
8
+ {
9
+ "role": "bias",
10
+ "dtype": "T",
11
+ "description": "The bias (or additive mask) tensor. Its element count must be an integral number of flattened softmax rows and that row count must divide the data row count."
12
+ }
13
+ ],
14
+ "outputs": [
15
+ {
16
+ "role": "output",
17
+ "dtype": "T",
18
+ "rank": "ranks.data",
19
+ "shape": "shapes.data",
20
+ "description": "The output tensor; same shape as data."
21
+ }
22
+ ],
23
+ "attributes": { "axis": 1 },
24
+ "attributeConstraints": { "is_inner_broadcast": { "required": true } },
25
+ "attributeDescriptions": {
26
+ "axis": "The axis from which softmax is applied; dimensions from `axis` onward are included in the softmax reduction.",
27
+ "is_inner_broadcast": "When 1, bias is broadcast across dimensions from `broadcast_axis` to `axis-1`; when 0, bias is broadcast across dimensions 0 to `broadcast_axis-1`."
28
+ },
29
+ "typeConstraints": { "T": ["float32", "float16"] },
30
+ "args": {
31
+ "data": { "kind": "tensor", "semantic": "data", "role": "input" },
32
+ "bias": { "kind": "tensor", "semantic": "bias", "role": "input" },
33
+ "output": { "kind": "tensor", "semantic": "output", "role": "output" }
34
+ },
35
+ "derive": {
36
+ "axisNorm": "attrs.axis if attrs.axis >= 0 else attrs.axis + ranks.data",
37
+ "batchCount": "outer(shapes.data, axisNorm)",
38
+ "blockSize": "dim(shapes.data, axisNorm) * inner(shapes.data, axisNorm)",
39
+ "biasBlockCount": "numel(shapes.bias) / max(1, blockSize)",
40
+ "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)"
41
+ },
42
+ "tunables": { "WORKGROUP_SIZE": 256, "BLOCK_COLS": 2048 },
43
+ "bindingSets": {
44
+ "biasSoftmax": [
45
+ {
46
+ "name": "data",
47
+ "arg": "data",
48
+ "semantic": "data",
49
+ "buffer": { "type": "read-only-storage" },
50
+ "elementType": "$scalar"
51
+ },
52
+ {
53
+ "name": "bias",
54
+ "arg": "bias",
55
+ "semantic": "bias",
56
+ "buffer": { "type": "read-only-storage" },
57
+ "elementType": "$scalar"
58
+ },
59
+ {
60
+ "name": "output",
61
+ "arg": "output",
62
+ "semantic": "output",
63
+ "buffer": { "type": "storage" },
64
+ "elementType": "$scalar"
65
+ },
66
+ {
67
+ "name": "params",
68
+ "semantic": "kernel.params",
69
+ "buffer": { "type": "uniform" },
70
+ "struct": {
71
+ "name": "Params",
72
+ "fields": [
73
+ { "name": "blockSize", "type": "u32", "value": "blockSize" },
74
+ { "name": "batchCount", "type": "u32", "value": "batchCount" }
75
+ ]
76
+ }
77
+ }
78
+ ]
79
+ },
80
+ "variants": [
81
+ {
82
+ "id": "longrow_split",
83
+ "priority": 40,
84
+ "when": ["numel(shapes.data) == numel(shapes.output)", "ranks.data >= 1", "attrs.axis + ranks.data >= 0", "attrs.axis < ranks.data", "biasContract", "blockSize >= 65536", "batchCount > 0", "batchCount <= device.limits.maxComputeWorkgroupsPerDimension", "ceilDiv(blockSize, tunables.BLOCK_COLS) <= device.limits.maxComputeWorkgroupsPerDimension", "f16Ok(dtypes.T)"],
85
+ "constants": { "scalar": "dtypes.T", "usesF16": "dtypes.T == \"f16\"", "combineSubgroups": false },
86
+ "intermediates": [
87
+ {
88
+ "id": "blockMax",
89
+ "dtype": "float32",
90
+ "shape": "[outer(shapes.data, axisNorm) * ceilDiv(dim(shapes.data, axisNorm) * inner(shapes.data, axisNorm), tunables.BLOCK_COLS)]"
91
+ },
92
+ {
93
+ "id": "blockSum",
94
+ "dtype": "float32",
95
+ "shape": "[outer(shapes.data, axisNorm) * ceilDiv(dim(shapes.data, axisNorm) * inner(shapes.data, axisNorm), tunables.BLOCK_COLS)]"
96
+ },
97
+ { "id": "rowMax", "dtype": "float32", "shape": "[outer(shapes.data, axisNorm)]" },
98
+ { "id": "rowSum", "dtype": "float32", "shape": "[outer(shapes.data, axisNorm)]" }
99
+ ],
100
+ "passes": [
101
+ {
102
+ "id": "block_stats",
103
+ "name": "BiasSoftmax.LongRowBlockStats",
104
+ "source": {
105
+ "shader": "bias-softmax-longrow-stats.wgsl.jinja",
106
+ "inputs": {
107
+ "stage": "\"block\"",
108
+ "isInnerBroadcast": "attrs.is_inner_broadcast != 0",
109
+ "biasBlockCount": "max(1, biasBlockCount)",
110
+ "innerRepeat": "max(1, batchCount / max(1, biasBlockCount))"
111
+ }
112
+ },
113
+ "bindings": [
114
+ {
115
+ "name": "data",
116
+ "arg": "data",
117
+ "semantic": "data",
118
+ "buffer": { "type": "read-only-storage" },
119
+ "elementType": "$scalar"
120
+ },
121
+ {
122
+ "name": "bias",
123
+ "arg": "bias",
124
+ "semantic": "bias",
125
+ "buffer": { "type": "read-only-storage" },
126
+ "elementType": "$scalar"
127
+ },
128
+ { "name": "blockMax", "semantic": "blockMax", "buffer": { "type": "storage" }, "elementType": "f32" },
129
+ { "name": "blockSum", "semantic": "blockSum", "buffer": { "type": "storage" }, "elementType": "f32" },
130
+ {
131
+ "name": "params",
132
+ "semantic": "kernel.params",
133
+ "buffer": { "type": "uniform" },
134
+ "struct": {
135
+ "name": "Params",
136
+ "fields": [
137
+ { "name": "blockSize", "type": "u32", "value": "blockSize" },
138
+ { "name": "blocks", "type": "u32", "value": "ceilDiv(blockSize, tunables.BLOCK_COLS)" }
139
+ ]
140
+ }
141
+ }
142
+ ],
143
+ "dispatch": { "x": "ceilDiv(blockSize, tunables.BLOCK_COLS)", "y": "batchCount" }
144
+ },
145
+ {
146
+ "id": "row_stats",
147
+ "name": "BiasSoftmax.LongRowStats",
148
+ "source": { "shader": "bias-softmax-longrow-stats.wgsl.jinja", "inputs": { "stage": "\"row\"" } },
149
+ "bindings": [
150
+ {
151
+ "name": "blockMax",
152
+ "semantic": "blockMax",
153
+ "buffer": { "type": "read-only-storage" },
154
+ "elementType": "f32"
155
+ },
156
+ {
157
+ "name": "blockSum",
158
+ "semantic": "blockSum",
159
+ "buffer": { "type": "read-only-storage" },
160
+ "elementType": "f32"
161
+ },
162
+ { "name": "rowMax", "semantic": "rowMax", "buffer": { "type": "storage" }, "elementType": "f32" },
163
+ { "name": "rowSum", "semantic": "rowSum", "buffer": { "type": "storage" }, "elementType": "f32" },
164
+ {
165
+ "name": "params",
166
+ "semantic": "kernel.params",
167
+ "buffer": { "type": "uniform" },
168
+ "struct": {
169
+ "name": "Params",
170
+ "fields": [{ "name": "blocks", "type": "u32", "value": "ceilDiv(blockSize, tunables.BLOCK_COLS)" }]
171
+ }
172
+ }
173
+ ],
174
+ "dispatch": { "x": "batchCount" }
175
+ },
176
+ {
177
+ "id": "normalize",
178
+ "name": "BiasSoftmax.LongRowNormalize",
179
+ "source": {
180
+ "shader": "bias-softmax-longrow-normalize.wgsl.jinja",
181
+ "inputs": {
182
+ "isInnerBroadcast": "attrs.is_inner_broadcast != 0",
183
+ "biasBlockCount": "max(1, biasBlockCount)",
184
+ "innerRepeat": "max(1, batchCount / max(1, biasBlockCount))"
185
+ }
186
+ },
187
+ "bindings": [
188
+ {
189
+ "name": "data",
190
+ "arg": "data",
191
+ "semantic": "data",
192
+ "buffer": { "type": "read-only-storage" },
193
+ "elementType": "$scalar"
194
+ },
195
+ {
196
+ "name": "bias",
197
+ "arg": "bias",
198
+ "semantic": "bias",
199
+ "buffer": { "type": "read-only-storage" },
200
+ "elementType": "$scalar"
201
+ },
202
+ { "name": "rowMax", "semantic": "rowMax", "buffer": { "type": "read-only-storage" }, "elementType": "f32" },
203
+ { "name": "rowSum", "semantic": "rowSum", "buffer": { "type": "read-only-storage" }, "elementType": "f32" },
204
+ {
205
+ "name": "output",
206
+ "arg": "output",
207
+ "semantic": "output",
208
+ "buffer": { "type": "storage" },
209
+ "elementType": "$scalar"
210
+ },
211
+ {
212
+ "name": "params",
213
+ "semantic": "kernel.params",
214
+ "buffer": { "type": "uniform" },
215
+ "struct": { "name": "Params", "fields": [{ "name": "blockSize", "type": "u32", "value": "blockSize" }] }
216
+ }
217
+ ],
218
+ "dispatch": { "x": "ceilDiv(blockSize, tunables.BLOCK_COLS)", "y": "batchCount" }
219
+ }
220
+ ]
221
+ },
222
+ {
223
+ "id": "packed_rows",
224
+ "priority": 30,
225
+ "when": ["numel(shapes.data) == numel(shapes.output)", "ranks.data >= 1", "attrs.axis + ranks.data >= 0", "attrs.axis < ranks.data", "biasContract", "blockSize > 0", "blockSize <= 8", "batchCount >= 64", "f16Ok(dtypes.T)"],
226
+ "constants": {
227
+ "scalar": "dtypes.T",
228
+ "usesF16": "dtypes.T == \"f16\"",
229
+ "combineSubgroups": false,
230
+ "packedRows": true
231
+ },
232
+ "passes": [
233
+ {
234
+ "id": "main",
235
+ "name": "BiasSoftmax.PackedRows",
236
+ "source": {
237
+ "shader": "bias-softmax.wgsl.jinja",
238
+ "inputs": {
239
+ "blockSize": "blockSize",
240
+ "isInnerBroadcast": "attrs.is_inner_broadcast != 0",
241
+ "biasBlockCount": "max(1, biasBlockCount)",
242
+ "innerRepeat": "max(1, batchCount / max(1, biasBlockCount))"
243
+ }
244
+ },
245
+ "bindings": "biasSoftmax",
246
+ "dispatch": { "gridStride": "batchCount", "workgroupSize": "tunables.WORKGROUP_SIZE" }
247
+ }
248
+ ]
249
+ },
250
+ {
251
+ "id": "adaptive_row",
252
+ "priority": 10,
253
+ "when": ["numel(shapes.data) == numel(shapes.output)", "ranks.data >= 1", "attrs.axis + ranks.data >= 0", "attrs.axis < ranks.data", "biasContract", "numel(shapes.data) >= 0", "f16Ok(dtypes.T)"],
254
+ "constants": {
255
+ "packedRows": false,
256
+ "scalar": "dtypes.T",
257
+ "usesF16": "dtypes.T == \"f16\"",
258
+ "combineSubgroups": "device.features.has(\"subgroups\")"
259
+ },
260
+ "passes": [
261
+ {
262
+ "id": "main",
263
+ "name": "BiasSoftmax.AdaptiveRow",
264
+ "source": {
265
+ "shader": "bias-softmax.wgsl.jinja",
266
+ "inputs": {
267
+ "blockSize": "blockSize",
268
+ "isInnerBroadcast": "attrs.is_inner_broadcast != 0",
269
+ "biasBlockCount": "max(1, biasBlockCount)",
270
+ "innerRepeat": "max(1, batchCount / max(1, biasBlockCount))"
271
+ }
272
+ },
273
+ "bindings": "biasSoftmax",
274
+ "dispatch": { "workgroups": "batchCount" }
275
+ }
276
+ ]
277
+ }
278
+ ]
279
+ }
build/webgpu/metadata.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "com.microsoft.BiasSoftmax",
3
+ "id": "_com_microsoft_biassoftmax_webgpu_d835fbf",
4
+ "version": 1,
5
+ "license": "Apache-2.0",
6
+ "backend": { "type": "webgpu" },
7
+ "digest": {
8
+ "algorithm": "sha256",
9
+ "files": {
10
+ "bench.json": "byPgkRP7DSHGPJmC+gIf4Ataa/cGEumT0wZKQTNmAbc=",
11
+ "bias-softmax-longrow-normalize.wgsl.jinja": "as8qI0C8JI2Hv60Gubt6JYGw7b2viGi8HjugoaR6sIM=",
12
+ "bias-softmax-longrow-stats.wgsl.jinja": "etXAjT/0FmGx03+aKetnioDZrZuZPxryTqxTDp2d5nY=",
13
+ "bias-softmax.wgsl.jinja": "JCf3tYMWpCyp8PRZMRx8q52g8Kg5CdcX0qzWX6F1w5M=",
14
+ "manifest.json": "Hmp1oerHvNRx2b3UbkPVfUJUK+xDiuZOFGY5e8ej5h0=",
15
+ "test.json": "tPesDd/IQUsZCm/THfvKdxo+IMX0k7DLG/Ggay8AFnk="
16
+ }
17
+ },
18
+ "provenance": { "kernel": { "sha": "2e7068faf55e7f43df740015f6d1ee49391a41c5", "dirty": false } },
19
+ "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/com.microsoft.BiasSoftmax" }
20
+ }
build/webgpu/test.json ADDED
@@ -0,0 +1,442 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "op": "com.microsoft.BiasSoftmax",
3
+ "cases": [
4
+ {
5
+ "name": "inner_broadcast_consecutive_row_groups",
6
+ "provenance": {
7
+ "source": "onnxruntime/contrib_ops/cuda/math/bias_softmax_impl.cu",
8
+ "notes": "Distinguishes ORT's flattened inner-broadcast row grouping from both outer mode and NumPy right-aligned broadcasting."
9
+ },
10
+ "attrs": { "axis": -1, "is_inner_broadcast": 1 },
11
+ "inputs": {
12
+ "data": { "dtype": "float32", "shape": [2, 3, 2], "data": { "kind": "constant", "value": 0.0 } },
13
+ "bias": { "dtype": "float32", "shape": [2, 2], "data": { "kind": "values", "values": [0.0, 2.0, 2.0, 0.0] } }
14
+ },
15
+ "outputs": {
16
+ "output": {
17
+ "dtype": "float32",
18
+ "shape": [2, 3, 2],
19
+ "tolerance": 0.000001,
20
+ "relTolerance": 0.000001,
21
+ "data": {
22
+ "kind": "values",
23
+ "values": [0.119202922022, 0.880797077978, 0.119202922022, 0.880797077978, 0.119202922022, 0.880797077978, 0.880797077978, 0.119202922022, 0.880797077978, 0.119202922022, 0.880797077978, 0.119202922022]
24
+ }
25
+ }
26
+ }
27
+ },
28
+ {
29
+ "name": "outer_broadcast_cycles_bias_rows",
30
+ "provenance": {
31
+ "source": "onnxruntime/contrib_ops/cuda/math/bias_softmax_impl.cu",
32
+ "notes": "Uses the same non-NumPy-broadcastable shapes as the inner case and pins ORT's cyclic outer-broadcast row selection."
33
+ },
34
+ "attrs": { "axis": -1, "is_inner_broadcast": 0 },
35
+ "inputs": {
36
+ "data": { "dtype": "float32", "shape": [2, 3, 2], "data": { "kind": "constant", "value": 0.0 } },
37
+ "bias": { "dtype": "float32", "shape": [2, 2], "data": { "kind": "values", "values": [0.0, 2.0, 2.0, 0.0] } }
38
+ },
39
+ "outputs": {
40
+ "output": {
41
+ "dtype": "float32",
42
+ "shape": [2, 3, 2],
43
+ "tolerance": 0.000001,
44
+ "relTolerance": 0.000001,
45
+ "data": {
46
+ "kind": "values",
47
+ "values": [0.119202922022, 0.880797077978, 0.880797077978, 0.119202922022, 0.119202922022, 0.880797077978, 0.880797077978, 0.119202922022, 0.119202922022, 0.880797077978, 0.880797077978, 0.119202922022]
48
+ }
49
+ }
50
+ }
51
+ },
52
+ {
53
+ "name": "ort_inner_broadcast_full_suffix_softmax",
54
+ "provenance": {
55
+ "source": "onnxruntime/test/contrib_ops/bias_softmax_op_test.cc",
56
+ "test": "BiasSoftmaxTest.InnerBroadcastFullBiasBatch",
57
+ "notes": "Small deterministic projection of ORT's suffix softmax semantics."
58
+ },
59
+ "attrs": { "axis": 1, "is_inner_broadcast": 1 },
60
+ "inputs": {
61
+ "data": { "dtype": "float32", "shape": [1, 2, 2], "data": { "kind": "values", "values": [0.0, 0.0, 0.0, 0.0] } },
62
+ "bias": { "dtype": "float32", "shape": [1, 2, 2], "data": { "kind": "values", "values": [0.0, 1.0, 2.0, 3.0] } }
63
+ },
64
+ "outputs": {
65
+ "output": {
66
+ "dtype": "float32",
67
+ "shape": [1, 2, 2],
68
+ "tolerance": 0.000001,
69
+ "relTolerance": 0.000001,
70
+ "data": { "kind": "values", "values": [0.03205860328, 0.087144318742, 0.23688281809, 0.643914259888] }
71
+ }
72
+ }
73
+ },
74
+ {
75
+ "name": "ort_inner_broadcast_empty_bias_batch",
76
+ "provenance": {
77
+ "source": "onnxruntime/test/contrib_ops/bias_softmax_op_test.cc",
78
+ "test": "BiasSoftmaxTest.InnerBroadcastEmptyBiasBatch",
79
+ "notes": "Compact deterministic projection where one bias batch broadcasts across two input batches."
80
+ },
81
+ "attrs": { "axis": 1, "is_inner_broadcast": 1 },
82
+ "inputs": {
83
+ "data": {
84
+ "dtype": "float32",
85
+ "shape": [2, 2, 2],
86
+ "data": { "kind": "values", "values": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] }
87
+ },
88
+ "bias": { "dtype": "float32", "shape": [1, 2, 2], "data": { "kind": "values", "values": [0.0, 1.0, 2.0, 3.0] } }
89
+ },
90
+ "outputs": {
91
+ "output": {
92
+ "dtype": "float32",
93
+ "shape": [2, 2, 2],
94
+ "tolerance": 0.000001,
95
+ "relTolerance": 0.000001,
96
+ "data": {
97
+ "kind": "values",
98
+ "values": [0.03205860328, 0.087144318742, 0.23688281809, 0.643914259888, 0.03205860328, 0.087144318742, 0.23688281809, 0.643914259888]
99
+ }
100
+ }
101
+ }
102
+ },
103
+ {
104
+ "name": "ort_inner_broadcast_negative_axis",
105
+ "provenance": {
106
+ "source": "onnxruntime/test/contrib_ops/bias_softmax_op_test.cc",
107
+ "test": "BiasSoftmaxTest.InnerBroadcastNegativeAxis",
108
+ "notes": "Small deterministic projection of ORT's negative-axis inner broadcast pattern."
109
+ },
110
+ "attrs": { "axis": -2, "is_inner_broadcast": 1 },
111
+ "inputs": {
112
+ "data": {
113
+ "dtype": "float32",
114
+ "shape": [2, 2, 2],
115
+ "data": { "kind": "values", "values": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] }
116
+ },
117
+ "bias": { "dtype": "float32", "shape": [1, 2, 2], "data": { "kind": "values", "values": [0.0, 1.0, 2.0, 3.0] } }
118
+ },
119
+ "outputs": {
120
+ "output": {
121
+ "dtype": "float32",
122
+ "shape": [2, 2, 2],
123
+ "tolerance": 0.000001,
124
+ "relTolerance": 0.000001,
125
+ "data": {
126
+ "kind": "values",
127
+ "values": [0.03205860328, 0.087144318742, 0.23688281809, 0.643914259888, 0.03205860328, 0.087144318742, 0.23688281809, 0.643914259888]
128
+ }
129
+ }
130
+ }
131
+ },
132
+ {
133
+ "name": "ort_outer_broadcast_negative_axis",
134
+ "provenance": {
135
+ "source": "onnxruntime/test/contrib_ops/bias_softmax_op_test.cc",
136
+ "test": "BiasSoftmaxTest.OuterBroadcastNegativeAxis",
137
+ "notes": "Small deterministic projection of ORT's negative-axis outer broadcast pattern."
138
+ },
139
+ "attrs": { "axis": -1, "is_inner_broadcast": 0 },
140
+ "inputs": {
141
+ "data": {
142
+ "dtype": "float32",
143
+ "shape": [2, 2, 3],
144
+ "data": { "kind": "values", "values": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] }
145
+ },
146
+ "bias": {
147
+ "dtype": "float32",
148
+ "shape": [2, 3],
149
+ "data": { "kind": "values", "values": [0.0, 1.0, 2.0, 2.0, 1.0, 0.0] }
150
+ }
151
+ },
152
+ "outputs": {
153
+ "output": {
154
+ "dtype": "float32",
155
+ "shape": [2, 2, 3],
156
+ "tolerance": 0.000001,
157
+ "relTolerance": 0.000001,
158
+ "data": {
159
+ "kind": "values",
160
+ "values": [0.09003057317, 0.244728471055, 0.665240955775, 0.665240955775, 0.244728471055, 0.09003057317, 0.09003057317, 0.244728471055, 0.665240955775, 0.665240955775, 0.244728471055, 0.09003057317]
161
+ }
162
+ }
163
+ }
164
+ },
165
+ {
166
+ "name": "ort_outer_broadcast_full_bias_batch",
167
+ "provenance": {
168
+ "source": "onnxruntime/test/contrib_ops/bias_softmax_op_test.cc",
169
+ "test": "BiasSoftmaxTest.OuterBroadcastFullBiasBatch",
170
+ "notes": "Compact deterministic projection where every input batch has its own full bias row."
171
+ },
172
+ "attrs": { "axis": -1, "is_inner_broadcast": 0 },
173
+ "inputs": {
174
+ "data": {
175
+ "dtype": "float32",
176
+ "shape": [2, 2, 3],
177
+ "data": { "kind": "values", "values": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] }
178
+ },
179
+ "bias": {
180
+ "dtype": "float32",
181
+ "shape": [2, 2, 3],
182
+ "data": { "kind": "values", "values": [0.0, 1.0, 2.0, 2.0, 1.0, 0.0, -1.0, 0.0, 1.0, 1.0, 0.0, -1.0] }
183
+ }
184
+ },
185
+ "outputs": {
186
+ "output": {
187
+ "dtype": "float32",
188
+ "shape": [2, 2, 3],
189
+ "tolerance": 0.000001,
190
+ "relTolerance": 0.000001,
191
+ "data": {
192
+ "kind": "values",
193
+ "values": [0.09003057317, 0.244728471055, 0.665240955775, 0.665240955775, 0.244728471055, 0.09003057317, 0.09003057317, 0.244728471055, 0.665240955775, 0.665240955775, 0.244728471055, 0.09003057317]
194
+ }
195
+ }
196
+ }
197
+ },
198
+ {
199
+ "name": "f32_large_gap_subnormal_tail_gpu_gap",
200
+ "skipGpu": {
201
+ "category": "permanent",
202
+ "reason": "Portable WGSL floating-point semantics do not guarantee preservation of the subnormal values required by this fixture. Backend evidence: WebGPU/Metal flushes subnormals to zero in f32; the 87.5-point logit gap makes the tail probability subnormal, which collapses to zero instead of ORT's positive subnormal value."
203
+ },
204
+ "provenance": {
205
+ "source": "onnxruntime/test/contrib_ops/bias_softmax_op_test.cc",
206
+ "test": "BiasSoftmaxTest.InnerBroadcastFullBiasBatch",
207
+ "notes": "BiasSoftmax is a suffix softmax over data+bias; an 87.5-point gap leaves a valid positive subnormal probability tail in ORT CPU."
208
+ },
209
+ "attrs": { "axis": 1, "is_inner_broadcast": 1 },
210
+ "inputs": {
211
+ "data": { "dtype": "float32", "shape": [1, 2], "data": { "kind": "values", "values": [0.0, -87.5] } },
212
+ "bias": { "dtype": "float32", "shape": [1, 2], "data": { "kind": "values", "values": [0.0, 0.0] } }
213
+ },
214
+ "outputs": {
215
+ "output": {
216
+ "dtype": "float32",
217
+ "shape": [1, 2],
218
+ "tolerance": 2e-45,
219
+ "relTolerance": 0,
220
+ "data": { "kind": "values", "values": [1.0, 9.982351397596697e-39] }
221
+ }
222
+ }
223
+ },
224
+ {
225
+ "name": "ort_outer_broadcast_empty_bias_batch",
226
+ "provenance": {
227
+ "source": "onnxruntime/test/contrib_ops/bias_softmax_op_test.cc",
228
+ "test": "BiasSoftmaxTest.OuterBroadcastEmptyBiasBatch",
229
+ "notes": "Compact deterministic projection where one outer-broadcast bias row is reused for every input batch."
230
+ },
231
+ "attrs": { "axis": -1, "is_inner_broadcast": 0 },
232
+ "inputs": {
233
+ "data": {
234
+ "dtype": "float32",
235
+ "shape": [2, 2, 3],
236
+ "data": { "kind": "values", "values": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] }
237
+ },
238
+ "bias": { "dtype": "float32", "shape": [1, 3], "data": { "kind": "values", "values": [0.0, 1.0, 2.0] } }
239
+ },
240
+ "outputs": {
241
+ "output": {
242
+ "dtype": "float32",
243
+ "shape": [2, 2, 3],
244
+ "tolerance": 0.000001,
245
+ "relTolerance": 0.000001,
246
+ "data": {
247
+ "kind": "values",
248
+ "values": [0.09003057317, 0.244728471055, 0.665240955775, 0.09003057317, 0.244728471055, 0.665240955775, 0.09003057317, 0.244728471055, 0.665240955775, 0.09003057317, 0.244728471055, 0.665240955775]
249
+ }
250
+ }
251
+ }
252
+ },
253
+ {
254
+ "name": "empty_zero_dim",
255
+ "attrs": { "axis": 1, "is_inner_broadcast": 1 },
256
+ "inputs": {
257
+ "data": { "dtype": "float32", "shape": [0, 2, 2], "data": { "kind": "values", "values": [] } },
258
+ "bias": { "dtype": "float32", "shape": [0, 2, 2], "data": { "kind": "values", "values": [] } }
259
+ },
260
+ "outputs": { "output": { "dtype": "float32", "shape": [0, 2, 2], "tolerance": 0 } }
261
+ },
262
+ {
263
+ "name": "f16_default_axis_two_element_row",
264
+ "attrs": { "is_inner_broadcast": 1 },
265
+ "inputs": {
266
+ "data": { "dtype": "float16", "shape": [1, 2], "data": { "kind": "values", "values": [0.0, 0.0] } },
267
+ "bias": { "dtype": "float16", "shape": [1, 2], "data": { "kind": "values", "values": [0.0, 0.0] } }
268
+ },
269
+ "outputs": {
270
+ "output": {
271
+ "dtype": "float16",
272
+ "shape": [1, 2],
273
+ "tolerance": 0,
274
+ "data": { "kind": "values", "values": [0.5, 0.5] }
275
+ }
276
+ }
277
+ },
278
+ {
279
+ "name": "dispatch_cliff_rows_65537",
280
+ "provenance": {
281
+ "notes": "65,537 short rows cross the per-dimension workgroup limit. The selected packed route keeps the dispatch small and grid-strides; an all-variant run also retains coverage of the generic kernel's 2D-folded row reconstruction."
282
+ },
283
+ "attrs": { "axis": 1, "is_inner_broadcast": 0 },
284
+ "inputs": {
285
+ "data": {
286
+ "dtype": "float32",
287
+ "shape": [65537, 4],
288
+ "data": { "kind": "cycle", "values": [0.1, 0.2, 0.3, 0.4, 0.5] }
289
+ },
290
+ "bias": { "dtype": "float32", "shape": [65537, 4], "data": { "kind": "constant", "value": 0.0 } }
291
+ },
292
+ "outputs": { "output": { "dtype": "float32", "shape": [65537, 4], "tolerance": 0.0001 } }
293
+ },
294
+ {
295
+ "name": "fully_masked_row_neg_inf_bias",
296
+ "provenance": {
297
+ "notes": "Fully additive-masked softmax row (bias = -Infinity over the whole suffix block). GPU (FLT_MAX-init online softmax), the TS reference, ai.onnx.Softmax, and ORT's WebGPU EP all emit 0 for a fully-masked row (a NaN would poison attention). Pinned to 0."
298
+ },
299
+ "attrs": { "axis": 1, "is_inner_broadcast": 0 },
300
+ "inputs": {
301
+ "data": { "dtype": "float32", "shape": [1, 4], "data": { "kind": "values", "values": [0.0, 0.0, 0.0, 0.0] } },
302
+ "bias": {
303
+ "dtype": "float32",
304
+ "shape": [1, 4],
305
+ "data": { "kind": "values", "values": ["-Infinity", "-Infinity", "-Infinity", "-Infinity"] }
306
+ }
307
+ },
308
+ "outputs": {
309
+ "output": {
310
+ "dtype": "float32",
311
+ "shape": [1, 4],
312
+ "tolerance": 0.000001,
313
+ "data": { "kind": "values", "values": [0.0, 0.0, 0.0, 0.0] }
314
+ }
315
+ }
316
+ },
317
+ {
318
+ "name": "axis0_full_reduce_single_block",
319
+ "provenance": {
320
+ "notes": "axis=0 => outer(data,0)=1 => one softmax block over the whole tensor (batchCount=1, blockSize=numel). ORT requires one complete flattened bias row, so bias has the full [4,6] shape."
321
+ },
322
+ "attrs": { "axis": 0, "is_inner_broadcast": 0 },
323
+ "inputs": {
324
+ "data": { "dtype": "float32", "shape": [4, 6], "data": { "kind": "linspace", "start": -3.0, "end": 3.0 } },
325
+ "bias": {
326
+ "dtype": "float32",
327
+ "shape": [4, 6],
328
+ "data": {
329
+ "kind": "values",
330
+ "values": [0.5, -0.5, 1.0, -1.0, 0.25, -0.25, 0.5, -0.5, 1.0, -1.0, 0.25, -0.25, 0.5, -0.5, 1.0, -1.0, 0.25, -0.25, 0.5, -0.5, 1.0, -1.0, 0.25, -0.25]
331
+ }
332
+ }
333
+ },
334
+ "outputs": { "output": { "dtype": "float32", "shape": [4, 6], "tolerance": 0.000001, "relTolerance": 0.000001 } }
335
+ },
336
+ {
337
+ "name": "bias_fold_skip_middle_dim_axis1",
338
+ "provenance": {
339
+ "notes": "bias [1,3,4] broadcast over batch axis of data [2,3,4], axis=1 (suffix softmax over 3*4=12). Exercises the compiled bias_index() fold when a size-1 bias axis is skipped and the reduction spans multiple axes. Truth from the TS reference."
340
+ },
341
+ "attrs": { "axis": 1, "is_inner_broadcast": 0 },
342
+ "inputs": {
343
+ "data": { "dtype": "float32", "shape": [2, 3, 4], "data": { "kind": "linspace", "start": -2.0, "end": 2.0 } },
344
+ "bias": { "dtype": "float32", "shape": [1, 3, 4], "data": { "kind": "linspace", "start": -1.0, "end": 1.0 } }
345
+ },
346
+ "outputs": {
347
+ "output": { "dtype": "float32", "shape": [2, 3, 4], "tolerance": 0.000001, "relTolerance": 0.000001 }
348
+ }
349
+ },
350
+ {
351
+ "name": "many_two_element_rows_axis1",
352
+ "provenance": {
353
+ "notes": "Compact companion for bench case biassoftmax-f32-launchbound-262144x2-axis1: many tiny softmax rows stress the scalar-row fallback and folded dispatch accounting."
354
+ },
355
+ "attrs": { "axis": 1, "is_inner_broadcast": 0 },
356
+ "inputs": {
357
+ "data": {
358
+ "dtype": "float32",
359
+ "shape": [4096, 2],
360
+ "data": { "kind": "fillFloat32", "scale": 0.25, "sinStep": 0.17, "cosStep": 0.31 }
361
+ },
362
+ "bias": { "dtype": "float32", "shape": [1, 2], "data": { "kind": "values", "values": [0.125, -0.25] } }
363
+ },
364
+ "outputs": {
365
+ "output": { "dtype": "float32", "shape": [4096, 2], "tolerance": 0.000001, "relTolerance": 0.000001 }
366
+ }
367
+ },
368
+ {
369
+ "name": "longrow_split_inner_broadcast_axis1_4x65536_groups2",
370
+ "provenance": {
371
+ "notes": "A long split row with consecutive inner-broadcast groups checks that each group reads the correct bias row across the full reduction."
372
+ },
373
+ "attrs": { "axis": 1, "is_inner_broadcast": 1 },
374
+ "inputs": {
375
+ "data": {
376
+ "dtype": "float32",
377
+ "shape": [4, 65536],
378
+ "data": { "kind": "fillFloat32", "scale": 0.5, "sinStep": 0.0017, "cosStep": 0.0031 }
379
+ },
380
+ "bias": {
381
+ "dtype": "float32",
382
+ "shape": [2, 65536],
383
+ "data": { "kind": "fillFloat32", "scale": 0.25, "sinStep": 0.0011, "cosStep": 0.0023 }
384
+ }
385
+ },
386
+ "outputs": {
387
+ "output": { "dtype": "float32", "shape": [4, 65536], "tolerance": 0.000001, "relTolerance": 0.00001 }
388
+ }
389
+ },
390
+ {
391
+ "name": "longrow_split_f16_axis1_4x65536_groups2",
392
+ "provenance": {
393
+ "notes": "Exercises float16 storage on the split long-row path over 65536 elements. A -30-to-0 ramp concentrates probability mass so normal float16 outputs and underflowing tail zeros coexist, while the row statistics remain in float32 scratch."
394
+ },
395
+ "attrs": { "axis": 1, "is_inner_broadcast": 1 },
396
+ "inputs": {
397
+ "data": { "dtype": "float16", "shape": [4, 65536], "data": { "kind": "linspace", "start": -30.0, "end": 0.0 } },
398
+ "bias": { "dtype": "float16", "shape": [2, 65536], "data": { "kind": "constant", "value": 0.0 } }
399
+ },
400
+ "outputs": { "output": { "dtype": "float16", "shape": [4, 65536], "tolerance": 1e-7, "relTolerance": 0.01 } }
401
+ },
402
+ {
403
+ "name": "attn_rows_axis2_4x64x256",
404
+ "provenance": {
405
+ "notes": "Compact companion for bench case biassoftmax-f32-attn-32x512x512, preserving the attention-score layout with a broadcast bias over many axis-2 softmax rows."
406
+ },
407
+ "attrs": { "axis": 2, "is_inner_broadcast": 0 },
408
+ "inputs": {
409
+ "data": {
410
+ "dtype": "float32",
411
+ "shape": [4, 64, 256],
412
+ "data": { "kind": "fillFloat32", "scale": 0.5, "sinStep": 0.017, "cosStep": 0.031 }
413
+ },
414
+ "bias": {
415
+ "dtype": "float32",
416
+ "shape": [1, 64, 256],
417
+ "data": { "kind": "fillFloat32", "scale": 0.25, "sinStep": 0.011, "cosStep": 0.023 }
418
+ }
419
+ },
420
+ "outputs": {
421
+ "output": { "dtype": "float32", "shape": [4, 64, 256], "tolerance": 0.000001, "relTolerance": 0.000001 }
422
+ }
423
+ },
424
+ {
425
+ "name": "rank8_inner_broadcast_full_suffix_softmax",
426
+ "attrs": { "axis": 1, "is_inner_broadcast": 1 },
427
+ "inputs": {
428
+ "data": {
429
+ "dtype": "float32",
430
+ "shape": [1, 2, 1, 2, 1, 2, 2, 2],
431
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.27 }
432
+ },
433
+ "bias": {
434
+ "dtype": "float32",
435
+ "shape": [1, 2, 1, 2, 1, 2, 2, 2],
436
+ "data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.11 }
437
+ }
438
+ },
439
+ "outputs": { "output": { "dtype": "float32", "shape": [1, 2, 1, 2, 1, 2, 2, 2], "tolerance": 0.000002 } }
440
+ }
441
+ ]
442
+ }