sync 91d990483a17
Browse files- README.md +61 -13
- build/webgpu/attention-rank4-tiled.wgsl.jinja +26 -13
- build/webgpu/attn-flash-decode-splitk-merge.wgsl.jinja +2 -1
- build/webgpu/attn-flash-decode-splitk.wgsl.jinja +31 -37
- build/webgpu/attn-flash-online.wgsl.jinja +30 -60
- build/webgpu/attn-flash-prefill-cluster.wgsl.jinja +172 -70
- build/webgpu/attn-flash-q32-broadcast.wgsl.jinja +1 -4
- build/webgpu/attn-materialized-apply-f32.wgsl.jinja +5 -7
- build/webgpu/attn-materialized-rowstats-combine-f32.wgsl.jinja +3 -4
- build/webgpu/attn-materialized-score-f32.wgsl.jinja +6 -8
- build/webgpu/attn-materialized-sgmat-f32.wgsl.jinja +17 -14
- build/webgpu/attn-materialized-softmax-f32.wgsl.jinja +27 -34
- build/webgpu/attn-online-scalar.wgsl.jinja +6 -28
- build/webgpu/attn-small-head-parallel.wgsl.jinja +0 -3
- build/webgpu/bench.json +1 -2
- build/webgpu/manifest.json +0 -0
- build/webgpu/metadata.json +67 -20
- build/webgpu/mha-small-seq-blocked.wgsl.jinja +166 -0
- build/webgpu/mha-small-seq.wgsl.jinja +3 -3
- build/webgpu/test.json +292 -50
README.md
CHANGED
|
@@ -12,25 +12,25 @@ tags:
|
|
| 12 |
|
| 13 |
## Description
|
| 14 |
|
| 15 |
-
Computes multi-head self- or cross-attention over explicit query, key, and value tensors, with an optional fused QKV projection bias and additive attention bias. Supports causal masking through `unidirectional` and a configurable score scale that defaults to `1 / sqrt(head_size)`.
|
| 16 |
|
| 17 |
See the [ONNX Runtime `MultiHeadAttention` contrib-operator spec](https://github.com/microsoft/onnxruntime/blob/main/docs/ContribOperators.md#com.microsoft.MultiHeadAttention) for the reference semantics.
|
| 18 |
|
| 19 |
## Inputs
|
| 20 |
|
| 21 |
-
| Name |
|
| 22 |
| --- | --- | --- | --- | --- | --- | --- |
|
| 23 |
-
| `
|
| 24 |
-
| `
|
| 25 |
-
| `
|
| 26 |
-
| `
|
| 27 |
-
| `
|
| 28 |
|
| 29 |
## Outputs
|
| 30 |
|
| 31 |
-
| Name |
|
| 32 |
| --- | --- | --- | --- | --- | --- | --- |
|
| 33 |
-
| `
|
| 34 |
|
| 35 |
## Attributes
|
| 36 |
|
|
@@ -38,9 +38,9 @@ Attributes and default values (overridable per request):
|
|
| 38 |
|
| 39 |
| Attribute | Default | Description |
|
| 40 |
| --- | --- | --- |
|
| 41 |
-
| `unidirectional` | `0` | When 1, applies an upper-left causal mask: query position `i` attends only to key positions `0..min(i, kv_sequence_length - 1)`. |
|
| 42 |
| `num_heads` | — | Number of attention heads. |
|
| 43 |
| `scale` | — | Optional score scale; zero or omission selects `1 / sqrt(head_size)`. |
|
|
|
|
| 44 |
|
| 45 |
## Type constraints
|
| 46 |
|
|
@@ -48,13 +48,56 @@ Attributes and default values (overridable per request):
|
|
| 48 |
| --- | --- |
|
| 49 |
| `T` | `float32`, `float16` |
|
| 50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
## Device requirements
|
| 52 |
|
| 53 |
Some implementation variants require `subgroup-matrix`, `shader-f16`, and `subgroups`. These are route-specific capabilities, not package-wide requirements; availability also depends on the request shape and dtype.
|
| 54 |
|
| 55 |
## Files
|
| 56 |
|
| 57 |
-
- [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, provenance)
|
| 58 |
- [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
|
| 59 |
- [`test.json`](build/webgpu/test.json) — correctness cases
|
| 60 |
- [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
|
|
@@ -71,14 +114,19 @@ Some implementation variants require `subgroup-matrix`, `shader-f16`, and `subgr
|
|
| 71 |
- [`attn-materialized-softmax-f32.wgsl.jinja`](build/webgpu/attn-materialized-softmax-f32.wgsl.jinja)
|
| 72 |
- [`attn-online-scalar.wgsl.jinja`](build/webgpu/attn-online-scalar.wgsl.jinja)
|
| 73 |
- [`attn-small-head-parallel.wgsl.jinja`](build/webgpu/attn-small-head-parallel.wgsl.jinja)
|
|
|
|
| 74 |
- [`mha-small-seq.wgsl.jinja`](build/webgpu/mha-small-seq.wgsl.jinja)
|
| 75 |
|
| 76 |
## Use with `@huggingface/kernels`
|
| 77 |
|
| 78 |
-
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
|
|
|
|
| 82 |
|
| 83 |
Replace each `*Data` placeholder with a typed array containing the corresponding input data.
|
| 84 |
|
|
|
|
| 12 |
|
| 13 |
## Description
|
| 14 |
|
| 15 |
+
Computes multi-head self- or cross-attention over explicit query, key, and value tensors, with an optional fused QKV projection bias and additive attention bias. Supports causal masking through `unidirectional` and a configurable score scale that defaults to `1 / sqrt(head_size)`. Query, key, and value must have the same hidden width; distinct value-head widths, KV-cache, key-padding-mask, and diagnostic-QK modes are unsupported.
|
| 16 |
|
| 17 |
See the [ONNX Runtime `MultiHeadAttention` contrib-operator spec](https://github.com/microsoft/onnxruntime/blob/main/docs/ContribOperators.md#com.microsoft.MultiHeadAttention) for the reference semantics.
|
| 18 |
|
| 19 |
## Inputs
|
| 20 |
|
| 21 |
+
| Name | Upstream name | Logical dtype | Rank | Shape | Description | Presence |
|
| 22 |
| --- | --- | --- | --- | --- | --- | --- |
|
| 23 |
+
| `queryT` | `query` | `T` | `3` | — | Query tensor of shape `(batch_size, sequence_length, hidden_size)`. | required |
|
| 24 |
+
| `keyT` | `key` | `T` | `3` | — | Key tensor of shape `(batch_size, kv_sequence_length, hidden_size)`; its hidden width must equal the query hidden width. | required |
|
| 25 |
+
| `valueT` | `value` | `T` | `3` | — | Value tensor of shape `(batch_size, kv_sequence_length, v_hidden_size)`; `v_hidden_size` must equal the query hidden width. | required |
|
| 26 |
+
| `biasT` | `bias` | `T` | `1` | — | Optional fused bias from input projection with shape `(hidden_size + hidden_size + v_hidden_size)`. | optional |
|
| 27 |
+
| `attentionBiasT` | `attention_bias` | `T` | `4` | — | Optional additive bias applied to the attention scores before softmax, of shape `(batch_size or 1, num_heads or 1, sequence_length, kv_sequence_length)`; the first two dimensions broadcast. | optional |
|
| 28 |
|
| 29 |
## Outputs
|
| 30 |
|
| 31 |
+
| Name | Upstream name | Logical dtype | Rank | Shape | Description | Presence |
|
| 32 |
| --- | --- | --- | --- | --- | --- | --- |
|
| 33 |
+
| `outputT` | `output` | `T` | `3` | `[queryT[0], queryT[1], valueT[2]]` | Attention output of shape `(batch_size, sequence_length, v_hidden_size)`. | required |
|
| 34 |
|
| 35 |
## Attributes
|
| 36 |
|
|
|
|
| 38 |
|
| 39 |
| Attribute | Default | Description |
|
| 40 |
| --- | --- | --- |
|
|
|
|
| 41 |
| `num_heads` | — | Number of attention heads. |
|
| 42 |
| `scale` | — | Optional score scale; zero or omission selects `1 / sqrt(head_size)`. |
|
| 43 |
+
| `unidirectional` | `0` | When 1, applies an upper-left causal mask: query position `i` attends only to key positions `0..min(i, kv_sequence_length - 1)`. |
|
| 44 |
|
| 45 |
## Type constraints
|
| 46 |
|
|
|
|
| 48 |
| --- | --- |
|
| 49 |
| `T` | `float32`, `float16` |
|
| 50 |
|
| 51 |
+
## Implementation variants
|
| 52 |
+
|
| 53 |
+
One implementation is selected per call from the device capabilities, the request shapes and the dtypes; these notes say what each one covers.
|
| 54 |
+
|
| 55 |
+
- `qkv_bias_small_seq_blocked` — Blocked whole-head attention for short key sequences with bias and optional causal masking. A workgroup stages one head's K and V once, then assigns each query in a small block to a group of key lanes whose register-resident online-softmax partials are merged in one pass.
|
| 56 |
+
- `qkv_no_bias_small_seq_blocked` — Blocked whole-head attention for short key sequences without bias and with optional causal masking. A workgroup stages one head's K and V once, then assigns each query in a small block to a group of key lanes whose register-resident online-softmax partials are merged in one pass.
|
| 57 |
+
- `qkv_no_bias_small_seq` — Whole-head attention for short bidirectional float32 requests without bias. One workgroup stages a head's complete key and value planes, and each participating invocation owns one query row through scoring, normalization, and context accumulation.
|
| 58 |
+
- `qkv_no_bias_tiled_nosg` — Portable tiled prefill route for devices without an admissible subgroup path. It computes attention online without materializing the full score matrix.
|
| 59 |
+
- `qkv_no_bias_tiled_attn_bias_nosg` — Portable tiled prefill route for additive attention bias when no subgroup path is admissible. It computes attention online without materializing the full score matrix.
|
| 60 |
+
- `qkv_no_bias_materialized_sgmat_fused_f32` — Materializes FP32 scores for subgroup-matrix score and value products, then folds softmax normalization into the apply pass using row statistics emitted by the score pass.
|
| 61 |
+
- `qkv_bias_materialized_sgmat_fused_f32` — Materializes FP32 scores for subgroup-matrix score and value products, then folds softmax normalization into the apply pass using row statistics emitted by the score pass.
|
| 62 |
+
- `qkv_no_bias_materialized_sgmat_fused_f16` — Materializes FP32 scores for subgroup-matrix score and value products, then folds softmax normalization into the apply pass using row statistics emitted by the score pass.
|
| 63 |
+
- `qkv_no_bias_flash_cluster_lpq4_nosg` — Keeps query/key tiles and online-softmax state inside one workgroup instead of materializing all scores. The family supplies subgroup and portable forms for compatible prefill shapes.
|
| 64 |
+
- `qkv_no_bias_flash_cluster_nosg` — Keeps query/key tiles and online-softmax state inside one workgroup instead of materializing all scores. The family supplies subgroup and portable forms for compatible prefill shapes.
|
| 65 |
+
- `qkv_bias_flash_cluster_nosg` — Keeps query/key tiles and online-softmax state inside one workgroup instead of materializing all scores. The family supplies subgroup and portable forms for compatible prefill shapes.
|
| 66 |
+
- `qkv_no_bias_flash_cluster_lpq4` — Keeps query/key tiles and online-softmax state inside one workgroup instead of materializing all scores. The family supplies subgroup and portable forms for compatible prefill shapes.
|
| 67 |
+
- `qkv_no_bias_flash_cluster` — Keeps query/key tiles and online-softmax state inside one workgroup instead of materializing all scores. The family supplies subgroup and portable forms for compatible prefill shapes.
|
| 68 |
+
- `qkv_bias_flash_cluster` — Keeps query/key tiles and online-softmax state inside one workgroup instead of materializing all scores. The family supplies subgroup and portable forms for compatible prefill shapes.
|
| 69 |
+
- `qkv_no_bias_flash_cluster_attn_bias` — Keeps query/key tiles and online-softmax state inside one workgroup instead of materializing all scores. The family supplies subgroup and portable forms for compatible prefill shapes.
|
| 70 |
+
- `qkv_bias_flash_cluster_attn_bias` — Keeps query/key tiles and online-softmax state inside one workgroup instead of materializing all scores. The family supplies subgroup and portable forms for compatible prefill shapes.
|
| 71 |
+
- `qkv_no_bias_flash_splitk_nosg` — Partitions the key reduction across workgroups and merges partial softmax statistics and values. Used when decode or another short query has too few independent query/head workgroups; the merge reassociates the key reduction.
|
| 72 |
+
- `qkv_bias_flash_splitk_nosg` — Partitions the key reduction across workgroups and merges partial softmax statistics and values. Used when decode or another short query has too few independent query/head workgroups; the merge reassociates the key reduction.
|
| 73 |
+
- `qkv_no_bias_flash_splitk` — Partitions the key reduction across workgroups and merges partial softmax statistics and values. Used when decode or another short query has too few independent query/head workgroups; the merge reassociates the key reduction.
|
| 74 |
+
- `qkv_bias_flash_splitk` — Partitions the key reduction across workgroups and merges partial softmax statistics and values. Used when decode or another short query has too few independent query/head workgroups; the merge reassociates the key reduction.
|
| 75 |
+
- `qkv_no_bias_materialized_gemm_f32` — Materializes float32 scores and applies tiled score, softmax, and value passes without requiring subgroup-matrix support.
|
| 76 |
+
- `qkv_bias_materialized_gemm_f32` — Materializes float32 scores and applies tiled score, softmax, and value passes without requiring subgroup-matrix support.
|
| 77 |
+
- `qkv_no_bias_materialized_gemm_fused_f32` — Materializes FP32 scores while publishing each row's maximum, combines those row statistics, then normalizes and applies the scores in one pass. This route is eligible for score matrices above the configured size threshold.
|
| 78 |
+
- `qkv_bias_materialized_gemm_fused_f32` — Materializes FP32 scores while publishing each row's maximum, combines those row statistics, then normalizes and applies the scores in one pass. This route is eligible for score matrices above the configured size threshold.
|
| 79 |
+
- `qkv_no_bias_attn_bias` — Portable scalar online attention with bias and additive-mask support. It applies online softmax while accumulating context, without materializing a score tensor.
|
| 80 |
+
- `qkv_bias_attn_bias` — Portable scalar online attention with bias and additive-mask support. It applies online softmax while accumulating context, without materializing a score tensor.
|
| 81 |
+
- `qkv_no_bias` — Portable scalar online attention with bias and additive-mask support. It applies online softmax while accumulating context, without materializing a score tensor.
|
| 82 |
+
- `qkv_bias` — Portable scalar online attention with bias and additive-mask support. It applies online softmax while accumulating context, without materializing a score tensor.
|
| 83 |
+
- `qkv_no_bias_flash` — Computes one-pass online-softmax attention without score scratch. Family suffixes select subgroup, portable, and additive-attention-bias forms without changing the algorithm.
|
| 84 |
+
- `qkv_bias_flash` — Computes one-pass online-softmax attention with fused projection bias and no score scratch. Family suffixes select subgroup, portable, and additive-attention-bias forms without changing the algorithm.
|
| 85 |
+
- `qkv_no_bias_flash_nosg` — Computes one-pass online-softmax attention without score scratch. Family suffixes select subgroup, portable, and additive-attention-bias forms without changing the algorithm.
|
| 86 |
+
- `qkv_bias_flash_nosg` — Computes one-pass online-softmax attention with fused projection bias and no score scratch. Family suffixes select subgroup, portable, and additive-attention-bias forms without changing the algorithm.
|
| 87 |
+
- `qkv_no_bias_flash_attn_bias` — Computes one-pass online-softmax attention without score scratch. Family suffixes select subgroup, portable, and additive-attention-bias forms without changing the algorithm.
|
| 88 |
+
- `qkv_bias_flash_attn_bias` — Computes one-pass online-softmax attention with fused projection bias and no score scratch. Family suffixes select subgroup, portable, and additive-attention-bias forms without changing the algorithm.
|
| 89 |
+
- `qkv_no_bias_flash_attn_bias_nosg` — Computes one-pass online-softmax attention without score scratch. Family suffixes select subgroup, portable, and additive-attention-bias forms without changing the algorithm.
|
| 90 |
+
- `qkv_bias_flash_attn_bias_nosg` — Computes one-pass online-softmax attention with fused projection bias and no score scratch. Family suffixes select subgroup, portable, and additive-attention-bias forms without changing the algorithm.
|
| 91 |
+
- `qkv_no_bias_materialized_sgmat_f32` — Materializes FP32 scores for subgroup-matrix score and value products, with normalization performed by a separate softmax pass.
|
| 92 |
+
- `qkv_bias_materialized_sgmat_f32` — Materializes FP32 scores for subgroup-matrix score and value products, with normalization performed by a separate softmax pass.
|
| 93 |
+
|
| 94 |
## Device requirements
|
| 95 |
|
| 96 |
Some implementation variants require `subgroup-matrix`, `shader-f16`, and `subgroups`. These are route-specific capabilities, not package-wide requirements; availability also depends on the request shape and dtype.
|
| 97 |
|
| 98 |
## Files
|
| 99 |
|
| 100 |
+
- [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, per-variant templates, provenance)
|
| 101 |
- [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
|
| 102 |
- [`test.json`](build/webgpu/test.json) — correctness cases
|
| 103 |
- [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
|
|
|
|
| 114 |
- [`attn-materialized-softmax-f32.wgsl.jinja`](build/webgpu/attn-materialized-softmax-f32.wgsl.jinja)
|
| 115 |
- [`attn-online-scalar.wgsl.jinja`](build/webgpu/attn-online-scalar.wgsl.jinja)
|
| 116 |
- [`attn-small-head-parallel.wgsl.jinja`](build/webgpu/attn-small-head-parallel.wgsl.jinja)
|
| 117 |
+
- [`mha-small-seq-blocked.wgsl.jinja`](build/webgpu/mha-small-seq-blocked.wgsl.jinja)
|
| 118 |
- [`mha-small-seq.wgsl.jinja`](build/webgpu/mha-small-seq.wgsl.jinja)
|
| 119 |
|
| 120 |
## Use with `@huggingface/kernels`
|
| 121 |
|
| 122 |
+
```sh
|
| 123 |
+
npm install --save-exact @huggingface/kernels@0.0.1-preview.2
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
+
Required output shapes and logical data types are inferred from the supplied inputs and attributes; result tensors are allocated automatically.
|
| 127 |
|
| 128 |
The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
|
| 129 |
+
It follows the `v1` branch as fixes land. To pin exact artifact bytes, pass a 40-character commit `revision` instead of `version`.
|
| 130 |
|
| 131 |
Replace each `*Data` placeholder with a typed array containing the corresponding input data.
|
| 132 |
|
build/webgpu/attention-rank4-tiled.wgsl.jinja
CHANGED
|
@@ -1,19 +1,15 @@
|
|
| 1 |
-
{% if
|
| 2 |
-
enable f16;
|
| 3 |
-
{% endif %}
|
| 4 |
{{ env.wgsl.resourceDeclarations }}
|
| 5 |
|
| 6 |
// Thread-per-query online-softmax flash fallback for rank-4 (BNSH) attention with
|
| 7 |
// no optional feature requirements. Each invocation owns one full query row and walks the keys
|
| 8 |
// independently, avoiding per-key workgroup barriers and tree reductions.
|
| 9 |
//
|
| 10 |
-
// BLOCK_M consecutive queries of one (batch, q_head) are tiled into a workgroup
|
| 11 |
-
//
|
| 12 |
-
// L2 cache (read once per head, broadcast across the tile). The per-query online
|
| 13 |
// accumulator lives in shared memory laid out d-major (d*BLOCK_M + tid) so the
|
| 14 |
-
// BLOCK_M threads touch consecutive addresses for a fixed d
|
| 15 |
-
//
|
| 16 |
-
// the workgroup-storage budget;
|
| 17 |
// larger heads use the scalar online-attention fallback.
|
| 18 |
const BLOCK_M: u32 = {{ blockM }}u;
|
| 19 |
|
|
@@ -34,13 +30,12 @@ fn kv_head(q_head: u32) -> u32 {
|
|
| 34 |
@compute @workgroup_size(BLOCK_M, 1, 1)
|
| 35 |
fn main(
|
| 36 |
@builtin(workgroup_id) wg: vec3<u32>,
|
| 37 |
-
@builtin(num_workgroups) nwg: vec3<u32>,
|
| 38 |
@builtin(local_invocation_id) lid: vec3<u32>
|
| 39 |
) {
|
| 40 |
let tid = lid.x;
|
| 41 |
// 2D-folded tile index over (batch, q_head, q_seq tile); wg.y carries the high
|
| 42 |
-
// bits past the
|
| 43 |
-
let tile = wg.x + wg.y *
|
| 44 |
let qTiles = (params.qSeq + BLOCK_M - 1u) / BLOCK_M;
|
| 45 |
// count = numel(Y) = batch·qHeads·qSeq·vHeadSize, so count/(vHeadSize·qSeq) =
|
| 46 |
// batch·qHeads. Drop the over-dispatched tail from the 2D fold.
|
|
@@ -56,8 +51,14 @@ fn main(
|
|
| 56 |
if (qs >= params.qSeq) { return; }
|
| 57 |
let kh = kv_head(qh);
|
| 58 |
|
|
|
|
| 59 |
// Token-major packed QKV: row = (batch*seq + token)*HIDDEN + head*head_dim.
|
| 60 |
let qBase = (batch * params.qSeq + qs) * params.qHidden + qh * params.headSize;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
let scale = scale_value();
|
| 62 |
|
| 63 |
for (var d: u32 = 0u; d < params.vHeadSize; d = d + 1u) {
|
|
@@ -82,10 +83,14 @@ fn main(
|
|
| 82 |
// A rejected bool-mask key has zero softmax mass. Skipping it is safe here:
|
| 83 |
// this thread-per-query kernel has no barriers inside the key loop.
|
| 84 |
if (masked) { continue; }
|
| 85 |
-
//
|
| 86 |
var score: f32 = -3.4028234663852886e38;
|
| 87 |
if (!masked) {
|
|
|
|
| 88 |
let kRow = (batch * params.kvSeq + ks) * params.kvHidden + kh * params.headSize;
|
|
|
|
|
|
|
|
|
|
| 89 |
var dot: f32 = 0.0;
|
| 90 |
for (var d: u32 = 0u; d < params.headSize; d = d + 1u) {
|
| 91 |
dot = dot + f32(q[qBase + d]) * f32(k[kRow + d]);
|
|
@@ -105,14 +110,22 @@ fn main(
|
|
| 105 |
let weight = exp(score - next_max);
|
| 106 |
running_max = next_max;
|
| 107 |
running_denom = running_denom * prev_scale + weight;
|
|
|
|
| 108 |
let vRow = (batch * params.kvSeq + ks) * params.vHidden + kh * params.vHeadSize;
|
|
|
|
|
|
|
|
|
|
| 109 |
for (var d: u32 = 0u; d < params.vHeadSize; d = d + 1u) {
|
| 110 |
acc[d * BLOCK_M + tid] = acc[d * BLOCK_M + tid] * prev_scale + weight * f32(v[vRow + d]);
|
| 111 |
}
|
| 112 |
}
|
| 113 |
|
| 114 |
let inv_denom = select(0.0, 1.0 / running_denom, running_denom > 0.0);
|
|
|
|
| 115 |
let yBase = (batch * params.qSeq + qs) * (params.qHeads * params.vHeadSize) + qh * params.vHeadSize;
|
|
|
|
|
|
|
|
|
|
| 116 |
for (var d: u32 = 0u; d < params.vHeadSize; d = d + 1u) {
|
| 117 |
y[yBase + d] = {{ scalar }}(acc[d * BLOCK_M + tid] * inv_denom);
|
| 118 |
}
|
|
|
|
| 1 |
+
{% set layout = layout if layout is defined else "bnsh" %}
|
|
|
|
|
|
|
| 2 |
{{ env.wgsl.resourceDeclarations }}
|
| 3 |
|
| 4 |
// Thread-per-query online-softmax flash fallback for rank-4 (BNSH) attention with
|
| 5 |
// no optional feature requirements. Each invocation owns one full query row and walks the keys
|
| 6 |
// independently, avoiding per-key workgroup barriers and tree reductions.
|
| 7 |
//
|
| 8 |
+
// BLOCK_M consecutive queries of one (batch, q_head) are tiled into a workgroup,
|
| 9 |
+
// so adjacent query threads reuse the same K/V row at each key index. The per-query online
|
|
|
|
| 10 |
// accumulator lives in shared memory laid out d-major (d*BLOCK_M + tid) so the
|
| 11 |
+
// BLOCK_M threads touch consecutive addresses for a fixed d. The value-head
|
| 12 |
+
// width is capped so the accumulator fits in the workgroup-storage budget;
|
|
|
|
| 13 |
// larger heads use the scalar online-attention fallback.
|
| 14 |
const BLOCK_M: u32 = {{ blockM }}u;
|
| 15 |
|
|
|
|
| 30 |
@compute @workgroup_size(BLOCK_M, 1, 1)
|
| 31 |
fn main(
|
| 32 |
@builtin(workgroup_id) wg: vec3<u32>,
|
|
|
|
| 33 |
@builtin(local_invocation_id) lid: vec3<u32>
|
| 34 |
) {
|
| 35 |
let tid = lid.x;
|
| 36 |
// 2D-folded tile index over (batch, q_head, q_seq tile); wg.y carries the high
|
| 37 |
+
// bits past the per-axis dispatch fold width.
|
| 38 |
+
let tile = wg.x + wg.y * {{ DISPATCH_FOLD_WIDTH }}u;
|
| 39 |
let qTiles = (params.qSeq + BLOCK_M - 1u) / BLOCK_M;
|
| 40 |
// count = numel(Y) = batch·qHeads·qSeq·vHeadSize, so count/(vHeadSize·qSeq) =
|
| 41 |
// batch·qHeads. Drop the over-dispatched tail from the 2D fold.
|
|
|
|
| 51 |
if (qs >= params.qSeq) { return; }
|
| 52 |
let kh = kv_head(qh);
|
| 53 |
|
| 54 |
+
{% if layout == "bsh" %}
|
| 55 |
// Token-major packed QKV: row = (batch*seq + token)*HIDDEN + head*head_dim.
|
| 56 |
let qBase = (batch * params.qSeq + qs) * params.qHidden + qh * params.headSize;
|
| 57 |
+
{% else %}
|
| 58 |
+
let qBase = ((batch * params.qHeads + qh) * params.qSeq + qs) * params.headSize;
|
| 59 |
+
let kBase = (batch * params.kvHeads + kh) * params.kvSeq;
|
| 60 |
+
let vBase = (batch * params.kvHeads + kh) * params.kvSeq;
|
| 61 |
+
{% endif %}
|
| 62 |
let scale = scale_value();
|
| 63 |
|
| 64 |
for (var d: u32 = 0u; d < params.vHeadSize; d = d + 1u) {
|
|
|
|
| 83 |
// A rejected bool-mask key has zero softmax mass. Skipping it is safe here:
|
| 84 |
// this thread-per-query kernel has no barriers inside the key loop.
|
| 85 |
if (masked) { continue; }
|
| 86 |
+
// Adjacent query threads load the same K/V row for this key.
|
| 87 |
var score: f32 = -3.4028234663852886e38;
|
| 88 |
if (!masked) {
|
| 89 |
+
{% if layout == "bsh" %}
|
| 90 |
let kRow = (batch * params.kvSeq + ks) * params.kvHidden + kh * params.headSize;
|
| 91 |
+
{% else %}
|
| 92 |
+
let kRow = (kBase + ks) * params.headSize;
|
| 93 |
+
{% endif %}
|
| 94 |
var dot: f32 = 0.0;
|
| 95 |
for (var d: u32 = 0u; d < params.headSize; d = d + 1u) {
|
| 96 |
dot = dot + f32(q[qBase + d]) * f32(k[kRow + d]);
|
|
|
|
| 110 |
let weight = exp(score - next_max);
|
| 111 |
running_max = next_max;
|
| 112 |
running_denom = running_denom * prev_scale + weight;
|
| 113 |
+
{% if layout == "bsh" %}
|
| 114 |
let vRow = (batch * params.kvSeq + ks) * params.vHidden + kh * params.vHeadSize;
|
| 115 |
+
{% else %}
|
| 116 |
+
let vRow = (vBase + ks) * params.vHeadSize;
|
| 117 |
+
{% endif %}
|
| 118 |
for (var d: u32 = 0u; d < params.vHeadSize; d = d + 1u) {
|
| 119 |
acc[d * BLOCK_M + tid] = acc[d * BLOCK_M + tid] * prev_scale + weight * f32(v[vRow + d]);
|
| 120 |
}
|
| 121 |
}
|
| 122 |
|
| 123 |
let inv_denom = select(0.0, 1.0 / running_denom, running_denom > 0.0);
|
| 124 |
+
{% if layout == "bsh" %}
|
| 125 |
let yBase = (batch * params.qSeq + qs) * (params.qHeads * params.vHeadSize) + qh * params.vHeadSize;
|
| 126 |
+
{% else %}
|
| 127 |
+
let yBase = ((batch * params.qHeads + qh) * params.qSeq + qs) * params.vHeadSize;
|
| 128 |
+
{% endif %}
|
| 129 |
for (var d: u32 = 0u; d < params.vHeadSize; d = d + 1u) {
|
| 130 |
y[yBase + d] = {{ scalar }}(acc[d * BLOCK_M + tid] * inv_denom);
|
| 131 |
}
|
build/webgpu/attn-flash-decode-splitk-merge.wgsl.jinja
CHANGED
|
@@ -3,6 +3,7 @@ enable f16;
|
|
| 3 |
{% endif %}
|
| 4 |
{% if splitQueries is not defined %}{% set splitQueries = false %}{% endif %}
|
| 5 |
{% if hasGate is not defined %}{% set hasGate = false %}{% endif %}
|
|
|
|
| 6 |
{{ env.wgsl.resourceDeclarations }}
|
| 7 |
|
| 8 |
// Split-K flash decode, pass 2 of 2. Combines the per-split un-normalized online
|
|
@@ -105,7 +106,7 @@ fn main(
|
|
| 105 |
outValue = outValue + vec4<f32>(bias[vBiasBase], bias[vBiasBase + 1u], bias[vBiasBase + 2u], bias[vBiasBase + 3u]);
|
| 106 |
{% endif %}
|
| 107 |
{% if hasGate %}
|
| 108 |
-
//
|
| 109 |
let gateV = vec4<f32>(gate[qBaseV4 + d4]);
|
| 110 |
outValue = outValue * (vec4<f32>(1.0) / (vec4<f32>(1.0) + exp(-gateV)));
|
| 111 |
{% endif %}
|
|
|
|
| 3 |
{% endif %}
|
| 4 |
{% if splitQueries is not defined %}{% set splitQueries = false %}{% endif %}
|
| 5 |
{% if hasGate is not defined %}{% set hasGate = false %}{% endif %}
|
| 6 |
+
{% set qSeq = qSeq | default(0) %}
|
| 7 |
{{ env.wgsl.resourceDeclarations }}
|
| 8 |
|
| 9 |
// Split-K flash decode, pass 2 of 2. Combines the per-split un-normalized online
|
|
|
|
| 106 |
outValue = outValue + vec4<f32>(bias[vBiasBase], bias[vBiasBase + 1u], bias[vBiasBase + 2u], bias[vBiasBase + 3u]);
|
| 107 |
{% endif %}
|
| 108 |
{% if hasGate %}
|
| 109 |
+
// The gated route multiplies the normalized attention output elementwise by its gate.
|
| 110 |
let gateV = vec4<f32>(gate[qBaseV4 + d4]);
|
| 111 |
outValue = outValue * (vec4<f32>(1.0) / (vec4<f32>(1.0) + exp(-gateV)));
|
| 112 |
{% endif %}
|
build/webgpu/attn-flash-decode-splitk.wgsl.jinja
CHANGED
|
@@ -4,17 +4,17 @@
|
|
| 4 |
{% if cacheSeqlens is not defined %}{% set cacheSeqlens = false %}{% endif %}
|
| 5 |
{% if hasMask is not defined %}{% set hasMask = false %}{% endif %}
|
| 6 |
{% if maskIsBool is not defined %}{% set maskIsBool = false %}{% endif %}
|
| 7 |
-
{% set
|
|
|
|
|
|
|
|
|
|
| 8 |
{% if useSubgroups %}
|
| 9 |
enable subgroups;
|
| 10 |
{% endif %}
|
| 11 |
-
{% if usesF16 %}
|
| 12 |
-
enable f16;
|
| 13 |
-
{% endif %}
|
| 14 |
{{ env.wgsl.resourceDeclarations }}
|
| 15 |
|
| 16 |
-
// Split-K flash attention, pass 1 of 2; the merge pass follows.
|
| 17 |
-
//
|
| 18 |
//
|
| 19 |
// The non-split flash decode launches only `batch * numHeads` workgroups, each
|
| 20 |
// sweeping the whole KV sequence serially in WG-key tiles. This pass splits the
|
|
@@ -81,20 +81,22 @@ var<workgroup> vacc_sh: array<vec4<f32>, WG>;
|
|
| 81 |
// calls before their shared partial storage is reused.
|
| 82 |
{% set combineSubgroups = combineSubgroups is defined and combineSubgroups %}
|
| 83 |
{% if combineSubgroups %}
|
| 84 |
-
//
|
| 85 |
-
//
|
| 86 |
-
//
|
| 87 |
-
//
|
| 88 |
-
//
|
| 89 |
-
//
|
| 90 |
-
//
|
|
|
|
| 91 |
var<workgroup> partialM: array<f32, WG>;
|
| 92 |
var<workgroup> partialD: array<f32, WG>;
|
|
|
|
| 93 |
var<workgroup> combinedMD: vec2<f32>;
|
| 94 |
|
| 95 |
// When the whole workgroup is one subgroup the subgroup reduce already covers
|
| 96 |
-
// it (no barriers, no shared state)
|
| 97 |
-
//
|
| 98 |
fn combine_partials(m: f32, d: f32, lidx: u32, sgSize: u32) -> vec2<f32> {
|
| 99 |
let sgM = subgroupMax(m);
|
| 100 |
// A lane with no elements contributes d == 0 (exact identity). A +inf
|
|
@@ -105,31 +107,24 @@ fn combine_partials(m: f32, d: f32, lidx: u32, sgSize: u32) -> vec2<f32> {
|
|
| 105 |
if (sgSize == WG) {
|
| 106 |
return vec2<f32>(sgM, sgD);
|
| 107 |
}
|
| 108 |
-
let subgroupCount = (WG + sgSize - 1u) / sgSize;
|
| 109 |
-
// Pre-seed every fold slot with the (max, denom) identity. The fold below reads a
|
| 110 |
-
// fixed subgroupCount slots in ordinal order (for determinism), but a slot whose
|
| 111 |
-
// subgroup elects no leader this call — e.g. a fully out-of-window key tile in the
|
| 112 |
-
// flash-attention loop that re-uses this shared memory each iteration — would
|
| 113 |
-
// otherwise read stale shared memory. Identity makes such a slot a no-op.
|
| 114 |
-
// (max identity = -FLT_MAX, denom identity = 0.)
|
| 115 |
-
if (lidx < subgroupCount) {
|
| 116 |
-
partialM[lidx] = -FLT_MAX;
|
| 117 |
-
partialD[lidx] = 0.0;
|
| 118 |
-
}
|
| 119 |
-
workgroupBarrier();
|
| 120 |
if (subgroupElect()) {
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
}
|
| 125 |
workgroupBarrier();
|
| 126 |
if (lidx == 0u) {
|
| 127 |
var accM = -FLT_MAX;
|
| 128 |
var accD = 0.0;
|
| 129 |
-
for (var
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
}
|
| 134 |
combinedMD = vec2<f32>(accM, accD);
|
| 135 |
}
|
|
@@ -144,9 +139,8 @@ var<workgroup> partialM: array<f32, {{ mdExtent }}>;
|
|
| 144 |
var<workgroup> partialD: array<f32, {{ mdExtent }}>;
|
| 145 |
{% if mdStreamed %}
|
| 146 |
|
| 147 |
-
// In-place fold of {{ mdStreams }} streams.
|
| 148 |
-
//
|
| 149 |
-
// from slot s * WG afterwards.
|
| 150 |
fn combine_partials_streams(lidx: u32) {
|
| 151 |
workgroupBarrier();
|
| 152 |
var stride = WG / 2u;
|
|
|
|
| 4 |
{% if cacheSeqlens is not defined %}{% set cacheSeqlens = false %}{% endif %}
|
| 5 |
{% if hasMask is not defined %}{% set hasMask = false %}{% endif %}
|
| 6 |
{% if maskIsBool is not defined %}{% set maskIsBool = false %}{% endif %}
|
| 7 |
+
{% set scale = scale | default("0.0") %}
|
| 8 |
+
{% if hasRotary is not defined %}{% set hasRotary = false %}{% endif %}
|
| 9 |
+
{% set qSeq = qSeq | default(0) %}
|
| 10 |
+
{% set splitKWorkgroupSize = workgroupSizeSpec if workgroupSizeSpec is defined else tunables.WORKGROUP_SIZE %}
|
| 11 |
{% if useSubgroups %}
|
| 12 |
enable subgroups;
|
| 13 |
{% endif %}
|
|
|
|
|
|
|
|
|
|
| 14 |
{{ env.wgsl.resourceDeclarations }}
|
| 15 |
|
| 16 |
+
// Split-K flash attention, pass 1 of 2; the merge pass follows. This geometry
|
| 17 |
+
// handles decode and short-query, long-context prefill inputs.
|
| 18 |
//
|
| 19 |
// The non-split flash decode launches only `batch * numHeads` workgroups, each
|
| 20 |
// sweeping the whole KV sequence serially in WG-key tiles. This pass splits the
|
|
|
|
| 81 |
// calls before their shared partial storage is reused.
|
| 82 |
{% set combineSubgroups = combineSubgroups is defined and combineSubgroups %}
|
| 83 |
{% if combineSubgroups %}
|
| 84 |
+
// Cross-subgroup merge that assumes nothing about which invocations share a
|
| 85 |
+
// subgroup or how many subgroups there are: each subgroup's elected lane
|
| 86 |
+
// publishes the subgroup pair in the slot at its OWN invocation index and sets
|
| 87 |
+
// that index's bit in a workgroup bitmask; thread 0 then folds exactly the
|
| 88 |
+
// published slots, in ascending index order (the online (m, d) merge is not
|
| 89 |
+
// float-associative, so the order is fixed), and clears the mask for the next
|
| 90 |
+
// call as it reads it. Workgroup memory starts zeroed, so the mask needs no
|
| 91 |
+
// setup. Same three collectives as a single-subgroup reduce, two barriers.
|
| 92 |
var<workgroup> partialM: array<f32, WG>;
|
| 93 |
var<workgroup> partialD: array<f32, WG>;
|
| 94 |
+
var<workgroup> leaderMask: array<atomic<u32>, (WG + 31u) / 32u>;
|
| 95 |
var<workgroup> combinedMD: vec2<f32>;
|
| 96 |
|
| 97 |
// When the whole workgroup is one subgroup the subgroup reduce already covers
|
| 98 |
+
// it (no barriers, no shared state). `subgroup_size` is the size of the current
|
| 99 |
+
// subgroup and uniform, so the test is exact and may guard the barriers below.
|
| 100 |
fn combine_partials(m: f32, d: f32, lidx: u32, sgSize: u32) -> vec2<f32> {
|
| 101 |
let sgM = subgroupMax(m);
|
| 102 |
// A lane with no elements contributes d == 0 (exact identity). A +inf
|
|
|
|
| 107 |
if (sgSize == WG) {
|
| 108 |
return vec2<f32>(sgM, sgD);
|
| 109 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
if (subgroupElect()) {
|
| 111 |
+
partialM[lidx] = sgM;
|
| 112 |
+
partialD[lidx] = sgD;
|
| 113 |
+
atomicOr(&leaderMask[lidx / 32u], 1u << (lidx % 32u));
|
| 114 |
}
|
| 115 |
workgroupBarrier();
|
| 116 |
if (lidx == 0u) {
|
| 117 |
var accM = -FLT_MAX;
|
| 118 |
var accD = 0.0;
|
| 119 |
+
for (var w = 0u; w < (WG + 31u) / 32u; w = w + 1u) {
|
| 120 |
+
var bits = atomicExchange(&leaderMask[w], 0u);
|
| 121 |
+
while (bits != 0u) {
|
| 122 |
+
let slot = w * 32u + firstTrailingBit(bits);
|
| 123 |
+
bits = bits & (bits - 1u);
|
| 124 |
+
let mNew = max(accM, partialM[slot]);
|
| 125 |
+
accD = accD * exp_shift(accM, mNew) + partialD[slot] * exp_shift(partialM[slot], mNew);
|
| 126 |
+
accM = mNew;
|
| 127 |
+
}
|
| 128 |
}
|
| 129 |
combinedMD = vec2<f32>(accM, accD);
|
| 130 |
}
|
|
|
|
| 139 |
var<workgroup> partialD: array<f32, {{ mdExtent }}>;
|
| 140 |
{% if mdStreamed %}
|
| 141 |
|
| 142 |
+
// In-place fold of {{ mdStreams }} streams. Input partials occupy
|
| 143 |
+
// partialM/partialD; stream s returns its merged pair in slot s * WG.
|
|
|
|
| 144 |
fn combine_partials_streams(lidx: u32) {
|
| 145 |
workgroupBarrier();
|
| 146 |
var stride = WG / 2u;
|
build/webgpu/attn-flash-online.wgsl.jinja
CHANGED
|
@@ -1,13 +1,10 @@
|
|
| 1 |
-
{% if usesF16 %}
|
| 2 |
-
enable f16;
|
| 3 |
-
{% endif %}
|
| 4 |
{% if combineSubgroups %}
|
| 5 |
enable subgroups;
|
| 6 |
{% endif %}
|
| 7 |
{{ env.wgsl.resourceDeclarations }}
|
| 8 |
|
| 9 |
-
// Flash-style tiled online-softmax attention
|
| 10 |
-
//
|
| 11 |
// (batch, head, query token); the workgroup sweeps the KV sequence in tiles
|
| 12 |
// of WG keys, each thread owning one key of the tile (full q·k dot in
|
| 13 |
// registers, vec4 loads), so a tile needs a single fused (max, sum-exp)
|
|
@@ -16,9 +13,6 @@ enable subgroups;
|
|
| 16 |
// per tile; the full score matrix is never materialized.
|
| 17 |
// Layout: token-major [batch, seq, heads * headDim]; Q and KV hidden strides
|
| 18 |
// are compiled constants.
|
| 19 |
-
{% if headsFromParams %}
|
| 20 |
-
// Q/KV head counts are uniforms; the GQA mapping is h / (qHeads / kvHeads).
|
| 21 |
-
{% endif %}
|
| 22 |
{% if hasBias %}
|
| 23 |
// Packed [Q; K; V] bias rows. The K bias adds the same dot(q, biasK) to every
|
| 24 |
// key score, which softmax cancels, so only the Q and V biases are applied.
|
|
@@ -32,10 +26,8 @@ const HEAD_DIM: u32 = {{ headDim }}u;
|
|
| 32 |
const HEAD_DIM_V4: u32 = {{ headDimV4 }}u;
|
| 33 |
const Q_HIDDEN_V4: u32 = {{ qHiddenV4 }}u;
|
| 34 |
const KV_HIDDEN_V4: u32 = {{ kvHiddenV4 }}u;
|
| 35 |
-
{% if not headsFromParams %}
|
| 36 |
const Q_HEADS: u32 = {{ qNumHeads }}u;
|
| 37 |
const KV_HEADS: u32 = {{ kvNumHeads }}u;
|
| 38 |
-
{% endif %}
|
| 39 |
{% set qHeads = "params.qHeads" if headsFromParams else "Q_HEADS" %}
|
| 40 |
{% set kvHeads = "params.kvHeads" if headsFromParams else "KV_HEADS" %}
|
| 41 |
const WG: u32 = {{ tunables.WORKGROUP_SIZE }}u;
|
|
@@ -73,20 +65,22 @@ var<workgroup> probs: array<f32, WG>;
|
|
| 73 |
// calls before their shared partial storage is reused.
|
| 74 |
{% set combineSubgroups = combineSubgroups is defined and combineSubgroups %}
|
| 75 |
{% if combineSubgroups %}
|
| 76 |
-
//
|
| 77 |
-
//
|
| 78 |
-
//
|
| 79 |
-
//
|
| 80 |
-
//
|
| 81 |
-
//
|
| 82 |
-
//
|
|
|
|
| 83 |
var<workgroup> partialM: array<f32, WG>;
|
| 84 |
var<workgroup> partialD: array<f32, WG>;
|
|
|
|
| 85 |
var<workgroup> combinedMD: vec2<f32>;
|
| 86 |
|
| 87 |
// When the whole workgroup is one subgroup the subgroup reduce already covers
|
| 88 |
-
// it (no barriers, no shared state)
|
| 89 |
-
//
|
| 90 |
fn combine_partials(m: f32, d: f32, lidx: u32, sgSize: u32) -> vec2<f32> {
|
| 91 |
let sgM = subgroupMax(m);
|
| 92 |
// A lane with no elements contributes d == 0 (exact identity). A +inf
|
|
@@ -97,31 +91,24 @@ fn combine_partials(m: f32, d: f32, lidx: u32, sgSize: u32) -> vec2<f32> {
|
|
| 97 |
if (sgSize == WG) {
|
| 98 |
return vec2<f32>(sgM, sgD);
|
| 99 |
}
|
| 100 |
-
let subgroupCount = (WG + sgSize - 1u) / sgSize;
|
| 101 |
-
// Pre-seed every fold slot with the (max, denom) identity. The fold below reads a
|
| 102 |
-
// fixed subgroupCount slots in ordinal order (for determinism), but a slot whose
|
| 103 |
-
// subgroup elects no leader this call — e.g. a fully out-of-window key tile in the
|
| 104 |
-
// flash-attention loop that re-uses this shared memory each iteration — would
|
| 105 |
-
// otherwise read stale shared memory. Identity makes such a slot a no-op.
|
| 106 |
-
// (max identity = -FLT_MAX, denom identity = 0.)
|
| 107 |
-
if (lidx < subgroupCount) {
|
| 108 |
-
partialM[lidx] = -FLT_MAX;
|
| 109 |
-
partialD[lidx] = 0.0;
|
| 110 |
-
}
|
| 111 |
-
workgroupBarrier();
|
| 112 |
if (subgroupElect()) {
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
}
|
| 117 |
workgroupBarrier();
|
| 118 |
if (lidx == 0u) {
|
| 119 |
var accM = -FLT_MAX;
|
| 120 |
var accD = 0.0;
|
| 121 |
-
for (var
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
}
|
| 126 |
combinedMD = vec2<f32>(accM, accD);
|
| 127 |
}
|
|
@@ -136,9 +123,8 @@ var<workgroup> partialM: array<f32, {{ mdExtent }}>;
|
|
| 136 |
var<workgroup> partialD: array<f32, {{ mdExtent }}>;
|
| 137 |
{% if mdStreamed %}
|
| 138 |
|
| 139 |
-
// In-place fold of {{ mdStreams }} streams.
|
| 140 |
-
//
|
| 141 |
-
// from slot s * WG afterwards.
|
| 142 |
fn combine_partials_streams(lidx: u32) {
|
| 143 |
workgroupBarrier();
|
| 144 |
var stride = WG / 2u;
|
|
@@ -197,8 +183,8 @@ fn combine_partials(m: f32, d: f32, lidx: u32) -> vec2<f32> {
|
|
| 197 |
{% endif %}
|
| 198 |
|
| 199 |
|
| 200 |
-
//
|
| 201 |
-
//
|
| 202 |
{% if ATTN_SCALE_DIM is not defined %}{% set ATTN_SCALE_DIM = "HEAD_DIM" %}{% endif %}
|
| 203 |
fn scale_value() -> f32 {
|
| 204 |
if (params.scale != 0.0) { return params.scale; }
|
|
@@ -257,11 +243,7 @@ fn main(
|
|
| 257 |
// Causal upper bound: query qi attends only keys 0..qi, so stop after the tile
|
| 258 |
// containing qi and skip the unattended tail. Non-causal keeps the full kvSeq
|
| 259 |
// sweep.
|
| 260 |
-
{% if hasCausal %}
|
| 261 |
var keyBoundV = select(params.kvSeq, min(params.kvSeq, qi + 1u), params.isCausal != 0u);
|
| 262 |
-
{% else %}
|
| 263 |
-
var keyBoundV = params.kvSeq;
|
| 264 |
-
{% endif %}
|
| 265 |
var keyFloor: u32 = 0u;
|
| 266 |
let keyBound = keyBoundV;
|
| 267 |
// Start at the WG-aligned tile containing keyFloor (sub-floor lanes mask out).
|
|
@@ -279,7 +261,7 @@ fn main(
|
|
| 279 |
var score = -FLT_MAX;
|
| 280 |
var m = -FLT_MAX;
|
| 281 |
var dPart = 0.0;
|
| 282 |
-
var keyAllowed = kj < keyBound
|
| 283 |
if (keyAllowed) {
|
| 284 |
let kRowV4 = kvBaseV4 + kj * kvTokenStrideV4;
|
| 285 |
{% if hasMask %}
|
|
@@ -287,17 +269,6 @@ fn main(
|
|
| 287 |
// [q, k] masks set batch/head strides to 0).
|
| 288 |
let maskIndex = b * params.maskBatchStride + h * params.maskHeadStride + qi * params.maskSeqStride + kj;
|
| 289 |
{% endif %}
|
| 290 |
-
{% if hasMask and maskIsBool %}
|
| 291 |
-
if (attn_mask[maskIndex] == 0u) {
|
| 292 |
-
keyAllowed = false;
|
| 293 |
-
} else {
|
| 294 |
-
var acc: f32 = 0.0;
|
| 295 |
-
for (var d4: u32 = 0u; d4 < HEAD_DIM_V4; d4 = d4 + 1u) {
|
| 296 |
-
acc = acc + dot(q_shared[d4], vec4<f32>(key[kRowV4 + d4]));
|
| 297 |
-
}
|
| 298 |
-
score = acc * scale;
|
| 299 |
-
}
|
| 300 |
-
{% else %}
|
| 301 |
var acc: f32 = 0.0;
|
| 302 |
for (var d4: u32 = 0u; d4 < HEAD_DIM_V4; d4 = d4 + 1u) {
|
| 303 |
acc = acc + dot(q_shared[d4], vec4<f32>(key[kRowV4 + d4]));
|
|
@@ -305,7 +276,6 @@ fn main(
|
|
| 305 |
score = acc * scale;
|
| 306 |
{% if hasMask %}
|
| 307 |
score = score + f32(attn_mask[maskIndex]);
|
| 308 |
-
{% endif %}
|
| 309 |
{% endif %}
|
| 310 |
m = score;
|
| 311 |
dPart = select(0.0, 1.0, keyAllowed);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
{% if combineSubgroups %}
|
| 2 |
enable subgroups;
|
| 3 |
{% endif %}
|
| 4 |
{{ env.wgsl.resourceDeclarations }}
|
| 5 |
|
| 6 |
+
// Flash-style tiled online-softmax attention for vec4-aligned head dimensions.
|
| 7 |
+
// One workgroup per
|
| 8 |
// (batch, head, query token); the workgroup sweeps the KV sequence in tiles
|
| 9 |
// of WG keys, each thread owning one key of the tile (full q·k dot in
|
| 10 |
// registers, vec4 loads), so a tile needs a single fused (max, sum-exp)
|
|
|
|
| 13 |
// per tile; the full score matrix is never materialized.
|
| 14 |
// Layout: token-major [batch, seq, heads * headDim]; Q and KV hidden strides
|
| 15 |
// are compiled constants.
|
|
|
|
|
|
|
|
|
|
| 16 |
{% if hasBias %}
|
| 17 |
// Packed [Q; K; V] bias rows. The K bias adds the same dot(q, biasK) to every
|
| 18 |
// key score, which softmax cancels, so only the Q and V biases are applied.
|
|
|
|
| 26 |
const HEAD_DIM_V4: u32 = {{ headDimV4 }}u;
|
| 27 |
const Q_HIDDEN_V4: u32 = {{ qHiddenV4 }}u;
|
| 28 |
const KV_HIDDEN_V4: u32 = {{ kvHiddenV4 }}u;
|
|
|
|
| 29 |
const Q_HEADS: u32 = {{ qNumHeads }}u;
|
| 30 |
const KV_HEADS: u32 = {{ kvNumHeads }}u;
|
|
|
|
| 31 |
{% set qHeads = "params.qHeads" if headsFromParams else "Q_HEADS" %}
|
| 32 |
{% set kvHeads = "params.kvHeads" if headsFromParams else "KV_HEADS" %}
|
| 33 |
const WG: u32 = {{ tunables.WORKGROUP_SIZE }}u;
|
|
|
|
| 65 |
// calls before their shared partial storage is reused.
|
| 66 |
{% set combineSubgroups = combineSubgroups is defined and combineSubgroups %}
|
| 67 |
{% if combineSubgroups %}
|
| 68 |
+
// Cross-subgroup merge that assumes nothing about which invocations share a
|
| 69 |
+
// subgroup or how many subgroups there are: each subgroup's elected lane
|
| 70 |
+
// publishes the subgroup pair in the slot at its OWN invocation index and sets
|
| 71 |
+
// that index's bit in a workgroup bitmask; thread 0 then folds exactly the
|
| 72 |
+
// published slots, in ascending index order (the online (m, d) merge is not
|
| 73 |
+
// float-associative, so the order is fixed), and clears the mask for the next
|
| 74 |
+
// call as it reads it. Workgroup memory starts zeroed, so the mask needs no
|
| 75 |
+
// setup. Same three collectives as a single-subgroup reduce, two barriers.
|
| 76 |
var<workgroup> partialM: array<f32, WG>;
|
| 77 |
var<workgroup> partialD: array<f32, WG>;
|
| 78 |
+
var<workgroup> leaderMask: array<atomic<u32>, (WG + 31u) / 32u>;
|
| 79 |
var<workgroup> combinedMD: vec2<f32>;
|
| 80 |
|
| 81 |
// When the whole workgroup is one subgroup the subgroup reduce already covers
|
| 82 |
+
// it (no barriers, no shared state). `subgroup_size` is the size of the current
|
| 83 |
+
// subgroup and uniform, so the test is exact and may guard the barriers below.
|
| 84 |
fn combine_partials(m: f32, d: f32, lidx: u32, sgSize: u32) -> vec2<f32> {
|
| 85 |
let sgM = subgroupMax(m);
|
| 86 |
// A lane with no elements contributes d == 0 (exact identity). A +inf
|
|
|
|
| 91 |
if (sgSize == WG) {
|
| 92 |
return vec2<f32>(sgM, sgD);
|
| 93 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
if (subgroupElect()) {
|
| 95 |
+
partialM[lidx] = sgM;
|
| 96 |
+
partialD[lidx] = sgD;
|
| 97 |
+
atomicOr(&leaderMask[lidx / 32u], 1u << (lidx % 32u));
|
| 98 |
}
|
| 99 |
workgroupBarrier();
|
| 100 |
if (lidx == 0u) {
|
| 101 |
var accM = -FLT_MAX;
|
| 102 |
var accD = 0.0;
|
| 103 |
+
for (var w = 0u; w < (WG + 31u) / 32u; w = w + 1u) {
|
| 104 |
+
var bits = atomicExchange(&leaderMask[w], 0u);
|
| 105 |
+
while (bits != 0u) {
|
| 106 |
+
let slot = w * 32u + firstTrailingBit(bits);
|
| 107 |
+
bits = bits & (bits - 1u);
|
| 108 |
+
let mNew = max(accM, partialM[slot]);
|
| 109 |
+
accD = accD * exp_shift(accM, mNew) + partialD[slot] * exp_shift(partialM[slot], mNew);
|
| 110 |
+
accM = mNew;
|
| 111 |
+
}
|
| 112 |
}
|
| 113 |
combinedMD = vec2<f32>(accM, accD);
|
| 114 |
}
|
|
|
|
| 123 |
var<workgroup> partialD: array<f32, {{ mdExtent }}>;
|
| 124 |
{% if mdStreamed %}
|
| 125 |
|
| 126 |
+
// In-place fold of {{ mdStreams }} streams. Input partials occupy
|
| 127 |
+
// partialM/partialD; stream s returns its merged pair in slot s * WG.
|
|
|
|
| 128 |
fn combine_partials_streams(lidx: u32) {
|
| 129 |
workgroupBarrier();
|
| 130 |
var stride = WG / 2u;
|
|
|
|
| 183 |
{% endif %}
|
| 184 |
|
| 185 |
|
| 186 |
+
// An explicit-zero specialization bakes the scale as 0. Otherwise,
|
| 187 |
+
// params.scale == 0 encodes an omitted scale and selects 1/sqrt(headDim).
|
| 188 |
{% if ATTN_SCALE_DIM is not defined %}{% set ATTN_SCALE_DIM = "HEAD_DIM" %}{% endif %}
|
| 189 |
fn scale_value() -> f32 {
|
| 190 |
if (params.scale != 0.0) { return params.scale; }
|
|
|
|
| 243 |
// Causal upper bound: query qi attends only keys 0..qi, so stop after the tile
|
| 244 |
// containing qi and skip the unattended tail. Non-causal keeps the full kvSeq
|
| 245 |
// sweep.
|
|
|
|
| 246 |
var keyBoundV = select(params.kvSeq, min(params.kvSeq, qi + 1u), params.isCausal != 0u);
|
|
|
|
|
|
|
|
|
|
| 247 |
var keyFloor: u32 = 0u;
|
| 248 |
let keyBound = keyBoundV;
|
| 249 |
// Start at the WG-aligned tile containing keyFloor (sub-floor lanes mask out).
|
|
|
|
| 261 |
var score = -FLT_MAX;
|
| 262 |
var m = -FLT_MAX;
|
| 263 |
var dPart = 0.0;
|
| 264 |
+
var keyAllowed = kj < keyBound && (params.isCausal == 0u || kj <= qi);
|
| 265 |
if (keyAllowed) {
|
| 266 |
let kRowV4 = kvBaseV4 + kj * kvTokenStrideV4;
|
| 267 |
{% if hasMask %}
|
|
|
|
| 269 |
// [q, k] masks set batch/head strides to 0).
|
| 270 |
let maskIndex = b * params.maskBatchStride + h * params.maskHeadStride + qi * params.maskSeqStride + kj;
|
| 271 |
{% endif %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
var acc: f32 = 0.0;
|
| 273 |
for (var d4: u32 = 0u; d4 < HEAD_DIM_V4; d4 = d4 + 1u) {
|
| 274 |
acc = acc + dot(q_shared[d4], vec4<f32>(key[kRowV4 + d4]));
|
|
|
|
| 276 |
score = acc * scale;
|
| 277 |
{% if hasMask %}
|
| 278 |
score = score + f32(attn_mask[maskIndex]);
|
|
|
|
| 279 |
{% endif %}
|
| 280 |
m = score;
|
| 281 |
dPart = select(0.0, 1.0, keyAllowed);
|
build/webgpu/attn-flash-prefill-cluster.wgsl.jinja
CHANGED
|
@@ -1,4 +1,6 @@
|
|
| 1 |
-
{% set sourceProfile =
|
|
|
|
|
|
|
| 2 |
{% set QSEQ = "params.seq_len" if sourceProfile == 1 else "params.qSeq" %}
|
| 3 |
{% set KVSEQ = "(params.past_len + params.seq_len)" if sourceProfile == 1 else "params.kvSeq" %}
|
| 4 |
{% set IS_CAUSAL = "1u" if sourceProfile == 1 else "params.isCausal" %}
|
|
@@ -7,6 +9,7 @@
|
|
| 7 |
{% set KEY = "cache_keys" if sourceProfile == 1 else "key" %}
|
| 8 |
{% set VALUE = "cache_values" if sourceProfile == 1 else "value" %}
|
| 9 |
{% set OUTPUT = "attn_out" if sourceProfile == 1 else "output" %}
|
|
|
|
| 10 |
{% if useSubgroups is not defined %}{% set useSubgroups = true %}{% endif %}
|
| 11 |
{% if batchNoSgReduction is not defined %}{% set batchNoSgReduction = false %}{% endif %}
|
| 12 |
{% if hasMask is not defined %}{% set hasMask = false %}{% endif %}
|
|
@@ -27,9 +30,6 @@
|
|
| 27 |
{% if useSubgroups %}
|
| 28 |
enable subgroups;
|
| 29 |
{% endif %}
|
| 30 |
-
{% if usesF16 %}
|
| 31 |
-
enable f16;
|
| 32 |
-
{% endif %}
|
| 33 |
{{ env.wgsl.resourceDeclarations }}
|
| 34 |
{% set ST = "f16" if usesF16 else "f32" %}
|
| 35 |
// K/V tiles are staged as f16 when this specialization uses f16, reducing
|
|
@@ -43,12 +43,14 @@ enable f16;
|
|
| 43 |
{% set MASK_ELEMENT = "maskValue" if STAGE_MASK else "attn_mask[maskIndex]" %}
|
| 44 |
{% set MASK_ADDITIVE = "maskValue" if STAGE_MASK else "f32(attn_mask[maskIndex])" %}
|
| 45 |
{% set SLICE_COUNT = ((headDimV4 / LPQ) | int) %}
|
|
|
|
|
|
|
| 46 |
|
| 47 |
// Tiled flash prefill attention with configurable-width query clusters for
|
| 48 |
// token-major [batch, seq, heads*headDim] attention ops. Each workgroup covers
|
| 49 |
// TILE_Q queries with one LPQ-lane cluster per query; K/V are staged in
|
| 50 |
-
// workgroup memory once per TILE_K tile. Within a cluster, each lane holds
|
| 51 |
-
//
|
| 52 |
// combined with subgroup shuffles when available, or shared-memory reductions
|
| 53 |
// on compatibility variants. Dot products, online softmax, and weighted-value
|
| 54 |
// sums accumulate in f32. Optional masks are broadcast-strided; the
|
|
@@ -62,7 +64,8 @@ enable f16;
|
|
| 62 |
const HEAD_DIM: u32 = {{ headDim }}u;
|
| 63 |
const HEAD_DIM_V4: u32 = {{ headDimV4 }}u;
|
| 64 |
const Q_HIDDEN_V4: u32 = {{ qHiddenV4 }}u; // bsh token-major hidden stride
|
| 65 |
-
const
|
|
|
|
| 66 |
const Q_HEADS: u32 = {{ qNumHeads }}u;
|
| 67 |
const KV_HEADS: u32 = {{ kvNumHeads }}u;
|
| 68 |
const TILE_Q: u32 = {{ TILE_Q }}u;
|
|
@@ -70,7 +73,12 @@ const TILE_Q: u32 = {{ TILE_Q }}u;
|
|
| 70 |
const LPQ: u32 = {{ LPQ }}u;
|
| 71 |
const SLICE: u32 = HEAD_DIM / (4u * LPQ); // vec4s per lane slice
|
| 72 |
const TILE_K: u32 = {{ TILE_K }}u;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
const WG: u32 = TILE_Q * LPQ;
|
|
|
|
| 74 |
{% if MASK_IS_INT %}
|
| 75 |
// Key-keep masks in contrib attention use a finite low logit for a rejected
|
| 76 |
// key. Logical ONNX bool masks use the exclusion sentinel instead, so a fully
|
|
@@ -103,8 +111,12 @@ var<workgroup> red: array<f32, WG>;
|
|
| 103 |
|
| 104 |
{% if ATTN_SCALE_DIM is not defined %}{% set ATTN_SCALE_DIM = "HEAD_DIM" %}{% endif %}
|
| 105 |
fn scale_value() -> f32 {
|
|
|
|
|
|
|
|
|
|
| 106 |
if (params.scale != 0.0) { return params.scale; }
|
| 107 |
return inverseSqrt(f32({{ ATTN_SCALE_DIM }}));
|
|
|
|
| 108 |
}
|
| 109 |
|
| 110 |
{% if quantCacheFormat %}
|
|
@@ -171,36 +183,48 @@ fn main(
|
|
| 171 |
// portability.
|
| 172 |
if (sgSize < LPQ || sgSize % LPQ != 0u) { return; }
|
| 173 |
{% endif %}
|
|
|
|
|
|
|
|
|
|
| 174 |
let qSub = tid / LPQ;
|
|
|
|
| 175 |
let lane8 = tid % LPQ;
|
| 176 |
-
|
| 177 |
-
let
|
|
|
|
|
|
|
| 178 |
let hKv = h / (Q_HEADS / KV_HEADS);
|
| 179 |
let SCALE = scale_value();
|
| 180 |
|
| 181 |
-
// Per-thread q slice
|
| 182 |
-
|
| 183 |
-
let
|
|
|
|
| 184 |
{% for c in range(SLICE_COUNT) %}
|
| 185 |
-
var
|
| 186 |
{% if hasBias %}
|
| 187 |
-
|
| 188 |
{% endif %}
|
| 189 |
-
var
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
{% endfor %}
|
| 191 |
-
var m: f32 = NEG_INF;
|
| 192 |
-
var l: f32 = 0.0;
|
| 193 |
{% if useSeqlens %}
|
| 194 |
|
| 195 |
// Resident rows of a windowed cache: the survivors were shifted down to [0, kvActive),
|
| 196 |
// so query/key DISTANCE is unchanged and every bound below reads as if the cache were
|
| 197 |
// exactly kvActive long. Rotary is excluded from this path (it would need the absolute
|
| 198 |
// position, not the cache-relative one), so pastLenForRope keeps the physical length.
|
| 199 |
-
|
|
|
|
|
|
|
|
|
|
| 200 |
{% endif %}
|
| 201 |
// Causal ceiling per query; the key loop runs over the workgroup's union range
|
| 202 |
// (uniform trip count), masking out-of-range (query, key) pairs.
|
| 203 |
-
{% if hasCausal or hasWindow %}
|
| 204 |
// Upper-left causal and/or sliding-window bounds. Query qIdx sits at
|
| 205 |
// absolute position p = kvSeq - qSeq + qIdx, so it attends keys [minKj, maxKj):
|
| 206 |
// the causal ceiling p+1 (clamped to kvSeq for non-square chunks) and, when
|
|
@@ -209,36 +233,26 @@ fn main(
|
|
| 209 |
// pastLen = kvSeq - qSeq: query tokens are the last qSeq positions of the cache;
|
| 210 |
// saturate so an out-of-contract qSeq > kvSeq can't underflow u32. Used for the
|
| 211 |
// sliding window always, and for the causal ceiling only when right-aligned.
|
| 212 |
-
{%
|
| 213 |
-
var maxKjV = select({{ KVA }}, min(qIdx + 1u, {{ KVA }}), {{ IS_CAUSAL }} != 0u);
|
| 214 |
-
{
|
| 215 |
-
|
| 216 |
-
{% endif %}
|
| 217 |
-
var minKjV: u32 = 0u;
|
| 218 |
let lastQ = min(wg.x * TILE_Q + TILE_Q - 1u, {{ QSEQ }} - 1u);
|
| 219 |
-
{% if hasCausal %}
|
| 220 |
var wgEndV = select({{ KVA }}, min(lastQ + 1u, {{ KVA }}), {{ IS_CAUSAL }} != 0u);
|
| 221 |
-
{% else %}
|
| 222 |
-
var wgEndV = {{ KVA }};
|
| 223 |
-
{% endif %}
|
| 224 |
var wgStartV: u32 = 0u;
|
| 225 |
-
|
| 226 |
-
let
|
|
|
|
|
|
|
| 227 |
let wgEnd = wgEndV;
|
| 228 |
let wgStart = wgStartV;
|
| 229 |
-
{% else %}
|
| 230 |
-
let maxKj = {{ KVA }};
|
| 231 |
-
let minKj: u32 = 0u;
|
| 232 |
-
let wgEnd = {{ KVA }};
|
| 233 |
-
let wgStart: u32 = 0u;
|
| 234 |
-
{% endif %}
|
| 235 |
let kvBatch4 = b * {{ KVSEQ }} * KV_HIDDEN_V4 + hKv * HEAD_DIM_V4;
|
| 236 |
|
| 237 |
var kStart: u32 = wgStart;
|
| 238 |
loop {
|
| 239 |
if (kStart >= wgEnd) { break; }
|
| 240 |
|
| 241 |
-
// Cooperative K/V tile load
|
| 242 |
workgroupBarrier();
|
| 243 |
for (var i: u32 = tid; i < TILE_K * HEAD_DIM_V4; i = i + WG) {
|
| 244 |
let slot = i / HEAD_DIM_V4;
|
|
@@ -274,8 +288,7 @@ fn main(
|
|
| 274 |
}
|
| 275 |
{% endif %}
|
| 276 |
workgroupBarrier();
|
| 277 |
-
// TILE_K
|
| 278 |
-
// multiplying source size, while the q/o register slices remain named.
|
| 279 |
{% if not useSubgroups and batchNoSgReduction %}
|
| 280 |
// First publish every key's partial dot without intervening barriers.
|
| 281 |
var s: array<f32, TILE_K>;
|
|
@@ -330,6 +343,67 @@ fn main(
|
|
| 330 |
{% endif %}
|
| 331 |
}
|
| 332 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 333 |
{% else %}
|
| 334 |
var s: array<f32, TILE_K>;
|
| 335 |
for (var kk: u32 = 0u; kk < TILE_K; kk = kk + 1u) {
|
|
@@ -392,30 +466,55 @@ fn main(
|
|
| 392 |
|
| 393 |
// Per-thread online softmax over the tile. s[kk] is reused to hold the
|
| 394 |
// exponentiated probabilities for the PV accumulation below.
|
| 395 |
-
|
|
|
|
| 396 |
for (var kk: u32 = 1u; kk < TILE_K; kk = kk + 1u) {
|
| 397 |
-
tileMax = max(tileMax, s[kk]);
|
| 398 |
}
|
| 399 |
-
let newMax = max(m, tileMax);
|
| 400 |
-
let corr = select(exp(m - newMax), 0.0, m == NEG_INF);
|
| 401 |
-
var pSum: f32 = 0.0;
|
| 402 |
for (var kk: u32 = 0u; kk < TILE_K; kk = kk + 1u) {
|
| 403 |
-
let pk = select(0.0, exp(s[kk] - newMax), s[kk] != NEG_INF);
|
| 404 |
-
s[kk] = pk;
|
| 405 |
-
pSum = pSum + pk;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
}
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
// A boundary tile holds keys outside this query's attended range, and their staged V rows can
|
| 410 |
-
// be anything — a dynamic-rows prefill legitimately leaves pad rows unwritten, so stale NaN is
|
| 411 |
-
// reachable. An excluded key's weight is exactly 0.0, but 0.0 * NaN is NaN, so the multiply
|
| 412 |
-
// cannot be trusted to drop it: the guarded loop selects the V operand away instead. Interior
|
| 413 |
-
// tiles keep the unguarded FMA chain; the guard changes nothing there anyway (for finite v,
|
| 414 |
-
// acc + 0.0 * v == acc bit-for-bit), it only costs the extra select.
|
| 415 |
-
// Scope: only RANGE exclusion (minKj/maxKj) needs this. Every op that binds
|
| 416 |
-
// a mask supplies materialized K/V, so a mask-rejected key's exact zero
|
| 417 |
-
// weight multiplies finite data. If a masked op ever grows a producer that
|
| 418 |
-
// skips rows, that argument dies and the mask path needs the guard.
|
| 419 |
let tileInterior = kStart >= minKj && kStart + TILE_K <= maxKj;
|
| 420 |
{% for c in range(SLICE_COUNT) %}
|
| 421 |
{
|
|
@@ -432,31 +531,34 @@ fn main(
|
|
| 432 |
o{{ c }} = acc;
|
| 433 |
}
|
| 434 |
{% endfor %}
|
|
|
|
| 435 |
|
| 436 |
kStart = kStart + TILE_K;
|
| 437 |
}
|
| 438 |
|
| 439 |
-
|
| 440 |
-
|
|
|
|
|
|
|
| 441 |
{% if hasHeadSink %}
|
| 442 |
// The head sink is a learned logit that competes with the keys but carries
|
| 443 |
// no value, so it enters the denominator only and the weighted sum above is
|
| 444 |
-
// untouched. Renormalizing against max(m, sink) keeps the exponentials in
|
| 445 |
// range when the sink dominates a fully-masked row.
|
| 446 |
let sink = f32(head_sink[h]);
|
| 447 |
-
let finalM = max(m, sink);
|
| 448 |
-
let accScale = exp(m - finalM);
|
| 449 |
-
let inv = accScale / (exp(sink - finalM) + l * accScale);
|
| 450 |
{% else %}
|
| 451 |
-
// l == 0 means this query had no probability-bearing key: either its
|
| 452 |
// causal/window range is empty or its logical bool mask rejects every key.
|
| 453 |
// Emit 0 rather than 0/0. Other attention paths enforce the same empty-row
|
| 454 |
// contract by selecting on positive global mass.
|
| 455 |
-
let inv = select(0.0, 1.0 / l, l > 0.0);
|
| 456 |
{% endif %}
|
| 457 |
-
{% macro attention_value(c) %}o{{ c }} * inv{% if hasBias %} + load_bias4(2u * {{ Q_HIDDEN }}u + h * HEAD_DIM, lane8 * SLICE + {{ c }}u){% endif %}{% endmacro %}
|
| 458 |
{% for c in range(SLICE_COUNT) %}
|
| 459 |
-
{{ OUTPUT }}[outBase4 + {{ c }}u] = vec4<{{ scalar }}>({{ attention_value(c) }});
|
| 460 |
{% endfor %}
|
| 461 |
}
|
|
|
|
| 462 |
}
|
|
|
|
| 1 |
+
{% set sourceProfile = sourceProfile if sourceProfile is defined else 0 %}
|
| 2 |
+
{% set scaling = scaling | default("0.0") %}
|
| 3 |
+
{% set qkvStrideV4 = qkvStrideV4 | default(0) %}
|
| 4 |
{% set QSEQ = "params.seq_len" if sourceProfile == 1 else "params.qSeq" %}
|
| 5 |
{% set KVSEQ = "(params.past_len + params.seq_len)" if sourceProfile == 1 else "params.kvSeq" %}
|
| 6 |
{% set IS_CAUSAL = "1u" if sourceProfile == 1 else "params.isCausal" %}
|
|
|
|
| 9 |
{% set KEY = "cache_keys" if sourceProfile == 1 else "key" %}
|
| 10 |
{% set VALUE = "cache_values" if sourceProfile == 1 else "value" %}
|
| 11 |
{% set OUTPUT = "attn_out" if sourceProfile == 1 else "output" %}
|
| 12 |
+
{% if sourceProfile == 1 %}{% set ATTN_SCALE_OVERRIDE = scaling %}{% endif %}
|
| 13 |
{% if useSubgroups is not defined %}{% set useSubgroups = true %}{% endif %}
|
| 14 |
{% if batchNoSgReduction is not defined %}{% set batchNoSgReduction = false %}{% endif %}
|
| 15 |
{% if hasMask is not defined %}{% set hasMask = false %}{% endif %}
|
|
|
|
| 30 |
{% if useSubgroups %}
|
| 31 |
enable subgroups;
|
| 32 |
{% endif %}
|
|
|
|
|
|
|
|
|
|
| 33 |
{{ env.wgsl.resourceDeclarations }}
|
| 34 |
{% set ST = "f16" if usesF16 else "f32" %}
|
| 35 |
// K/V tiles are staged as f16 when this specialization uses f16, reducing
|
|
|
|
| 43 |
{% set MASK_ELEMENT = "maskValue" if STAGE_MASK else "attn_mask[maskIndex]" %}
|
| 44 |
{% set MASK_ADDITIVE = "maskValue" if STAGE_MASK else "f32(attn_mask[maskIndex])" %}
|
| 45 |
{% set SLICE_COUNT = ((headDimV4 / LPQ) | int) %}
|
| 46 |
+
{% set QPL = (2 if (not usesF16 and SLICE_COUNT <= 4 and TILE_K <= 16 and TILE_Q % 2 == 0) else 1) if useSubgroups else 1 %}
|
| 47 |
+
{% macro qn(name, qi) %}{{ name }}{% if qi > 0 %}_q{{ qi }}{% endif %}{% endmacro %}
|
| 48 |
|
| 49 |
// Tiled flash prefill attention with configurable-width query clusters for
|
| 50 |
// token-major [batch, seq, heads*headDim] attention ops. Each workgroup covers
|
| 51 |
// TILE_Q queries with one LPQ-lane cluster per query; K/V are staged in
|
| 52 |
+
// workgroup memory once per TILE_K tile. Within a cluster, each lane holds named
|
| 53 |
+
// q/o slices of HEAD_DIM/LPQ dimensions. Per-lane dot fragments are
|
| 54 |
// combined with subgroup shuffles when available, or shared-memory reductions
|
| 55 |
// on compatibility variants. Dot products, online softmax, and weighted-value
|
| 56 |
// sums accumulate in f32. Optional masks are broadcast-strided; the
|
|
|
|
| 64 |
const HEAD_DIM: u32 = {{ headDim }}u;
|
| 65 |
const HEAD_DIM_V4: u32 = {{ headDimV4 }}u;
|
| 66 |
const Q_HIDDEN_V4: u32 = {{ qHiddenV4 }}u; // bsh token-major hidden stride
|
| 67 |
+
{% if sourceProfile == 1 %}const QKV_STRIDE_V4: u32 = {{ qkvStrideV4 }}u; // packed [Q; K; V] input row stride
|
| 68 |
+
{% endif %}const KV_HIDDEN_V4: u32 = {{ kvHiddenV4 }}u;
|
| 69 |
const Q_HEADS: u32 = {{ qNumHeads }}u;
|
| 70 |
const KV_HEADS: u32 = {{ kvNumHeads }}u;
|
| 71 |
const TILE_Q: u32 = {{ TILE_Q }}u;
|
|
|
|
| 73 |
const LPQ: u32 = {{ LPQ }}u;
|
| 74 |
const SLICE: u32 = HEAD_DIM / (4u * LPQ); // vec4s per lane slice
|
| 75 |
const TILE_K: u32 = {{ TILE_K }}u;
|
| 76 |
+
{% if QPL > 1 %}
|
| 77 |
+
const QPL: u32 = {{ QPL }}u;
|
| 78 |
+
const WG: u32 = (TILE_Q / QPL) * LPQ;
|
| 79 |
+
{% else %}
|
| 80 |
const WG: u32 = TILE_Q * LPQ;
|
| 81 |
+
{% endif %}
|
| 82 |
{% if MASK_IS_INT %}
|
| 83 |
// Key-keep masks in contrib attention use a finite low logit for a rejected
|
| 84 |
// key. Logical ONNX bool masks use the exclusion sentinel instead, so a fully
|
|
|
|
| 111 |
|
| 112 |
{% if ATTN_SCALE_DIM is not defined %}{% set ATTN_SCALE_DIM = "HEAD_DIM" %}{% endif %}
|
| 113 |
fn scale_value() -> f32 {
|
| 114 |
+
{% if ATTN_SCALE_OVERRIDE is defined %}
|
| 115 |
+
return {{ ATTN_SCALE_OVERRIDE }};
|
| 116 |
+
{% else %}
|
| 117 |
if (params.scale != 0.0) { return params.scale; }
|
| 118 |
return inverseSqrt(f32({{ ATTN_SCALE_DIM }}));
|
| 119 |
+
{% endif %}
|
| 120 |
}
|
| 121 |
|
| 122 |
{% if quantCacheFormat %}
|
|
|
|
| 183 |
// portability.
|
| 184 |
if (sgSize < LPQ || sgSize % LPQ != 0u) { return; }
|
| 185 |
{% endif %}
|
| 186 |
+
{% if QPL > 1 %}
|
| 187 |
+
let qSub = (tid / LPQ) * QPL;
|
| 188 |
+
{% else %}
|
| 189 |
let qSub = tid / LPQ;
|
| 190 |
+
{% endif %}
|
| 191 |
let lane8 = tid % LPQ;
|
| 192 |
+
{% for qi in range(QPL) %}
|
| 193 |
+
let {{ qn("qIdx", qi) }} = wg.x * TILE_Q + qSub{% if qi > 0 %} + {{ qi }}u{% endif %};
|
| 194 |
+
let {{ qn("qValid", qi) }} = {{ qn("qIdx", qi) }} < {{ QSEQ }} && h < Q_HEADS;
|
| 195 |
+
{% endfor %}
|
| 196 |
let hKv = h / (Q_HEADS / KV_HEADS);
|
| 197 |
let SCALE = scale_value();
|
| 198 |
|
| 199 |
+
// Per-thread q slice and output accumulator, each stored in SLICE named vec4 values.
|
| 200 |
+
{% for qi in range(QPL) %}
|
| 201 |
+
let {{ qn("qClamped", qi) }} = min({{ qn("qIdx", qi) }}, {{ QSEQ }} - 1u);
|
| 202 |
+
let {{ qn("qBase4", qi) }} = (b * {{ QSEQ }} + {{ qn("qClamped", qi) }}) * {{ Q_STRIDE }} + h * HEAD_DIM_V4 + lane8 * SLICE;
|
| 203 |
{% for c in range(SLICE_COUNT) %}
|
| 204 |
+
var {{ qn("qr" ~ c, qi) }} = vec4<f32>({{ QUERY }}[{{ qn("qBase4", qi) }} + {{ c }}u]);
|
| 205 |
{% if hasBias %}
|
| 206 |
+
{{ qn("qr" ~ c, qi) }} = {{ qn("qr" ~ c, qi) }} + load_bias4(h * HEAD_DIM, lane8 * SLICE + {{ c }}u);
|
| 207 |
{% endif %}
|
| 208 |
+
var {{ qn("o" ~ c, qi) }} = vec4<f32>(0.0);
|
| 209 |
+
{% endfor %}
|
| 210 |
+
{% endfor %}
|
| 211 |
+
{% for qi in range(QPL) %}
|
| 212 |
+
var {{ qn("m", qi) }}: f32 = NEG_INF;
|
| 213 |
+
var {{ qn("l", qi) }}: f32 = 0.0;
|
| 214 |
{% endfor %}
|
|
|
|
|
|
|
| 215 |
{% if useSeqlens %}
|
| 216 |
|
| 217 |
// Resident rows of a windowed cache: the survivors were shifted down to [0, kvActive),
|
| 218 |
// so query/key DISTANCE is unchanged and every bound below reads as if the cache were
|
| 219 |
// exactly kvActive long. Rotary is excluded from this path (it would need the absolute
|
| 220 |
// position, not the cache-relative one), so pastLenForRope keeps the physical length.
|
| 221 |
+
// Clamping below by the query count matches the cache-update passes and the scalar
|
| 222 |
+
// path: a right-padded batch (seqlens_k[b]+1 < qSeq) still appends its whole chunk,
|
| 223 |
+
// so its queries score against all of it.
|
| 224 |
+
let kvActive = min({{ KVSEQ }}, max({{ QSEQ }}, u32(seqlens_k[b]) + 1u));
|
| 225 |
{% endif %}
|
| 226 |
// Causal ceiling per query; the key loop runs over the workgroup's union range
|
| 227 |
// (uniform trip count), masking out-of-range (query, key) pairs.
|
|
|
|
| 228 |
// Upper-left causal and/or sliding-window bounds. Query qIdx sits at
|
| 229 |
// absolute position p = kvSeq - qSeq + qIdx, so it attends keys [minKj, maxKj):
|
| 230 |
// the causal ceiling p+1 (clamped to kvSeq for non-square chunks) and, when
|
|
|
|
| 233 |
// pastLen = kvSeq - qSeq: query tokens are the last qSeq positions of the cache;
|
| 234 |
// saturate so an out-of-contract qSeq > kvSeq can't underflow u32. Used for the
|
| 235 |
// sliding window always, and for the causal ceiling only when right-aligned.
|
| 236 |
+
{% for qi in range(QPL) %}
|
| 237 |
+
var {{ qn("maxKjV", qi) }} = select({{ KVA }}, min({{ qn("qIdx", qi) }} + 1u, {{ KVA }}), {{ IS_CAUSAL }} != 0u);
|
| 238 |
+
var {{ qn("minKjV", qi) }}: u32 = 0u;
|
| 239 |
+
{% endfor %}
|
|
|
|
|
|
|
| 240 |
let lastQ = min(wg.x * TILE_Q + TILE_Q - 1u, {{ QSEQ }} - 1u);
|
|
|
|
| 241 |
var wgEndV = select({{ KVA }}, min(lastQ + 1u, {{ KVA }}), {{ IS_CAUSAL }} != 0u);
|
|
|
|
|
|
|
|
|
|
| 242 |
var wgStartV: u32 = 0u;
|
| 243 |
+
{% for qi in range(QPL) %}
|
| 244 |
+
let {{ qn("maxKj", qi) }} = {{ qn("maxKjV", qi) }};
|
| 245 |
+
let {{ qn("minKj", qi) }} = {{ qn("minKjV", qi) }};
|
| 246 |
+
{% endfor %}
|
| 247 |
let wgEnd = wgEndV;
|
| 248 |
let wgStart = wgStartV;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
let kvBatch4 = b * {{ KVSEQ }} * KV_HIDDEN_V4 + hKv * HEAD_DIM_V4;
|
| 250 |
|
| 251 |
var kStart: u32 = wgStart;
|
| 252 |
loop {
|
| 253 |
if (kStart >= wgEnd) { break; }
|
| 254 |
|
| 255 |
+
// Cooperative K/V tile load with out-of-bounds keys zero-filled.
|
| 256 |
workgroupBarrier();
|
| 257 |
for (var i: u32 = tid; i < TILE_K * HEAD_DIM_V4; i = i + WG) {
|
| 258 |
let slot = i / HEAD_DIM_V4;
|
|
|
|
| 288 |
}
|
| 289 |
{% endif %}
|
| 290 |
workgroupBarrier();
|
| 291 |
+
// TILE_K is a small shader constant; the loop updates the named q/o slices in place.
|
|
|
|
| 292 |
{% if not useSubgroups and batchNoSgReduction %}
|
| 293 |
// First publish every key's partial dot without intervening barriers.
|
| 294 |
var s: array<f32, TILE_K>;
|
|
|
|
| 343 |
{% endif %}
|
| 344 |
}
|
| 345 |
}
|
| 346 |
+
{% elif QPL > 1 %}
|
| 347 |
+
{% for qi in range(QPL) %}
|
| 348 |
+
var {{ qn("s", qi) }}: array<f32, TILE_K>;
|
| 349 |
+
{% endfor %}
|
| 350 |
+
for (var kk: u32 = 0u; kk < TILE_K; kk = kk + 1u) {
|
| 351 |
+
let kj = kStart + kk;
|
| 352 |
+
{% for qi in range(QPL) %}
|
| 353 |
+
{{ qn("s", qi) }}[kk] = NEG_INF;
|
| 354 |
+
var {{ qn("part", qi) }}: f32 = 0.0;
|
| 355 |
+
{% endfor %}
|
| 356 |
+
let kb = kk * HEAD_DIM_V4 + lane8 * SLICE;
|
| 357 |
+
{% for c in range(SLICE_COUNT) %}
|
| 358 |
+
let kw{{ c }} = vec4<f32>(k_tile[kb + {{ c }}u]);
|
| 359 |
+
{% for qi in range(QPL) %}
|
| 360 |
+
{{ qn("part", qi) }} = {{ qn("part", qi) }} + dot({{ qn("qr" ~ c, qi) }}, kw{{ c }});
|
| 361 |
+
{% endfor %}
|
| 362 |
+
{% endfor %}
|
| 363 |
+
// One butterfly carries every query's partial: the shuffles cost the same
|
| 364 |
+
// whether the payload is one score or QPL of them.
|
| 365 |
+
var partv = vec{{ QPL }}<f32>({% for qi in range(QPL) %}{{ qn("part", qi) }}{% if not loop.last %}, {% endif %}{% endfor %});
|
| 366 |
+
partv = partv + subgroupShuffleXor(partv, 1u);
|
| 367 |
+
{% for shift in [2, 4] %}
|
| 368 |
+
{% if LPQ > shift %}
|
| 369 |
+
partv = partv + subgroupShuffleXor(partv, {{ shift }}u);
|
| 370 |
+
{% endif %}
|
| 371 |
+
{% endfor %}
|
| 372 |
+
{% for qi in range(QPL) %}
|
| 373 |
+
{
|
| 374 |
+
let sc = partv[{{ qi }}];
|
| 375 |
+
if (kj >= {{ qn("minKj", qi) }} && kj < {{ qn("maxKj", qi) }}) {
|
| 376 |
+
{% if hasMask %}
|
| 377 |
+
{% if STAGE_MASK %}
|
| 378 |
+
let maskValue = mask_tile[(qSub + {{ qi }}u) * TILE_K + kk];
|
| 379 |
+
{% else %}
|
| 380 |
+
// Broadcast-strided mask address (a 0 stride collapses that axis; rank-2
|
| 381 |
+
// [q, k] masks set batch/head strides to 0). {{ qn("qClamped", qi) }} keeps the seq index
|
| 382 |
+
// in-bounds for padding queries in the last tile (their output is dropped).
|
| 383 |
+
let maskIndex = b * params.maskBatchStride + h * params.maskHeadStride + {{ qn("qClamped", qi) }} * params.maskSeqStride + kj;
|
| 384 |
+
{% endif %}
|
| 385 |
+
{% if maskIsKeyKeep %}
|
| 386 |
+
// A broadcast key mask uses 1 for a retained key and 0 for padding.
|
| 387 |
+
{{ qn("s", qi) }}[kk] = {{ score_expr("sc") }} + (1.0 - f32({{ MASK_ELEMENT }})) * MASK_NEG;
|
| 388 |
+
{% elif maskIsBool %}
|
| 389 |
+
// Logical bool: a rejected key contributes no softmax mass. Leaving
|
| 390 |
+
// the initialized NEG_INF sentinel in place makes a fully masked row
|
| 391 |
+
// land on the zero-denominator output guard below.
|
| 392 |
+
if ({{ MASK_ELEMENT }} != 0u) {
|
| 393 |
+
{{ qn("s", qi) }}[kk] = {{ score_expr("sc") }};
|
| 394 |
+
} else {
|
| 395 |
+
{{ qn("s", qi) }}[kk] = MASK_NEG;
|
| 396 |
+
}
|
| 397 |
+
{% else %}
|
| 398 |
+
{{ qn("s", qi) }}[kk] = {{ score_expr("sc") }} + {{ MASK_ADDITIVE }};
|
| 399 |
+
{% endif %}
|
| 400 |
+
{% else %}
|
| 401 |
+
{{ qn("s", qi) }}[kk] = {{ score_expr("sc") }};
|
| 402 |
+
{% endif %}
|
| 403 |
+
}
|
| 404 |
+
}
|
| 405 |
+
{% endfor %}
|
| 406 |
+
}
|
| 407 |
{% else %}
|
| 408 |
var s: array<f32, TILE_K>;
|
| 409 |
for (var kk: u32 = 0u; kk < TILE_K; kk = kk + 1u) {
|
|
|
|
| 466 |
|
| 467 |
// Per-thread online softmax over the tile. s[kk] is reused to hold the
|
| 468 |
// exponentiated probabilities for the PV accumulation below.
|
| 469 |
+
{% for qi in range(QPL) %}
|
| 470 |
+
var {{ qn("tileMax", qi) }}: f32 = {{ qn("s", qi) }}[0];
|
| 471 |
for (var kk: u32 = 1u; kk < TILE_K; kk = kk + 1u) {
|
| 472 |
+
{{ qn("tileMax", qi) }} = max({{ qn("tileMax", qi) }}, {{ qn("s", qi) }}[kk]);
|
| 473 |
}
|
| 474 |
+
let {{ qn("newMax", qi) }} = max({{ qn("m", qi) }}, {{ qn("tileMax", qi) }});
|
| 475 |
+
let {{ qn("corr", qi) }} = select(exp({{ qn("m", qi) }} - {{ qn("newMax", qi) }}), 0.0, {{ qn("m", qi) }} == NEG_INF);
|
| 476 |
+
var {{ qn("pSum", qi) }}: f32 = 0.0;
|
| 477 |
for (var kk: u32 = 0u; kk < TILE_K; kk = kk + 1u) {
|
| 478 |
+
let pk = select(0.0, exp({{ qn("s", qi) }}[kk] - {{ qn("newMax", qi) }}), {{ qn("s", qi) }}[kk] != NEG_INF);
|
| 479 |
+
{{ qn("s", qi) }}[kk] = pk;
|
| 480 |
+
{{ qn("pSum", qi) }} = {{ qn("pSum", qi) }} + pk;
|
| 481 |
+
}
|
| 482 |
+
{{ qn("l", qi) }} = {{ qn("l", qi) }} * {{ qn("corr", qi) }} + {{ qn("pSum", qi) }};
|
| 483 |
+
{{ qn("m", qi) }} = {{ qn("newMax", qi) }};
|
| 484 |
+
{% endfor %}
|
| 485 |
+
// A boundary tile can address V rows outside a query's attended range, and
|
| 486 |
+
// a dynamic-row producer may leave those rows unwritten. Because 0 * NaN is
|
| 487 |
+
// NaN, the guarded loop selects the V operand away for range-excluded keys.
|
| 488 |
+
// Interior tiles use the unguarded FMA chain. Mask exclusion applies to
|
| 489 |
+
// materialized V rows and does not require this range guard.
|
| 490 |
+
{% if QPL > 1 %}
|
| 491 |
+
let tileInterior = {% for qi in range(QPL) %}(kStart >= {{ qn("minKj", qi) }} && kStart + TILE_K <= {{ qn("maxKj", qi) }}){% if not loop.last %} && {% endif %}{% endfor %};
|
| 492 |
+
{% for c in range(SLICE_COUNT) %}
|
| 493 |
+
{
|
| 494 |
+
{% for qi in range(QPL) %}
|
| 495 |
+
var {{ qn("acc", qi) }} = {{ qn("o" ~ c, qi) }} * {{ qn("corr", qi) }};
|
| 496 |
+
{% endfor %}
|
| 497 |
+
if (tileInterior) {
|
| 498 |
+
for (var kk: u32 = 0u; kk < TILE_K; kk = kk + 1u) {
|
| 499 |
+
let vw = vec4<f32>(v_tile[kk * HEAD_DIM_V4 + lane8 * SLICE + {{ c }}u]);
|
| 500 |
+
{% for qi in range(QPL) %}
|
| 501 |
+
{{ qn("acc", qi) }} = {{ qn("acc", qi) }} + {{ qn("s", qi) }}[kk] * vw;
|
| 502 |
+
{% endfor %}
|
| 503 |
+
}
|
| 504 |
+
} else {
|
| 505 |
+
for (var kk: u32 = 0u; kk < TILE_K; kk = kk + 1u) {
|
| 506 |
+
let vw = vec4<f32>(v_tile[kk * HEAD_DIM_V4 + lane8 * SLICE + {{ c }}u]);
|
| 507 |
+
{% for qi in range(QPL) %}
|
| 508 |
+
{{ qn("acc", qi) }} = {{ qn("acc", qi) }} + {{ qn("s", qi) }}[kk] * select(vec4<f32>(), vw, {{ qn("s", qi) }}[kk] != 0.0);
|
| 509 |
+
{% endfor %}
|
| 510 |
+
}
|
| 511 |
+
}
|
| 512 |
+
{% for qi in range(QPL) %}
|
| 513 |
+
{{ qn("o" ~ c, qi) }} = {{ qn("acc", qi) }};
|
| 514 |
+
{% endfor %}
|
| 515 |
}
|
| 516 |
+
{% endfor %}
|
| 517 |
+
{% else %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 518 |
let tileInterior = kStart >= minKj && kStart + TILE_K <= maxKj;
|
| 519 |
{% for c in range(SLICE_COUNT) %}
|
| 520 |
{
|
|
|
|
| 531 |
o{{ c }} = acc;
|
| 532 |
}
|
| 533 |
{% endfor %}
|
| 534 |
+
{% endif %}
|
| 535 |
|
| 536 |
kStart = kStart + TILE_K;
|
| 537 |
}
|
| 538 |
|
| 539 |
+
{% macro attention_value(c, qi) %}{{ qn("o" ~ c, qi) }} * inv{% if hasBias %} + load_bias4(2u * {{ Q_HIDDEN }}u + h * HEAD_DIM, lane8 * SLICE + {{ c }}u){% endif %}{% endmacro %}
|
| 540 |
+
{% for qi in range(QPL) %}
|
| 541 |
+
if ({{ qn("qValid", qi) }}) {
|
| 542 |
+
let outBase4 = (b * {{ QSEQ }} + {{ qn("qIdx", qi) }}) * Q_HIDDEN_V4 + h * HEAD_DIM_V4 + lane8 * SLICE;
|
| 543 |
{% if hasHeadSink %}
|
| 544 |
// The head sink is a learned logit that competes with the keys but carries
|
| 545 |
// no value, so it enters the denominator only and the weighted sum above is
|
| 546 |
+
// untouched. Renormalizing against max({{ qn("m", qi) }}, sink) keeps the exponentials in
|
| 547 |
// range when the sink dominates a fully-masked row.
|
| 548 |
let sink = f32(head_sink[h]);
|
| 549 |
+
let finalM = max({{ qn("m", qi) }}, sink);
|
| 550 |
+
let accScale = exp({{ qn("m", qi) }} - finalM);
|
| 551 |
+
let inv = accScale / (exp(sink - finalM) + {{ qn("l", qi) }} * accScale);
|
| 552 |
{% else %}
|
| 553 |
+
// {{ qn("l", qi) }} == 0 means this query had no probability-bearing key: either its
|
| 554 |
// causal/window range is empty or its logical bool mask rejects every key.
|
| 555 |
// Emit 0 rather than 0/0. Other attention paths enforce the same empty-row
|
| 556 |
// contract by selecting on positive global mass.
|
| 557 |
+
let inv = select(0.0, 1.0 / {{ qn("l", qi) }}, {{ qn("l", qi) }} > 0.0);
|
| 558 |
{% endif %}
|
|
|
|
| 559 |
{% for c in range(SLICE_COUNT) %}
|
| 560 |
+
{{ OUTPUT }}[outBase4 + {{ c }}u] = vec4<{{ scalar }}>({{ attention_value(c, qi) }});
|
| 561 |
{% endfor %}
|
| 562 |
}
|
| 563 |
+
{% endfor %}
|
| 564 |
}
|
build/webgpu/attn-flash-q32-broadcast.wgsl.jinja
CHANGED
|
@@ -9,12 +9,9 @@
|
|
| 9 |
// cancels in softmax. Arbitrary attention masks use the fallback routes.
|
| 10 |
{% set ST = "f16" if usesF16 else "f32" %}
|
| 11 |
{% set components = ["x", "y", "z", "w"] %}
|
| 12 |
-
{% set USE_SUBGROUPS =
|
| 13 |
{% set Q_STEP = qStep if qStep is defined else 32 %}
|
| 14 |
{% set CAUSAL = false if (hasCausal is defined and not hasCausal) else true %}
|
| 15 |
-
{% if usesF16 %}
|
| 16 |
-
enable f16;
|
| 17 |
-
{% endif %}
|
| 18 |
{% if USE_SUBGROUPS %}
|
| 19 |
enable subgroups;
|
| 20 |
{% endif %}
|
|
|
|
| 9 |
// cancels in softmax. Arbitrary attention masks use the fallback routes.
|
| 10 |
{% set ST = "f16" if usesF16 else "f32" %}
|
| 11 |
{% set components = ["x", "y", "z", "w"] %}
|
| 12 |
+
{% set USE_SUBGROUPS = useSubgroups if useSubgroups is defined else true %}
|
| 13 |
{% set Q_STEP = qStep if qStep is defined else 32 %}
|
| 14 |
{% set CAUSAL = false if (hasCausal is defined and not hasCausal) else true %}
|
|
|
|
|
|
|
|
|
|
| 15 |
{% if USE_SUBGROUPS %}
|
| 16 |
enable subgroups;
|
| 17 |
{% endif %}
|
build/webgpu/attn-materialized-apply-f32.wgsl.jinja
CHANGED
|
@@ -1,16 +1,14 @@
|
|
| 1 |
{{ env.wgsl.resourceDeclarations }}
|
| 2 |
|
| 3 |
-
// Register-blocked normalized-scores @ V GEMM
|
| 4 |
// Score rows are head-major regardless of the external tensor layout.
|
| 5 |
//
|
| 6 |
// V is stored row-major in the head dimension, so its tile groups four of those
|
| 7 |
// per vector word and the micro-tile accumulates four output columns at a time.
|
| 8 |
-
// The score tile groups four
|
| 9 |
-
//
|
| 10 |
-
// multiply-adds.
|
| 11 |
-
|
| 12 |
-
// workgroup reads for the same arithmetic.
|
| 13 |
-
{% set FUSED_SOFTMAX = source.fusedSoftmax is defined and source.fusedSoftmax %}
|
| 14 |
|
| 15 |
const Q_HEADS: u32 = {{ qNumHeads }}u;
|
| 16 |
const KV_HEADS: u32 = {{ kvNumHeads }}u;
|
|
|
|
| 1 |
{{ env.wgsl.resourceDeclarations }}
|
| 2 |
|
| 3 |
+
// Register-blocked normalized-scores @ V GEMM with BHSD and BSH specializations.
|
| 4 |
// Score rows are head-major regardless of the external tensor layout.
|
| 5 |
//
|
| 6 |
// V is stored row-major in the head dimension, so its tile groups four of those
|
| 7 |
// per vector word and the micro-tile accumulates four output columns at a time.
|
| 8 |
+
// The score tile groups four keys per word along the reduction axis. One step
|
| 9 |
+
// over four keys reads TM + TN vector words for TM * TN * 4 fused
|
| 10 |
+
// multiply-adds.
|
| 11 |
+
{% set FUSED_SOFTMAX = fusedSoftmax is defined and fusedSoftmax %}
|
|
|
|
|
|
|
| 12 |
|
| 13 |
const Q_HEADS: u32 = {{ qNumHeads }}u;
|
| 14 |
const KV_HEADS: u32 = {{ kvNumHeads }}u;
|
build/webgpu/attn-materialized-rowstats-combine-f32.wgsl.jinja
CHANGED
|
@@ -12,7 +12,7 @@
|
|
| 12 |
// `maxOnly` means the producer published only a row max per slot because
|
| 13 |
// computing the denominator there would double the exp count. Fold maxima and
|
| 14 |
// leave the denominator to the apply pass, which sees every row element anyway.
|
| 15 |
-
{% set MAX_ONLY =
|
| 16 |
{{ env.wgsl.resourceDeclarations }}
|
| 17 |
|
| 18 |
const SLOTS: u32 = {{ statSlots }}u;
|
|
@@ -52,10 +52,9 @@ fn exp_shift(value: f32, maxValue: f32) -> f32 {
|
|
| 52 |
|
| 53 |
@compute @workgroup_size(WG, 1, 1)
|
| 54 |
fn main(
|
| 55 |
-
@builtin(global_invocation_id) gid: vec3<u32>
|
| 56 |
-
@builtin(num_workgroups) nwg: vec3<u32>
|
| 57 |
) {
|
| 58 |
-
let row = gid.x + gid.y *
|
| 59 |
if (row >= params.rows) { return; }
|
| 60 |
|
| 61 |
// `row` already runs over (batch, head, query) together, and the partial
|
|
|
|
| 12 |
// `maxOnly` means the producer published only a row max per slot because
|
| 13 |
// computing the denominator there would double the exp count. Fold maxima and
|
| 14 |
// leave the denominator to the apply pass, which sees every row element anyway.
|
| 15 |
+
{% set MAX_ONLY = maxOnly is defined and maxOnly %}
|
| 16 |
{{ env.wgsl.resourceDeclarations }}
|
| 17 |
|
| 18 |
const SLOTS: u32 = {{ statSlots }}u;
|
|
|
|
| 52 |
|
| 53 |
@compute @workgroup_size(WG, 1, 1)
|
| 54 |
fn main(
|
| 55 |
+
@builtin(global_invocation_id) gid: vec3<u32>
|
|
|
|
| 56 |
) {
|
| 57 |
+
let row = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * WG;
|
| 58 |
if (row >= params.rows) { return; }
|
| 59 |
|
| 60 |
// `row` already runs over (batch, head, query) together, and the partial
|
build/webgpu/attn-materialized-score-f32.wgsl.jinja
CHANGED
|
@@ -1,21 +1,20 @@
|
|
| 1 |
{{ env.wgsl.resourceDeclarations }}
|
| 2 |
|
| 3 |
-
// f32 prefill score GEMM
|
| 4 |
-
//
|
| 5 |
-
// it turns the shuffle/reduction-heavy online QK loop into a regular 64x64
|
| 6 |
-
// register-blocked GEMM. Q and K are both stored row-major in the head
|
| 7 |
// dimension — the axis this GEMM reduces over — so both tiles group four of
|
| 8 |
// its values per vector word, every staging read is contiguous, and the
|
| 9 |
// micro-tile accumulates through dot(): one step reads TM + TN vector words
|
| 10 |
// for TM * TN * 4 fused multiply-adds. K keeps a one-word shared stride
|
| 11 |
-
// padding, which
|
|
|
|
| 12 |
{% if scalarAccumulators is not defined %}{% set scalarAccumulators = false %}{% endif %}
|
| 13 |
{% if maskIsKeyKeep is not defined %}{% set maskIsKeyKeep = false %}{% endif %}
|
| 14 |
{% if scalarAccumulators %}
|
| 15 |
{% set TM_JINJA = (materializedQueryTile / materializedWorkgroupDim)|int %}
|
| 16 |
{% set TN_JINJA = (materializedKeyTile / materializedWorkgroupDim)|int %}
|
| 17 |
{% endif %}
|
| 18 |
-
{% set EMIT_ROW_STATS =
|
| 19 |
|
| 20 |
const Q_HEADS: u32 = {{ qNumHeads }}u;
|
| 21 |
const KV_HEADS: u32 = {{ kvNumHeads }}u;
|
|
@@ -109,8 +108,7 @@ fn main(
|
|
| 109 |
let li = lid.y * WG_DIM + lid.x;
|
| 110 |
|
| 111 |
{% if scalarAccumulators %}
|
| 112 |
-
// Keep every register-tile cell statically addressable.
|
| 113 |
-
// private array may fail to scalarize and spill hot GEMM accumulators to memory.
|
| 114 |
{% for i in range(TM_JINJA) %}
|
| 115 |
{% for j in range(TN_JINJA) %}
|
| 116 |
var acc{{ i }}_{{ j }}: f32 = 0.0;
|
|
|
|
| 1 |
{{ env.wgsl.resourceDeclarations }}
|
| 2 |
|
| 3 |
+
// f32 prefill score GEMM with BHSD and packed BSH specializations. It computes
|
| 4 |
+
// the score matrix as a 64x64 register-blocked product. Q and K are both stored row-major in the head
|
|
|
|
|
|
|
| 5 |
// dimension — the axis this GEMM reduces over — so both tiles group four of
|
| 6 |
// its values per vector word, every staging read is contiguous, and the
|
| 7 |
// micro-tile accumulates through dot(): one step reads TM + TN vector words
|
| 8 |
// for TM * TN * 4 fused multiply-adds. K keeps a one-word shared stride
|
| 9 |
+
// padding, which changes the transposed workgroup-memory stride and reduces
|
| 10 |
+
// bank-conflict risk on banked implementations.
|
| 11 |
{% if scalarAccumulators is not defined %}{% set scalarAccumulators = false %}{% endif %}
|
| 12 |
{% if maskIsKeyKeep is not defined %}{% set maskIsKeyKeep = false %}{% endif %}
|
| 13 |
{% if scalarAccumulators %}
|
| 14 |
{% set TM_JINJA = (materializedQueryTile / materializedWorkgroupDim)|int %}
|
| 15 |
{% set TN_JINJA = (materializedKeyTile / materializedWorkgroupDim)|int %}
|
| 16 |
{% endif %}
|
| 17 |
+
{% set EMIT_ROW_STATS = emitRowStats is defined and emitRowStats %}
|
| 18 |
|
| 19 |
const Q_HEADS: u32 = {{ qNumHeads }}u;
|
| 20 |
const KV_HEADS: u32 = {{ kvNumHeads }}u;
|
|
|
|
| 108 |
let li = lid.y * WG_DIM + lid.x;
|
| 109 |
|
| 110 |
{% if scalarAccumulators %}
|
| 111 |
+
// Keep every register-tile cell statically addressable.
|
|
|
|
| 112 |
{% for i in range(TM_JINJA) %}
|
| 113 |
{% for j in range(TN_JINJA) %}
|
| 114 |
var acc{{ i }}_{{ j }}: f32 = 0.0;
|
build/webgpu/attn-materialized-sgmat-f32.wgsl.jinja
CHANGED
|
@@ -9,17 +9,18 @@ enable subgroup_size_control;
|
|
| 9 |
enable chromium_experimental_subgroup_matrix;
|
| 10 |
diagnostic(off, chromium.subgroup_matrix_uniformity);
|
| 11 |
|
|
|
|
| 12 |
{{ env.wgsl.resourceDeclarations }}
|
| 13 |
|
| 14 |
-
{% set layout =
|
| 15 |
{% set headMajor = layout == "bhsd" %}
|
| 16 |
-
{% set kvHeadMajor = (
|
| 17 |
-
{% macro q_index(row, d) %}(b * params.qSeq + {{ row }}) * HIDDEN + h * HEAD_DIM + {{ d }}{% endmacro %}
|
| 18 |
-
{% macro kv_index(seq, d) %}(b * params.kvSeq + {{ seq }}) * KV_HIDDEN + h_kv * HEAD_DIM + {{ d }}{% endmacro %}
|
| 19 |
{% set OUT_ROW_STRIDE = "HEAD_DIM" if headMajor else "HIDDEN" %}
|
| 20 |
{% set KV_ROW_STRIDE = "HEAD_DIM" if kvHeadMajor else "KV_HIDDEN" %}
|
| 21 |
-
{% set scorePhase =
|
| 22 |
-
{% set FUSED_SOFTMAX =
|
| 23 |
{% set PRIVATE_ROW_STATS = FUSED_SOFTMAX and (materializedSgmatPrivateRowStats is defined and materializedSgmatPrivateRowStats) %}
|
| 24 |
{% macro score_value(index, guard) %}
|
| 25 |
{% if FUSED_SOFTMAX %}
|
|
@@ -44,7 +45,7 @@ select(0.0, scores[{{ index }}], {{ guard[1] }})
|
|
| 44 |
{% set SUBGROUP_COUNT = SUBGROUP_ROWS * SUBGROUP_COLS %}
|
| 45 |
{% set WORKGROUP_THREADS = SUBGROUP_COUNT * 32 %}
|
| 46 |
{% if hasBias is not defined %}{% set hasBias = false %}{% endif %}
|
| 47 |
-
{% set EMIT_ROW_STATS =
|
| 48 |
{% set DIRECT_SCORE_STORE = materializedSgmatDirectScoreStore and not EMIT_ROW_STATS %}
|
| 49 |
{% set DIRECT_APPLY_STORE = materializedSgmatDirectApplyStore and not hasBias and MT == "f32" %}
|
| 50 |
{% set DIRECT_OUTPUT_STORE = DIRECT_SCORE_STORE if scorePhase else DIRECT_APPLY_STORE %}
|
|
@@ -63,7 +64,9 @@ const KV_HEADS: u32 = {{ kvNumHeads }}u;
|
|
| 63 |
const KV_HEADS: u32 = HEADS;
|
| 64 |
{% endif %}
|
| 65 |
const HEAD_DIM: u32 = {{ headDim }}u;
|
|
|
|
| 66 |
const HIDDEN: u32 = {{ qHidden }}u;
|
|
|
|
| 67 |
{% if hasBias %}
|
| 68 |
/* Packed [Q; K; V] bias. Q folds into the query tile before the GEMM. K is
|
| 69 |
* omitted: expanding (q + bq).(k + bk) leaves a (q + bq).bk term that is
|
|
@@ -71,9 +74,11 @@ const HIDDEN: u32 = {{ qHidden }}u;
|
|
| 71 |
* constant. V is token-independent, so sum_k p[k] * bv = bv after
|
| 72 |
* normalization; it is added after the apply GEMM. */
|
| 73 |
{% endif %}
|
|
|
|
| 74 |
// Packed K/V rows span KV_HEADS heads, so they are narrower than the query
|
| 75 |
// row whenever queries are grouped. Q and the output keep HIDDEN.
|
| 76 |
const KV_HIDDEN: u32 = KV_HEADS * HEAD_DIM;
|
|
|
|
| 77 |
{% if EMIT_ROW_STATS %}
|
| 78 |
const STAT_SLOTS: u32 = {{ statSlots }}u;
|
| 79 |
{% endif %}
|
|
@@ -191,7 +196,7 @@ fn main(
|
|
| 191 |
{% endif %}
|
| 192 |
{% endif %}
|
| 193 |
for (var k_base = 0u; k_base < inner; k_base += TILE_K) {
|
| 194 |
-
{% if
|
| 195 |
// Full interior PV tiles can be loaded directly from storage. Query,
|
| 196 |
// reduction, and output-dimension tails use the guarded shared path below.
|
| 197 |
if (
|
|
@@ -229,10 +234,8 @@ fn main(
|
|
| 229 |
{% endif %}
|
| 230 |
|
| 231 |
{% if TILE_M_VALUE == 64 and TILE_N_VALUE == 64 and TILE_K_VALUE == 32 and WORKGROUP_THREADS == 256 %}
|
| 232 |
-
// BM64/BN64/WG256
|
| 233 |
-
//
|
| 234 |
-
// row, preserving the original coalesced load schedule while retaining
|
| 235 |
-
// the generalized K-tail guards.
|
| 236 |
let a_row = li / 4u;
|
| 237 |
let a_col = (li % 4u) * 8u;
|
| 238 |
for (var i = 0u; i < 8u; i++) {
|
|
@@ -304,10 +307,10 @@ fn main(
|
|
| 304 |
let loaded =
|
| 305 |
{{ "f16(" if MT == "f16" else "" }}{{ score_value("score_base + row * params.kvSeq + k", ["tile_row", "row < params.qSeq && k < params.kvSeq"]) }}{{ ")" if MT == "f16" else "" }};
|
| 306 |
{% else %}
|
| 307 |
-
var loaded =
|
| 308 |
if (row < params.qSeq && k < params.kvSeq) {
|
| 309 |
let score_base = (b * HEADS + h) * params.qSeq * params.kvSeq;
|
| 310 |
-
loaded =
|
| 311 |
}
|
| 312 |
{% endif %}
|
| 313 |
tile_A[idx] = loaded;
|
|
|
|
| 9 |
enable chromium_experimental_subgroup_matrix;
|
| 10 |
diagnostic(off, chromium.subgroup_matrix_uniformity);
|
| 11 |
|
| 12 |
+
|
| 13 |
{{ env.wgsl.resourceDeclarations }}
|
| 14 |
|
| 15 |
+
{% set layout = layout | default("bsh") %}
|
| 16 |
{% set headMajor = layout == "bhsd" %}
|
| 17 |
+
{% set kvHeadMajor = (kvLayout | default(layout)) == "bhsd" %}
|
| 18 |
+
{% macro q_index(row, d) %}{% if headMajor %}((b * HEADS + h) * params.qSeq + {{ row }}) * HEAD_DIM + {{ d }}{% else %}(b * params.qSeq + {{ row }}) * HIDDEN + h * HEAD_DIM + {{ d }}{% endif %}{% endmacro %}
|
| 19 |
+
{% macro kv_index(seq, d) %}{% if kvHeadMajor %}((b * KV_HEADS + h_kv) * params.kvSeq + {{ seq }}) * HEAD_DIM + {{ d }}{% else %}(b * params.kvSeq + {{ seq }}) * KV_HIDDEN + h_kv * HEAD_DIM + {{ d }}{% endif %}{% endmacro %}
|
| 20 |
{% set OUT_ROW_STRIDE = "HEAD_DIM" if headMajor else "HIDDEN" %}
|
| 21 |
{% set KV_ROW_STRIDE = "HEAD_DIM" if kvHeadMajor else "KV_HIDDEN" %}
|
| 22 |
+
{% set scorePhase = phase == "score" %}
|
| 23 |
+
{% set FUSED_SOFTMAX = fusedSoftmax is defined and fusedSoftmax %}
|
| 24 |
{% set PRIVATE_ROW_STATS = FUSED_SOFTMAX and (materializedSgmatPrivateRowStats is defined and materializedSgmatPrivateRowStats) %}
|
| 25 |
{% macro score_value(index, guard) %}
|
| 26 |
{% if FUSED_SOFTMAX %}
|
|
|
|
| 45 |
{% set SUBGROUP_COUNT = SUBGROUP_ROWS * SUBGROUP_COLS %}
|
| 46 |
{% set WORKGROUP_THREADS = SUBGROUP_COUNT * 32 %}
|
| 47 |
{% if hasBias is not defined %}{% set hasBias = false %}{% endif %}
|
| 48 |
+
{% set EMIT_ROW_STATS = emitRowStats is defined and emitRowStats %}
|
| 49 |
{% set DIRECT_SCORE_STORE = materializedSgmatDirectScoreStore and not EMIT_ROW_STATS %}
|
| 50 |
{% set DIRECT_APPLY_STORE = materializedSgmatDirectApplyStore and not hasBias and MT == "f32" %}
|
| 51 |
{% set DIRECT_OUTPUT_STORE = DIRECT_SCORE_STORE if scorePhase else DIRECT_APPLY_STORE %}
|
|
|
|
| 64 |
const KV_HEADS: u32 = HEADS;
|
| 65 |
{% endif %}
|
| 66 |
const HEAD_DIM: u32 = {{ headDim }}u;
|
| 67 |
+
{% if layout != "bhsd" or hasBias %}
|
| 68 |
const HIDDEN: u32 = {{ qHidden }}u;
|
| 69 |
+
{% endif %}
|
| 70 |
{% if hasBias %}
|
| 71 |
/* Packed [Q; K; V] bias. Q folds into the query tile before the GEMM. K is
|
| 72 |
* omitted: expanding (q + bq).(k + bk) leaves a (q + bq).bk term that is
|
|
|
|
| 74 |
* constant. V is token-independent, so sum_k p[k] * bv = bv after
|
| 75 |
* normalization; it is added after the apply GEMM. */
|
| 76 |
{% endif %}
|
| 77 |
+
{% if not kvHeadMajor %}
|
| 78 |
// Packed K/V rows span KV_HEADS heads, so they are narrower than the query
|
| 79 |
// row whenever queries are grouped. Q and the output keep HIDDEN.
|
| 80 |
const KV_HIDDEN: u32 = KV_HEADS * HEAD_DIM;
|
| 81 |
+
{% endif %}
|
| 82 |
{% if EMIT_ROW_STATS %}
|
| 83 |
const STAT_SLOTS: u32 = {{ statSlots }}u;
|
| 84 |
{% endif %}
|
|
|
|
| 196 |
{% endif %}
|
| 197 |
{% endif %}
|
| 198 |
for (var k_base = 0u; k_base < inner; k_base += TILE_K) {
|
| 199 |
+
{% if phase == "apply" and not FUSED_SOFTMAX and MT == "f32" %}
|
| 200 |
// Full interior PV tiles can be loaded directly from storage. Query,
|
| 201 |
// reduction, and output-dimension tails use the guarded shared path below.
|
| 202 |
if (
|
|
|
|
| 234 |
{% endif %}
|
| 235 |
|
| 236 |
{% if TILE_M_VALUE == 64 and TILE_N_VALUE == 64 and TILE_K_VALUE == 32 and WORKGROUP_THREADS == 256 %}
|
| 237 |
+
// In the BM64/BN64/BK32/WG256 geometry, four adjacent threads each own one
|
| 238 |
+
// contiguous eight-element segment of a row. K-tail guards remain active.
|
|
|
|
|
|
|
| 239 |
let a_row = li / 4u;
|
| 240 |
let a_col = (li % 4u) * 8u;
|
| 241 |
for (var i = 0u; i < 8u; i++) {
|
|
|
|
| 307 |
let loaded =
|
| 308 |
{{ "f16(" if MT == "f16" else "" }}{{ score_value("score_base + row * params.kvSeq + k", ["tile_row", "row < params.qSeq && k < params.kvSeq"]) }}{{ ")" if MT == "f16" else "" }};
|
| 309 |
{% else %}
|
| 310 |
+
var loaded = 0.0;
|
| 311 |
if (row < params.qSeq && k < params.kvSeq) {
|
| 312 |
let score_base = (b * HEADS + h) * params.qSeq * params.kvSeq;
|
| 313 |
+
loaded = scores[score_base + row * params.kvSeq + k];
|
| 314 |
}
|
| 315 |
{% endif %}
|
| 316 |
tile_A[idx] = loaded;
|
build/webgpu/attn-materialized-softmax-f32.wgsl.jinja
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
{% set CACHE_VEC4 =
|
| 2 |
{% set CAUSAL_ROWS = causalRows is defined and causalRows %}
|
| 3 |
{% set SCALE_IN = scaleInSoftmax is defined and scaleInSoftmax %}
|
| 4 |
{% set RUNTIME_COLS = colsFromParams is defined and colsFromParams %}
|
|
@@ -50,20 +50,22 @@ fn exp_shift(value: f32, maxValue: f32) -> f32 {
|
|
| 50 |
// calls before their shared partial storage is reused.
|
| 51 |
{% set combineSubgroups = combineSubgroups is defined and combineSubgroups %}
|
| 52 |
{% if combineSubgroups %}
|
| 53 |
-
//
|
| 54 |
-
//
|
| 55 |
-
//
|
| 56 |
-
//
|
| 57 |
-
//
|
| 58 |
-
//
|
| 59 |
-
//
|
|
|
|
| 60 |
var<workgroup> partialM: array<f32, WG>;
|
| 61 |
var<workgroup> partialD: array<f32, WG>;
|
|
|
|
| 62 |
var<workgroup> combinedMD: vec2<f32>;
|
| 63 |
|
| 64 |
// When the whole workgroup is one subgroup the subgroup reduce already covers
|
| 65 |
-
// it (no barriers, no shared state)
|
| 66 |
-
//
|
| 67 |
fn combine_partials(m: f32, d: f32, lidx: u32, sgSize: u32) -> vec2<f32> {
|
| 68 |
let sgM = subgroupMax(m);
|
| 69 |
// A lane with no elements contributes d == 0 (exact identity). A +inf
|
|
@@ -74,31 +76,24 @@ fn combine_partials(m: f32, d: f32, lidx: u32, sgSize: u32) -> vec2<f32> {
|
|
| 74 |
if (sgSize == WG) {
|
| 75 |
return vec2<f32>(sgM, sgD);
|
| 76 |
}
|
| 77 |
-
let subgroupCount = (WG + sgSize - 1u) / sgSize;
|
| 78 |
-
// Pre-seed every fold slot with the (max, denom) identity. The fold below reads a
|
| 79 |
-
// fixed subgroupCount slots in ordinal order (for determinism), but a slot whose
|
| 80 |
-
// subgroup elects no leader this call — e.g. a fully out-of-window key tile in the
|
| 81 |
-
// flash-attention loop that re-uses this shared memory each iteration — would
|
| 82 |
-
// otherwise read stale shared memory. Identity makes such a slot a no-op.
|
| 83 |
-
// (max identity = -FLT_MAX, denom identity = 0.)
|
| 84 |
-
if (lidx < subgroupCount) {
|
| 85 |
-
partialM[lidx] = -FLT_MAX;
|
| 86 |
-
partialD[lidx] = 0.0;
|
| 87 |
-
}
|
| 88 |
-
workgroupBarrier();
|
| 89 |
if (subgroupElect()) {
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
}
|
| 94 |
workgroupBarrier();
|
| 95 |
if (lidx == 0u) {
|
| 96 |
var accM = -FLT_MAX;
|
| 97 |
var accD = 0.0;
|
| 98 |
-
for (var
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
}
|
| 103 |
combinedMD = vec2<f32>(accM, accD);
|
| 104 |
}
|
|
@@ -113,9 +108,8 @@ var<workgroup> partialM: array<f32, {{ mdExtent }}>;
|
|
| 113 |
var<workgroup> partialD: array<f32, {{ mdExtent }}>;
|
| 114 |
{% if mdStreamed %}
|
| 115 |
|
| 116 |
-
// In-place fold of {{ mdStreams }} streams.
|
| 117 |
-
//
|
| 118 |
-
// from slot s * WG afterwards.
|
| 119 |
fn combine_partials_streams(lidx: u32) {
|
| 120 |
workgroupBarrier();
|
| 121 |
var stride = WG / 2u;
|
|
@@ -177,12 +171,11 @@ fn combine_partials(m: f32, d: f32, lidx: u32) -> vec2<f32> {
|
|
| 177 |
@compute @workgroup_size(WG, 1, 1)
|
| 178 |
fn main(
|
| 179 |
@builtin(workgroup_id) wg: vec3<u32>,
|
| 180 |
-
@builtin(num_workgroups) nwg: vec3<u32>,
|
| 181 |
@builtin(local_invocation_id) lid: vec3<u32>{% if useSubgroups %},
|
| 182 |
@builtin(subgroup_size) sgSize: u32
|
| 183 |
{% endif %}
|
| 184 |
) {
|
| 185 |
-
let row = wg.x + wg.y *
|
| 186 |
if (row >= {{ ROWS_EXPR }}) { return; }
|
| 187 |
{% if CACHE_VEC4 %}
|
| 188 |
let base4 = row * COLS4;
|
|
|
|
| 1 |
+
{% set CACHE_VEC4 = cacheVec4 if cacheVec4 is defined else false %}
|
| 2 |
{% set CAUSAL_ROWS = causalRows is defined and causalRows %}
|
| 3 |
{% set SCALE_IN = scaleInSoftmax is defined and scaleInSoftmax %}
|
| 4 |
{% set RUNTIME_COLS = colsFromParams is defined and colsFromParams %}
|
|
|
|
| 50 |
// calls before their shared partial storage is reused.
|
| 51 |
{% set combineSubgroups = combineSubgroups is defined and combineSubgroups %}
|
| 52 |
{% if combineSubgroups %}
|
| 53 |
+
// Cross-subgroup merge that assumes nothing about which invocations share a
|
| 54 |
+
// subgroup or how many subgroups there are: each subgroup's elected lane
|
| 55 |
+
// publishes the subgroup pair in the slot at its OWN invocation index and sets
|
| 56 |
+
// that index's bit in a workgroup bitmask; thread 0 then folds exactly the
|
| 57 |
+
// published slots, in ascending index order (the online (m, d) merge is not
|
| 58 |
+
// float-associative, so the order is fixed), and clears the mask for the next
|
| 59 |
+
// call as it reads it. Workgroup memory starts zeroed, so the mask needs no
|
| 60 |
+
// setup. Same three collectives as a single-subgroup reduce, two barriers.
|
| 61 |
var<workgroup> partialM: array<f32, WG>;
|
| 62 |
var<workgroup> partialD: array<f32, WG>;
|
| 63 |
+
var<workgroup> leaderMask: array<atomic<u32>, (WG + 31u) / 32u>;
|
| 64 |
var<workgroup> combinedMD: vec2<f32>;
|
| 65 |
|
| 66 |
// When the whole workgroup is one subgroup the subgroup reduce already covers
|
| 67 |
+
// it (no barriers, no shared state). `subgroup_size` is the size of the current
|
| 68 |
+
// subgroup and uniform, so the test is exact and may guard the barriers below.
|
| 69 |
fn combine_partials(m: f32, d: f32, lidx: u32, sgSize: u32) -> vec2<f32> {
|
| 70 |
let sgM = subgroupMax(m);
|
| 71 |
// A lane with no elements contributes d == 0 (exact identity). A +inf
|
|
|
|
| 76 |
if (sgSize == WG) {
|
| 77 |
return vec2<f32>(sgM, sgD);
|
| 78 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
if (subgroupElect()) {
|
| 80 |
+
partialM[lidx] = sgM;
|
| 81 |
+
partialD[lidx] = sgD;
|
| 82 |
+
atomicOr(&leaderMask[lidx / 32u], 1u << (lidx % 32u));
|
| 83 |
}
|
| 84 |
workgroupBarrier();
|
| 85 |
if (lidx == 0u) {
|
| 86 |
var accM = -FLT_MAX;
|
| 87 |
var accD = 0.0;
|
| 88 |
+
for (var w = 0u; w < (WG + 31u) / 32u; w = w + 1u) {
|
| 89 |
+
var bits = atomicExchange(&leaderMask[w], 0u);
|
| 90 |
+
while (bits != 0u) {
|
| 91 |
+
let slot = w * 32u + firstTrailingBit(bits);
|
| 92 |
+
bits = bits & (bits - 1u);
|
| 93 |
+
let mNew = max(accM, partialM[slot]);
|
| 94 |
+
accD = accD * exp_shift(accM, mNew) + partialD[slot] * exp_shift(partialM[slot], mNew);
|
| 95 |
+
accM = mNew;
|
| 96 |
+
}
|
| 97 |
}
|
| 98 |
combinedMD = vec2<f32>(accM, accD);
|
| 99 |
}
|
|
|
|
| 108 |
var<workgroup> partialD: array<f32, {{ mdExtent }}>;
|
| 109 |
{% if mdStreamed %}
|
| 110 |
|
| 111 |
+
// In-place fold of {{ mdStreams }} streams. Input partials occupy
|
| 112 |
+
// partialM/partialD; stream s returns its merged pair in slot s * WG.
|
|
|
|
| 113 |
fn combine_partials_streams(lidx: u32) {
|
| 114 |
workgroupBarrier();
|
| 115 |
var stride = WG / 2u;
|
|
|
|
| 171 |
@compute @workgroup_size(WG, 1, 1)
|
| 172 |
fn main(
|
| 173 |
@builtin(workgroup_id) wg: vec3<u32>,
|
|
|
|
| 174 |
@builtin(local_invocation_id) lid: vec3<u32>{% if useSubgroups %},
|
| 175 |
@builtin(subgroup_size) sgSize: u32
|
| 176 |
{% endif %}
|
| 177 |
) {
|
| 178 |
+
let row = wg.x + wg.y * {{ DISPATCH_FOLD_WIDTH }}u;
|
| 179 |
if (row >= {{ ROWS_EXPR }}) { return; }
|
| 180 |
{% if CACHE_VEC4 %}
|
| 181 |
let base4 = row * COLS4;
|
build/webgpu/attn-online-scalar.wgsl.jinja
CHANGED
|
@@ -1,25 +1,14 @@
|
|
| 1 |
-
{% if usesF16 %}
|
| 2 |
-
enable f16;
|
| 3 |
-
{% endif %}
|
| 4 |
{{ env.wgsl.resourceDeclarations }}
|
| 5 |
-
{% set MASK_BATCH = "batch * params.maskBatchStride + " if
|
| 6 |
|
| 7 |
// Online-softmax attention fallback with no feature requirements: one
|
| 8 |
// workgroup per (batch, head, query token) walks the keys serially; the
|
| 9 |
// workgroup cooperates on each q·k dot (tree reduction) and on the running
|
| 10 |
-
// V accumulator, with the online rescale applied per key.
|
| 11 |
-
//
|
| 12 |
// Layout: rank-3 token-major [batch, seq, heads * headDim].
|
| 13 |
-
{% if headsFromParams %}
|
| 14 |
-
// Q/KV head counts are uniforms; the GQA mapping is h / (qHeads / kvHeads).
|
| 15 |
-
{% endif %}
|
| 16 |
-
{% if hasKeyLimit %}
|
| 17 |
-
// params.keyLimit caps the key range when nonzero.
|
| 18 |
-
{% endif %}
|
| 19 |
-
{% if scaleFallbackRsqrt %}
|
| 20 |
// An omitted scale uses 1/sqrt(headDim); explicit zero remains zero through
|
| 21 |
// the scaleIsExplicitZero specialization.
|
| 22 |
-
{% endif %}
|
| 23 |
{% if hasBias %}
|
| 24 |
// Packed [Q; K; V] bias rows are applied during the serial key walk.
|
| 25 |
{% endif %}
|
|
@@ -30,10 +19,8 @@ enable f16;
|
|
| 30 |
const HEAD_DIM: u32 = {{ headDim }}u;
|
| 31 |
const Q_HIDDEN: u32 = {{ qHidden }}u;
|
| 32 |
const KV_HIDDEN: u32 = {{ kvHidden }}u;
|
| 33 |
-
{% if not headsFromParams %}
|
| 34 |
const Q_HEADS: u32 = {{ qNumHeads }}u;
|
| 35 |
const KV_HEADS: u32 = {{ kvNumHeads }}u;
|
| 36 |
-
{% endif %}
|
| 37 |
{% set qHeads = "params.qHeads" if headsFromParams else "Q_HEADS" %}
|
| 38 |
{% set kvHeads = "params.kvHeads" if headsFromParams else "KV_HEADS" %}
|
| 39 |
{% set scale = scale | default("0.0") %}
|
|
@@ -80,18 +67,12 @@ fn {{ name }}(value: f32, tid: u32) -> f32 {
|
|
| 80 |
|
| 81 |
{{ wgsl_tree_reduce_f32("reduce_sum", "add", "partial", "WG") }}
|
| 82 |
|
| 83 |
-
{% if scaleFallbackRsqrt %}
|
| 84 |
{% if ATTN_SCALE_DIM is not defined %}{% set ATTN_SCALE_DIM = "HEAD_DIM" %}{% endif %}
|
| 85 |
fn scale_value() -> f32 {
|
| 86 |
if (params.scale != 0.0) { return params.scale; }
|
| 87 |
return inverseSqrt(f32({{ ATTN_SCALE_DIM }}));
|
| 88 |
}
|
| 89 |
|
| 90 |
-
{% else %}
|
| 91 |
-
fn scale_value() -> f32 {
|
| 92 |
-
return {{ scale }};
|
| 93 |
-
}
|
| 94 |
-
{% endif %}
|
| 95 |
|
| 96 |
@compute @workgroup_size(WG, 1, 1)
|
| 97 |
fn main(
|
|
@@ -132,12 +113,7 @@ fn main(
|
|
| 132 |
|
| 133 |
var maxKj = params.kvSeq;
|
| 134 |
var minKj: u32 = 0u;
|
| 135 |
-
{% if hasCausal %}
|
| 136 |
maxKj = min(maxKj, select(params.kvSeq, query_token + 1u, params.isCausal != 0u));
|
| 137 |
-
{% endif %}
|
| 138 |
-
{% if hasKeyLimit %}
|
| 139 |
-
maxKj = min(maxKj, select(params.kvSeq, params.keyLimit, params.keyLimit > 0u));
|
| 140 |
-
{% endif %}
|
| 141 |
|
| 142 |
for (var key_token: u32 = minKj; key_token < maxKj; key_token = key_token + 1u) {
|
| 143 |
let kRow = kvBase + key_token * kvTokenStride;
|
|
@@ -183,7 +159,9 @@ fn main(
|
|
| 183 |
workgroupBarrier();
|
| 184 |
}
|
| 185 |
|
| 186 |
-
|
|
|
|
|
|
|
| 187 |
for (var d: u32 = tid; d < HEAD_DIM; d = d + WG) {
|
| 188 |
output[qBase + d] = {{ outputScalar }}(running_out[d] * inv_denom);
|
| 189 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
{{ env.wgsl.resourceDeclarations }}
|
| 2 |
+
{% set MASK_BATCH = "batch * params.maskBatchStride + " if layout == "bsh" else "" %}
|
| 3 |
|
| 4 |
// Online-softmax attention fallback with no feature requirements: one
|
| 5 |
// workgroup per (batch, head, query token) walks the keys serially; the
|
| 6 |
// workgroup cooperates on each q·k dot (tree reduction) and on the running
|
| 7 |
+
// V accumulator, with the online rescale applied per key. This path requires
|
| 8 |
+
// no subgroup or subgroup-matrix features.
|
| 9 |
// Layout: rank-3 token-major [batch, seq, heads * headDim].
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
// An omitted scale uses 1/sqrt(headDim); explicit zero remains zero through
|
| 11 |
// the scaleIsExplicitZero specialization.
|
|
|
|
| 12 |
{% if hasBias %}
|
| 13 |
// Packed [Q; K; V] bias rows are applied during the serial key walk.
|
| 14 |
{% endif %}
|
|
|
|
| 19 |
const HEAD_DIM: u32 = {{ headDim }}u;
|
| 20 |
const Q_HIDDEN: u32 = {{ qHidden }}u;
|
| 21 |
const KV_HIDDEN: u32 = {{ kvHidden }}u;
|
|
|
|
| 22 |
const Q_HEADS: u32 = {{ qNumHeads }}u;
|
| 23 |
const KV_HEADS: u32 = {{ kvNumHeads }}u;
|
|
|
|
| 24 |
{% set qHeads = "params.qHeads" if headsFromParams else "Q_HEADS" %}
|
| 25 |
{% set kvHeads = "params.kvHeads" if headsFromParams else "KV_HEADS" %}
|
| 26 |
{% set scale = scale | default("0.0") %}
|
|
|
|
| 67 |
|
| 68 |
{{ wgsl_tree_reduce_f32("reduce_sum", "add", "partial", "WG") }}
|
| 69 |
|
|
|
|
| 70 |
{% if ATTN_SCALE_DIM is not defined %}{% set ATTN_SCALE_DIM = "HEAD_DIM" %}{% endif %}
|
| 71 |
fn scale_value() -> f32 {
|
| 72 |
if (params.scale != 0.0) { return params.scale; }
|
| 73 |
return inverseSqrt(f32({{ ATTN_SCALE_DIM }}));
|
| 74 |
}
|
| 75 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
@compute @workgroup_size(WG, 1, 1)
|
| 78 |
fn main(
|
|
|
|
| 113 |
|
| 114 |
var maxKj = params.kvSeq;
|
| 115 |
var minKj: u32 = 0u;
|
|
|
|
| 116 |
maxKj = min(maxKj, select(params.kvSeq, query_token + 1u, params.isCausal != 0u));
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
|
| 118 |
for (var key_token: u32 = minKj; key_token < maxKj; key_token = key_token + 1u) {
|
| 119 |
let kRow = kvBase + key_token * kvTokenStride;
|
|
|
|
| 159 |
workgroupBarrier();
|
| 160 |
}
|
| 161 |
|
| 162 |
+
// A query whose every key is masked to -inf (or whose key range is empty)
|
| 163 |
+
// leaves running_denom at zero; emit a zero row rather than 0 * (1/0) = NaN.
|
| 164 |
+
let inv_denom = select(0.0, 1.0 / running_denom, running_denom > 0.0);
|
| 165 |
for (var d: u32 = tid; d < HEAD_DIM; d = d + WG) {
|
| 166 |
output[qBase + d] = {{ outputScalar }}(running_out[d] * inv_denom);
|
| 167 |
}
|
build/webgpu/attn-small-head-parallel.wgsl.jinja
CHANGED
|
@@ -1,6 +1,3 @@
|
|
| 1 |
-
{% if usesF16 %}
|
| 2 |
-
enable f16;
|
| 3 |
-
{% endif %}
|
| 4 |
{{ env.wgsl.resourceDeclarations }}
|
| 5 |
|
| 6 |
// Small-head attention is dominated by the per-key barriers in the generic
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
{{ env.wgsl.resourceDeclarations }}
|
| 2 |
|
| 3 |
// Small-head attention is dominated by the per-key barriers in the generic
|
build/webgpu/bench.json
CHANGED
|
@@ -1,10 +1,9 @@
|
|
| 1 |
{
|
| 2 |
-
"op": "com.microsoft.MultiHeadAttention",
|
| 3 |
"cases": [
|
| 4 |
{
|
| 5 |
"name": "minilm-self-attention-b1-s20-h12-d32",
|
| 6 |
"provenance": {
|
| 7 |
-
"notes": "
|
| 8 |
},
|
| 9 |
"vars": { "batch": 1, "qSeq": 20, "kvSeq": 20, "heads": 12, "headDim": 32 },
|
| 10 |
"attrs": { "num_heads": 12 },
|
|
|
|
| 1 |
{
|
|
|
|
| 2 |
"cases": [
|
| 3 |
{
|
| 4 |
"name": "minilm-self-attention-b1-s20-h12-d32",
|
| 5 |
"provenance": {
|
| 6 |
+
"notes": "Twelve heads at sequence length 20 mark the upper boundary of the whole-head small-sequence route; `minilm-self-attention-b1-s24-h12-d32` exercises the adjacent flash route."
|
| 7 |
},
|
| 8 |
"vars": { "batch": 1, "qSeq": 20, "kvSeq": 20, "heads": 12, "headDim": 32 },
|
| 9 |
"attrs": { "num_heads": 12 },
|
build/webgpu/manifest.json
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
build/webgpu/metadata.json
CHANGED
|
@@ -1,31 +1,78 @@
|
|
| 1 |
{
|
| 2 |
"name": "com.microsoft.MultiHeadAttention",
|
| 3 |
-
"id": "
|
| 4 |
"version": 1,
|
| 5 |
"license": "Apache-2.0",
|
| 6 |
"backend": { "type": "webgpu" },
|
| 7 |
"digest": {
|
| 8 |
"algorithm": "sha256",
|
| 9 |
"files": {
|
| 10 |
-
"attention-rank4-tiled.wgsl.jinja": "
|
| 11 |
-
"attn-flash-decode-splitk-merge.wgsl.jinja": "
|
| 12 |
-
"attn-flash-decode-splitk.wgsl.jinja": "
|
| 13 |
-
"attn-flash-online.wgsl.jinja": "
|
| 14 |
-
"attn-flash-prefill-cluster.wgsl.jinja": "
|
| 15 |
-
"attn-flash-q32-broadcast.wgsl.jinja": "
|
| 16 |
-
"attn-materialized-apply-f32.wgsl.jinja": "
|
| 17 |
-
"attn-materialized-rowstats-combine-f32.wgsl.jinja": "
|
| 18 |
-
"attn-materialized-score-f32.wgsl.jinja": "
|
| 19 |
-
"attn-materialized-sgmat-f32.wgsl.jinja": "
|
| 20 |
-
"attn-materialized-softmax-f32.wgsl.jinja": "
|
| 21 |
-
"attn-online-scalar.wgsl.jinja": "
|
| 22 |
-
"attn-small-head-parallel.wgsl.jinja": "
|
| 23 |
-
"bench.json": "
|
| 24 |
-
"manifest.json": "
|
| 25 |
-
"mha-small-seq.wgsl.jinja": "
|
| 26 |
-
"
|
|
|
|
| 27 |
}
|
| 28 |
},
|
| 29 |
-
"provenance": { "kernel": { "sha": "
|
| 30 |
-
"webgpu": {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"name": "com.microsoft.MultiHeadAttention",
|
| 3 |
+
"id": "_com_microsoft_multiheadattention_webgpu_5144476",
|
| 4 |
"version": 1,
|
| 5 |
"license": "Apache-2.0",
|
| 6 |
"backend": { "type": "webgpu" },
|
| 7 |
"digest": {
|
| 8 |
"algorithm": "sha256",
|
| 9 |
"files": {
|
| 10 |
+
"attention-rank4-tiled.wgsl.jinja": "gYl58ecIVTxJJkvnK/6lNcbPKaRT24cfz5Yx0MxxxxE=",
|
| 11 |
+
"attn-flash-decode-splitk-merge.wgsl.jinja": "XSAMF/uzQKJg9pZZDeqmViaVXuj47Vd7Pk404Z1J48I=",
|
| 12 |
+
"attn-flash-decode-splitk.wgsl.jinja": "dqxX8taw1tpo/QQkFr2zJ7TEEtQtOspTYEYHADXtESs=",
|
| 13 |
+
"attn-flash-online.wgsl.jinja": "hCxO/6VGkyixwyoT/kuYeHgcEkFoRwk/ZmiLlnwa0jM=",
|
| 14 |
+
"attn-flash-prefill-cluster.wgsl.jinja": "igbsg5xRuFz7alnwmL74hhh1JUqfDB5PfUslsEz5rjA=",
|
| 15 |
+
"attn-flash-q32-broadcast.wgsl.jinja": "8qgKXydwiH2shac/fkdHi+FR+c8pOMDFbNp9Zs8KAnc=",
|
| 16 |
+
"attn-materialized-apply-f32.wgsl.jinja": "aF7JSJj8rQN8wGTV7IWmxkqMDbXOnGBSI3tsI1CxZNk=",
|
| 17 |
+
"attn-materialized-rowstats-combine-f32.wgsl.jinja": "+/5m03sN44zTn9q7cquDea2UNhEO1ujzm1J7yPai4OY=",
|
| 18 |
+
"attn-materialized-score-f32.wgsl.jinja": "gCvBC1T1CLEmNxcpo04CLBQdFBkEVMJ9Xhv1wbQxCsI=",
|
| 19 |
+
"attn-materialized-sgmat-f32.wgsl.jinja": "Ez9UJggPXMZ6OyzUQu8xLtg8bZd+lTCoEGDwLkNHNL0=",
|
| 20 |
+
"attn-materialized-softmax-f32.wgsl.jinja": "DCuz4gFKXDCdR9UMnfD+mYFjBghnv12GxtEsPKH52mE=",
|
| 21 |
+
"attn-online-scalar.wgsl.jinja": "6+hU7MzwHz69fjciv94xsPxN+o9rwm42u9Cin2C+AL8=",
|
| 22 |
+
"attn-small-head-parallel.wgsl.jinja": "oxFE6hQZKTRugz9hwuS+cob00PskT8iYqP4e+YWtHKA=",
|
| 23 |
+
"bench.json": "b/WdaAhEP5NpRXNB5w1mivRNTfALrBDJisDONxTf4Yo=",
|
| 24 |
+
"manifest.json": "+/bNCAjkG5OoutFq7O4vQJqb2yMxzICsKvFXsRE50bA=",
|
| 25 |
+
"mha-small-seq-blocked.wgsl.jinja": "zznGERrPco5uH9tKiQ2QE+ArO3vho/nEv69wT8IgY1U=",
|
| 26 |
+
"mha-small-seq.wgsl.jinja": "UsbUIkvgWL/RgOBSm7lhPGIatA36yTQR4Z2gpHVLqx4=",
|
| 27 |
+
"test.json": "NlMA/wlmGSpogAuhSZ/kcQ01dd3M+CvkPF0d/VBmIpM="
|
| 28 |
}
|
| 29 |
},
|
| 30 |
+
"provenance": { "kernel": { "sha": "91d990483a174128daf7673f3f37a7c890493ae1", "dirty": false } },
|
| 31 |
+
"webgpu": {
|
| 32 |
+
"manifestSpec": "2.0",
|
| 33 |
+
"variants": {
|
| 34 |
+
"qkv_bias_small_seq_blocked": ["mha-small-seq-blocked.wgsl.jinja"],
|
| 35 |
+
"qkv_no_bias_small_seq_blocked": ["mha-small-seq-blocked.wgsl.jinja"],
|
| 36 |
+
"qkv_no_bias_small_seq": ["mha-small-seq.wgsl.jinja"],
|
| 37 |
+
"qkv_no_bias_tiled_nosg": ["attention-rank4-tiled.wgsl.jinja"],
|
| 38 |
+
"qkv_bias_flash_q32_broadcast_f32_d128": ["attn-flash-q32-broadcast.wgsl.jinja"],
|
| 39 |
+
"qkv_no_bias_small_head_parallel": ["attn-small-head-parallel.wgsl.jinja"],
|
| 40 |
+
"qkv_no_bias_tiled_attn_bias_nosg": ["attention-rank4-tiled.wgsl.jinja"],
|
| 41 |
+
"qkv_no_bias_materialized_sgmat_fused_f32": ["attn-materialized-rowstats-combine-f32.wgsl.jinja", "attn-materialized-sgmat-f32.wgsl.jinja"],
|
| 42 |
+
"qkv_bias_materialized_sgmat_fused_f32": ["attn-materialized-rowstats-combine-f32.wgsl.jinja", "attn-materialized-sgmat-f32.wgsl.jinja"],
|
| 43 |
+
"qkv_no_bias_materialized_sgmat_fused_f16": ["attn-materialized-rowstats-combine-f32.wgsl.jinja", "attn-materialized-sgmat-f32.wgsl.jinja"],
|
| 44 |
+
"qkv_no_bias_flash_cluster_lpq4_nosg": ["attn-flash-prefill-cluster.wgsl.jinja"],
|
| 45 |
+
"qkv_no_bias_flash_cluster_nosg": ["attn-flash-prefill-cluster.wgsl.jinja"],
|
| 46 |
+
"qkv_bias_flash_cluster_nosg": ["attn-flash-prefill-cluster.wgsl.jinja"],
|
| 47 |
+
"qkv_no_bias_flash_cluster_lpq4": ["attn-flash-prefill-cluster.wgsl.jinja"],
|
| 48 |
+
"qkv_no_bias_flash_cluster": ["attn-flash-prefill-cluster.wgsl.jinja"],
|
| 49 |
+
"qkv_bias_flash_cluster": ["attn-flash-prefill-cluster.wgsl.jinja"],
|
| 50 |
+
"qkv_no_bias_flash_cluster_attn_bias": ["attn-flash-prefill-cluster.wgsl.jinja"],
|
| 51 |
+
"qkv_bias_flash_cluster_attn_bias": ["attn-flash-prefill-cluster.wgsl.jinja"],
|
| 52 |
+
"qkv_no_bias_flash_splitk_nosg": ["attn-flash-decode-splitk-merge.wgsl.jinja", "attn-flash-decode-splitk.wgsl.jinja"],
|
| 53 |
+
"qkv_bias_flash_splitk_nosg": ["attn-flash-decode-splitk-merge.wgsl.jinja", "attn-flash-decode-splitk.wgsl.jinja"],
|
| 54 |
+
"qkv_no_bias_flash_splitk": ["attn-flash-decode-splitk-merge.wgsl.jinja", "attn-flash-decode-splitk.wgsl.jinja"],
|
| 55 |
+
"qkv_bias_flash_splitk": ["attn-flash-decode-splitk-merge.wgsl.jinja", "attn-flash-decode-splitk.wgsl.jinja"],
|
| 56 |
+
"qkv_no_bias_materialized_gemm_f32": ["attn-materialized-apply-f32.wgsl.jinja", "attn-materialized-score-f32.wgsl.jinja", "attn-materialized-softmax-f32.wgsl.jinja"],
|
| 57 |
+
"qkv_bias_materialized_gemm_f32": ["attn-materialized-apply-f32.wgsl.jinja", "attn-materialized-score-f32.wgsl.jinja", "attn-materialized-softmax-f32.wgsl.jinja"],
|
| 58 |
+
"qkv_no_bias_materialized_gemm_fused_f32": ["attn-materialized-apply-f32.wgsl.jinja", "attn-materialized-rowstats-combine-f32.wgsl.jinja", "attn-materialized-score-f32.wgsl.jinja"],
|
| 59 |
+
"qkv_bias_materialized_gemm_fused_f32": ["attn-materialized-apply-f32.wgsl.jinja", "attn-materialized-rowstats-combine-f32.wgsl.jinja", "attn-materialized-score-f32.wgsl.jinja"],
|
| 60 |
+
"qkv_no_bias_flash_q32_broadcast": ["attn-flash-q32-broadcast.wgsl.jinja"],
|
| 61 |
+
"qkv_no_bias_flash_q32_shared": ["attn-flash-q32-broadcast.wgsl.jinja"],
|
| 62 |
+
"qkv_no_bias_attn_bias": ["attn-online-scalar.wgsl.jinja"],
|
| 63 |
+
"qkv_bias_attn_bias": ["attn-online-scalar.wgsl.jinja"],
|
| 64 |
+
"qkv_no_bias": ["attn-online-scalar.wgsl.jinja"],
|
| 65 |
+
"qkv_bias": ["attn-online-scalar.wgsl.jinja"],
|
| 66 |
+
"qkv_no_bias_flash": ["attn-flash-online.wgsl.jinja"],
|
| 67 |
+
"qkv_bias_flash": ["attn-flash-online.wgsl.jinja"],
|
| 68 |
+
"qkv_no_bias_flash_nosg": ["attn-flash-online.wgsl.jinja"],
|
| 69 |
+
"qkv_bias_flash_nosg": ["attn-flash-online.wgsl.jinja"],
|
| 70 |
+
"qkv_no_bias_flash_attn_bias": ["attn-flash-online.wgsl.jinja"],
|
| 71 |
+
"qkv_bias_flash_attn_bias": ["attn-flash-online.wgsl.jinja"],
|
| 72 |
+
"qkv_no_bias_flash_attn_bias_nosg": ["attn-flash-online.wgsl.jinja"],
|
| 73 |
+
"qkv_bias_flash_attn_bias_nosg": ["attn-flash-online.wgsl.jinja"],
|
| 74 |
+
"qkv_no_bias_materialized_sgmat_f32": ["attn-materialized-sgmat-f32.wgsl.jinja", "attn-materialized-softmax-f32.wgsl.jinja"],
|
| 75 |
+
"qkv_bias_materialized_sgmat_f32": ["attn-materialized-sgmat-f32.wgsl.jinja", "attn-materialized-softmax-f32.wgsl.jinja"]
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
}
|
build/webgpu/mha-small-seq-blocked.wgsl.jinja
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{{ env.wgsl.resourceDeclarations }}
|
| 2 |
+
|
| 3 |
+
// Blocked whole-head attention for short key sequences with optional projection
|
| 4 |
+
// bias and causal masking. One workgroup owns
|
| 5 |
+
// (batch, head) and a block of QUERY_BLOCK query rows; it stages the head's
|
| 6 |
+
// complete K and V planes in workgroup memory once, then gives every query
|
| 7 |
+
// KEY_LANES lanes: lane (q, kl) scores keys kl, kl + KEY_LANES, ... against
|
| 8 |
+
// its private Q row, keeps an online-softmax (max, denominator, context)
|
| 9 |
+
// partial in registers, and the query's KEY_LANES partials merge through
|
| 10 |
+
// workgroup memory in one pass. The full score row never exists.
|
| 11 |
+
//
|
| 12 |
+
// The complete (query, key) block is folded with one reduction round per query
|
| 13 |
+
// block. KEY_LANES independently accumulate interleaved keys before that fold.
|
| 14 |
+
const HEAD_DIM: u32 = {{ headDim }}u;
|
| 15 |
+
const HEAD_DIM_V4: u32 = {{ headDimV4 }}u;
|
| 16 |
+
const HIDDEN_V4: u32 = {{ hiddenV4 }}u;
|
| 17 |
+
const KV_SEQ: u32 = {{ kvSeq }}u;
|
| 18 |
+
const QUERY_BLOCK: u32 = {{ queryBlock }}u;
|
| 19 |
+
const KEY_LANES: u32 = {{ keyLanes }}u;
|
| 20 |
+
const WG: u32 = QUERY_BLOCK * KEY_LANES;
|
| 21 |
+
// FLT_MAX, not -inf, as the online (m, d) accumulator init: merges must keep
|
| 22 |
+
// `m - m` finite so an empty lane / all--inf row contributes the exact
|
| 23 |
+
// accumulator identity (m, d) = (-FLT_MAX, 0). Operator epilogues interpret
|
| 24 |
+
// a zero final denominator according to their public semantics. Using -inf
|
| 25 |
+
// here changes +inf-row behavior.
|
| 26 |
+
const FLT_MAX: f32 = 3.4028234663852886e38;
|
| 27 |
+
|
| 28 |
+
fn is_finite_f32(value: f32) -> bool {
|
| 29 |
+
return select(false, value <= FLT_MAX, value >= -FLT_MAX);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
// x - m that is exactly 0 when x equals a finite m, so exp(shifted) == 1
|
| 33 |
+
// exactly at the row max. `x - x` on an infinite max is a legal fast-math
|
| 34 |
+
// fold to 0, which would silently turn +inf rows finite — the explicit
|
| 35 |
+
// equality test keeps the NaN propagation of the serial kernels.
|
| 36 |
+
fn shifted_value(value: f32, maxValue: f32) -> f32 {
|
| 37 |
+
let equalFiniteMax = select(false, value == maxValue, is_finite_f32(maxValue));
|
| 38 |
+
return select(value - maxValue, 0.0, equalFiniteMax);
|
| 39 |
+
}
|
| 40 |
+
fn exp_shift(value: f32, maxValue: f32) -> f32 {
|
| 41 |
+
return exp(shifted_value(value, maxValue));
|
| 42 |
+
}
|
| 43 |
+
{% if ATTN_SCALE_DIM is not defined %}{% set ATTN_SCALE_DIM = "HEAD_DIM" %}{% endif %}
|
| 44 |
+
fn scale_value() -> f32 {
|
| 45 |
+
if (params.scale != 0.0) { return params.scale; }
|
| 46 |
+
return inverseSqrt(f32({{ ATTN_SCALE_DIM }}));
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
{% if hasBias %}
|
| 50 |
+
fn load_bias4(base: u32, d4: u32) -> vec4<f32> {
|
| 51 |
+
let offset = base + d4 * 4u;
|
| 52 |
+
return vec4<f32>(bias[offset], bias[offset + 1u], bias[offset + 2u], bias[offset + 3u]);
|
| 53 |
+
}
|
| 54 |
+
{% endif %}
|
| 55 |
+
|
| 56 |
+
var<workgroup> kShared: array<vec4<f32>, KV_SEQ * HEAD_DIM_V4>;
|
| 57 |
+
var<workgroup> vShared: array<vec4<f32>, KV_SEQ * HEAD_DIM_V4>;
|
| 58 |
+
// Per-lane partials for the merge: (max, denom) and the context row.
|
| 59 |
+
var<workgroup> partM: array<f32, WG>;
|
| 60 |
+
var<workgroup> partD: array<f32, WG>;
|
| 61 |
+
var<workgroup> partO: array<vec4<f32>, WG * HEAD_DIM_V4>;
|
| 62 |
+
|
| 63 |
+
@compute @workgroup_size({{ queryBlock * keyLanes }}, 1, 1)
|
| 64 |
+
fn main(
|
| 65 |
+
@builtin(workgroup_id) wid: vec3<u32>,
|
| 66 |
+
@builtin(local_invocation_id) lid: vec3<u32>
|
| 67 |
+
) {
|
| 68 |
+
let head = wid.y;
|
| 69 |
+
let batch = wid.z;
|
| 70 |
+
let tid = lid.x;
|
| 71 |
+
let ql = tid / KEY_LANES;
|
| 72 |
+
let kl = tid % KEY_LANES;
|
| 73 |
+
let qi = wid.x * QUERY_BLOCK + ql;
|
| 74 |
+
let headV4 = head * HEAD_DIM_V4;
|
| 75 |
+
|
| 76 |
+
// Lanes stage this head's K and V planes from consecutive vec4 elements,
|
| 77 |
+
// advancing by the workgroup width even when their query row is absent.
|
| 78 |
+
let kvBase = batch * KV_SEQ * HIDDEN_V4;
|
| 79 |
+
for (var i = tid; i < KV_SEQ * HEAD_DIM_V4; i = i + WG) {
|
| 80 |
+
let s = i / HEAD_DIM_V4;
|
| 81 |
+
let d4 = i % HEAD_DIM_V4;
|
| 82 |
+
let src = kvBase + s * HIDDEN_V4 + headV4 + d4;
|
| 83 |
+
kShared[i] = vec4<f32>(key[src]);
|
| 84 |
+
vShared[i] = vec4<f32>(value[src]);
|
| 85 |
+
}
|
| 86 |
+
workgroupBarrier();
|
| 87 |
+
|
| 88 |
+
let scale = scale_value();
|
| 89 |
+
let rowValid = qi < params.qSeq;
|
| 90 |
+
// A query past the end scores no keys: its partials are the identity and it
|
| 91 |
+
// stores nothing, so every barrier below stays uniform. For a causal request,
|
| 92 |
+
// query qi attends keys 0 through qi.
|
| 93 |
+
let keyBound = select(0u, select(KV_SEQ, min(KV_SEQ, qi + 1u), params.isCausal != 0u), rowValid);
|
| 94 |
+
|
| 95 |
+
// Private Q row (with the Q bias folded in), read from the clamped row so an
|
| 96 |
+
// absent query still reads in bounds.
|
| 97 |
+
let qRowV4 = (batch * params.qSeq + min(qi, params.qSeq - 1u)) * HIDDEN_V4 + headV4;
|
| 98 |
+
{% for d in range(headDimV4) %}
|
| 99 |
+
var q{{ d }} = vec4<f32>(query[qRowV4 + {{ d }}u]);
|
| 100 |
+
{% if hasBias %}
|
| 101 |
+
q{{ d }} = q{{ d }} + load_bias4(head * HEAD_DIM, {{ d }}u);
|
| 102 |
+
{% endif %}
|
| 103 |
+
{% endfor %}
|
| 104 |
+
|
| 105 |
+
// Lane-local online softmax over keys kl, kl + KEY_LANES, ...
|
| 106 |
+
var m = -FLT_MAX;
|
| 107 |
+
var d = 0.0;
|
| 108 |
+
{% for c in range(headDimV4) %}
|
| 109 |
+
var o{{ c }} = vec4<f32>(0.0);
|
| 110 |
+
{% endfor %}
|
| 111 |
+
for (var kj = kl; kj < keyBound; kj = kj + KEY_LANES) {
|
| 112 |
+
let kBase = kj * HEAD_DIM_V4;
|
| 113 |
+
var s = 0.0;
|
| 114 |
+
{% for c in range(headDimV4) %}
|
| 115 |
+
s = s + dot(q{{ c }}, kShared[kBase + {{ c }}u]);
|
| 116 |
+
{% endfor %}
|
| 117 |
+
s = s * scale;
|
| 118 |
+
let mNew = max(m, s);
|
| 119 |
+
let corr = exp_shift(m, mNew);
|
| 120 |
+
let p = exp_shift(s, mNew);
|
| 121 |
+
d = d * corr + p;
|
| 122 |
+
{% for c in range(headDimV4) %}
|
| 123 |
+
o{{ c }} = o{{ c }} * corr + p * vShared[kBase + {{ c }}u];
|
| 124 |
+
{% endfor %}
|
| 125 |
+
m = mNew;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
partM[tid] = m;
|
| 129 |
+
partD[tid] = d;
|
| 130 |
+
{% for c in range(headDimV4) %}
|
| 131 |
+
partO[tid * HEAD_DIM_V4 + {{ c }}u] = o{{ c }};
|
| 132 |
+
{% endfor %}
|
| 133 |
+
workgroupBarrier();
|
| 134 |
+
|
| 135 |
+
// The query's leader lane merges its KEY_LANES partials with the stable
|
| 136 |
+
// online rule, then applies the V bias once and stores the row.
|
| 137 |
+
if (kl == 0u && rowValid) {
|
| 138 |
+
let base = ql * KEY_LANES;
|
| 139 |
+
var mAll = -FLT_MAX;
|
| 140 |
+
for (var i = 0u; i < KEY_LANES; i = i + 1u) {
|
| 141 |
+
mAll = max(mAll, partM[base + i]);
|
| 142 |
+
}
|
| 143 |
+
var dAll = 0.0;
|
| 144 |
+
{% for c in range(headDimV4) %}
|
| 145 |
+
var out{{ c }} = vec4<f32>(0.0);
|
| 146 |
+
{% endfor %}
|
| 147 |
+
for (var i = 0u; i < KEY_LANES; i = i + 1u) {
|
| 148 |
+
let w = exp_shift(partM[base + i], mAll);
|
| 149 |
+
dAll = dAll + partD[base + i] * w;
|
| 150 |
+
{% for c in range(headDimV4) %}
|
| 151 |
+
out{{ c }} = out{{ c }} + partO[(base + i) * HEAD_DIM_V4 + {{ c }}u] * w;
|
| 152 |
+
{% endfor %}
|
| 153 |
+
}
|
| 154 |
+
let hasMass = dAll > 0.0;
|
| 155 |
+
let inv = select(0.0, 1.0 / dAll, hasMass);
|
| 156 |
+
let outRowV4 = (batch * params.qSeq + qi) * HIDDEN_V4 + headV4;
|
| 157 |
+
{% for c in range(headDimV4) %}
|
| 158 |
+
var v{{ c }} = out{{ c }} * inv;
|
| 159 |
+
{% if hasBias %}
|
| 160 |
+
// V bias is token-independent, so add it once after the softmax-weighted sum.
|
| 161 |
+
v{{ c }} = v{{ c }} + load_bias4(2u * {{ hidden }}u + head * HEAD_DIM, {{ c }}u) * select(0.0, 1.0, hasMass);
|
| 162 |
+
{% endif %}
|
| 163 |
+
output[outRowV4 + {{ c }}u] = v{{ c }};
|
| 164 |
+
{% endfor %}
|
| 165 |
+
}
|
| 166 |
+
}
|
build/webgpu/mha-small-seq.wgsl.jinja
CHANGED
|
@@ -28,9 +28,9 @@ fn main(
|
|
| 28 |
let batch = wid.y;
|
| 29 |
let headBase = head * HEAD_DIM;
|
| 30 |
|
| 31 |
-
//
|
| 32 |
-
//
|
| 33 |
-
//
|
| 34 |
for (var i = lid.x; i < KV_SEQ * HEAD_DIM; i += WG) {
|
| 35 |
let s = i / HEAD_DIM;
|
| 36 |
let d = i % HEAD_DIM;
|
|
|
|
| 28 |
let batch = wid.y;
|
| 29 |
let headBase = head * HEAD_DIM;
|
| 30 |
|
| 31 |
+
// Lanes stage this head's K and V planes from consecutive flattened elements,
|
| 32 |
+
// advancing by the workgroup width. Every lane reaches the barrier before a
|
| 33 |
+
// lane without a query row exits.
|
| 34 |
for (var i = lid.x; i < KV_SEQ * HEAD_DIM; i += WG) {
|
| 35 |
let s = i / HEAD_DIM;
|
| 36 |
let d = i % HEAD_DIM;
|
build/webgpu/test.json
CHANGED
|
@@ -1,5 +1,4 @@
|
|
| 1 |
{
|
| 2 |
-
"op": "com.microsoft.MultiHeadAttention",
|
| 3 |
"cases": [
|
| 4 |
{
|
| 5 |
"name": "cross_attention_no_bias",
|
|
@@ -186,7 +185,7 @@
|
|
| 186 |
"provenance": {
|
| 187 |
"source": "onnxruntime/test/contrib_ops/multihead_attention_op_test.cc",
|
| 188 |
"test": "MultiHeadAttentionTest.CrossAttention_Batch1_HeadSize16",
|
| 189 |
-
"notes": "
|
| 190 |
},
|
| 191 |
"attrs": { "num_heads": 2 },
|
| 192 |
"inputs": {
|
|
@@ -279,7 +278,7 @@
|
|
| 279 |
"valueT": {
|
| 280 |
"dtype": "float32",
|
| 281 |
"shape": [1, 64, 256],
|
| 282 |
-
"data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.
|
| 283 |
}
|
| 284 |
},
|
| 285 |
"outputs": { "outputT": { "dtype": "float32", "shape": [1, 64, 256], "tolerance": 0.0002 } }
|
|
@@ -350,7 +349,7 @@
|
|
| 350 |
"valueT": {
|
| 351 |
"dtype": "float32",
|
| 352 |
"shape": [1, 512, 256],
|
| 353 |
-
"data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.
|
| 354 |
}
|
| 355 |
},
|
| 356 |
"outputs": { "outputT": { "dtype": "float32", "shape": [1, 1, 256], "tolerance": 0.0002 } }
|
|
@@ -399,7 +398,7 @@
|
|
| 399 |
"valueT": {
|
| 400 |
"dtype": "float32",
|
| 401 |
"shape": [1, 2048, 256],
|
| 402 |
-
"data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.
|
| 403 |
}
|
| 404 |
},
|
| 405 |
"outputs": { "outputT": { "dtype": "float32", "shape": [1, 1, 256], "tolerance": 0.0003 } }
|
|
@@ -407,7 +406,7 @@
|
|
| 407 |
{
|
| 408 |
"name": "splitk_decode_head_dim128_no_bias_kv2048",
|
| 409 |
"provenance": {
|
| 410 |
-
"notes": "
|
| 411 |
},
|
| 412 |
"attrs": { "num_heads": 2, "scale": 0.08838834764831845 },
|
| 413 |
"inputs": {
|
|
@@ -424,7 +423,7 @@
|
|
| 424 |
"valueT": {
|
| 425 |
"dtype": "float32",
|
| 426 |
"shape": [1, 2048, 256],
|
| 427 |
-
"data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.
|
| 428 |
}
|
| 429 |
},
|
| 430 |
"outputs": { "outputT": { "dtype": "float32", "shape": [1, 1, 256], "tolerance": 0.0003 } }
|
|
@@ -446,7 +445,7 @@
|
|
| 446 |
"valueT": {
|
| 447 |
"dtype": "float32",
|
| 448 |
"shape": [2, 768, 512],
|
| 449 |
-
"data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.
|
| 450 |
}
|
| 451 |
},
|
| 452 |
"outputs": { "outputT": { "dtype": "float32", "shape": [2, 1, 512], "tolerance": 0.0003 } }
|
|
@@ -508,7 +507,7 @@
|
|
| 508 |
{
|
| 509 |
"name": "flash_self_attention_head_dim128_no_bias_f16",
|
| 510 |
"provenance": {
|
| 511 |
-
"notes": "
|
| 512 |
},
|
| 513 |
"attrs": { "num_heads": 4, "scale": 0.08838834764831845 },
|
| 514 |
"inputs": {
|
|
@@ -525,10 +524,10 @@
|
|
| 525 |
"valueT": {
|
| 526 |
"dtype": "float16",
|
| 527 |
"shape": [1, 64, 512],
|
| 528 |
-
"data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.
|
| 529 |
}
|
| 530 |
},
|
| 531 |
-
"outputs": { "outputT": { "dtype": "float16", "shape": [1, 64, 512], "tolerance": 0.
|
| 532 |
},
|
| 533 |
{
|
| 534 |
"name": "flash_decode_head_dim64_no_bias_f16",
|
|
@@ -547,10 +546,10 @@
|
|
| 547 |
"valueT": {
|
| 548 |
"dtype": "float16",
|
| 549 |
"shape": [1, 512, 256],
|
| 550 |
-
"data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.
|
| 551 |
}
|
| 552 |
},
|
| 553 |
-
"outputs": { "outputT": { "dtype": "float16", "shape": [1, 1, 256], "tolerance": 0.
|
| 554 |
},
|
| 555 |
{
|
| 556 |
"name": "cross_attention_projection_bias_f16",
|
|
@@ -583,7 +582,7 @@
|
|
| 583 |
"name": "causal_cross_attention_q2_kv4_exact",
|
| 584 |
"provenance": {
|
| 585 |
"source": "onnxruntime/contrib_ops/cpu/bert/attention_helper.h",
|
| 586 |
-
"notes": "
|
| 587 |
},
|
| 588 |
"attrs": { "num_heads": 1, "unidirectional": 1 },
|
| 589 |
"inputs": {
|
|
@@ -662,7 +661,7 @@
|
|
| 662 |
{
|
| 663 |
"name": "dispatch_cliff_qseq_65537",
|
| 664 |
"provenance": {
|
| 665 |
-
"notes": "
|
| 666 |
},
|
| 667 |
"attrs": { "num_heads": 1 },
|
| 668 |
"inputs": {
|
|
@@ -758,7 +757,7 @@
|
|
| 758 |
{
|
| 759 |
"name": "cluster_prefill_qseq64_h16_d64_compact",
|
| 760 |
"provenance": {
|
| 761 |
-
"notes": "
|
| 762 |
},
|
| 763 |
"attrs": { "num_heads": 16, "scale": 0.125 },
|
| 764 |
"inputs": {
|
|
@@ -785,7 +784,7 @@
|
|
| 785 |
{
|
| 786 |
"name": "flux2_joint_d128_q32_h8_compact",
|
| 787 |
"provenance": {
|
| 788 |
-
"notes": "
|
| 789 |
},
|
| 790 |
"attrs": { "num_heads": 8, "scale": 0.08838834764831845 },
|
| 791 |
"inputs": {
|
|
@@ -812,7 +811,7 @@
|
|
| 812 |
{
|
| 813 |
"name": "flux2_joint_d128_q32_h8_bias_compact",
|
| 814 |
"provenance": {
|
| 815 |
-
"notes": "
|
| 816 |
},
|
| 817 |
"attrs": { "num_heads": 8, "scale": 0.08838834764831845 },
|
| 818 |
"inputs": {
|
|
@@ -869,7 +868,7 @@
|
|
| 869 |
{
|
| 870 |
"name": "q31_kv511_h8_d64_double_threshold_compact",
|
| 871 |
"provenance": {
|
| 872 |
-
"notes": "
|
| 873 |
},
|
| 874 |
"attrs": { "num_heads": 8, "scale": 0.125 },
|
| 875 |
"inputs": {
|
|
@@ -886,7 +885,7 @@
|
|
| 886 |
"valueT": {
|
| 887 |
"dtype": "float32",
|
| 888 |
"shape": [1, 511, 512],
|
| 889 |
-
"data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.
|
| 890 |
}
|
| 891 |
},
|
| 892 |
"outputs": {
|
|
@@ -913,7 +912,7 @@
|
|
| 913 |
"valueT": {
|
| 914 |
"dtype": "float32",
|
| 915 |
"shape": [1, 2048, 128],
|
| 916 |
-
"data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.
|
| 917 |
}
|
| 918 |
},
|
| 919 |
"outputs": {
|
|
@@ -938,7 +937,7 @@
|
|
| 938 |
"valueT": {
|
| 939 |
"dtype": "float32",
|
| 940 |
"shape": [1, 512, 128],
|
| 941 |
-
"data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.
|
| 942 |
}
|
| 943 |
},
|
| 944 |
"outputs": {
|
|
@@ -948,7 +947,7 @@
|
|
| 948 |
{
|
| 949 |
"name": "small_head_parallel_q3_kv65_h2_d6_compact",
|
| 950 |
"provenance": {
|
| 951 |
-
"notes": "
|
| 952 |
},
|
| 953 |
"attrs": { "num_heads": 2, "scale": 0.408248290463863 },
|
| 954 |
"inputs": {
|
|
@@ -975,7 +974,7 @@
|
|
| 975 |
{
|
| 976 |
"name": "small_head_parallel_f16_q3_kv65_h2_d6",
|
| 977 |
"provenance": {
|
| 978 |
-
"notes": "Float16
|
| 979 |
},
|
| 980 |
"attrs": { "num_heads": 2, "scale": 0.408248290463863 },
|
| 981 |
"inputs": {
|
|
@@ -1000,7 +999,7 @@
|
|
| 1000 |
{
|
| 1001 |
"name": "short_query_splitk_single_partition_q16_kv2048_h8_d32",
|
| 1002 |
"provenance": {
|
| 1003 |
-
"notes": "
|
| 1004 |
},
|
| 1005 |
"attrs": { "num_heads": 8, "scale": 0.17677669529663687 },
|
| 1006 |
"inputs": {
|
|
@@ -1017,7 +1016,7 @@
|
|
| 1017 |
"valueT": {
|
| 1018 |
"dtype": "float32",
|
| 1019 |
"shape": [1, 2048, 256],
|
| 1020 |
-
"data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.
|
| 1021 |
}
|
| 1022 |
},
|
| 1023 |
"outputs": {
|
|
@@ -1044,7 +1043,7 @@
|
|
| 1044 |
"valueT": {
|
| 1045 |
"dtype": "float32",
|
| 1046 |
"shape": [1, 512, 128],
|
| 1047 |
-
"data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.
|
| 1048 |
}
|
| 1049 |
},
|
| 1050 |
"outputs": {
|
|
@@ -1079,7 +1078,7 @@
|
|
| 1079 |
{
|
| 1080 |
"name": "materialized_sgmat_fused_f16_q1024_h4_d80_no_bias",
|
| 1081 |
"provenance": {
|
| 1082 |
-
"notes": "
|
| 1083 |
},
|
| 1084 |
"attrs": { "num_heads": 4, "scale": 0.125 },
|
| 1085 |
"inputs": {
|
|
@@ -1104,7 +1103,7 @@
|
|
| 1104 |
{
|
| 1105 |
"name": "materialized_sgmat_q512_kv512_h1_d128_bias",
|
| 1106 |
"provenance": {
|
| 1107 |
-
"notes": "
|
| 1108 |
},
|
| 1109 |
"attrs": { "num_heads": 1, "scale": 0.08838834764831845 },
|
| 1110 |
"inputs": {
|
|
@@ -1136,7 +1135,7 @@
|
|
| 1136 |
{
|
| 1137 |
"name": "portable_head_dim320_no_bias",
|
| 1138 |
"provenance": {
|
| 1139 |
-
"notes": "
|
| 1140 |
},
|
| 1141 |
"attrs": { "num_heads": 1, "scale": 0.05590169943749474 },
|
| 1142 |
"inputs": {
|
|
@@ -1163,7 +1162,7 @@
|
|
| 1163 |
{
|
| 1164 |
"name": "splitk_decode_q1_kv1500_h6_d64_underfilled_wide_subgroup",
|
| 1165 |
"provenance": {
|
| 1166 |
-
"notes": "A six-head f32 decode geometry where
|
| 1167 |
},
|
| 1168 |
"attrs": { "num_heads": 6, "scale": 0.125 },
|
| 1169 |
"inputs": {
|
|
@@ -1180,7 +1179,7 @@
|
|
| 1180 |
"valueT": {
|
| 1181 |
"dtype": "float32",
|
| 1182 |
"shape": [1, 1500, 384],
|
| 1183 |
-
"data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.
|
| 1184 |
}
|
| 1185 |
},
|
| 1186 |
"outputs": {
|
|
@@ -1207,7 +1206,7 @@
|
|
| 1207 |
"valueT": {
|
| 1208 |
"dtype": "float32",
|
| 1209 |
"shape": [1, 1500, 128],
|
| 1210 |
-
"data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.
|
| 1211 |
}
|
| 1212 |
},
|
| 1213 |
"outputs": {
|
|
@@ -1217,7 +1216,7 @@
|
|
| 1217 |
{
|
| 1218 |
"name": "materialized_prefill_q512_h1_d64_bias",
|
| 1219 |
"provenance": {
|
| 1220 |
-
"notes": "
|
| 1221 |
},
|
| 1222 |
"attrs": { "num_heads": 1, "scale": 0.125 },
|
| 1223 |
"inputs": {
|
|
@@ -1281,7 +1280,7 @@
|
|
| 1281 |
{
|
| 1282 |
"name": "materialized_sgmat_cached_q520_kv516_h2_d80_all_tails",
|
| 1283 |
"provenance": {
|
| 1284 |
-
"notes": "
|
| 1285 |
},
|
| 1286 |
"attrs": { "num_heads": 2, "scale": 0.11180339887498948 },
|
| 1287 |
"inputs": {
|
|
@@ -1298,7 +1297,7 @@
|
|
| 1298 |
"valueT": {
|
| 1299 |
"dtype": "float32",
|
| 1300 |
"shape": [1, 516, 160],
|
| 1301 |
-
"data": { "kind": "fillFloat32", "scale": 0.06, "sinStep": 0.
|
| 1302 |
}
|
| 1303 |
},
|
| 1304 |
"outputs": {
|
|
@@ -1308,7 +1307,7 @@
|
|
| 1308 |
{
|
| 1309 |
"name": "materialized_sgmat_q520_kv516_h2_d80_all_tails_bias",
|
| 1310 |
"provenance": {
|
| 1311 |
-
"notes": "
|
| 1312 |
},
|
| 1313 |
"attrs": { "num_heads": 2, "scale": 0.11180339887498948 },
|
| 1314 |
"inputs": {
|
|
@@ -1357,7 +1356,7 @@
|
|
| 1357 |
"valueT": {
|
| 1358 |
"dtype": "float32",
|
| 1359 |
"shape": [1, 515, 80],
|
| 1360 |
-
"data": { "kind": "fillFloat32", "scale": 0.06, "sinStep": 0.
|
| 1361 |
}
|
| 1362 |
},
|
| 1363 |
"outputs": { "outputT": { "dtype": "float32", "shape": [1, 513, 80], "tolerance": 0.001, "relTolerance": 0.001 } }
|
|
@@ -1365,7 +1364,7 @@
|
|
| 1365 |
{
|
| 1366 |
"name": "materialized_sgmat_cached_batch2_q512_kv512_h1_d32",
|
| 1367 |
"provenance": {
|
| 1368 |
-
"notes": "
|
| 1369 |
},
|
| 1370 |
"attrs": { "num_heads": 1, "scale": 0.17677669529663687 },
|
| 1371 |
"inputs": {
|
|
@@ -1382,7 +1381,7 @@
|
|
| 1382 |
"valueT": {
|
| 1383 |
"dtype": "float32",
|
| 1384 |
"shape": [2, 512, 32],
|
| 1385 |
-
"data": { "kind": "fillFloat32", "scale": 0.06, "sinStep": 0.
|
| 1386 |
}
|
| 1387 |
},
|
| 1388 |
"outputs": { "outputT": { "dtype": "float32", "shape": [2, 512, 32], "tolerance": 0.001, "relTolerance": 0.001 } }
|
|
@@ -1645,6 +1644,76 @@
|
|
| 1645 |
},
|
| 1646 |
"outputs": { "outputT": { "dtype": "float32", "shape": [1, 4, 12], "tolerance": 0.00002 } }
|
| 1647 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1648 |
{
|
| 1649 |
"name": "attn_bias_flash_f16_b1_h8_s32",
|
| 1650 |
"attrs": { "num_heads": 8 },
|
|
@@ -1662,7 +1731,7 @@
|
|
| 1662 |
"valueT": {
|
| 1663 |
"dtype": "float16",
|
| 1664 |
"shape": [1, 32, 256],
|
| 1665 |
-
"data": { "kind": "fillFloat32", "sinStep": 0.
|
| 1666 |
},
|
| 1667 |
"attentionBiasT": {
|
| 1668 |
"dtype": "float16",
|
|
@@ -1675,7 +1744,7 @@
|
|
| 1675 |
{
|
| 1676 |
"name": "flash_q30_below_cluster_h9_d64_no_bias",
|
| 1677 |
"provenance": {
|
| 1678 |
-
"notes": "
|
| 1679 |
},
|
| 1680 |
"attrs": { "num_heads": 9, "scale": 0.125 },
|
| 1681 |
"inputs": {
|
|
@@ -1702,7 +1771,7 @@
|
|
| 1702 |
{
|
| 1703 |
"name": "flash_q30_below_cluster_h9_d64_bias",
|
| 1704 |
"provenance": {
|
| 1705 |
-
"notes": "Fused
|
| 1706 |
},
|
| 1707 |
"attrs": { "num_heads": 9, "scale": 0.125 },
|
| 1708 |
"inputs": {
|
|
@@ -1734,7 +1803,7 @@
|
|
| 1734 |
{
|
| 1735 |
"name": "flash_q30_below_cluster_h9_d64_attn_bias",
|
| 1736 |
"provenance": {
|
| 1737 |
-
"notes": "Additive attention
|
| 1738 |
},
|
| 1739 |
"attrs": { "num_heads": 9, "scale": 0.125 },
|
| 1740 |
"inputs": {
|
|
@@ -1891,7 +1960,7 @@
|
|
| 1891 |
{
|
| 1892 |
"name": "materialized_sgmat_q512_kv512_h2_d64_offset_value_scale_lock",
|
| 1893 |
"provenance": {
|
| 1894 |
-
"notes": "
|
| 1895 |
},
|
| 1896 |
"attrs": { "num_heads": 2, "scale": 0.125 },
|
| 1897 |
"inputs": {
|
|
@@ -1918,7 +1987,7 @@
|
|
| 1918 |
{
|
| 1919 |
"name": "materialized_sgmat_fused_q1024_kv1024_h4_d64_offset_value_scale_lock",
|
| 1920 |
"provenance": {
|
| 1921 |
-
"notes": "
|
| 1922 |
},
|
| 1923 |
"attrs": { "num_heads": 4, "scale": 0.125 },
|
| 1924 |
"inputs": {
|
|
@@ -1977,7 +2046,7 @@
|
|
| 1977 |
{
|
| 1978 |
"name": "splitk_decode_h4_d64_kv2048_no_bias_ramp_value_scale_lock",
|
| 1979 |
"provenance": {
|
| 1980 |
-
"notes": "
|
| 1981 |
},
|
| 1982 |
"attrs": { "num_heads": 4, "scale": 0.125 },
|
| 1983 |
"inputs": {
|
|
@@ -2002,7 +2071,7 @@
|
|
| 2002 |
{
|
| 2003 |
"name": "splitk_decode_h4_d64_kv2048_no_bias_f16_ramp_value_scale_lock",
|
| 2004 |
"provenance": {
|
| 2005 |
-
"notes": "
|
| 2006 |
},
|
| 2007 |
"attrs": { "num_heads": 4, "scale": 0.125 },
|
| 2008 |
"inputs": {
|
|
@@ -2027,7 +2096,7 @@
|
|
| 2027 |
{
|
| 2028 |
"name": "flash_q32_h4_d128_no_bias_f16_ramp_value_scale_lock",
|
| 2029 |
"provenance": {
|
| 2030 |
-
"notes": "
|
| 2031 |
},
|
| 2032 |
"attrs": { "num_heads": 4, "scale": 0.08838834764831845 },
|
| 2033 |
"inputs": {
|
|
@@ -2052,7 +2121,7 @@
|
|
| 2052 |
{
|
| 2053 |
"name": "short_query_splitk_single_partition_q16_kv2048_h8_d32_ramp_value_scale_lock",
|
| 2054 |
"provenance": {
|
| 2055 |
-
"notes": "The adaptive short-query path uses one partition
|
| 2056 |
},
|
| 2057 |
"attrs": { "num_heads": 8, "scale": 0.17677669529663687 },
|
| 2058 |
"inputs": {
|
|
@@ -2073,6 +2142,179 @@
|
|
| 2073 |
}
|
| 2074 |
},
|
| 2075 |
"outputs": { "outputT": { "dtype": "float32", "shape": [1, 16, 256], "tolerance": 0.00005 } }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2076 |
}
|
| 2077 |
]
|
| 2078 |
}
|
|
|
|
| 1 |
{
|
|
|
|
| 2 |
"cases": [
|
| 3 |
{
|
| 4 |
"name": "cross_attention_no_bias",
|
|
|
|
| 185 |
"provenance": {
|
| 186 |
"source": "onnxruntime/test/contrib_ops/multihead_attention_op_test.cc",
|
| 187 |
"test": "MultiHeadAttentionTest.CrossAttention_Batch1_HeadSize16",
|
| 188 |
+
"notes": "ORT cross-attention with batch 1, two query rows, three key/value rows, two heads of width 16, and no bias."
|
| 189 |
},
|
| 190 |
"attrs": { "num_heads": 2 },
|
| 191 |
"inputs": {
|
|
|
|
| 278 |
"valueT": {
|
| 279 |
"dtype": "float32",
|
| 280 |
"shape": [1, 64, 256],
|
| 281 |
+
"data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.02454369261, "cosStep": 0.0007669903939 }
|
| 282 |
}
|
| 283 |
},
|
| 284 |
"outputs": { "outputT": { "dtype": "float32", "shape": [1, 64, 256], "tolerance": 0.0002 } }
|
|
|
|
| 349 |
"valueT": {
|
| 350 |
"dtype": "float32",
|
| 351 |
"shape": [1, 512, 256],
|
| 352 |
+
"data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.02454369261, "cosStep": 0.00009587379924 }
|
| 353 |
}
|
| 354 |
},
|
| 355 |
"outputs": { "outputT": { "dtype": "float32", "shape": [1, 1, 256], "tolerance": 0.0002 } }
|
|
|
|
| 398 |
"valueT": {
|
| 399 |
"dtype": "float32",
|
| 400 |
"shape": [1, 2048, 256],
|
| 401 |
+
"data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.02454369261, "cosStep": 0.00002396844981 }
|
| 402 |
}
|
| 403 |
},
|
| 404 |
"outputs": { "outputT": { "dtype": "float32", "shape": [1, 1, 256], "tolerance": 0.0003 } }
|
|
|
|
| 406 |
{
|
| 407 |
"name": "splitk_decode_head_dim128_no_bias_kv2048",
|
| 408 |
"provenance": {
|
| 409 |
+
"notes": "A compact decode with eight heads, head size 128, and a long KV sequence exercises split-K attention."
|
| 410 |
},
|
| 411 |
"attrs": { "num_heads": 2, "scale": 0.08838834764831845 },
|
| 412 |
"inputs": {
|
|
|
|
| 423 |
"valueT": {
|
| 424 |
"dtype": "float32",
|
| 425 |
"shape": [1, 2048, 256],
|
| 426 |
+
"data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.02454369261, "cosStep": 0.00002396844981 }
|
| 427 |
}
|
| 428 |
},
|
| 429 |
"outputs": { "outputT": { "dtype": "float32", "shape": [1, 1, 256], "tolerance": 0.0003 } }
|
|
|
|
| 445 |
"valueT": {
|
| 446 |
"dtype": "float32",
|
| 447 |
"shape": [2, 768, 512],
|
| 448 |
+
"data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.0122718463, "cosStep": 0.00001597896654 }
|
| 449 |
}
|
| 450 |
},
|
| 451 |
"outputs": { "outputT": { "dtype": "float32", "shape": [2, 1, 512], "tolerance": 0.0003 } }
|
|
|
|
| 507 |
{
|
| 508 |
"name": "flash_self_attention_head_dim128_no_bias_f16",
|
| 509 |
"provenance": {
|
| 510 |
+
"notes": "A float16 prefill with head size 128 exercises the q32 broadcast route with subgroups and the shared-memory q32 route without them."
|
| 511 |
},
|
| 512 |
"attrs": { "num_heads": 4, "scale": 0.08838834764831845 },
|
| 513 |
"inputs": {
|
|
|
|
| 524 |
"valueT": {
|
| 525 |
"dtype": "float16",
|
| 526 |
"shape": [1, 64, 512],
|
| 527 |
+
"data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.0122718463, "cosStep": 0.000383495197 }
|
| 528 |
}
|
| 529 |
},
|
| 530 |
+
"outputs": { "outputT": { "dtype": "float16", "shape": [1, 64, 512], "tolerance": 0.002 } }
|
| 531 |
},
|
| 532 |
{
|
| 533 |
"name": "flash_decode_head_dim64_no_bias_f16",
|
|
|
|
| 546 |
"valueT": {
|
| 547 |
"dtype": "float16",
|
| 548 |
"shape": [1, 512, 256],
|
| 549 |
+
"data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.02454369261, "cosStep": 0.00009587379924 }
|
| 550 |
}
|
| 551 |
},
|
| 552 |
+
"outputs": { "outputT": { "dtype": "float16", "shape": [1, 1, 256], "tolerance": 0.002 } }
|
| 553 |
},
|
| 554 |
{
|
| 555 |
"name": "cross_attention_projection_bias_f16",
|
|
|
|
| 582 |
"name": "causal_cross_attention_q2_kv4_exact",
|
| 583 |
"provenance": {
|
| 584 |
"source": "onnxruntime/contrib_ops/cpu/bert/attention_helper.h",
|
| 585 |
+
"notes": "Upper-left causal masking with two queries and four keys. The second query averages only the first two values, producing exactly 1.5."
|
| 586 |
},
|
| 587 |
"attrs": { "num_heads": 1, "unidirectional": 1 },
|
| 588 |
"inputs": {
|
|
|
|
| 661 |
{
|
| 662 |
"name": "dispatch_cliff_qseq_65537",
|
| 663 |
"provenance": {
|
| 664 |
+
"notes": "A 65,537-row query exceeds the flash-path cap and exercises the scalar fallback's folded dispatch. The x dimension is capped at 65,535, so a grid-stride iteration handles the final two distinct rows; two keys keep expected-value evaluation compact."
|
| 665 |
},
|
| 666 |
"attrs": { "num_heads": 1 },
|
| 667 |
"inputs": {
|
|
|
|
| 757 |
{
|
| 758 |
"name": "cluster_prefill_qseq64_h16_d64_compact",
|
| 759 |
"provenance": {
|
| 760 |
+
"notes": "A compact sequence-64, 16-head prefill exercises the clustered route and its no-subgroups fallback."
|
| 761 |
},
|
| 762 |
"attrs": { "num_heads": 16, "scale": 0.125 },
|
| 763 |
"inputs": {
|
|
|
|
| 784 |
{
|
| 785 |
"name": "flux2_joint_d128_q32_h8_compact",
|
| 786 |
"provenance": {
|
| 787 |
+
"notes": "Head size 128 and `qSeq * heads >= 256` keep the flash-cluster route and its no-subgroups fallback eligible at a compact scale."
|
| 788 |
},
|
| 789 |
"attrs": { "num_heads": 8, "scale": 0.08838834764831845 },
|
| 790 |
"inputs": {
|
|
|
|
| 811 |
{
|
| 812 |
"name": "flux2_joint_d128_q32_h8_bias_compact",
|
| 813 |
"provenance": {
|
| 814 |
+
"notes": "A compact fused-bias case exercises the boundary of 32 four-wide head segments across 32 lanes."
|
| 815 |
},
|
| 816 |
"attrs": { "num_heads": 8, "scale": 0.08838834764831845 },
|
| 817 |
"inputs": {
|
|
|
|
| 868 |
{
|
| 869 |
"name": "q31_kv511_h8_d64_double_threshold_compact",
|
| 870 |
"provenance": {
|
| 871 |
+
"notes": "`qSeq * heads = 248` and `kvSeq = 511` lie immediately below the two flash admission boundaries, selecting the scalar fallback."
|
| 872 |
},
|
| 873 |
"attrs": { "num_heads": 8, "scale": 0.125 },
|
| 874 |
"inputs": {
|
|
|
|
| 885 |
"valueT": {
|
| 886 |
"dtype": "float32",
|
| 887 |
"shape": [1, 511, 512],
|
| 888 |
+
"data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.0122718463, "cosStep": 0.0000480307096 }
|
| 889 |
}
|
| 890 |
},
|
| 891 |
"outputs": {
|
|
|
|
| 912 |
"valueT": {
|
| 913 |
"dtype": "float32",
|
| 914 |
"shape": [1, 2048, 128],
|
| 915 |
+
"data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.04908738521, "cosStep": 0.00004793689962 }
|
| 916 |
}
|
| 917 |
},
|
| 918 |
"outputs": {
|
|
|
|
| 937 |
"valueT": {
|
| 938 |
"dtype": "float32",
|
| 939 |
"shape": [1, 512, 128],
|
| 940 |
+
"data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.04908738521, "cosStep": 0.0001917475985 }
|
| 941 |
}
|
| 942 |
},
|
| 943 |
"outputs": {
|
|
|
|
| 947 |
{
|
| 948 |
"name": "small_head_parallel_q3_kv65_h2_d6_compact",
|
| 949 |
"provenance": {
|
| 950 |
+
"notes": "Head size 6 exercises the key-parallel fallback with multiple keys per lane and a compact expected output."
|
| 951 |
},
|
| 952 |
"attrs": { "num_heads": 2, "scale": 0.408248290463863 },
|
| 953 |
"inputs": {
|
|
|
|
| 974 |
{
|
| 975 |
"name": "small_head_parallel_f16_q3_kv65_h2_d6",
|
| 976 |
"provenance": {
|
| 977 |
+
"notes": "Float16 head size 6 exercises the key-parallel fallback. Values remain near one while queries and keys stay small, keeping scale errors observable."
|
| 978 |
},
|
| 979 |
"attrs": { "num_heads": 2, "scale": 0.408248290463863 },
|
| 980 |
"inputs": {
|
|
|
|
| 999 |
{
|
| 1000 |
"name": "short_query_splitk_single_partition_q16_kv2048_h8_d32",
|
| 1001 |
"provenance": {
|
| 1002 |
+
"notes": "Sixteen queries over eight heads supply 128 workgroups, so the adaptive short-query flash path uses one K partition and normalizes in-kernel."
|
| 1003 |
},
|
| 1004 |
"attrs": { "num_heads": 8, "scale": 0.17677669529663687 },
|
| 1005 |
"inputs": {
|
|
|
|
| 1016 |
"valueT": {
|
| 1017 |
"dtype": "float32",
|
| 1018 |
"shape": [1, 2048, 256],
|
| 1019 |
+
"data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.02454369261, "cosStep": 0.00002396844981 }
|
| 1020 |
}
|
| 1021 |
},
|
| 1022 |
"outputs": {
|
|
|
|
| 1043 |
"valueT": {
|
| 1044 |
"dtype": "float32",
|
| 1045 |
"shape": [1, 512, 128],
|
| 1046 |
+
"data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.04908738521, "cosStep": 0.0001917475985 }
|
| 1047 |
}
|
| 1048 |
},
|
| 1049 |
"outputs": {
|
|
|
|
| 1078 |
{
|
| 1079 |
"name": "materialized_sgmat_fused_f16_q1024_h4_d80_no_bias",
|
| 1080 |
"provenance": {
|
| 1081 |
+
"notes": "Float16 head size 80 is not divisible by 32, selecting guarded tile staging on the fused materialized route at the 16 MiB score threshold."
|
| 1082 |
},
|
| 1083 |
"attrs": { "num_heads": 4, "scale": 0.125 },
|
| 1084 |
"inputs": {
|
|
|
|
| 1103 |
{
|
| 1104 |
"name": "materialized_sgmat_q512_kv512_h1_d128_bias",
|
| 1105 |
"provenance": {
|
| 1106 |
+
"notes": "Head size 128 selects the subgroup-matrix prefill's unguarded Q tile load. Q bias is fused into that load and V bias follows normalization; K bias adds one constant to every row logit and must cancel under softmax."
|
| 1107 |
},
|
| 1108 |
"attrs": { "num_heads": 1, "scale": 0.08838834764831845 },
|
| 1109 |
"inputs": {
|
|
|
|
| 1135 |
{
|
| 1136 |
"name": "portable_head_dim320_no_bias",
|
| 1137 |
"provenance": {
|
| 1138 |
+
"notes": "Head size 320 exercises the portable route used by wide vision and multimodal attention blocks."
|
| 1139 |
},
|
| 1140 |
"attrs": { "num_heads": 1, "scale": 0.05590169943749474 },
|
| 1141 |
"inputs": {
|
|
|
|
| 1162 |
{
|
| 1163 |
"name": "splitk_decode_q1_kv1500_h6_d64_underfilled_wide_subgroup",
|
| 1164 |
"provenance": {
|
| 1165 |
+
"notes": "A six-head f32 decode geometry where head size 64 supplies only 16 vec4 dot products to a 32-lane subgroup. The case exercises the portable split-K route instead of an underfilled cooperative QK subgroup."
|
| 1166 |
},
|
| 1167 |
"attrs": { "num_heads": 6, "scale": 0.125 },
|
| 1168 |
"inputs": {
|
|
|
|
| 1179 |
"valueT": {
|
| 1180 |
"dtype": "float32",
|
| 1181 |
"shape": [1, 1500, 384],
|
| 1182 |
+
"data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.01636246174, "cosStep": 0.00002181661565 }
|
| 1183 |
}
|
| 1184 |
},
|
| 1185 |
"outputs": {
|
|
|
|
| 1206 |
"valueT": {
|
| 1207 |
"dtype": "float32",
|
| 1208 |
"shape": [1, 1500, 128],
|
| 1209 |
+
"data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.04908738521, "cosStep": 0.00006544984695 }
|
| 1210 |
}
|
| 1211 |
},
|
| 1212 |
"outputs": {
|
|
|
|
| 1216 |
{
|
| 1217 |
"name": "materialized_prefill_q512_h1_d64_bias",
|
| 1218 |
"provenance": {
|
| 1219 |
+
"notes": "An even key/value length exercises register-cached vec4 softmax on the portable materialized route with fused Q bias and post-normalization V bias."
|
| 1220 |
},
|
| 1221 |
"attrs": { "num_heads": 1, "scale": 0.125 },
|
| 1222 |
"inputs": {
|
|
|
|
| 1280 |
{
|
| 1281 |
"name": "materialized_sgmat_cached_q520_kv516_h2_d80_all_tails",
|
| 1282 |
"provenance": {
|
| 1283 |
+
"notes": "Head size 80 exercises the BK32 tail, query length 520 and key/value length 516 exercise both matrix tile tails, and the even key count keeps register-cached vec4 softmax eligible."
|
| 1284 |
},
|
| 1285 |
"attrs": { "num_heads": 2, "scale": 0.11180339887498948 },
|
| 1286 |
"inputs": {
|
|
|
|
| 1297 |
"valueT": {
|
| 1298 |
"dtype": "float32",
|
| 1299 |
"shape": [1, 516, 160],
|
| 1300 |
+
"data": { "kind": "fillFloat32", "scale": 0.06, "sinStep": 0.03926990817, "cosStep": 0.0001522089464 }
|
| 1301 |
}
|
| 1302 |
},
|
| 1303 |
"outputs": {
|
|
|
|
| 1307 |
{
|
| 1308 |
"name": "materialized_sgmat_q520_kv516_h2_d80_all_tails_bias",
|
| 1309 |
"provenance": {
|
| 1310 |
+
"notes": "Head size 80 selects the bounds-checked Q tile load with fused bias. Query length 520 and key/value length 516 keep both matrix tails live, exposing any bias applied to padded rows."
|
| 1311 |
},
|
| 1312 |
"attrs": { "num_heads": 2, "scale": 0.11180339887498948 },
|
| 1313 |
"inputs": {
|
|
|
|
| 1356 |
"valueT": {
|
| 1357 |
"dtype": "float32",
|
| 1358 |
"shape": [1, 515, 80],
|
| 1359 |
+
"data": { "kind": "fillFloat32", "scale": 0.06, "sinStep": 0.07853981634, "cosStep": 0.0003050089955 }
|
| 1360 |
}
|
| 1361 |
},
|
| 1362 |
"outputs": { "outputT": { "dtype": "float32", "shape": [1, 513, 80], "tolerance": 0.001, "relTolerance": 0.001 } }
|
|
|
|
| 1364 |
{
|
| 1365 |
"name": "materialized_sgmat_cached_batch2_q512_kv512_h1_d32",
|
| 1366 |
"provenance": {
|
| 1367 |
+
"notes": "Two batches and head size 32 exercise the narrow-head subgroup-matrix route and the workgroup-z mapping to batch and head."
|
| 1368 |
},
|
| 1369 |
"attrs": { "num_heads": 1, "scale": 0.17677669529663687 },
|
| 1370 |
"inputs": {
|
|
|
|
| 1381 |
"valueT": {
|
| 1382 |
"dtype": "float32",
|
| 1383 |
"shape": [2, 512, 32],
|
| 1384 |
+
"data": { "kind": "fillFloat32", "scale": 0.06, "sinStep": 0.1963495408, "cosStep": 0.000383495197 }
|
| 1385 |
}
|
| 1386 |
},
|
| 1387 |
"outputs": { "outputT": { "dtype": "float32", "shape": [2, 512, 32], "tolerance": 0.001, "relTolerance": 0.001 } }
|
|
|
|
| 1644 |
},
|
| 1645 |
"outputs": { "outputT": { "dtype": "float32", "shape": [1, 4, 12], "tolerance": 0.00002 } }
|
| 1646 |
},
|
| 1647 |
+
{
|
| 1648 |
+
"name": "attn_bias_scalar_all_neg_inf_row_zero_q2_kv32_h8_d64",
|
| 1649 |
+
"provenance": {
|
| 1650 |
+
"notes": "BSH float32 Q/K/V uses eight heads of width 64, two query tokens, and 32 keys. Rank-4 attention bias is broadcast over batch and heads; query row 1 is -Infinity at every key while row 0 is finite. The portable scalar online-attention route must return zero for every head in row 1 without forming NaN from a zero denominator; row 0 follows the ordinary softmax-weighted value calculation."
|
| 1651 |
+
},
|
| 1652 |
+
"attrs": { "num_heads": 8 },
|
| 1653 |
+
"inputs": {
|
| 1654 |
+
"queryT": {
|
| 1655 |
+
"dtype": "float32",
|
| 1656 |
+
"shape": [1, 2, 512],
|
| 1657 |
+
"data": { "kind": "fillFloat32", "scale": 0.25, "sinStep": 0.017, "cosStep": 0.031 }
|
| 1658 |
+
},
|
| 1659 |
+
"keyT": {
|
| 1660 |
+
"dtype": "float32",
|
| 1661 |
+
"shape": [1, 32, 512],
|
| 1662 |
+
"data": { "kind": "fillFloat32", "scale": 0.25, "sinStep": 0.011, "cosStep": 0.023 }
|
| 1663 |
+
},
|
| 1664 |
+
"valueT": {
|
| 1665 |
+
"dtype": "float32",
|
| 1666 |
+
"shape": [1, 32, 512],
|
| 1667 |
+
"data": { "kind": "fillFloat32", "scale": 0.25, "sinStep": 0.007, "cosStep": 0.041 }
|
| 1668 |
+
},
|
| 1669 |
+
"attentionBiasT": {
|
| 1670 |
+
"dtype": "float32",
|
| 1671 |
+
"shape": [1, 1, 2, 32],
|
| 1672 |
+
"data": {
|
| 1673 |
+
"kind": "values",
|
| 1674 |
+
"values": [0.0, 0.193, 0.296, 0.259, 0.1, -0.105, -0.261, -0.295, -0.189, 0.005, 0.197, 0.296, 0.256, 0.096, -0.11, -0.264, -0.294, -0.185, 0.01, 0.201, 0.297, 0.254, 0.091, -0.115, -0.266, -0.293, -0.181, 0.015, 0.205, 0.298, 0.251, 0.086, "-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"]
|
| 1675 |
+
}
|
| 1676 |
+
}
|
| 1677 |
+
},
|
| 1678 |
+
"outputs": {
|
| 1679 |
+
"outputT": { "dtype": "float32", "shape": [1, 2, 512], "tolerance": 0.00002, "relTolerance": 0.00002 }
|
| 1680 |
+
}
|
| 1681 |
+
},
|
| 1682 |
+
{
|
| 1683 |
+
"name": "attn_bias_scalar_single_finite_key_q1_kv32_h8_d64",
|
| 1684 |
+
"provenance": {
|
| 1685 |
+
"notes": "Rank-4 bias has one finite entry at key 3 and -Infinity elsewhere. The scalar softmax therefore places all mass on key 3, so every head returns value row 3. This distinguishes a valid one-key denominator from `attn_bias_scalar_all_neg_inf_row_zero_q2_kv32_h8_d64`."
|
| 1686 |
+
},
|
| 1687 |
+
"attrs": { "num_heads": 8 },
|
| 1688 |
+
"inputs": {
|
| 1689 |
+
"queryT": {
|
| 1690 |
+
"dtype": "float32",
|
| 1691 |
+
"shape": [1, 1, 512],
|
| 1692 |
+
"data": { "kind": "fillFloat32", "scale": 0.25, "sinStep": 0.017, "cosStep": 0.031 }
|
| 1693 |
+
},
|
| 1694 |
+
"keyT": {
|
| 1695 |
+
"dtype": "float32",
|
| 1696 |
+
"shape": [1, 32, 512],
|
| 1697 |
+
"data": { "kind": "fillFloat32", "scale": 0.25, "sinStep": 0.011, "cosStep": 0.023 }
|
| 1698 |
+
},
|
| 1699 |
+
"valueT": {
|
| 1700 |
+
"dtype": "float32",
|
| 1701 |
+
"shape": [1, 32, 512],
|
| 1702 |
+
"data": { "kind": "fillFloat32", "scale": 0.25, "sinStep": 0.007, "cosStep": 0.041 }
|
| 1703 |
+
},
|
| 1704 |
+
"attentionBiasT": {
|
| 1705 |
+
"dtype": "float32",
|
| 1706 |
+
"shape": [1, 1, 1, 32],
|
| 1707 |
+
"data": {
|
| 1708 |
+
"kind": "values",
|
| 1709 |
+
"values": ["-Infinity", "-Infinity", "-Infinity", 0.0, "-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"]
|
| 1710 |
+
}
|
| 1711 |
+
}
|
| 1712 |
+
},
|
| 1713 |
+
"outputs": {
|
| 1714 |
+
"outputT": { "dtype": "float32", "shape": [1, 1, 512], "tolerance": 0.00001, "relTolerance": 0.00001 }
|
| 1715 |
+
}
|
| 1716 |
+
},
|
| 1717 |
{
|
| 1718 |
"name": "attn_bias_flash_f16_b1_h8_s32",
|
| 1719 |
"attrs": { "num_heads": 8 },
|
|
|
|
| 1731 |
"valueT": {
|
| 1732 |
"dtype": "float16",
|
| 1733 |
"shape": [1, 32, 256],
|
| 1734 |
+
"data": { "kind": "fillFloat32", "sinStep": 0.02454369261, "cosStep": 0.001533980788 }
|
| 1735 |
},
|
| 1736 |
"attentionBiasT": {
|
| 1737 |
"dtype": "float16",
|
|
|
|
| 1744 |
{
|
| 1745 |
"name": "flash_q30_below_cluster_h9_d64_no_bias",
|
| 1746 |
"provenance": {
|
| 1747 |
+
"notes": "Query length 30 is below the cooperative-cluster threshold while 270 query-head rows clear the flash admission floor. The case exercises one-query-row-per-workgroup flash with and without subgroups."
|
| 1748 |
},
|
| 1749 |
"attrs": { "num_heads": 9, "scale": 0.125 },
|
| 1750 |
"inputs": {
|
|
|
|
| 1771 |
{
|
| 1772 |
"name": "flash_q30_below_cluster_h9_d64_bias",
|
| 1773 |
"provenance": {
|
| 1774 |
+
"notes": "Fused QKV bias with query length 30 exercises the biased one-query-row flash route below the clustered and split-K thresholds."
|
| 1775 |
},
|
| 1776 |
"attrs": { "num_heads": 9, "scale": 0.125 },
|
| 1777 |
"inputs": {
|
|
|
|
| 1803 |
{
|
| 1804 |
"name": "flash_q30_below_cluster_h9_d64_attn_bias",
|
| 1805 |
"provenance": {
|
| 1806 |
+
"notes": "Additive attention bias with query length 30 exercises the per-query-row flash route below the tiled-prefill threshold on tiers without subgroups."
|
| 1807 |
},
|
| 1808 |
"attrs": { "num_heads": 9, "scale": 0.125 },
|
| 1809 |
"inputs": {
|
|
|
|
| 1960 |
{
|
| 1961 |
"name": "materialized_sgmat_q512_kv512_h2_d64_offset_value_scale_lock",
|
| 1962 |
"provenance": {
|
| 1963 |
+
"notes": "Values oscillate around 1.0, keeping the attention average at order-one magnitude. This makes both the denominator published by row statistics and the division in the subgroup-matrix apply pass observable."
|
| 1964 |
},
|
| 1965 |
"attrs": { "num_heads": 2, "scale": 0.125 },
|
| 1966 |
"inputs": {
|
|
|
|
| 1987 |
{
|
| 1988 |
"name": "materialized_sgmat_fused_q1024_kv1024_h4_d64_offset_value_scale_lock",
|
| 1989 |
"provenance": {
|
| 1990 |
+
"notes": "The 16.8 MiB score tensor selects the fused row-statistics route. Values oscillate around 1.0 so a per-tile, combined-denominator, or apply-scale error remains observable."
|
| 1991 |
},
|
| 1992 |
"attrs": { "num_heads": 4, "scale": 0.125 },
|
| 1993 |
"inputs": {
|
|
|
|
| 2046 |
{
|
| 2047 |
"name": "splitk_decode_h4_d64_kv2048_no_bias_ramp_value_scale_lock",
|
| 2048 |
"provenance": {
|
| 2049 |
+
"notes": "A monotone value ramp makes each output an order-one function of the weighted key index. This exposes both uniform scale errors and incorrect exponential offsets in the cross-partition split-K combine."
|
| 2050 |
},
|
| 2051 |
"attrs": { "num_heads": 4, "scale": 0.125 },
|
| 2052 |
"inputs": {
|
|
|
|
| 2071 |
{
|
| 2072 |
"name": "splitk_decode_h4_d64_kv2048_no_bias_f16_ramp_value_scale_lock",
|
| 2073 |
"provenance": {
|
| 2074 |
+
"notes": "A monotone value ramp keeps float16 decode outputs at order-one magnitude, making split-K accumulation, cross-partition rescaling, and final normalization observable."
|
| 2075 |
},
|
| 2076 |
"attrs": { "num_heads": 4, "scale": 0.125 },
|
| 2077 |
"inputs": {
|
|
|
|
| 2096 |
{
|
| 2097 |
"name": "flash_q32_h4_d128_no_bias_f16_ramp_value_scale_lock",
|
| 2098 |
"provenance": {
|
| 2099 |
+
"notes": "A monotone value ramp gives each float16 q32 row a distinct order-one output, exposing the register-blocked epilogue division and an incorrect broadcast lane."
|
| 2100 |
},
|
| 2101 |
"attrs": { "num_heads": 4, "scale": 0.08838834764831845 },
|
| 2102 |
"inputs": {
|
|
|
|
| 2121 |
{
|
| 2122 |
"name": "short_query_splitk_single_partition_q16_kv2048_h8_d32_ramp_value_scale_lock",
|
| 2123 |
"provenance": {
|
| 2124 |
+
"notes": "The adaptive short-query path uses one partition because query-head parallelism already supplies enough work. A value ramp keeps all 16 outputs at order-one magnitude, exposing in-kernel normalization errors."
|
| 2125 |
},
|
| 2126 |
"attrs": { "num_heads": 8, "scale": 0.17677669529663687 },
|
| 2127 |
"inputs": {
|
|
|
|
| 2142 |
}
|
| 2143 |
},
|
| 2144 |
"outputs": { "outputT": { "dtype": "float32", "shape": [1, 16, 256], "tolerance": 0.00005 } }
|
| 2145 |
+
},
|
| 2146 |
+
{
|
| 2147 |
+
"name": "small_seq_blocked_bias_causal_1x64x64_h4d16",
|
| 2148 |
+
"provenance": {
|
| 2149 |
+
"notes": "Short-sequence attention with 64 queries, 64 keys, four heads of width 16, fused bias, and causal masking."
|
| 2150 |
+
},
|
| 2151 |
+
"attrs": { "num_heads": 4, "unidirectional": 1 },
|
| 2152 |
+
"inputs": {
|
| 2153 |
+
"queryT": {
|
| 2154 |
+
"dtype": "float32",
|
| 2155 |
+
"shape": [1, 64, 64],
|
| 2156 |
+
"data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31 }
|
| 2157 |
+
},
|
| 2158 |
+
"keyT": {
|
| 2159 |
+
"dtype": "float32",
|
| 2160 |
+
"shape": [1, 64, 64],
|
| 2161 |
+
"data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.23 }
|
| 2162 |
+
},
|
| 2163 |
+
"valueT": {
|
| 2164 |
+
"dtype": "float32",
|
| 2165 |
+
"shape": [1, 64, 64],
|
| 2166 |
+
"data": { "kind": "fillFloat32", "sinStep": 0.07, "cosStep": 0.41 }
|
| 2167 |
+
},
|
| 2168 |
+
"biasT": {
|
| 2169 |
+
"dtype": "float32",
|
| 2170 |
+
"shape": [192],
|
| 2171 |
+
"data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.13 }
|
| 2172 |
+
}
|
| 2173 |
+
},
|
| 2174 |
+
"outputs": { "outputT": { "dtype": "float32", "shape": [1, 64, 64], "tolerance": 0.00002 } }
|
| 2175 |
+
},
|
| 2176 |
+
{
|
| 2177 |
+
"name": "small_seq_blocked_no_bias_causal_q13_kv40_h2d16",
|
| 2178 |
+
"provenance": {
|
| 2179 |
+
"notes": "Thirteen queries leave five active rows in the final eight-row block, and the key count is not divisible by sixteen. Causal masking makes out-of-range lanes and inactive rows contribute the identity partial."
|
| 2180 |
+
},
|
| 2181 |
+
"attrs": { "num_heads": 2, "unidirectional": 1 },
|
| 2182 |
+
"inputs": {
|
| 2183 |
+
"queryT": {
|
| 2184 |
+
"dtype": "float32",
|
| 2185 |
+
"shape": [1, 13, 32],
|
| 2186 |
+
"data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31 }
|
| 2187 |
+
},
|
| 2188 |
+
"keyT": {
|
| 2189 |
+
"dtype": "float32",
|
| 2190 |
+
"shape": [1, 40, 32],
|
| 2191 |
+
"data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.23 }
|
| 2192 |
+
},
|
| 2193 |
+
"valueT": {
|
| 2194 |
+
"dtype": "float32",
|
| 2195 |
+
"shape": [1, 40, 32],
|
| 2196 |
+
"data": { "kind": "fillFloat32", "sinStep": 0.07, "cosStep": 0.41 }
|
| 2197 |
+
}
|
| 2198 |
+
},
|
| 2199 |
+
"outputs": { "outputT": { "dtype": "float32", "shape": [1, 13, 32], "tolerance": 0.00002 } }
|
| 2200 |
+
},
|
| 2201 |
+
{
|
| 2202 |
+
"name": "small_seq_blocked_bias_bidirectional_q24_kv40_h3d32",
|
| 2203 |
+
"provenance": {
|
| 2204 |
+
"notes": "Head dim 32 (eight vec4 per row, the route's register ceiling) with bias and no causal mask; kv > q so every query sees all forty keys."
|
| 2205 |
+
},
|
| 2206 |
+
"attrs": { "num_heads": 3, "unidirectional": 0 },
|
| 2207 |
+
"inputs": {
|
| 2208 |
+
"queryT": {
|
| 2209 |
+
"dtype": "float32",
|
| 2210 |
+
"shape": [1, 24, 96],
|
| 2211 |
+
"data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31 }
|
| 2212 |
+
},
|
| 2213 |
+
"keyT": {
|
| 2214 |
+
"dtype": "float32",
|
| 2215 |
+
"shape": [1, 40, 96],
|
| 2216 |
+
"data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.23 }
|
| 2217 |
+
},
|
| 2218 |
+
"valueT": {
|
| 2219 |
+
"dtype": "float32",
|
| 2220 |
+
"shape": [1, 40, 96],
|
| 2221 |
+
"data": { "kind": "fillFloat32", "sinStep": 0.07, "cosStep": 0.41 }
|
| 2222 |
+
},
|
| 2223 |
+
"biasT": {
|
| 2224 |
+
"dtype": "float32",
|
| 2225 |
+
"shape": [288],
|
| 2226 |
+
"data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.13 }
|
| 2227 |
+
}
|
| 2228 |
+
},
|
| 2229 |
+
"outputs": { "outputT": { "dtype": "float32", "shape": [1, 24, 96], "tolerance": 0.00002 } }
|
| 2230 |
+
},
|
| 2231 |
+
{
|
| 2232 |
+
"name": "small_seq_blocked_no_bias_bidirectional_q3_kv5_h1d8",
|
| 2233 |
+
"provenance": {
|
| 2234 |
+
"notes": "Fewer keys than key lanes (5 keys, 8 lanes after the power-of-two floor) and fewer queries than one block: most lanes hold no key and merge identities."
|
| 2235 |
+
},
|
| 2236 |
+
"attrs": { "num_heads": 1, "unidirectional": 0 },
|
| 2237 |
+
"inputs": {
|
| 2238 |
+
"queryT": {
|
| 2239 |
+
"dtype": "float32",
|
| 2240 |
+
"shape": [1, 3, 8],
|
| 2241 |
+
"data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31 }
|
| 2242 |
+
},
|
| 2243 |
+
"keyT": {
|
| 2244 |
+
"dtype": "float32",
|
| 2245 |
+
"shape": [1, 5, 8],
|
| 2246 |
+
"data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.23 }
|
| 2247 |
+
},
|
| 2248 |
+
"valueT": {
|
| 2249 |
+
"dtype": "float32",
|
| 2250 |
+
"shape": [1, 5, 8],
|
| 2251 |
+
"data": { "kind": "fillFloat32", "sinStep": 0.07, "cosStep": 0.41 }
|
| 2252 |
+
}
|
| 2253 |
+
},
|
| 2254 |
+
"outputs": { "outputT": { "dtype": "float32", "shape": [1, 3, 8], "tolerance": 0.00002 } }
|
| 2255 |
+
},
|
| 2256 |
+
{
|
| 2257 |
+
"name": "small_seq_blocked_bias_causal_batch2_q16_kv16_h2d16",
|
| 2258 |
+
"provenance": {
|
| 2259 |
+
"notes": "Two batches of two heads at exactly two query blocks; the batch axis rides dispatch z and the bias is shared across batches."
|
| 2260 |
+
},
|
| 2261 |
+
"attrs": { "num_heads": 2, "unidirectional": 1 },
|
| 2262 |
+
"inputs": {
|
| 2263 |
+
"queryT": {
|
| 2264 |
+
"dtype": "float32",
|
| 2265 |
+
"shape": [2, 16, 32],
|
| 2266 |
+
"data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31 }
|
| 2267 |
+
},
|
| 2268 |
+
"keyT": {
|
| 2269 |
+
"dtype": "float32",
|
| 2270 |
+
"shape": [2, 16, 32],
|
| 2271 |
+
"data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.23 }
|
| 2272 |
+
},
|
| 2273 |
+
"valueT": {
|
| 2274 |
+
"dtype": "float32",
|
| 2275 |
+
"shape": [2, 16, 32],
|
| 2276 |
+
"data": { "kind": "fillFloat32", "sinStep": 0.07, "cosStep": 0.41 }
|
| 2277 |
+
},
|
| 2278 |
+
"biasT": {
|
| 2279 |
+
"dtype": "float32",
|
| 2280 |
+
"shape": [96],
|
| 2281 |
+
"data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.13 }
|
| 2282 |
+
}
|
| 2283 |
+
},
|
| 2284 |
+
"outputs": { "outputT": { "dtype": "float32", "shape": [2, 16, 32], "tolerance": 0.00002 } }
|
| 2285 |
+
},
|
| 2286 |
+
{
|
| 2287 |
+
"name": "small_seq_positive_infinity_query",
|
| 2288 |
+
"provenance": {
|
| 2289 |
+
"notes": "The unmasked whole-head route always includes an `exp(0)` term, so a finite row cannot have a zero denominator. A positive-infinity query produces non-finite scores and the expected NaN result."
|
| 2290 |
+
},
|
| 2291 |
+
"attrs": { "num_heads": 2 },
|
| 2292 |
+
"inputs": {
|
| 2293 |
+
"queryT": {
|
| 2294 |
+
"dtype": "float32",
|
| 2295 |
+
"shape": [1, 4, 8],
|
| 2296 |
+
"data": { "kind": "cycle", "values": ["Infinity", 0.5, -0.25, 0.75, 0.125, -0.5, 0.25, 1.0] }
|
| 2297 |
+
},
|
| 2298 |
+
"keyT": {
|
| 2299 |
+
"dtype": "float32",
|
| 2300 |
+
"shape": [1, 4, 8],
|
| 2301 |
+
"data": { "kind": "fillFloat32", "sinStep": 0.011, "cosStep": 0.023, "scale": 0.4 }
|
| 2302 |
+
},
|
| 2303 |
+
"valueT": {
|
| 2304 |
+
"dtype": "float32",
|
| 2305 |
+
"shape": [1, 4, 8],
|
| 2306 |
+
"data": { "kind": "fillFloat32", "sinStep": 0.007, "cosStep": 0.041, "scale": 0.4 }
|
| 2307 |
+
}
|
| 2308 |
+
},
|
| 2309 |
+
"outputs": {
|
| 2310 |
+
"outputT": {
|
| 2311 |
+
"dtype": "float32",
|
| 2312 |
+
"shape": [1, 4, 8],
|
| 2313 |
+
"tolerance": 0.00002,
|
| 2314 |
+
"relTolerance": 0.00002,
|
| 2315 |
+
"allowNaN": true
|
| 2316 |
+
}
|
| 2317 |
+
}
|
| 2318 |
}
|
| 2319 |
]
|
| 2320 |
}
|