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

sync 91d990483a17

Browse files
README.md CHANGED
@@ -18,16 +18,16 @@ See the [ONNX Runtime `BiasSoftmax` contrib-operator spec](https://github.com/mi
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
 
@@ -36,7 +36,7 @@ Attributes and default values (overridable per request):
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
 
@@ -46,7 +46,7 @@ Attributes and default values (overridable per request):
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
@@ -56,10 +56,14 @@ Attributes and default values (overridable per request):
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
 
 
18
 
19
  ## Inputs
20
 
21
+ | Name | Logical dtype | Rank | Shape | Description | Presence |
22
+ | --- | --- | --- | --- | --- | --- |
23
+ | `data` | `T` | — | — | The input data tensor. | required |
24
+ | `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 | Logical dtype | Rank | Shape | Description | Presence |
29
+ | --- | --- | --- | --- | --- | --- |
30
+ | `output` | `T` | same as `data` | same as `data` | The output tensor; same shape as data. | required |
31
 
32
  ## Attributes
33
 
 
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` | — | Let `data_row` and `bias_row` index flattened rows of length `product(data.shape[axis:])`. When 1, consecutive data-row groups reuse each bias row: `bias_row = floor(data_row / (data_row_count / bias_row_count))`. When 0, bias rows repeat cyclically: `bias_row = data_row % bias_row_count`. |
40
 
41
  ## Type constraints
42
 
 
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
 
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.
64
 
65
  The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
66
+ It follows the `v1` branch as fixes land. To pin exact artifact bytes, pass a 40-character commit `revision` instead of `version`.
67
 
68
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
69
 
build/webgpu/bench.json CHANGED
@@ -1,5 +1,4 @@
1
  {
2
- "op": "com.microsoft.BiasSoftmax",
3
  "tunableSpace": { "WORKGROUP_SIZE": [64, 128, 256], "BLOCK_COLS": [1024, 2048, 4096, 8192] },
4
  "cases": [
5
  {
 
1
  {
 
2
  "tunableSpace": { "WORKGROUP_SIZE": [64, 128, 256], "BLOCK_COLS": [1024, 2048, 4096, 8192] },
3
  "cases": [
4
  {
build/webgpu/bias-softmax-longrow-normalize.wgsl.jinja CHANGED
@@ -1,8 +1,5 @@
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;
@@ -10,10 +7,10 @@ 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
 
1
  // Parallel normalization for split long rows. One workgroup handles each
2
  // BLOCK_COLS slice and reuses the finished per-row statistics.
 
 
 
3
  {{ env.wgsl.resourceDeclarations }}
4
 
5
  const WG: u32 = {{ tunables.WORKGROUP_SIZE }}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
build/webgpu/bias-softmax-longrow-stats.wgsl.jinja CHANGED
@@ -1,21 +1,21 @@
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 %}
@@ -49,20 +49,22 @@ fn exp_shift(value: f32, maxValue: f32) -> f32 {
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
@@ -73,31 +75,24 @@ fn combine_partials(m: f32, d: f32, lidx: u32, sgSize: u32) -> vec2<f32> {
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
  }
@@ -112,9 +107,8 @@ 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;
@@ -176,7 +170,7 @@ fn combine_partials(m: f32, d: f32, lidx: u32) -> vec2<f32> {
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;
@@ -209,7 +203,7 @@ fn main(@builtin(workgroup_id) wid: vec3<u32>,
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;
 
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 %}
 
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
 
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
  }
 
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;
 
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;
 
203
 
204
  let merged = combine_partials(m, d, lid.x);
205
  if (lid.x == 0u) {
206
+ {% if stage == "block" %}
207
  let out = row * params.blocks + block;
208
  blockMax[out] = merged.x;
209
  blockSum[out] = merged.y;
build/webgpu/bias-softmax.wgsl.jinja CHANGED
@@ -2,9 +2,6 @@
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 %}
@@ -42,20 +39,22 @@ fn exp_shift(value: f32, maxValue: f32) -> f32 {
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
@@ -66,31 +65,24 @@ fn combine_partials(m: f32, d: f32, lidx: u32, sgSize: u32) -> vec2<f32> {
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
  }
@@ -105,9 +97,8 @@ 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;
@@ -170,16 +161,18 @@ fn combine_partials(m: f32, d: f32, lidx: u32) -> vec2<f32> {
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 %}
@@ -187,15 +180,14 @@ fn main(@builtin(workgroup_id) wid: vec3<u32>,
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);
@@ -224,7 +216,7 @@ fn main(@builtin(workgroup_id) wid: vec3<u32>,
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) {
@@ -243,10 +235,10 @@ fn main(@builtin(workgroup_id) wid: vec3<u32>,
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
  }
 
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 combineSubgroups %}
6
  enable subgroups;
7
  {% endif %}
 
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
44
+ // publishes the subgroup pair in the slot at its OWN invocation index and sets
45
+ // that index's bit in a workgroup bitmask; thread 0 then folds exactly the
46
+ // published slots, in ascending index order (the online (m, d) merge is not
47
+ // float-associative, so the order is fixed), and clears the mask for the next
48
+ // call as it reads it. Workgroup memory starts zeroed, so the mask needs no
49
+ // setup. Same three collectives as a single-subgroup reduce, two barriers.
50
  var<workgroup> partialM: array<f32, WG>;
51
  var<workgroup> partialD: array<f32, WG>;
52
+ var<workgroup> leaderMask: array<atomic<u32>, (WG + 31u) / 32u>;
53
  var<workgroup> combinedMD: vec2<f32>;
54
 
55
  // When the whole workgroup is one subgroup the subgroup reduce already covers
56
+ // it (no barriers, no shared state). `subgroup_size` is the size of the current
57
+ // subgroup and uniform, so the test is exact and may guard the barriers below.
58
  fn combine_partials(m: f32, d: f32, lidx: u32, sgSize: u32) -> vec2<f32> {
59
  let sgM = subgroupMax(m);
60
  // A lane with no elements contributes d == 0 (exact identity). A +inf
 
65
  if (sgSize == WG) {
66
  return vec2<f32>(sgM, sgD);
67
  }
 
 
 
 
 
 
 
 
 
 
 
 
68
  if (subgroupElect()) {
69
+ partialM[lidx] = sgM;
70
+ partialD[lidx] = sgD;
71
+ atomicOr(&leaderMask[lidx / 32u], 1u << (lidx % 32u));
72
  }
73
  workgroupBarrier();
74
  if (lidx == 0u) {
75
  var accM = -FLT_MAX;
76
  var accD = 0.0;
77
+ for (var w = 0u; w < (WG + 31u) / 32u; w = w + 1u) {
78
+ var bits = atomicExchange(&leaderMask[w], 0u);
79
+ while (bits != 0u) {
80
+ let slot = w * 32u + firstTrailingBit(bits);
81
+ bits = bits & (bits - 1u);
82
+ let mNew = max(accM, partialM[slot]);
83
+ accD = accD * exp_shift(accM, mNew) + partialD[slot] * exp_shift(partialM[slot], mNew);
84
+ accM = mNew;
85
+ }
86
  }
87
  combinedMD = vec2<f32>(accM, accD);
88
  }
 
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;
 
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
  // 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.
 
191
  if (is_finite_f32(v0) && is_finite_f32(v1)) {
192
  if (v0 >= v1) {
193
  let e = exp(v1 - v0);
 
216
  {% else %}
217
  // Short rows fit in private registers, so the normalization sweep does not
218
  // reread data or bias from storage.
219
+ var values: array<f32, {{ blockSize }}>;
220
  var m = -FLT_MAX;
221
  var denom = 0.0;
222
  for (var dd = 0u; dd < params.blockSize; dd += 1u) {
 
235
  {% endif %}
236
  }
237
  {% else %}
238
+ // 2D-folded row index: wid.y carries the high bits once the row count passes the
239
+ // per-axis dispatch fold width, and is zero when it does not; the
240
+ // row >= params.batchCount guard drops the over-dispatched tail.
241
+ let row = wid.x + wid.y * {{ DISPATCH_FOLD_WIDTH }}u;
242
  if (row >= params.batchCount) {
243
  return;
244
  }
build/webgpu/manifest.json CHANGED
@@ -2,36 +2,12 @@
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)",
@@ -39,50 +15,18 @@
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",
@@ -101,43 +45,24 @@
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" }
@@ -145,30 +70,16 @@
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" }
@@ -176,44 +87,19 @@
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
  }
@@ -222,8 +108,8 @@
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,
@@ -233,25 +119,37 @@
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\"",
@@ -261,17 +159,26 @@
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
  }
 
2
  "domain": "com.microsoft",
3
  "name": "BiasSoftmax",
4
  "sinceVersion": 1,
5
+ "inputs": { "data": { "dtype": "T" }, "bias": { "dtype": "T" } },
6
+ "outputs": { "output": { "dtype": "T", "rank": "ranks.data", "shape": "shapes.data" } },
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)",
 
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
  {
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))"
 
 
54
  },
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": [
63
+ { "name": "blockSize", "type": "u32", "value": "blockSize" },
64
+ { "name": "blocks", "type": "u32", "value": "ceilDiv(blockSize, tunables.BLOCK_COLS)" }
65
+ ]
 
 
 
 
 
66
  }
67
  ],
68
  "dispatch": { "x": "ceilDiv(blockSize, tunables.BLOCK_COLS)", "y": "batchCount" }
 
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)" }]
 
 
 
 
 
83
  }
84
  ],
85
  "dispatch": { "x": "batchCount" }
 
87
  {
88
  "id": "normalize",
89
  "name": "BiasSoftmax.LongRowNormalize",
90
+ "shader": "bias-softmax-longrow-normalize.wgsl.jinja",
91
+ "derive": {
92
+ "isInnerBroadcast": "attrs.is_inner_broadcast != 0",
93
+ "biasBlockCountSpec": "max(1, biasBlockCount)",
94
+ "innerRepeat": "max(1, batchCount / max(1, biasBlockCount))"
 
 
95
  },
96
  "bindings": [
97
+ "data",
98
+ "bias",
99
+ { "name": "rowMax", "buffer": "read-only-storage", "elementType": "f32" },
100
+ { "name": "rowSum", "buffer": "read-only-storage", "elementType": "f32" },
101
+ "output",
102
+ { "name": "params", "struct": [{ "name": "blockSize", "type": "u32", "value": "blockSize" }] }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  ],
104
  "dispatch": { "x": "ceilDiv(blockSize, tunables.BLOCK_COLS)", "y": "batchCount" }
105
  }
 
108
  {
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,
 
119
  {
120
  "id": "main",
121
  "name": "BiasSoftmax.PackedRows",
122
+ "shader": "bias-softmax.wgsl.jinja",
123
+ "derive": {
124
+ "isInnerBroadcast": "attrs.is_inner_broadcast != 0",
125
+ "biasBlockCountSpec": "max(1, biasBlockCount)",
126
+ "innerRepeat": "max(1, batchCount / max(1, biasBlockCount))"
 
 
 
127
  },
128
+ "bindings": [
129
+ "data",
130
+ "bias",
131
+ "output",
132
+ {
133
+ "name": "params",
134
+ "struct": [
135
+ { "name": "blockSize", "type": "u32", "value": "blockSize" },
136
+ { "name": "batchCount", "type": "u32", "value": "batchCount" }
137
+ ]
138
+ }
139
+ ],
140
+ "dispatch": {
141
+ "x": "min(ceilDiv((batchCount), (tunables.WORKGROUP_SIZE)), min(device.limits.maxComputeWorkgroupsPerDimension, 65535))",
142
+ "y": 1,
143
+ "z": 1
144
+ }
145
  }
146
  ]
147
  },
148
  {
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\"",
 
159
  {
160
  "id": "main",
161
  "name": "BiasSoftmax.AdaptiveRow",
162
+ "shader": "bias-softmax.wgsl.jinja",
163
+ "derive": {
164
+ "isInnerBroadcast": "attrs.is_inner_broadcast != 0",
165
+ "biasBlockCountSpec": "max(1, biasBlockCount)",
166
+ "innerRepeat": "max(1, batchCount / max(1, biasBlockCount))"
 
 
 
167
  },
168
+ "bindings": [
169
+ "data",
170
+ "bias",
171
+ "output",
172
+ {
173
+ "name": "params",
174
+ "struct": [
175
+ { "name": "blockSize", "type": "u32", "value": "blockSize" },
176
+ { "name": "batchCount", "type": "u32", "value": "batchCount" }
177
+ ]
178
+ }
179
+ ],
180
+ "dispatch": { "x": "min(batchCount, 65535)", "y": "ceilDiv(batchCount, 65535)", "z": 1 },
181
+ "subgroupCollectivesWidth": "portable"
182
  }
183
  ]
184
  }
build/webgpu/metadata.json CHANGED
@@ -1,20 +1,27 @@
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
  }
 
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
+ }
26
+ }
27
  }
build/webgpu/test.json CHANGED
@@ -1,11 +1,10 @@
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": {
@@ -29,7 +28,7 @@
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": {
@@ -54,7 +53,7 @@
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": {
@@ -105,7 +104,7 @@
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": {
@@ -134,7 +133,7 @@
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": {
@@ -199,12 +198,12 @@
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": {
@@ -278,7 +277,7 @@
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": {
@@ -294,7 +293,7 @@
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": {
@@ -314,10 +313,35 @@
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": {
@@ -336,7 +360,7 @@
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": {
@@ -350,7 +374,7 @@
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": {
@@ -402,7 +426,7 @@
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": {
 
1
  {
 
2
  "cases": [
3
  {
4
  "name": "inner_broadcast_consecutive_row_groups",
5
  "provenance": {
6
  "source": "onnxruntime/contrib_ops/cuda/math/bias_softmax_impl.cu",
7
+ "notes": "Distinguishes flattened inner-broadcast row grouping from outer mode and right-aligned broadcasting."
8
  },
9
  "attrs": { "axis": -1, "is_inner_broadcast": 1 },
10
  "inputs": {
 
28
  "name": "outer_broadcast_cycles_bias_rows",
29
  "provenance": {
30
  "source": "onnxruntime/contrib_ops/cuda/math/bias_softmax_impl.cu",
31
+ "notes": "Non-broadcastable data and bias shapes exercise cyclic outer-broadcast row selection."
32
  },
33
  "attrs": { "axis": -1, "is_inner_broadcast": 0 },
34
  "inputs": {
 
53
  "provenance": {
54
  "source": "onnxruntime/test/contrib_ops/bias_softmax_op_test.cc",
55
  "test": "BiasSoftmaxTest.InnerBroadcastFullBiasBatch",
56
+ "notes": "Small deterministic suffix-softmax case."
57
  },
58
  "attrs": { "axis": 1, "is_inner_broadcast": 1 },
59
  "inputs": {
 
104
  "provenance": {
105
  "source": "onnxruntime/test/contrib_ops/bias_softmax_op_test.cc",
106
  "test": "BiasSoftmaxTest.InnerBroadcastNegativeAxis",
107
+ "notes": "Negative-axis inner-broadcast case."
108
  },
109
  "attrs": { "axis": -2, "is_inner_broadcast": 1 },
110
  "inputs": {
 
133
  "provenance": {
134
  "source": "onnxruntime/test/contrib_ops/bias_softmax_op_test.cc",
135
  "test": "BiasSoftmaxTest.OuterBroadcastNegativeAxis",
136
+ "notes": "Negative-axis outer-broadcast case."
137
  },
138
  "attrs": { "axis": -1, "is_inner_broadcast": 0 },
139
  "inputs": {
 
198
  "name": "f32_large_gap_subnormal_tail_gpu_gap",
199
  "skipGpu": {
200
  "category": "permanent",
201
+ "reason": "Portable WGSL floating-point semantics do not guarantee preservation of subnormal values. The 87.5-point logit gap requires a positive subnormal probability tail, which may be flushed to zero."
202
  },
203
  "provenance": {
204
  "source": "onnxruntime/test/contrib_ops/bias_softmax_op_test.cc",
205
  "test": "BiasSoftmaxTest.InnerBroadcastFullBiasBatch",
206
+ "notes": "An 87.5-point logit gap leaves a positive subnormal probability tail after suffix softmax over data+bias."
207
  },
208
  "attrs": { "axis": 1, "is_inner_broadcast": 1 },
209
  "inputs": {
 
277
  {
278
  "name": "dispatch_cliff_rows_65537",
279
  "provenance": {
280
+ "notes": "65,537 short rows cross the per-dimension workgroup limit. The packed route grid-strides over rows, while the generic route reconstructs row indices from a two-dimensional dispatch."
281
  },
282
  "attrs": { "axis": 1, "is_inner_broadcast": 0 },
283
  "inputs": {
 
293
  {
294
  "name": "fully_masked_row_neg_inf_bias",
295
  "provenance": {
296
+ "notes": "The additive bias is -Infinity across the entire suffix block. The expected result uses the package's all-zero convention for a fully masked row, preventing NaNs from propagating into attention."
297
  },
298
  "attrs": { "axis": 1, "is_inner_broadcast": 0 },
299
  "inputs": {
 
313
  }
314
  }
315
  },
316
+ {
317
+ "name": "attn_rows_axis3_row1_all_neg_inf_bias_2x8x4x64",
318
+ "provenance": {
319
+ "notes": "Data has shape [2, 8, 4, 64], and bias [1, 1, 4, 64] uses outer broadcasting. Bias query row 1 is -Infinity at every key, so 16 of the 64 softmax rows are fully masked and must be all zero; the other rows remain finite. This operator's zero-denominator rule differs from applying ONNX Softmax directly to data plus bias, which would produce NaN for the fully masked rows."
320
+ },
321
+ "attrs": { "axis": 3, "is_inner_broadcast": 0 },
322
+ "inputs": {
323
+ "data": {
324
+ "dtype": "float32",
325
+ "shape": [2, 8, 4, 64],
326
+ "data": { "kind": "fillFloat32", "scale": 0.5, "sinStep": 0.017, "cosStep": 0.031 }
327
+ },
328
+ "bias": {
329
+ "dtype": "float32",
330
+ "shape": [1, 1, 4, 64],
331
+ "data": {
332
+ "kind": "values",
333
+ "values": [0.1, 0.1906, 0.2752, 0.3492, 0.4087, 0.4504, 0.4717, 0.4715, 0.4493, 0.4061, 0.3436, 0.265, 0.1738, 0.0744, -0.0283, -0.1293, -0.2239, -0.3075, -0.376, -0.4264, -0.4563, -0.4646, -0.4512, -0.417, -0.3642, -0.2957, -0.2155, -0.1278, -0.0373, 0.051, 0.1326, 0.2033, 0.2593, 0.2978, 0.3171, 0.3164, 0.2959, 0.2571, 0.2022, 0.1347, 0.0584, -0.0222, -0.1024, -0.1777, -0.2435, -0.296, -0.3319, -0.349, -0.3458, -0.3223, -0.2793, -0.2187, -0.1434, -0.0571, 0.0358, 0.1307, 0.2226, 0.3069, 0.379, 0.4352, 0.4725, 0.4887, 0.4829, 0.4552, "-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.4895, 0.458, 0.4063, 0.3372, 0.2542, 0.1614, 0.0636, -0.0343, -0.1275, -0.2114, -0.282, -0.3358, -0.3705, -0.3848, -0.3781, -0.3515, -0.3066, -0.2464, -0.1744, -0.0948, -0.0122, 0.0686, 0.1431, 0.207, 0.2566, 0.2892, 0.3028, 0.2966, 0.2708, 0.2266, 0.1664, 0.0935, 0.0118, -0.0744, -0.1602, -0.2409, -0.3121, -0.3695, -0.41, -0.4309, -0.4307, -0.4092, -0.3669, -0.3056, -0.2282, -0.1382, -0.0401, 0.0616, 0.1619, 0.2557, 0.3387, 0.4067, 0.4564, 0.4856, 0.4928, 0.4779, 0.4417, 0.3864, 0.3147, 0.2305, 0.1381, 0.0423, -0.052, -0.1401, 0.271, 0.1834, 0.0902, -0.0037, -0.0937, -0.175, -0.2436, -0.2963, -0.3304, -0.3445, -0.3382, -0.3123, -0.2685, -0.2096, -0.139, -0.061, 0.0199, 0.0989, 0.1716, 0.2336, 0.2813, 0.3118, 0.3232, 0.3144, 0.2858, 0.2386, 0.1751, 0.0984, 0.0126, -0.078, -0.1685, -0.2541, -0.3303, -0.3928, -0.4382, -0.4639, -0.4683, -0.4508, -0.4119, -0.3534, -0.278, -0.1892, -0.0912, 0.0112, 0.1131, 0.2095, 0.2959, 0.3681, 0.4226, 0.4571, 0.4701, 0.4612, 0.4311, 0.3818, 0.316, 0.2374, 0.1503, 0.0592, -0.0309, -0.1153, -0.1897, -0.2502, -0.2939, -0.3186]
334
+ }
335
+ }
336
+ },
337
+ "outputs": {
338
+ "output": { "dtype": "float32", "shape": [2, 8, 4, 64], "tolerance": 0.000001, "relTolerance": 0.000001 }
339
+ }
340
+ },
341
  {
342
  "name": "axis0_full_reduce_single_block",
343
  "provenance": {
344
+ "notes": "With axis=0, one softmax block covers the whole tensor. The bias supplies one complete flattened row with shape [4,6]."
345
  },
346
  "attrs": { "axis": 0, "is_inner_broadcast": 0 },
347
  "inputs": {
 
360
  {
361
  "name": "bias_fold_skip_middle_dim_axis1",
362
  "provenance": {
363
+ "notes": "Bias [1,3,4] broadcasts across the batch axis of data [2,3,4]. axis=1 reduces each 12-element suffix while the size-one bias axis is skipped during broadcast indexing."
364
  },
365
  "attrs": { "axis": 1, "is_inner_broadcast": 0 },
366
  "inputs": {
 
374
  {
375
  "name": "many_two_element_rows_axis1",
376
  "provenance": {
377
+ "notes": "Many two-element softmax rows exercise the scalar-row fallback and folded dispatch accounting used by `biassoftmax-f32-launchbound-262144x2-axis1`."
378
  },
379
  "attrs": { "axis": 1, "is_inner_broadcast": 0 },
380
  "inputs": {
 
426
  {
427
  "name": "attn_rows_axis2_4x64x256",
428
  "provenance": {
429
+ "notes": "An attention-score layout with broadcast bias exercises many axis-2 softmax rows at a compact scale representative of `biassoftmax-f32-attn-32x512x512`."
430
  },
431
  "attrs": { "axis": 2, "is_inner_broadcast": 0 },
432
  "inputs": {