Xenova HF Staff commited on
Commit
78d4fcb
·
verified ·
1 Parent(s): 2fad9e5

sync 91d990483a17

Browse files
README.md CHANGED
@@ -18,31 +18,31 @@ See the [ONNX Runtime `GroupQueryAttention` contrib-operator spec](https://githu
18
 
19
  ## Inputs
20
 
21
- | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
22
  | --- | --- | --- | --- | --- | --- | --- |
23
- | `query` | `queryT` | `T` | `3` | — | Query tensor of shape `(batch_size, sequence_length, num_heads * head_size)`. | required |
24
- | `key` | `keyT` | `T` | `3` | — | Key tensor of shape `(batch_size, kv_sequence_length, kv_num_heads * head_size)`. | required |
25
- | `value` | `valueT` | `T` | `3` | — | Value tensor of shape `(batch_size, kv_sequence_length, kv_num_heads * head_size)`. | required |
26
- | `past_key` | `pastKeyT` | `T_CACHE` | `4` | — | Optional cached key state in BNSH format. Its sequence axis is `max_sequence_length` when the past and present buffers are shared, otherwise `past_sequence_length`; int4 stores each signed value as a +8-biased nibble, with the even head coordinate low, packing two values per logical uint8 element and widening each byte to one u32 WebGPU buffer word. | optional |
27
- | `past_value` | `pastValueT` | `T_CACHE` | `4` | — | Optional cached value state in BNSH format with the same length and packing semantics as `past_key`. | optional |
28
- | `seqlens_k` | `seqlensKT` | `M` | `1` | — | Int32 tensor of shape `(batch_size)` containing each sample's total sequence length minus one. | required |
29
- | `total_sequence_length` | `totalSequenceLengthT` | `M` | `1` | — | Length-one int32 tensor containing the maximum total sequence length (past plus new) in the batch. | required |
30
- | `cos_cache` | `cosCacheT` | `T` | `2` | — | Optional cosine cache for rotary embeddings with shape `(max_sequence_length, head_size / 2)`. | optional |
31
- | `sin_cache` | `sinCacheT` | `T` | `2` | — | Optional sine cache for rotary embeddings with shape `(max_sequence_length, head_size / 2)`. | optional |
32
- | `attention_bias` | `attentionBiasT` | `T` | `4` | — | Optional additive term for QK scores with shape `(batch_size or 1, num_heads or 1, sequence_length, total_sequence_length)`; the first two dimensions broadcast. | optional |
33
- | `head_sink` | `headSinkT` | `T` | `1` | — | Optional per-head smooth factor of shape `(num_heads)` added to the softmax denominator. | optional |
34
- | `k_scale` | `kScaleT` | `T_KV_SCALE` | `1` | — | Optional float32 key-cache scale: one value for `PER_TENSOR`, or `kv_num_heads * head_size` values for `PER_CHANNEL`. | optional |
35
- | `v_scale` | `vScaleT` | `T_KV_SCALE` | `1` | — | Optional float32 value-cache scale with the same shape convention as `k_scale`. | optional |
36
- | `q_norm_weight` | `qNormWeightT` | `T` | `1` | — | Optional per-head RMS-normalization weight of shape `(head_size)` applied to queries before rotary embedding. It must be provided together with `k_norm_weight`. | optional |
37
- | `k_norm_weight` | `kNormWeightT` | `T` | `1` | — | Optional per-head RMS-normalization weight of shape `(head_size)` applied to keys before rotary embedding. It must be provided together with `q_norm_weight`. | optional |
38
 
39
  ## Outputs
40
 
41
- | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
42
  | --- | --- | --- | --- | --- | --- | --- |
43
- | `output` | `outputT` | `T` | `3` | same as `query` | Attention output of shape `(batch_size, sequence_length, hidden_size)`. | required |
44
- | `present_key` | `presentKeyT` | `T_CACHE` | `4` | — | Updated key cache in BNSH format. Its sequence axis is `max_sequence_length` for a shared buffer, otherwise `past_sequence_length + kv_sequence_length`; int4 stores each signed value as a +8-biased nibble, with the even head coordinate low, packing two values per logical uint8 element and widening each byte to one u32 WebGPU buffer word. | required |
45
- | `present_value` | `presentValueT` | `T_CACHE` | `4` | — | Updated value cache in BNSH format with the same length and packing semantics as `present_key`. | required |
46
 
47
  ## Attributes
48
 
@@ -53,16 +53,16 @@ Attributes and default values (overridable per request):
53
  | `causal` | `1` | Whether to apply a causal mask. Set to 0 for bidirectional attention; `local_window_size` must then be -1. |
54
  | `do_rotary` | `0` | Set to 1 to apply rotary position embeddings. The default 0 disables them. |
55
  | `k_quant_type` | `"NONE"` | Key-cache quantization mode: `NONE`, `PER_TENSOR`, or `PER_CHANNEL`. |
 
 
56
  | `local_window_size` | `-1` | Left window size for causal local attention. The default -1 disables local attention, and the value must be -1 when `causal` is 0. |
 
57
  | `qk_norm_epsilon` | `0.000001` | Epsilon for the per-head Q/K RMS normalization applied when both normalization weights are provided. |
 
58
  | `sliding_window_cache` | `0` | Set to 1 when past/present caches are fixed-size window buffers that evict old tokens from the front. Requires `local_window_size > 0` and enough cache capacity. |
59
  | `smooth_softmax` | `-1` | Set to 1 to enable the smooth-softmax denominator term. |
60
  | `softcap` | `0` | Positive softcap applied to attention scores. The default 0 disables soft-capping. |
61
  | `v_quant_type` | `"NONE"` | Value-cache quantization mode: `NONE`, `PER_TENSOR`, or `PER_CHANNEL`. |
62
- | `kv_cache_bit_width` | — | Quantized cache bit width, either 8 or 4. Four-bit values are packed two per uint8 element. |
63
- | `kv_num_heads` | — | Number of key/value attention heads. |
64
- | `num_heads` | — | Number of query attention heads. |
65
- | `scale` | — | Optional QK score scale; zero or omission selects `1 / sqrt(head_size)`. |
66
 
67
  ## Type constraints
68
 
@@ -73,13 +73,63 @@ Attributes and default values (overridable per request):
73
  | `T_KV_SCALE` | `float32` |
74
  | `M` | `int32` |
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  ## Device requirements
77
 
78
  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.
79
 
80
  ## Files
81
 
82
- - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, provenance)
83
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
84
  - [`test.json`](build/webgpu/test.json) — correctness cases
85
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
@@ -98,16 +148,19 @@ Some implementation variants require `subgroup-matrix`, `shader-f16`, and `subgr
98
 
99
  ## Use with `@huggingface/kernels`
100
 
101
- The loader automatically allocates outputs whose metadata it can derive from the manifest contract and this call.
 
 
 
 
102
 
103
- The explicit `outputs` entries provide shape and logical dtype metadata for the results listed below:
104
 
105
  - `presentKeyT`
106
  - `presentValueT`
107
 
108
- Each entry either requests an optional result or supplies metadata that cannot be inferred from the inputs.
109
-
110
  The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
 
111
 
112
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
113
 
 
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, num_heads * head_size)`. | required |
24
+ | `keyT` | `key` | `T` | `3` | — | Key tensor of shape `(batch_size, kv_sequence_length, kv_num_heads * head_size)`. | required |
25
+ | `valueT` | `value` | `T` | `3` | — | Value tensor of shape `(batch_size, kv_sequence_length, kv_num_heads * head_size)`. | required |
26
+ | `pastKeyT` | `past_key` | `T_CACHE` | `4` | — | Optional cached key state in BNSH format. Its sequence axis is `max_sequence_length` when the past and present buffers are shared, otherwise `past_sequence_length`; int4 stores each signed value as a +8-biased nibble, with the even head coordinate low, packing two values per logical uint8 element and widening each byte to one u32 WebGPU buffer word. | optional |
27
+ | `pastValueT` | `past_value` | `T_CACHE` | `4` | — | Optional cached value state in BNSH format with the same length and packing semantics as `past_key`. | optional |
28
+ | `seqlensKT` | `seqlens_k` | `M` | `1` | — | Int32 tensor of shape `(batch_size)` containing each sample's total sequence length minus one. | required |
29
+ | `totalSequenceLengthT` | `total_sequence_length` | `M` | `1` | — | Length-one int32 tensor containing the maximum total sequence length (past plus new) in the batch. | required |
30
+ | `cosCacheT` | `cos_cache` | `T` | `2` | — | Optional cosine cache for rotary embeddings with shape `(max_sequence_length, head_size / 2)`. | optional |
31
+ | `sinCacheT` | `sin_cache` | `T` | `2` | — | Optional sine cache for rotary embeddings with shape `(max_sequence_length, head_size / 2)`. | optional |
32
+ | `attentionBiasT` | `attention_bias` | `T` | `4` | — | Optional additive term for QK scores with shape `(batch_size or 1, num_heads or 1, sequence_length, total_sequence_length)`; the first two dimensions broadcast. | optional |
33
+ | `headSinkT` | `head_sink` | `T` | `1` | — | Optional per-head smooth factor of shape `(num_heads)` added to the softmax denominator. | optional |
34
+ | `kScaleT` | `k_scale` | `T_KV_SCALE` | `1` | — | Optional float32 key-cache scale: one value for `PER_TENSOR`, or `kv_num_heads * head_size` values for `PER_CHANNEL`. | optional |
35
+ | `vScaleT` | `v_scale` | `T_KV_SCALE` | `1` | — | Optional float32 value-cache scale with the same shape convention as `k_scale`. | optional |
36
+ | `qNormWeightT` | `q_norm_weight` | `T` | `1` | — | Optional per-head RMS-normalization weight of shape `(head_size)` applied to queries before rotary embedding. It must be provided together with `k_norm_weight`. | optional |
37
+ | `kNormWeightT` | `k_norm_weight` | `T` | `1` | — | Optional per-head RMS-normalization weight of shape `(head_size)` applied to keys before rotary embedding. It must be provided together with `q_norm_weight`. | optional |
38
 
39
  ## Outputs
40
 
41
+ | Name | Upstream name | Logical dtype | Rank | Shape | Description | Presence |
42
  | --- | --- | --- | --- | --- | --- | --- |
43
+ | `outputT` | `output` | `T` | `3` | same as `queryT` | Attention output of shape `(batch_size, sequence_length, hidden_size)`. | required |
44
+ | `presentKeyT` | `present_key` | `T_CACHE` | `4` | — | Updated key cache in BNSH format. Its sequence axis is `max_sequence_length` for a shared buffer, otherwise `past_sequence_length + kv_sequence_length`; int4 stores each signed value as a +8-biased nibble, with the even head coordinate low, packing two values per logical uint8 element and widening each byte to one u32 WebGPU buffer word. | required |
45
+ | `presentValueT` | `present_value` | `T_CACHE` | `4` | — | Updated value cache in BNSH format with the same length and packing semantics as `present_key`. | required |
46
 
47
  ## Attributes
48
 
 
53
  | `causal` | `1` | Whether to apply a causal mask. Set to 0 for bidirectional attention; `local_window_size` must then be -1. |
54
  | `do_rotary` | `0` | Set to 1 to apply rotary position embeddings. The default 0 disables them. |
55
  | `k_quant_type` | `"NONE"` | Key-cache quantization mode: `NONE`, `PER_TENSOR`, or `PER_CHANNEL`. |
56
+ | `kv_cache_bit_width` | — | Quantized cache bit width, either 8 or 4. Four-bit values are packed two per uint8 element. |
57
+ | `kv_num_heads` | — | Number of key/value attention heads. |
58
  | `local_window_size` | `-1` | Left window size for causal local attention. The default -1 disables local attention, and the value must be -1 when `causal` is 0. |
59
+ | `num_heads` | — | Number of query attention heads. |
60
  | `qk_norm_epsilon` | `0.000001` | Epsilon for the per-head Q/K RMS normalization applied when both normalization weights are provided. |
61
+ | `scale` | — | Optional QK score scale; zero or omission selects `1 / sqrt(head_size)`. |
62
  | `sliding_window_cache` | `0` | Set to 1 when past/present caches are fixed-size window buffers that evict old tokens from the front. Requires `local_window_size > 0` and enough cache capacity. |
63
  | `smooth_softmax` | `-1` | Set to 1 to enable the smooth-softmax denominator term. |
64
  | `softcap` | `0` | Positive softcap applied to attention scores. The default 0 disables soft-capping. |
65
  | `v_quant_type` | `"NONE"` | Value-cache quantization mode: `NONE`, `PER_TENSOR`, or `PER_CHANNEL`. |
 
 
 
 
66
 
67
  ## Type constraints
68
 
 
73
  | `T_KV_SCALE` | `float32` |
74
  | `M` | `int32` |
75
 
76
+ ## Implementation variants
77
+
78
+ One implementation is selected per call from the device capabilities, the request shapes and the dtypes; these notes say what each one covers.
79
+
80
+ - `qkv_present_materialized_sgmat_f32` — Materialized float32 subgroup-matrix prefill for bidirectional QKV without past state. `seqlens_k` is metadata-only; the score pass emits per-row softmax statistics, the apply pass normalizes and applies the scores, and the present copy preserves the supplied key and value tensors.
81
+ - `past_kv_materialized_sgmat_f32` — Materializes the causal score matrix with float32 subgroup-matrix tiles over the shared float cache, then applies the softmax-normalized weights with subgroup-matrix tiles. Both passes skip key tiles beyond each query tile's causal bound.
82
+ - `past_kv_bias_materialized_sgmat_f32` — Materializes causal scores with float32 subgroup-matrix tiles, broadcasts and adds the attention bias across batch and head dimensions, folds the biased scores into row statistics, and applies the normalized weights with subgroup-matrix tiles.
83
+ - `past_kv_materialized_sgmat_f16` — Materializes the causal score matrix with `f16` operand tiles feeding `f32`-accumulating subgroup matrices over the shared cache and applies the softmax-normalized weights the same way; scores and row statistics stay `f32`. The score pass skips key tiles past each query tile's causal bound and the apply pass stops its reduction there.
84
+ - `past_kv_rotary_materialized_sgmat_f32` — Rotates each query block at its absolute positions, then materializes causal scores against rotary-transformed cached keys and applies the normalized weights. The score and apply passes skip tiles beyond each query tile's causal bound.
85
+ - `past_kv_rotary_materialized_sgmat_f16` — Materializes the causal score matrix with `f16` operand tiles feeding `f32`-accumulating subgroup matrices over the shared cache and applies the softmax-normalized weights the same way; scores and row statistics stay `f32`. The score pass skips key tiles past each query tile's causal bound and the apply pass stops its reduction there.
86
+ - `new_kv_past_materialized_sgmat_f32` — For chunked prefill, merges the past cache with new key/value rows, materializes causal scores over the merged cache, and applies the normalized weights using the same right-aligned causal bound.
87
+ - `window_shift_materialized_sgmat_f32` — Materializes causal chunked-prefill attention with a windowed cache. The shift pass compacts surviving rows and appends the chunk; score and apply passes enforce both the sliding-window floor and causal bound.
88
+ - `share_append_materialized_sgmat_f32` — Materializes causal chunked-prefill attention while updating a shared-capacity cache in place. Score and apply passes bound every causal tile by the live length from `seqlens_k`, not the buffer capacity, so right-padded batches remain left-aligned.
89
+ - `new_kv_share_append_split` — Retains the existing cache and appends new key/value rows in separate passes before portable attention. It avoids rebuilding unchanged cache positions when the past and present allocations share capacity.
90
+ - `qkv_present_tiled_nosg` — Portable tiled prefill route that computes attention online and writes the present cache separately. It is used when the flash shape is valid but no suitable subgroup route is admissible.
91
+ - `quant_int8_decode_splitk` — Appends int8-quantized key/value rows, partitions cached decode across the key axis, and merges partial online-softmax results. Used when one workgroup per head exposes too little independent work.
92
+ - `qkv_present_flash_splitk` — Partitions direct Q/K/V attention across the key axis, merges partial online-softmax results, and writes the present cache separately. It serves short-query shapes needing more key-axis parallelism.
93
+ - `qkv_present_flash_cluster` — Computes clustered online-softmax prefill directly from Q/K/V and writes the present cache separately. The family provides subgroup and portable reductions for the same tiled algorithm.
94
+ - `quant_int8_decode_splitk_nosg` — Appends int8-quantized key/value rows, partitions cached decode across the key axis, and merges partial online-softmax results. Used when one workgroup per head exposes too little independent work.
95
+ - `qkv_present_flash_splitk_nosg` — Partitions direct Q/K/V attention across the key axis, merges partial online-softmax results, and writes the present cache separately. It serves short-query shapes needing more key-axis parallelism.
96
+ - `qkv_present_flash_cluster_nosg` — Computes clustered online-softmax prefill directly from Q/K/V and writes the present cache separately. The family provides subgroup and portable reductions for the same tiled algorithm.
97
+ - `past_kv_decode_splitk` — Updates the float cache in copy, merge, or window-shift mode, then partitions cached decode across the key axis and combines partial online-softmax results. The family includes subgroup and portable forms.
98
+ - `new_kv_past_decode_splitk` — Updates the float cache in copy, merge, or window-shift mode, then partitions cached decode across the key axis and combines partial online-softmax results. The family includes subgroup and portable forms.
99
+ - `window_shift_decode_splitk` — Updates the float cache in copy, merge, or window-shift mode, then partitions cached decode across the key axis and combines partial online-softmax results. The family includes subgroup and portable forms.
100
+ - `past_kv_decode_splitk_nosg` — Updates the float cache in copy, merge, or window-shift mode, then partitions cached decode across the key axis and combines partial online-softmax results. The family includes subgroup and portable forms.
101
+ - `new_kv_past_decode_splitk_nosg` — Updates the float cache in copy, merge, or window-shift mode, then partitions cached decode across the key axis and combines partial online-softmax results. The family includes subgroup and portable forms.
102
+ - `window_shift_decode_splitk_nosg` — Updates the float cache in copy, merge, or window-shift mode, then partitions cached decode across the key axis and combines partial online-softmax results. The family includes subgroup and portable forms.
103
+ - `past_kv_flash_prefill` — Updates the float cache in the selected mode and applies clustered online-softmax causal prefill with the configured cache, mask, rotary, softcap, bias, or head-sink features.
104
+ - `new_kv_past_flash_prefill` — Updates the float cache in the selected mode and applies clustered online-softmax causal prefill with the configured cache, mask, rotary, softcap, bias, or head-sink features.
105
+ - `past_kv_rotary_flash_prefill` — Updates the float cache in the selected mode and applies clustered online-softmax causal prefill with the configured cache, mask, rotary, softcap, bias, or head-sink features.
106
+ - `past_kv_softcap_flash_prefill` — Updates the float cache in the selected mode and applies clustered online-softmax causal prefill with the configured cache, mask, rotary, softcap, bias, or head-sink features.
107
+ - `past_kv_headsink_flash_prefill` — Updates the float cache in the selected mode and applies clustered online-softmax causal prefill with the configured cache, mask, rotary, softcap, bias, or head-sink features.
108
+ - `past_kv_bias_headsink_flash_prefill` — Updates the float cache in the selected mode and applies clustered online-softmax causal prefill with the configured cache, mask, rotary, softcap, bias, or head-sink features.
109
+ - `window_shift_flash_prefill` — Updates the float cache in the selected mode and applies clustered online-softmax causal prefill with the configured cache, mask, rotary, softcap, bias, or head-sink features.
110
+ - `past_kv_flash_prefill_nosg` — Updates the float cache in the selected mode and applies clustered online-softmax causal prefill with the configured cache, mask, rotary, softcap, bias, or head-sink features.
111
+ - `new_kv_past_flash_prefill_nosg` — Updates the float cache in the selected mode and applies clustered online-softmax causal prefill with the configured cache, mask, rotary, softcap, bias, or head-sink features.
112
+ - `past_kv_rotary_flash_prefill_nosg` — Updates the float cache in the selected mode and applies clustered online-softmax causal prefill with the configured cache, mask, rotary, softcap, bias, or head-sink features.
113
+ - `past_kv_softcap_flash_prefill_nosg` — Updates the float cache in the selected mode and applies clustered online-softmax causal prefill with the configured cache, mask, rotary, softcap, bias, or head-sink features.
114
+ - `past_kv_headsink_flash_prefill_nosg` — Updates the float cache in the selected mode and applies clustered online-softmax causal prefill with the configured cache, mask, rotary, softcap, bias, or head-sink features.
115
+ - `past_kv_bias_headsink_flash_prefill_nosg` — Updates the float cache in the selected mode and applies clustered online-softmax causal prefill with the configured cache, mask, rotary, softcap, bias, or head-sink features.
116
+ - `window_shift_flash_prefill_nosg` — Updates the float cache in the selected mode and applies clustered online-softmax causal prefill with the configured cache, mask, rotary, softcap, bias, or head-sink features.
117
+ - `quant_int8_flash_prefill` — Builds an int8 or int4 cache and applies clustered online-softmax prefill directly from the packed values. The family supplies subgroup and portable reductions.
118
+ - `quant_int4_flash_prefill` — Builds an int8 or int4 cache and applies clustered online-softmax prefill directly from the packed values. The family supplies subgroup and portable reductions.
119
+ - `quant_int8_flash_prefill_nosg` — Builds an int8 or int4 cache and applies clustered online-softmax prefill directly from the packed values. The family supplies subgroup and portable reductions.
120
+ - `quant_int4_flash_prefill_nosg` — Builds an int8 or int4 cache and applies clustered online-softmax prefill directly from the packed values. The family supplies subgroup and portable reductions.
121
+ - `share_append_split_decode_splitk` — Retains shared-capacity cache rows, appends new rows separately, then partitions decode across the key axis. It avoids rebuilding unchanged cache positions while exposing split-key parallelism.
122
+ - `share_append_split_decode_splitk_nosg` — Retains shared-capacity cache rows, appends new rows separately, then partitions decode across the key axis. It avoids rebuilding unchanged cache positions while exposing split-key parallelism.
123
+ - `share_append_split_flash_prefill` — Retains shared-capacity cache rows, appends new rows separately, then applies clustered causal prefill. It avoids rebuilding unchanged cache positions and includes subgroup and portable forms.
124
+ - `share_append_split_flash_prefill_nosg` — Retains shared-capacity cache rows, appends new rows separately, then applies clustered causal prefill. It avoids rebuilding unchanged cache positions and includes subgroup and portable forms.
125
+
126
  ## Device requirements
127
 
128
  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.
129
 
130
  ## Files
131
 
132
+ - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, per-variant templates, provenance)
133
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
134
  - [`test.json`](build/webgpu/test.json) — correctness cases
135
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
 
148
 
149
  ## Use with `@huggingface/kernels`
150
 
151
+ ```sh
152
+ npm install --save-exact @huggingface/kernels@0.0.1-preview.2
153
+ ```
154
+
155
+ Outputs with inferable metadata are allocated automatically. Explicit `outputs` entries request optional results or provide metadata that cannot be inferred from the supplied inputs and attributes.
156
 
157
+ This example supplies explicit metadata for:
158
 
159
  - `presentKeyT`
160
  - `presentValueT`
161
 
 
 
162
  The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
163
+ It follows the `v1` branch as fixes land. To pin exact artifact bytes, pass a 40-character commit `revision` instead of `version`.
164
 
165
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
166
 
build/webgpu/attention-rank4-tiled.wgsl.jinja CHANGED
@@ -1,19 +1,15 @@
1
- {% if usesF16 %}
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 so
11
- // they stream the same K/V rows at the same key index, so those global loads hit the
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 (no bank conflict) and
15
- // nothing spills to registers. The value-head width is capped so acc fits in
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 maxComputeWorkgroupsPerDimension dispatch limit.
43
- let tile = wg.x + wg.y * nwg.x;
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
- // The K/V row is shared across the tile, so these loads broadcast from L2.
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
@@ -14,10 +15,10 @@ enable f16;
14
  // One workgroup per (batch, head); one thread per output vec4 channel. The
15
  // per-split (max, denom) scan is tiny (NUM_SPLITS is small), so every thread
16
  // recomputes it locally and keeps the merge state in registers.
17
- {% if source.layout == "bhsd" %}
18
  // Output layout: rank-4 [batch, heads, seq, headDim], matching the split pass.
19
- {% elif source.layout == "layer_cache" %}
20
- // Output layout: flat [heads, headDim], matching Qwen3.5 decode.
21
  {% else %}
22
  // Output layout: token-major [batch, seq, heads * headDim], matching the split pass.
23
  {% endif %}
@@ -27,7 +28,7 @@ const NUM_SPLITS: u32 = {{ numSplits }}u;
27
  {% if splitQueries %}
28
  const Q_SEQ: u32 = {{ qSeq }}u;
29
  {% endif %}
30
- {% if source.layout == "bsh" %}
31
  const Q_HIDDEN_V4: u32 = {{ qHiddenV4 }}u;
32
  {% endif %}
33
  {% if hasBias %}
@@ -97,13 +98,13 @@ fn main(
97
  outV = outV + partial_out[pBase + d4] * w;
98
  }
99
 
100
- {% if source.layout == "bsh" %}
101
  {% if splitQueries %}
102
  let qBaseV4 = (b * Q_SEQ + queryToken) * Q_HIDDEN_V4 + h * HEAD_DIM_V4;
103
  {% else %}
104
  let qBaseV4 = b * Q_HIDDEN_V4 + h * HEAD_DIM_V4;
105
  {% endif %}
106
- {% elif source.layout == "layer_cache" %}
107
  let qBaseV4 = h * HEAD_DIM_V4;
108
  {% else %}
109
  {% if splitQueries %}
@@ -123,7 +124,7 @@ fn main(
123
  outValue = outValue + vec4<f32>(bias[vBiasBase], bias[vBiasBase + 1u], bias[vBiasBase + 2u], bias[vBiasBase + 3u]);
124
  {% endif %}
125
  {% if hasGate %}
126
- // Qwen3.5 full attention gates the normalized attention output elementwise.
127
  let gateV = vec4<f32>(gate[qBaseV4 + d4]);
128
  outValue = outValue * (vec4<f32>(1.0) / (vec4<f32>(1.0) + exp(-gateV)));
129
  {% 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
 
15
  // One workgroup per (batch, head); one thread per output vec4 channel. The
16
  // per-split (max, denom) scan is tiny (NUM_SPLITS is small), so every thread
17
  // recomputes it locally and keeps the merge state in registers.
18
+ {% if layout == "bhsd" %}
19
  // Output layout: rank-4 [batch, heads, seq, headDim], matching the split pass.
20
+ {% elif layout == "layer_cache" %}
21
+ // Output layout: flat [heads, headDim], matching the cached-decode split pass.
22
  {% else %}
23
  // Output layout: token-major [batch, seq, heads * headDim], matching the split pass.
24
  {% endif %}
 
28
  {% if splitQueries %}
29
  const Q_SEQ: u32 = {{ qSeq }}u;
30
  {% endif %}
31
+ {% if layout == "bsh" %}
32
  const Q_HIDDEN_V4: u32 = {{ qHiddenV4 }}u;
33
  {% endif %}
34
  {% if hasBias %}
 
98
  outV = outV + partial_out[pBase + d4] * w;
99
  }
100
 
101
+ {% if layout == "bsh" %}
102
  {% if splitQueries %}
103
  let qBaseV4 = (b * Q_SEQ + queryToken) * Q_HIDDEN_V4 + h * HEAD_DIM_V4;
104
  {% else %}
105
  let qBaseV4 = b * Q_HIDDEN_V4 + h * HEAD_DIM_V4;
106
  {% endif %}
107
+ {% elif layout == "layer_cache" %}
108
  let qBaseV4 = h * HEAD_DIM_V4;
109
  {% else %}
110
  {% if splitQueries %}
 
124
  outValue = outValue + vec4<f32>(bias[vBiasBase], bias[vBiasBase + 1u], bias[vBiasBase + 2u], bias[vBiasBase + 3u]);
125
  {% endif %}
126
  {% if hasGate %}
127
+ // The gated route multiplies the normalized attention output elementwise by its gate.
128
  let gateV = vec4<f32>(gate[qBaseV4 + d4]);
129
  outValue = outValue * (vec4<f32>(1.0) / (vec4<f32>(1.0) + exp(-gateV)));
130
  {% endif %}
build/webgpu/attn-flash-decode-splitk.wgsl.jinja CHANGED
@@ -4,17 +4,19 @@
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 splitKWorkgroupSize = source.workgroupSize if source.workgroupSize is defined else tunables.WORKGROUP_SIZE %}
 
 
 
 
 
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. Shared by
17
- // dense-attention decode and short-query/long-context prefill paths.
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
@@ -24,25 +26,24 @@ enable f16;
24
  // its range — the running (max, denom) and the softmax-weighted V sum before the
25
  // final divide — and the merge pass combines the per-split states with the online
26
  // rule.
27
- {% if source.layout == "bhsd" %}
28
  // Layout: rank-4 [batch, heads, seq, headDim] for Q/K/V.
29
- {% elif source.layout == "layer_cache" %}
30
  // Layout: flat query [heads, headDim] plus a persistent KV cache laid out
31
- // [layer, cacheLen, kvHeads, headDim]. This is the Qwen3.5 decode layout; the
32
- // dispatch has a single implicit batch.
33
  {% else %}
34
  // Layout: token-major [batch, seq, heads * headDim]; Q and KV hidden strides
35
  // are compiled constants.
36
  {% endif %}
37
- {% if (fusedQNormRope is defined and fusedQNormRope) or source.layout != "layer_cache" %}const HEAD_DIM: u32 = {{ headDim }}u;
38
  {% endif %}
39
  const HEAD_DIM_V4: u32 = {{ headDimV4 }}u;
40
  const Q_HEADS: u32 = {{ qNumHeads }}u;
41
  const KV_HEADS: u32 = {{ kvNumHeads }}u;
42
- {% if source.layout == "bsh" %}
43
  const Q_HIDDEN_V4: u32 = {{ qHiddenV4 }}u;
44
  const KV_HIDDEN_V4: u32 = {{ kvHiddenV4 }}u;
45
- {% elif source.layout == "layer_cache" %}
46
  const LAYER: u32 = {{ layer }}u;
47
  const CACHE_LEN: u32 = {{ cacheLen }}u;
48
  const ATTN_SCALE: f32 = {{ scale }};
@@ -96,20 +97,22 @@ var<workgroup> vacc_sh: array<vec4<f32>, WG>;
96
  // calls before their shared partial storage is reused.
97
  {% set combineSubgroups = combineSubgroups is defined and combineSubgroups %}
98
  {% if combineSubgroups %}
99
- // Per-subgroup partials are published into a deterministic slot: the subgroup's
100
- // ordinal index within the workgroup (lidx / sgSize). The online (m, d) merge
101
- // is not float-associative, so thread 0 must fold partials in a fixed order.
102
- // Subgroups partition a workgroup into contiguous ordinal ranges on supported
103
- // backends, so the ordinal slot is unique per subgroup and every slot in
104
- // [0, subgroupCount) is written (each subgroup elects one leader).
105
- // Sized for the worst case of one partial per invocation.
 
106
  var<workgroup> partialM: array<f32, WG>;
107
  var<workgroup> partialD: array<f32, WG>;
 
108
  var<workgroup> combinedMD: vec2<f32>;
109
 
110
  // When the whole workgroup is one subgroup the subgroup reduce already covers
111
- // it (no barriers, no shared state); otherwise subgroup leaders publish
112
- // partials through shared memory and thread 0 folds them in ordinal order.
113
  fn combine_partials(m: f32, d: f32, lidx: u32, sgSize: u32) -> vec2<f32> {
114
  let sgM = subgroupMax(m);
115
  // A lane with no elements contributes d == 0 (exact identity). A +inf
@@ -120,31 +123,24 @@ fn combine_partials(m: f32, d: f32, lidx: u32, sgSize: u32) -> vec2<f32> {
120
  if (sgSize == WG) {
121
  return vec2<f32>(sgM, sgD);
122
  }
123
- let subgroupCount = (WG + sgSize - 1u) / sgSize;
124
- // Pre-seed every fold slot with the (max, denom) identity. The fold below reads a
125
- // fixed subgroupCount slots in ordinal order (for determinism), but a slot whose
126
- // subgroup elects no leader this call — e.g. a fully out-of-window key tile in the
127
- // flash-attention loop that re-uses this shared memory each iteration — would
128
- // otherwise read stale shared memory. Identity makes such a slot a no-op.
129
- // (max identity = -FLT_MAX, denom identity = 0.)
130
- if (lidx < subgroupCount) {
131
- partialM[lidx] = -FLT_MAX;
132
- partialD[lidx] = 0.0;
133
- }
134
- workgroupBarrier();
135
  if (subgroupElect()) {
136
- let slot = lidx / sgSize;
137
- partialM[slot] = sgM;
138
- partialD[slot] = sgD;
139
  }
140
  workgroupBarrier();
141
  if (lidx == 0u) {
142
  var accM = -FLT_MAX;
143
  var accD = 0.0;
144
- for (var i = 0u; i < subgroupCount; i = i + 1u) {
145
- let mNew = max(accM, partialM[i]);
146
- accD = accD * exp_shift(accM, mNew) + partialD[i] * exp_shift(partialM[i], mNew);
147
- accM = mNew;
 
 
 
 
 
148
  }
149
  combinedMD = vec2<f32>(accM, accD);
150
  }
@@ -159,9 +155,8 @@ var<workgroup> partialM: array<f32, {{ mdExtent }}>;
159
  var<workgroup> partialD: array<f32, {{ mdExtent }}>;
160
  {% if mdStreamed %}
161
 
162
- // In-place fold of {{ mdStreams }} streams. The caller stores its per-thread
163
- // partials into partialM/partialD first and reads the merged pair of stream s
164
- // from slot s * WG afterwards.
165
  fn combine_partials_streams(lidx: u32) {
166
  workgroupBarrier();
167
  var stride = WG / 2u;
@@ -220,7 +215,7 @@ fn combine_partials(m: f32, d: f32, lidx: u32) -> vec2<f32> {
220
  {% endif %}
221
 
222
 
223
- {% if source.layout == "layer_cache" %}{% set ATTN_SCALE_OVERRIDE = "ATTN_SCALE" %}{% endif %}
224
  {% if ATTN_SCALE_DIM is not defined %}{% set ATTN_SCALE_DIM = "HEAD_DIM" %}{% endif %}
225
  fn scale_value() -> f32 {
226
  {% if ATTN_SCALE_OVERRIDE is defined %}
@@ -310,12 +305,12 @@ fn main(
310
  {% endif %}
311
  let h = wg.y;
312
  let b = wg.z;
313
- if (h >= Q_HEADS || split >= NUM_SPLITS{% if splitQueries %} || queryToken >= Q_SEQ{% endif %}{% if source.layout == "layer_cache" %} || params.past_len >= CACHE_LEN{% endif %}) {
314
  return;
315
  }
316
  let tid = lid.x;
317
  let hKv = h / (Q_HEADS / KV_HEADS);
318
- {% if source.layout == "layer_cache" %}
319
  let kvSeq = params.past_len + 1u;
320
  {% else %}
321
  let cacheSeq = params.kvSeq;
@@ -329,7 +324,7 @@ fn main(
329
  {% endif %}
330
 
331
  // Query row (decode uses token zero; short-query prefill folds the token into wg.x).
332
- {% if source.layout == "bsh" %}
333
  {% if splitQueries %}
334
  let qBaseV4 = (b * Q_SEQ + queryToken) * Q_HIDDEN_V4 + h * HEAD_DIM_V4;
335
  {% else %}
@@ -337,7 +332,7 @@ fn main(
337
  {% endif %}
338
  let kvBaseV4 = b * kvSeq * KV_HIDDEN_V4 + hKv * HEAD_DIM_V4;
339
  let kvTokenStrideV4 = KV_HIDDEN_V4;
340
- {% elif source.layout == "layer_cache" %}
341
  let qBaseV4 = h * HEAD_DIM_V4;
342
  let kvBaseV4 = (LAYER * CACHE_LEN * KV_HEADS + hKv) * HEAD_DIM_V4;
343
  let kvTokenStrideV4 = KV_HEADS * HEAD_DIM_V4;
 
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 layer = layer | default(0) %}
8
+ {% set cacheLen = cacheLen | default(0) %}
9
+ {% set scale = scale | default("0.0") %}
10
+ {% if hasRotary is not defined %}{% set hasRotary = false %}{% endif %}
11
+ {% set qSeq = qSeq | default(0) %}
12
+ {% set splitKWorkgroupSize = workgroupSizeSpec if workgroupSizeSpec is defined else tunables.WORKGROUP_SIZE %}
13
  {% if useSubgroups %}
14
  enable subgroups;
15
  {% endif %}
 
 
 
16
  {{ env.wgsl.resourceDeclarations }}
17
 
18
+ // Split-K flash attention, pass 1 of 2; the merge pass follows. This geometry
19
+ // handles decode and short-query, long-context prefill inputs.
20
  //
21
  // The non-split flash decode launches only `batch * numHeads` workgroups, each
22
  // sweeping the whole KV sequence serially in WG-key tiles. This pass splits the
 
26
  // its range — the running (max, denom) and the softmax-weighted V sum before the
27
  // final divide — and the merge pass combines the per-split states with the online
28
  // rule.
29
+ {% if layout == "bhsd" %}
30
  // Layout: rank-4 [batch, heads, seq, headDim] for Q/K/V.
31
+ {% elif layout == "layer_cache" %}
32
  // Layout: flat query [heads, headDim] plus a persistent KV cache laid out
33
+ // [layer, cacheLen, kvHeads, headDim]. The dispatch has a single implicit batch.
 
34
  {% else %}
35
  // Layout: token-major [batch, seq, heads * headDim]; Q and KV hidden strides
36
  // are compiled constants.
37
  {% endif %}
38
+ {% if (fusedQNormRope is defined and fusedQNormRope) or layout != "layer_cache" %}const HEAD_DIM: u32 = {{ headDim }}u;
39
  {% endif %}
40
  const HEAD_DIM_V4: u32 = {{ headDimV4 }}u;
41
  const Q_HEADS: u32 = {{ qNumHeads }}u;
42
  const KV_HEADS: u32 = {{ kvNumHeads }}u;
43
+ {% if layout == "bsh" %}
44
  const Q_HIDDEN_V4: u32 = {{ qHiddenV4 }}u;
45
  const KV_HIDDEN_V4: u32 = {{ kvHiddenV4 }}u;
46
+ {% elif layout == "layer_cache" %}
47
  const LAYER: u32 = {{ layer }}u;
48
  const CACHE_LEN: u32 = {{ cacheLen }}u;
49
  const ATTN_SCALE: f32 = {{ scale }};
 
97
  // calls before their shared partial storage is reused.
98
  {% set combineSubgroups = combineSubgroups is defined and combineSubgroups %}
99
  {% if combineSubgroups %}
100
+ // Cross-subgroup merge that assumes nothing about which invocations share a
101
+ // subgroup or how many subgroups there are: each subgroup's elected lane
102
+ // publishes the subgroup pair in the slot at its OWN invocation index and sets
103
+ // that index's bit in a workgroup bitmask; thread 0 then folds exactly the
104
+ // published slots, in ascending index order (the online (m, d) merge is not
105
+ // float-associative, so the order is fixed), and clears the mask for the next
106
+ // call as it reads it. Workgroup memory starts zeroed, so the mask needs no
107
+ // setup. Same three collectives as a single-subgroup reduce, two barriers.
108
  var<workgroup> partialM: array<f32, WG>;
109
  var<workgroup> partialD: array<f32, WG>;
110
+ var<workgroup> leaderMask: array<atomic<u32>, (WG + 31u) / 32u>;
111
  var<workgroup> combinedMD: vec2<f32>;
112
 
113
  // When the whole workgroup is one subgroup the subgroup reduce already covers
114
+ // it (no barriers, no shared state). `subgroup_size` is the size of the current
115
+ // subgroup and uniform, so the test is exact and may guard the barriers below.
116
  fn combine_partials(m: f32, d: f32, lidx: u32, sgSize: u32) -> vec2<f32> {
117
  let sgM = subgroupMax(m);
118
  // A lane with no elements contributes d == 0 (exact identity). A +inf
 
123
  if (sgSize == WG) {
124
  return vec2<f32>(sgM, sgD);
125
  }
 
 
 
 
 
 
 
 
 
 
 
 
126
  if (subgroupElect()) {
127
+ partialM[lidx] = sgM;
128
+ partialD[lidx] = sgD;
129
+ atomicOr(&leaderMask[lidx / 32u], 1u << (lidx % 32u));
130
  }
131
  workgroupBarrier();
132
  if (lidx == 0u) {
133
  var accM = -FLT_MAX;
134
  var accD = 0.0;
135
+ for (var w = 0u; w < (WG + 31u) / 32u; w = w + 1u) {
136
+ var bits = atomicExchange(&leaderMask[w], 0u);
137
+ while (bits != 0u) {
138
+ let slot = w * 32u + firstTrailingBit(bits);
139
+ bits = bits & (bits - 1u);
140
+ let mNew = max(accM, partialM[slot]);
141
+ accD = accD * exp_shift(accM, mNew) + partialD[slot] * exp_shift(partialM[slot], mNew);
142
+ accM = mNew;
143
+ }
144
  }
145
  combinedMD = vec2<f32>(accM, accD);
146
  }
 
155
  var<workgroup> partialD: array<f32, {{ mdExtent }}>;
156
  {% if mdStreamed %}
157
 
158
+ // In-place fold of {{ mdStreams }} streams. Input partials occupy
159
+ // partialM/partialD; stream s returns its merged pair in slot s * WG.
 
160
  fn combine_partials_streams(lidx: u32) {
161
  workgroupBarrier();
162
  var stride = WG / 2u;
 
215
  {% endif %}
216
 
217
 
218
+ {% if layout == "layer_cache" %}{% set ATTN_SCALE_OVERRIDE = "ATTN_SCALE" %}{% endif %}
219
  {% if ATTN_SCALE_DIM is not defined %}{% set ATTN_SCALE_DIM = "HEAD_DIM" %}{% endif %}
220
  fn scale_value() -> f32 {
221
  {% if ATTN_SCALE_OVERRIDE is defined %}
 
305
  {% endif %}
306
  let h = wg.y;
307
  let b = wg.z;
308
+ if (h >= Q_HEADS || split >= NUM_SPLITS{% if splitQueries %} || queryToken >= Q_SEQ{% endif %}{% if layout == "layer_cache" %} || params.past_len >= CACHE_LEN{% endif %}) {
309
  return;
310
  }
311
  let tid = lid.x;
312
  let hKv = h / (Q_HEADS / KV_HEADS);
313
+ {% if layout == "layer_cache" %}
314
  let kvSeq = params.past_len + 1u;
315
  {% else %}
316
  let cacheSeq = params.kvSeq;
 
324
  {% endif %}
325
 
326
  // Query row (decode uses token zero; short-query prefill folds the token into wg.x).
327
+ {% if layout == "bsh" %}
328
  {% if splitQueries %}
329
  let qBaseV4 = (b * Q_SEQ + queryToken) * Q_HIDDEN_V4 + h * HEAD_DIM_V4;
330
  {% else %}
 
332
  {% endif %}
333
  let kvBaseV4 = b * kvSeq * KV_HIDDEN_V4 + hKv * HEAD_DIM_V4;
334
  let kvTokenStrideV4 = KV_HIDDEN_V4;
335
+ {% elif layout == "layer_cache" %}
336
  let qBaseV4 = h * HEAD_DIM_V4;
337
  let kvBaseV4 = (LAYER * CACHE_LEN * KV_HEADS + hKv) * HEAD_DIM_V4;
338
  let kvTokenStrideV4 = KV_HEADS * HEAD_DIM_V4;
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, shared by every dense
10
- // attention op with vec4-aligned head dims. One workgroup per
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
- // Per-subgroup partials are published into a deterministic slot: the subgroup's
77
- // ordinal index within the workgroup (lidx / sgSize). The online (m, d) merge
78
- // is not float-associative, so thread 0 must fold partials in a fixed order.
79
- // Subgroups partition a workgroup into contiguous ordinal ranges on supported
80
- // backends, so the ordinal slot is unique per subgroup and every slot in
81
- // [0, subgroupCount) is written (each subgroup elects one leader).
82
- // Sized for the worst case of one partial per invocation.
 
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); otherwise subgroup leaders publish
89
- // partials through shared memory and thread 0 folds them in ordinal order.
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
- let slot = lidx / sgSize;
114
- partialM[slot] = sgM;
115
- partialD[slot] = sgD;
116
  }
117
  workgroupBarrier();
118
  if (lidx == 0u) {
119
  var accM = -FLT_MAX;
120
  var accD = 0.0;
121
- for (var i = 0u; i < subgroupCount; i = i + 1u) {
122
- let mNew = max(accM, partialM[i]);
123
- accD = accD * exp_shift(accM, mNew) + partialD[i] * exp_shift(partialM[i], mNew);
124
- accM = mNew;
 
 
 
 
 
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. The caller stores its per-thread
140
- // partials into partialM/partialD first and reads the merged pair of stream s
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
- // Omitted ONNX scale uses 1/sqrt(headDim). Explicit zero is handled by
201
- // specialization because zero is the runtime omitted-value sentinel.
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
  {% if hasWindow %}
267
  // Sliding window: query qi sits at absolute position p = kvSeq - qSeq
@@ -287,7 +269,7 @@ fn main(
287
  var score = -FLT_MAX;
288
  var m = -FLT_MAX;
289
  var dPart = 0.0;
290
- var keyAllowed = kj < keyBound{% if hasCausal %} && (params.isCausal == 0u || kj <= qi){% endif %}{% if hasWindow %} && kj >= keyFloor{% endif %};
291
  if (keyAllowed) {
292
  let kRowV4 = kvBaseV4 + kj * kvTokenStrideV4;
293
  {% if hasMask %}
 
 
 
 
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 = params.kvSeq;
 
247
  var keyFloor: u32 = 0u;
248
  {% if hasWindow %}
249
  // Sliding window: query qi sits at absolute position p = kvSeq - qSeq
 
269
  var score = -FLT_MAX;
270
  var m = -FLT_MAX;
271
  var dPart = 0.0;
272
+ var keyAllowed = kj < keyBound{% if hasWindow %} && kj >= keyFloor{% endif %};
273
  if (keyAllowed) {
274
  let kRowV4 = kvBaseV4 + kj * kvTokenStrideV4;
275
  {% if hasMask %}
build/webgpu/attn-flash-prefill-cluster.wgsl.jinja CHANGED
@@ -1,4 +1,6 @@
1
- {% set sourceProfile = source.sourceProfile if source.sourceProfile is defined else 0 %}
 
 
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 %}
@@ -14,7 +17,6 @@
14
  {% if maskIsBool is not defined %}{% set maskIsBool = false %}{% endif %}
15
  {% if hasSoftcap is not defined %}{% set hasSoftcap = false %}{% endif %}
16
  {% if hasHeadSink is not defined %}{% set hasHeadSink = false %}{% endif %}
17
- {% set Q_HIDDEN = qHidden | default(0) %}
18
  {% if quantCacheFormat is not defined %}{% set quantCacheFormat = "" %}{% endif %}
19
  {% if useSeqlens is not defined %}{% set useSeqlens = false %}{% endif %}
20
  // A windowed cache binds a fixed CAPACITY but keeps only the most recent
@@ -27,9 +29,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
@@ -44,16 +43,18 @@ enable f16;
44
  {% set MASK_ELEMENT = "maskValue" if STAGE_MASK else "attn_mask[maskIndex]" %}
45
  {% set MASK_ADDITIVE = "maskValue" if STAGE_MASK else "f32(attn_mask[maskIndex])" %}
46
  {% set SLICE_COUNT = ((headDimV4 / LPQ) | int) %}
 
 
47
  {% set ROPE_LANE_XOR = ((LPQ / 2) | int) %}
48
- {% set QL = source.qLayout if source.qLayout is defined else source.layout %}
49
- {% set KL = source.kvLayout if source.kvLayout is defined else source.layout %}
50
  {% set RIGHT = causalRightAlign is defined and causalRightAlign %}
51
 
52
  // Tiled flash prefill attention with configurable-width query clusters for
53
  // token-major [batch, seq, heads*headDim] attention ops. Each workgroup covers
54
  // TILE_Q queries with one LPQ-lane cluster per query; K/V are staged in
55
- // workgroup memory once per TILE_K tile. Within a cluster, each lane holds q/o
56
- // register slices of HEAD_DIM/LPQ dimensions. Per-lane dot fragments are
57
  // combined with subgroup shuffles when available, or shared-memory reductions
58
  // on compatibility variants. Dot products, online softmax, and weighted-value
59
  // sums accumulate in f32. Optional masks are broadcast-strided; the
@@ -68,7 +69,8 @@ const HEAD_DIM: u32 = {{ headDim }}u;
68
  const HEAD_DIM_V4: u32 = {{ headDimV4 }}u;
69
  {% if QL != "bhsd" %}
70
  const Q_HIDDEN_V4: u32 = {{ qHiddenV4 }}u; // bsh token-major hidden stride
71
- {% endif %}
 
72
  {% if KL != "bhsd" %}
73
  const KV_HIDDEN_V4: u32 = {{ kvHiddenV4 }}u;
74
  {% endif %}
@@ -83,7 +85,12 @@ const HALF_DIM: u32 = HEAD_DIM / 2u;
83
  const HALF_LPQ: u32 = LPQ / 2u;
84
  {% endif %}
85
  const TILE_K: u32 = {{ TILE_K }}u;
 
 
 
 
86
  const WG: u32 = TILE_Q * LPQ;
 
87
  {% if MASK_IS_INT %}
88
  // Key-keep masks in contrib attention use a finite low logit for a rejected
89
  // key. Logical ONNX bool masks use the exclusion sentinel instead, so a fully
@@ -116,8 +123,12 @@ var<workgroup> red: array<f32, WG>;
116
 
117
  {% if ATTN_SCALE_DIM is not defined %}{% set ATTN_SCALE_DIM = "HEAD_DIM" %}{% endif %}
118
  fn scale_value() -> f32 {
 
 
 
119
  if (params.scale != 0.0) { return params.scale; }
120
  return inverseSqrt(f32({{ ATTN_SCALE_DIM }}));
 
121
  }
122
 
123
  {% if quantCacheFormat %}
@@ -162,13 +173,6 @@ fn load_{{ kind }}4(indexV4: u32, d4: u32, hk: u32) -> vec4<f32> {
162
  {{ emit_quant_load4(quantCacheFormat, "value", VALUE, "v_scale") }}
163
  {% endif %}
164
 
165
- {% if hasBias %}
166
- fn load_bias4(base: u32, d4: u32) -> vec4<f32> {
167
- let offset = base + d4 * 4u;
168
- return vec4<f32>(bias[offset], bias[offset + 1u], bias[offset + 2u], bias[offset + 3u]);
169
- }
170
-
171
- {% endif %}
172
  @compute @workgroup_size(WG, 1, 1)
173
  fn main(
174
  @builtin(workgroup_id) wg: vec3<u32>,
@@ -184,74 +188,85 @@ fn main(
184
  // portability.
185
  if (sgSize < LPQ || sgSize % LPQ != 0u) { return; }
186
  {% endif %}
 
 
 
187
  let qSub = tid / LPQ;
 
188
  let lane8 = tid % LPQ;
189
- let qIdx = wg.x * TILE_Q + qSub;
190
- let qValid = qIdx < {{ QSEQ }} && h < Q_HEADS;
 
 
191
  let hKv = h / (Q_HEADS / KV_HEADS);
192
  let SCALE = scale_value();
193
 
194
- // Per-thread q slice + output accumulator (SLICE vec4s each) in registers.
195
- let qClamped = min(qIdx, {{ QSEQ }} - 1u);
 
196
  {% if QL == "bhsd" %}
197
- let qBase4 = ((b * Q_HEADS + h) * {{ QSEQ }} + qClamped) * HEAD_DIM_V4 + lane8 * SLICE;
198
  {% else %}
199
- let qBase4 = (b * {{ QSEQ }} + qClamped) * {{ Q_STRIDE }} + h * HEAD_DIM_V4 + lane8 * SLICE;
200
  {% endif %}
201
  {% for c in range(SLICE_COUNT) %}
202
- var qr{{ c }} = vec4<f32>({{ QUERY }}[qBase4 + {{ c }}u]);
203
- {% if hasBias %}
204
- qr{{ c }} = qr{{ c }} + load_bias4(h * HEAD_DIM, lane8 * SLICE + {{ c }}u);
205
- {% endif %}
206
- var o{{ c }} = vec4<f32>(0.0);
207
  {% endfor %}
208
  {% if FUSED_ROTARY %}
209
- // The present-cache keys are already rotary transformed. Transform Q once in
210
- // registers before the K loop, avoiding a separate full-size Q scratch pass.
211
- // NeoX half-split pairs lanes separated by LPQ/2; subgroup variants exchange
212
- // the paired register directly, while the portable path reloads that vec4.
213
  {% if not useSubgroups %}
214
  let qHeadBase4 = qBase4 - lane8 * SLICE;
215
  let pairedLane = (lane8 + HALF_LPQ) % LPQ;
216
  {% endif %}
217
  let ropeChunk = (lane8 % HALF_LPQ) * SLICE;
218
  let pastLenForRope = select(0u, {{ KVSEQ }} - {{ QSEQ }}, {{ KVSEQ }} >= {{ QSEQ }});
219
- let ropePosition = pastLenForRope + qClamped;
 
220
  {% for c in range(SLICE_COUNT) %}
221
  {% if useSubgroups %}
222
- let pairedQ{{ c }} = subgroupShuffleXor(qr{{ c }}, {{ ROPE_LANE_XOR }}u);
223
  {% else %}
224
- let pairedQ{{ c }} = vec4<f32>({{ QUERY }}[qHeadBase4 + pairedLane * SLICE + {{ c }}u]);
225
- {% endif %}
226
- let ropeBase{{ c }} = ropePosition * HALF_DIM + (ropeChunk + {{ c }}u) * 4u;
227
- let ropeCos{{ c }} = vec4<f32>(
228
- f32(cos_cache[ropeBase{{ c }}]),
229
- f32(cos_cache[ropeBase{{ c }} + 1u]),
230
- f32(cos_cache[ropeBase{{ c }} + 2u]),
231
- f32(cos_cache[ropeBase{{ c }} + 3u])
232
  );
233
- let ropeSin{{ c }} = vec4<f32>(
234
- f32(sin_cache[ropeBase{{ c }}]),
235
- f32(sin_cache[ropeBase{{ c }} + 1u]),
236
- f32(sin_cache[ropeBase{{ c }} + 2u]),
237
- f32(sin_cache[ropeBase{{ c }} + 3u])
238
  );
239
- qr{{ c }} = select(
240
- qr{{ c }} * ropeCos{{ c }} + pairedQ{{ c }} * ropeSin{{ c }},
241
- qr{{ c }} * ropeCos{{ c }} - pairedQ{{ c }} * ropeSin{{ c }},
242
  lane8 < HALF_LPQ
243
  );
244
  {% endfor %}
 
245
  {% endif %}
246
- var m: f32 = NEG_INF;
247
- var l: f32 = 0.0;
 
 
248
  {% if useSeqlens %}
249
 
250
  // Resident rows of a windowed cache: the survivors were shifted down to [0, kvActive),
251
  // so query/key DISTANCE is unchanged and every bound below reads as if the cache were
252
  // exactly kvActive long. Rotary is excluded from this path (it would need the absolute
253
  // position, not the cache-relative one), so pastLenForRope keeps the physical length.
254
- let kvActive = min({{ KVSEQ }}, u32(seqlens_k[b]) + 1u);
 
 
 
255
  {% endif %}
256
  // Causal ceiling per query; the key loop runs over the workgroup's union range
257
  // (uniform trip count), masking out-of-range (query, key) pairs.
@@ -267,16 +282,18 @@ fn main(
267
  {% if RIGHT or hasWindow %}
268
  let pastLen = select(0u, {{ KVA }} - {{ QSEQ }}, {{ KVA }} >= {{ QSEQ }});
269
  {% endif %}
 
270
  {% if hasCausal %}
271
  {% if RIGHT %}
272
- var maxKjV = select({{ KVA }}, min(pastLen + qIdx + 1u, {{ KVA }}), {{ IS_CAUSAL }} != 0u);
273
  {% else %}
274
- var maxKjV = select({{ KVA }}, min(qIdx + 1u, {{ KVA }}), {{ IS_CAUSAL }} != 0u);
275
  {% endif %}
276
  {% else %}
277
- var maxKjV = {{ KVA }};
278
  {% endif %}
279
- var minKjV: u32 = 0u;
 
280
  let lastQ = min(wg.x * TILE_Q + TILE_Q - 1u, {{ QSEQ }} - 1u);
281
  {% if hasCausal %}
282
  {% if RIGHT %}
@@ -289,16 +306,20 @@ fn main(
289
  {% endif %}
290
  var wgStartV: u32 = 0u;
291
  {% if hasWindow %}
292
- let qAbsP1 = pastLen + qIdx + 1u;
293
- maxKjV = min(maxKjV, qAbsP1);
294
- if (qAbsP1 > params.windowSize) { minKjV = qAbsP1 - params.windowSize; }
 
 
295
  let lastQAbsP1 = pastLen + lastQ + 1u;
296
  wgEndV = min(wgEndV, lastQAbsP1);
297
  let firstQAbsP1 = pastLen + wg.x * TILE_Q + 1u;
298
  if (firstQAbsP1 > params.windowSize) { wgStartV = firstQAbsP1 - params.windowSize; }
299
  {% endif %}
300
- let maxKj = maxKjV;
301
- let minKj = minKjV;
 
 
302
  let wgEnd = wgEndV;
303
  let wgStart = wgStartV;
304
  {% else %}
@@ -306,6 +327,10 @@ fn main(
306
  let minKj: u32 = 0u;
307
  let wgEnd = {{ KVA }};
308
  let wgStart: u32 = 0u;
 
 
 
 
309
  {% endif %}
310
  {% if KL == "bhsd" %}
311
  {% if quantCacheFormat == "int4" %}
@@ -322,7 +347,7 @@ fn main(
322
  loop {
323
  if (kStart >= wgEnd) { break; }
324
 
325
- // Cooperative K/V tile load (vec4-coalesced; OOB keys zero-filled).
326
  workgroupBarrier();
327
  for (var i: u32 = tid; i < TILE_K * HEAD_DIM_V4; i = i + WG) {
328
  let slot = i / HEAD_DIM_V4;
@@ -362,8 +387,7 @@ fn main(
362
  }
363
  {% endif %}
364
  workgroupBarrier();
365
- // TILE_K remains a small constant-trip loop: compilers can unroll it without
366
- // multiplying source size, while the q/o register slices remain named.
367
  {% if not useSubgroups and batchNoSgReduction %}
368
  // First publish every key's partial dot without intervening barriers.
369
  var s: array<f32, TILE_K>;
@@ -418,6 +442,67 @@ fn main(
418
  {% endif %}
419
  }
420
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
  {% else %}
422
  var s: array<f32, TILE_K>;
423
  for (var kk: u32 = 0u; kk < TILE_K; kk = kk + 1u) {
@@ -480,30 +565,55 @@ fn main(
480
 
481
  // Per-thread online softmax over the tile. s[kk] is reused to hold the
482
  // exponentiated probabilities for the PV accumulation below.
483
- var tileMax: f32 = s[0];
 
484
  for (var kk: u32 = 1u; kk < TILE_K; kk = kk + 1u) {
485
- tileMax = max(tileMax, s[kk]);
486
  }
487
- let newMax = max(m, tileMax);
488
- let corr = select(exp(m - newMax), 0.0, m == NEG_INF);
489
- var pSum: f32 = 0.0;
490
  for (var kk: u32 = 0u; kk < TILE_K; kk = kk + 1u) {
491
- let pk = select(0.0, exp(s[kk] - newMax), s[kk] != NEG_INF);
492
- s[kk] = pk;
493
- pSum = pSum + pk;
494
  }
495
- l = l * corr + pSum;
496
- m = newMax;
497
- // A boundary tile holds keys outside this query's attended range, and their staged V rows can
498
- // be anything a dynamic-rows prefill legitimately leaves pad rows unwritten, so stale NaN is
499
- // reachable. An excluded key's weight is exactly 0.0, but 0.0 * NaN is NaN, so the multiply
500
- // cannot be trusted to drop it: the guarded loop selects the V operand away instead. Interior
501
- // tiles keep the unguarded FMA chain; the guard changes nothing there anyway (for finite v,
502
- // acc + 0.0 * v == acc bit-for-bit), it only costs the extra select.
503
- // Scope: only RANGE exclusion (minKj/maxKj) needs this. Every op that binds
504
- // a mask supplies materialized K/V, so a mask-rejected key's exact zero
505
- // weight multiplies finite data. If a masked op ever grows a producer that
506
- // skips rows, that argument dies and the mask path needs the guard.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
507
  let tileInterior = kStart >= minKj && kStart + TILE_K <= maxKj;
508
  {% for c in range(SLICE_COUNT) %}
509
  {
@@ -520,35 +630,38 @@ fn main(
520
  o{{ c }} = acc;
521
  }
522
  {% endfor %}
 
523
 
524
  kStart = kStart + TILE_K;
525
  }
526
 
527
- if (qValid) {
 
 
528
  {% if QL == "bhsd" %}
529
- let outBase4 = ((b * Q_HEADS + h) * {{ QSEQ }} + qIdx) * HEAD_DIM_V4 + lane8 * SLICE;
530
  {% else %}
531
- let outBase4 = (b * {{ QSEQ }} + qIdx) * Q_HIDDEN_V4 + h * HEAD_DIM_V4 + lane8 * SLICE;
532
  {% endif %}
533
  {% if hasHeadSink %}
534
  // The head sink is a learned logit that competes with the keys but carries
535
  // no value, so it enters the denominator only and the weighted sum above is
536
- // untouched. Renormalizing against max(m, sink) keeps the exponentials in
537
  // range when the sink dominates a fully-masked row.
538
  let sink = f32(head_sink[h]);
539
- let finalM = max(m, sink);
540
- let accScale = exp(m - finalM);
541
- let inv = accScale / (exp(sink - finalM) + l * accScale);
542
  {% else %}
543
- // l == 0 means this query had no probability-bearing key: either its
544
  // causal/window range is empty or its logical bool mask rejects every key.
545
  // Emit 0 rather than 0/0. Other attention paths enforce the same empty-row
546
  // contract by selecting on positive global mass.
547
- let inv = select(0.0, 1.0 / l, l > 0.0);
548
  {% endif %}
549
- {% macro attention_value(c) %}o{{ c }} * inv{% if hasBias %} + load_bias4(2u * {{ Q_HIDDEN }}u + h * HEAD_DIM, lane8 * SLICE + {{ c }}u){% endif %}{% endmacro %}
550
  {% for c in range(SLICE_COUNT) %}
551
- {{ OUTPUT }}[outBase4 + {{ c }}u] = vec4<{{ scalar }}>({{ attention_value(c) }});
552
  {% endfor %}
553
  }
 
554
  }
 
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 %}
 
17
  {% if maskIsBool is not defined %}{% set maskIsBool = false %}{% endif %}
18
  {% if hasSoftcap is not defined %}{% set hasSoftcap = false %}{% endif %}
19
  {% if hasHeadSink is not defined %}{% set hasHeadSink = false %}{% endif %}
 
20
  {% if quantCacheFormat is not defined %}{% set quantCacheFormat = "" %}{% endif %}
21
  {% if useSeqlens is not defined %}{% set useSeqlens = false %}{% endif %}
22
  // A windowed cache binds a fixed CAPACITY but keeps only the most recent
 
29
  {% if useSubgroups %}
30
  enable subgroups;
31
  {% endif %}
 
 
 
32
  {{ env.wgsl.resourceDeclarations }}
33
  {% set ST = "f16" if usesF16 else "f32" %}
34
  // 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
  {% set ROPE_LANE_XOR = ((LPQ / 2) | int) %}
49
+ {% set QL = qLayout if qLayout is defined else layout %}
50
+ {% set KL = kvLayout if kvLayout is defined else layout %}
51
  {% set RIGHT = causalRightAlign is defined and causalRightAlign %}
52
 
53
  // Tiled flash prefill attention with configurable-width query clusters for
54
  // token-major [batch, seq, heads*headDim] attention ops. Each workgroup covers
55
  // TILE_Q queries with one LPQ-lane cluster per query; K/V are staged in
56
+ // workgroup memory once per TILE_K tile. Within a cluster, each lane holds named
57
+ // q/o slices of HEAD_DIM/LPQ dimensions. Per-lane dot fragments are
58
  // combined with subgroup shuffles when available, or shared-memory reductions
59
  // on compatibility variants. Dot products, online softmax, and weighted-value
60
  // sums accumulate in f32. Optional masks are broadcast-strided; the
 
69
  const HEAD_DIM_V4: u32 = {{ headDimV4 }}u;
70
  {% if QL != "bhsd" %}
71
  const Q_HIDDEN_V4: u32 = {{ qHiddenV4 }}u; // bsh token-major hidden stride
72
+ {% if sourceProfile == 1 %}const QKV_STRIDE_V4: u32 = {{ qkvStrideV4 }}u; // packed [Q; K; V] input row stride
73
+ {% endif %}{% endif %}
74
  {% if KL != "bhsd" %}
75
  const KV_HIDDEN_V4: u32 = {{ kvHiddenV4 }}u;
76
  {% endif %}
 
85
  const HALF_LPQ: u32 = LPQ / 2u;
86
  {% endif %}
87
  const TILE_K: u32 = {{ TILE_K }}u;
88
+ {% if QPL > 1 %}
89
+ const QPL: u32 = {{ QPL }}u;
90
+ const WG: u32 = (TILE_Q / QPL) * LPQ;
91
+ {% else %}
92
  const WG: u32 = TILE_Q * LPQ;
93
+ {% endif %}
94
  {% if MASK_IS_INT %}
95
  // Key-keep masks in contrib attention use a finite low logit for a rejected
96
  // key. Logical ONNX bool masks use the exclusion sentinel instead, so a fully
 
123
 
124
  {% if ATTN_SCALE_DIM is not defined %}{% set ATTN_SCALE_DIM = "HEAD_DIM" %}{% endif %}
125
  fn scale_value() -> f32 {
126
+ {% if ATTN_SCALE_OVERRIDE is defined %}
127
+ return {{ ATTN_SCALE_OVERRIDE }};
128
+ {% else %}
129
  if (params.scale != 0.0) { return params.scale; }
130
  return inverseSqrt(f32({{ ATTN_SCALE_DIM }}));
131
+ {% endif %}
132
  }
133
 
134
  {% if quantCacheFormat %}
 
173
  {{ emit_quant_load4(quantCacheFormat, "value", VALUE, "v_scale") }}
174
  {% endif %}
175
 
 
 
 
 
 
 
 
176
  @compute @workgroup_size(WG, 1, 1)
177
  fn main(
178
  @builtin(workgroup_id) wg: vec3<u32>,
 
188
  // portability.
189
  if (sgSize < LPQ || sgSize % LPQ != 0u) { return; }
190
  {% endif %}
191
+ {% if QPL > 1 %}
192
+ let qSub = (tid / LPQ) * QPL;
193
+ {% else %}
194
  let qSub = tid / LPQ;
195
+ {% endif %}
196
  let lane8 = tid % LPQ;
197
+ {% for qi in range(QPL) %}
198
+ let {{ qn("qIdx", qi) }} = wg.x * TILE_Q + qSub{% if qi > 0 %} + {{ qi }}u{% endif %};
199
+ let {{ qn("qValid", qi) }} = {{ qn("qIdx", qi) }} < {{ QSEQ }} && h < Q_HEADS;
200
+ {% endfor %}
201
  let hKv = h / (Q_HEADS / KV_HEADS);
202
  let SCALE = scale_value();
203
 
204
+ // Per-thread q slice and output accumulator, each stored in SLICE named vec4 values.
205
+ {% for qi in range(QPL) %}
206
+ let {{ qn("qClamped", qi) }} = min({{ qn("qIdx", qi) }}, {{ QSEQ }} - 1u);
207
  {% if QL == "bhsd" %}
208
+ let {{ qn("qBase4", qi) }} = ((b * Q_HEADS + h) * {{ QSEQ }} + {{ qn("qClamped", qi) }}) * HEAD_DIM_V4 + lane8 * SLICE;
209
  {% else %}
210
+ let {{ qn("qBase4", qi) }} = (b * {{ QSEQ }} + {{ qn("qClamped", qi) }}) * {{ Q_STRIDE }} + h * HEAD_DIM_V4 + lane8 * SLICE;
211
  {% endif %}
212
  {% for c in range(SLICE_COUNT) %}
213
+ var {{ qn("qr" ~ c, qi) }} = vec4<f32>({{ QUERY }}[{{ qn("qBase4", qi) }} + {{ c }}u]);
214
+ var {{ qn("o" ~ c, qi) }} = vec4<f32>(0.0);
215
+ {% endfor %}
 
 
216
  {% endfor %}
217
  {% if FUSED_ROTARY %}
218
+ // The present-cache keys are already rotary transformed. Transform Q once
219
+ // before the K loop, avoiding a separate full-size Q scratch pass. NeoX
220
+ // half-split pairs lanes separated by LPQ/2.
 
221
  {% if not useSubgroups %}
222
  let qHeadBase4 = qBase4 - lane8 * SLICE;
223
  let pairedLane = (lane8 + HALF_LPQ) % LPQ;
224
  {% endif %}
225
  let ropeChunk = (lane8 % HALF_LPQ) * SLICE;
226
  let pastLenForRope = select(0u, {{ KVSEQ }} - {{ QSEQ }}, {{ KVSEQ }} >= {{ QSEQ }});
227
+ {% for qi in range(QPL) %}
228
+ let {{ qn("ropePosition", qi) }} = pastLenForRope + {{ qn("qClamped", qi) }};
229
  {% for c in range(SLICE_COUNT) %}
230
  {% if useSubgroups %}
231
+ let {{ qn("pairedQ" ~ c, qi) }} = subgroupShuffleXor({{ qn("qr" ~ c, qi) }}, {{ ROPE_LANE_XOR }}u);
232
  {% else %}
233
+ let {{ qn("pairedQ" ~ c, qi) }} = vec4<f32>({{ QUERY }}[qHeadBase4 + pairedLane * SLICE + {{ c }}u]);
234
+ {% endif %}
235
+ let {{ qn("ropeBase" ~ c, qi) }} = {{ qn("ropePosition", qi) }} * HALF_DIM + (ropeChunk + {{ c }}u) * 4u;
236
+ let {{ qn("ropeCos" ~ c, qi) }} = vec4<f32>(
237
+ f32(cos_cache[{{ qn("ropeBase" ~ c, qi) }}]),
238
+ f32(cos_cache[{{ qn("ropeBase" ~ c, qi) }} + 1u]),
239
+ f32(cos_cache[{{ qn("ropeBase" ~ c, qi) }} + 2u]),
240
+ f32(cos_cache[{{ qn("ropeBase" ~ c, qi) }} + 3u])
241
  );
242
+ let {{ qn("ropeSin" ~ c, qi) }} = vec4<f32>(
243
+ f32(sin_cache[{{ qn("ropeBase" ~ c, qi) }}]),
244
+ f32(sin_cache[{{ qn("ropeBase" ~ c, qi) }} + 1u]),
245
+ f32(sin_cache[{{ qn("ropeBase" ~ c, qi) }} + 2u]),
246
+ f32(sin_cache[{{ qn("ropeBase" ~ c, qi) }} + 3u])
247
  );
248
+ {{ qn("qr" ~ c, qi) }} = select(
249
+ {{ qn("qr" ~ c, qi) }} * {{ qn("ropeCos" ~ c, qi) }} + {{ qn("pairedQ" ~ c, qi) }} * {{ qn("ropeSin" ~ c, qi) }},
250
+ {{ qn("qr" ~ c, qi) }} * {{ qn("ropeCos" ~ c, qi) }} - {{ qn("pairedQ" ~ c, qi) }} * {{ qn("ropeSin" ~ c, qi) }},
251
  lane8 < HALF_LPQ
252
  );
253
  {% endfor %}
254
+ {% endfor %}
255
  {% endif %}
256
+ {% for qi in range(QPL) %}
257
+ var {{ qn("m", qi) }}: f32 = NEG_INF;
258
+ var {{ qn("l", qi) }}: f32 = 0.0;
259
+ {% endfor %}
260
  {% if useSeqlens %}
261
 
262
  // Resident rows of a windowed cache: the survivors were shifted down to [0, kvActive),
263
  // so query/key DISTANCE is unchanged and every bound below reads as if the cache were
264
  // exactly kvActive long. Rotary is excluded from this path (it would need the absolute
265
  // position, not the cache-relative one), so pastLenForRope keeps the physical length.
266
+ // Clamping below by the query count matches the cache-update passes and the scalar
267
+ // path: a right-padded batch (seqlens_k[b]+1 < qSeq) still appends its whole chunk,
268
+ // so its queries score against all of it.
269
+ let kvActive = min({{ KVSEQ }}, max({{ QSEQ }}, u32(seqlens_k[b]) + 1u));
270
  {% endif %}
271
  // Causal ceiling per query; the key loop runs over the workgroup's union range
272
  // (uniform trip count), masking out-of-range (query, key) pairs.
 
282
  {% if RIGHT or hasWindow %}
283
  let pastLen = select(0u, {{ KVA }} - {{ QSEQ }}, {{ KVA }} >= {{ QSEQ }});
284
  {% endif %}
285
+ {% for qi in range(QPL) %}
286
  {% if hasCausal %}
287
  {% if RIGHT %}
288
+ var {{ qn("maxKjV", qi) }} = select({{ KVA }}, min(pastLen + {{ qn("qIdx", qi) }} + 1u, {{ KVA }}), {{ IS_CAUSAL }} != 0u);
289
  {% else %}
290
+ var {{ qn("maxKjV", qi) }} = select({{ KVA }}, min({{ qn("qIdx", qi) }} + 1u, {{ KVA }}), {{ IS_CAUSAL }} != 0u);
291
  {% endif %}
292
  {% else %}
293
+ var {{ qn("maxKjV", qi) }} = {{ KVA }};
294
  {% endif %}
295
+ var {{ qn("minKjV", qi) }}: u32 = 0u;
296
+ {% endfor %}
297
  let lastQ = min(wg.x * TILE_Q + TILE_Q - 1u, {{ QSEQ }} - 1u);
298
  {% if hasCausal %}
299
  {% if RIGHT %}
 
306
  {% endif %}
307
  var wgStartV: u32 = 0u;
308
  {% if hasWindow %}
309
+ {% for qi in range(QPL) %}
310
+ let {{ qn("qAbsP1", qi) }} = pastLen + {{ qn("qIdx", qi) }} + 1u;
311
+ {{ qn("maxKjV", qi) }} = min({{ qn("maxKjV", qi) }}, {{ qn("qAbsP1", qi) }});
312
+ if ({{ qn("qAbsP1", qi) }} > params.windowSize) { {{ qn("minKjV", qi) }} = {{ qn("qAbsP1", qi) }} - params.windowSize; }
313
+ {% endfor %}
314
  let lastQAbsP1 = pastLen + lastQ + 1u;
315
  wgEndV = min(wgEndV, lastQAbsP1);
316
  let firstQAbsP1 = pastLen + wg.x * TILE_Q + 1u;
317
  if (firstQAbsP1 > params.windowSize) { wgStartV = firstQAbsP1 - params.windowSize; }
318
  {% endif %}
319
+ {% for qi in range(QPL) %}
320
+ let {{ qn("maxKj", qi) }} = {{ qn("maxKjV", qi) }};
321
+ let {{ qn("minKj", qi) }} = {{ qn("minKjV", qi) }};
322
+ {% endfor %}
323
  let wgEnd = wgEndV;
324
  let wgStart = wgStartV;
325
  {% else %}
 
327
  let minKj: u32 = 0u;
328
  let wgEnd = {{ KVA }};
329
  let wgStart: u32 = 0u;
330
+ {% for qi in range(1, QPL) %}
331
+ let {{ qn("maxKj", qi) }} = maxKj;
332
+ let {{ qn("minKj", qi) }} = minKj;
333
+ {% endfor %}
334
  {% endif %}
335
  {% if KL == "bhsd" %}
336
  {% if quantCacheFormat == "int4" %}
 
347
  loop {
348
  if (kStart >= wgEnd) { break; }
349
 
350
+ // Cooperative K/V tile load with out-of-bounds keys zero-filled.
351
  workgroupBarrier();
352
  for (var i: u32 = tid; i < TILE_K * HEAD_DIM_V4; i = i + WG) {
353
  let slot = i / HEAD_DIM_V4;
 
387
  }
388
  {% endif %}
389
  workgroupBarrier();
390
+ // TILE_K is a small shader constant; the loop updates the named q/o slices in place.
 
391
  {% if not useSubgroups and batchNoSgReduction %}
392
  // First publish every key's partial dot without intervening barriers.
393
  var s: array<f32, TILE_K>;
 
442
  {% endif %}
443
  }
444
  }
445
+ {% elif QPL > 1 %}
446
+ {% for qi in range(QPL) %}
447
+ var {{ qn("s", qi) }}: array<f32, TILE_K>;
448
+ {% endfor %}
449
+ for (var kk: u32 = 0u; kk < TILE_K; kk = kk + 1u) {
450
+ let kj = kStart + kk;
451
+ {% for qi in range(QPL) %}
452
+ {{ qn("s", qi) }}[kk] = NEG_INF;
453
+ var {{ qn("part", qi) }}: f32 = 0.0;
454
+ {% endfor %}
455
+ let kb = kk * HEAD_DIM_V4 + lane8 * SLICE;
456
+ {% for c in range(SLICE_COUNT) %}
457
+ let kw{{ c }} = vec4<f32>(k_tile[kb + {{ c }}u]);
458
+ {% for qi in range(QPL) %}
459
+ {{ qn("part", qi) }} = {{ qn("part", qi) }} + dot({{ qn("qr" ~ c, qi) }}, kw{{ c }});
460
+ {% endfor %}
461
+ {% endfor %}
462
+ // One butterfly carries every query's partial: the shuffles cost the same
463
+ // whether the payload is one score or QPL of them.
464
+ var partv = vec{{ QPL }}<f32>({% for qi in range(QPL) %}{{ qn("part", qi) }}{% if not loop.last %}, {% endif %}{% endfor %});
465
+ partv = partv + subgroupShuffleXor(partv, 1u);
466
+ {% for shift in [2, 4] %}
467
+ {% if LPQ > shift %}
468
+ partv = partv + subgroupShuffleXor(partv, {{ shift }}u);
469
+ {% endif %}
470
+ {% endfor %}
471
+ {% for qi in range(QPL) %}
472
+ {
473
+ let sc = partv[{{ qi }}];
474
+ if (kj >= {{ qn("minKj", qi) }} && kj < {{ qn("maxKj", qi) }}) {
475
+ {% if hasMask %}
476
+ {% if STAGE_MASK %}
477
+ let maskValue = mask_tile[(qSub + {{ qi }}u) * TILE_K + kk];
478
+ {% else %}
479
+ // Broadcast-strided mask address (a 0 stride collapses that axis; rank-2
480
+ // [q, k] masks set batch/head strides to 0). {{ qn("qClamped", qi) }} keeps the seq index
481
+ // in-bounds for padding queries in the last tile (their output is dropped).
482
+ let maskIndex = b * params.maskBatchStride + h * params.maskHeadStride + {{ qn("qClamped", qi) }} * params.maskSeqStride + kj;
483
+ {% endif %}
484
+ {% if maskIsKeyKeep %}
485
+ // A broadcast key mask uses 1 for a retained key and 0 for padding.
486
+ {{ qn("s", qi) }}[kk] = {{ score_expr("sc") }} + (1.0 - f32({{ MASK_ELEMENT }})) * MASK_NEG;
487
+ {% elif maskIsBool %}
488
+ // Logical bool: a rejected key contributes no softmax mass. Leaving
489
+ // the initialized NEG_INF sentinel in place makes a fully masked row
490
+ // land on the zero-denominator output guard below.
491
+ if ({{ MASK_ELEMENT }} != 0u) {
492
+ {{ qn("s", qi) }}[kk] = {{ score_expr("sc") }};
493
+ } else {
494
+ {{ qn("s", qi) }}[kk] = MASK_NEG;
495
+ }
496
+ {% else %}
497
+ {{ qn("s", qi) }}[kk] = {{ score_expr("sc") }} + {{ MASK_ADDITIVE }};
498
+ {% endif %}
499
+ {% else %}
500
+ {{ qn("s", qi) }}[kk] = {{ score_expr("sc") }};
501
+ {% endif %}
502
+ }
503
+ }
504
+ {% endfor %}
505
+ }
506
  {% else %}
507
  var s: array<f32, TILE_K>;
508
  for (var kk: u32 = 0u; kk < TILE_K; kk = kk + 1u) {
 
565
 
566
  // Per-thread online softmax over the tile. s[kk] is reused to hold the
567
  // exponentiated probabilities for the PV accumulation below.
568
+ {% for qi in range(QPL) %}
569
+ var {{ qn("tileMax", qi) }}: f32 = {{ qn("s", qi) }}[0];
570
  for (var kk: u32 = 1u; kk < TILE_K; kk = kk + 1u) {
571
+ {{ qn("tileMax", qi) }} = max({{ qn("tileMax", qi) }}, {{ qn("s", qi) }}[kk]);
572
  }
573
+ let {{ qn("newMax", qi) }} = max({{ qn("m", qi) }}, {{ qn("tileMax", qi) }});
574
+ let {{ qn("corr", qi) }} = select(exp({{ qn("m", qi) }} - {{ qn("newMax", qi) }}), 0.0, {{ qn("m", qi) }} == NEG_INF);
575
+ var {{ qn("pSum", qi) }}: f32 = 0.0;
576
  for (var kk: u32 = 0u; kk < TILE_K; kk = kk + 1u) {
577
+ let pk = select(0.0, exp({{ qn("s", qi) }}[kk] - {{ qn("newMax", qi) }}), {{ qn("s", qi) }}[kk] != NEG_INF);
578
+ {{ qn("s", qi) }}[kk] = pk;
579
+ {{ qn("pSum", qi) }} = {{ qn("pSum", qi) }} + pk;
580
  }
581
+ {{ qn("l", qi) }} = {{ qn("l", qi) }} * {{ qn("corr", qi) }} + {{ qn("pSum", qi) }};
582
+ {{ qn("m", qi) }} = {{ qn("newMax", qi) }};
583
+ {% endfor %}
584
+ // A boundary tile can address V rows outside a query's attended range, and
585
+ // a dynamic-row producer may leave those rows unwritten. Because 0 * NaN is
586
+ // NaN, the guarded loop selects the V operand away for range-excluded keys.
587
+ // Interior tiles use the unguarded FMA chain. Mask exclusion applies to
588
+ // materialized V rows and does not require this range guard.
589
+ {% if QPL > 1 %}
590
+ let tileInterior = {% for qi in range(QPL) %}(kStart >= {{ qn("minKj", qi) }} && kStart + TILE_K <= {{ qn("maxKj", qi) }}){% if not loop.last %} && {% endif %}{% endfor %};
591
+ {% for c in range(SLICE_COUNT) %}
592
+ {
593
+ {% for qi in range(QPL) %}
594
+ var {{ qn("acc", qi) }} = {{ qn("o" ~ c, qi) }} * {{ qn("corr", qi) }};
595
+ {% endfor %}
596
+ if (tileInterior) {
597
+ for (var kk: u32 = 0u; kk < TILE_K; kk = kk + 1u) {
598
+ let vw = vec4<f32>(v_tile[kk * HEAD_DIM_V4 + lane8 * SLICE + {{ c }}u]);
599
+ {% for qi in range(QPL) %}
600
+ {{ qn("acc", qi) }} = {{ qn("acc", qi) }} + {{ qn("s", qi) }}[kk] * vw;
601
+ {% endfor %}
602
+ }
603
+ } else {
604
+ for (var kk: u32 = 0u; kk < TILE_K; kk = kk + 1u) {
605
+ let vw = vec4<f32>(v_tile[kk * HEAD_DIM_V4 + lane8 * SLICE + {{ c }}u]);
606
+ {% for qi in range(QPL) %}
607
+ {{ qn("acc", qi) }} = {{ qn("acc", qi) }} + {{ qn("s", qi) }}[kk] * select(vec4<f32>(), vw, {{ qn("s", qi) }}[kk] != 0.0);
608
+ {% endfor %}
609
+ }
610
+ }
611
+ {% for qi in range(QPL) %}
612
+ {{ qn("o" ~ c, qi) }} = {{ qn("acc", qi) }};
613
+ {% endfor %}
614
+ }
615
+ {% endfor %}
616
+ {% else %}
617
  let tileInterior = kStart >= minKj && kStart + TILE_K <= maxKj;
618
  {% for c in range(SLICE_COUNT) %}
619
  {
 
630
  o{{ c }} = acc;
631
  }
632
  {% endfor %}
633
+ {% endif %}
634
 
635
  kStart = kStart + TILE_K;
636
  }
637
 
638
+ {% macro attention_value(c, qi) %}{{ qn("o" ~ c, qi) }} * inv{% endmacro %}
639
+ {% for qi in range(QPL) %}
640
+ if ({{ qn("qValid", qi) }}) {
641
  {% if QL == "bhsd" %}
642
+ let outBase4 = ((b * Q_HEADS + h) * {{ QSEQ }} + {{ qn("qIdx", qi) }}) * HEAD_DIM_V4 + lane8 * SLICE;
643
  {% else %}
644
+ let outBase4 = (b * {{ QSEQ }} + {{ qn("qIdx", qi) }}) * Q_HIDDEN_V4 + h * HEAD_DIM_V4 + lane8 * SLICE;
645
  {% endif %}
646
  {% if hasHeadSink %}
647
  // The head sink is a learned logit that competes with the keys but carries
648
  // no value, so it enters the denominator only and the weighted sum above is
649
+ // untouched. Renormalizing against max({{ qn("m", qi) }}, sink) keeps the exponentials in
650
  // range when the sink dominates a fully-masked row.
651
  let sink = f32(head_sink[h]);
652
+ let finalM = max({{ qn("m", qi) }}, sink);
653
+ let accScale = exp({{ qn("m", qi) }} - finalM);
654
+ let inv = accScale / (exp(sink - finalM) + {{ qn("l", qi) }} * accScale);
655
  {% else %}
656
+ // {{ qn("l", qi) }} == 0 means this query had no probability-bearing key: either its
657
  // causal/window range is empty or its logical bool mask rejects every key.
658
  // Emit 0 rather than 0/0. Other attention paths enforce the same empty-row
659
  // contract by selecting on positive global mass.
660
+ let inv = select(0.0, 1.0 / {{ qn("l", qi) }}, {{ qn("l", qi) }} > 0.0);
661
  {% endif %}
 
662
  {% for c in range(SLICE_COUNT) %}
663
+ {{ OUTPUT }}[outBase4 + {{ c }}u] = vec4<{{ scalar }}>({{ attention_value(c, qi) }});
664
  {% endfor %}
665
  }
666
+ {% endfor %}
667
  }
build/webgpu/attn-flash-q32-broadcast.wgsl.jinja CHANGED
@@ -9,12 +9,8 @@
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 = source.useSubgroups if source.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 usesF16 %}
16
- enable f16;
17
- {% endif %}
18
  {% if USE_SUBGROUPS %}
19
  enable subgroups;
20
  {% endif %}
@@ -25,9 +21,6 @@ const Q_HEADS: u32 = {{ qNumHeads }}u;
25
  const KV_HEADS: u32 = {{ kvNumHeads }}u;
26
  const K_STEP: u32 = {{ kStep }}u;
27
  const Q_STEP: u32 = {{ Q_STEP }}u;
28
- {% if hasBias %}
29
- const HEAD_DIM: u32 = {{ headDim }}u;
30
- {% endif %}
31
  const Q_HIDDEN_V4: u32 = Q_HEADS * HEAD_DIM_V4;
32
  const KV_HIDDEN_V4: u32 = KV_HEADS * HEAD_DIM_V4;
33
  const NEG_INF: f32 = -3.4028234663852886e38;
@@ -50,12 +43,6 @@ fn scale_value() -> f32 {
50
  }
51
 
52
 
53
- {% if hasBias %}
54
- fn load_bias4(base: u32, d4: u32) -> vec4<f32> {
55
- let offset = base + d4 * 4u;
56
- return vec4<f32>(bias[offset], bias[offset + 1u], bias[offset + 2u], bias[offset + 3u]);
57
- }
58
- {% endif %}
59
 
60
  @compute @workgroup_size({{ Q_STEP }}, 1, 1)
61
  fn main(
@@ -86,9 +73,6 @@ fn main(
86
  for (var d4: u32 = 0u; d4 < HEAD_DIM_V4; d4 = d4 + 1u) {
87
  if (valid_q) {
88
  q_tile[d4] = query[qBaseV4 + d4];
89
- {% if hasBias %}
90
- q_tile[d4] = vec4<{{ ST }}>(vec4<f32>(q_tile[d4]) + load_bias4(h * HEAD_DIM, d4));
91
- {% endif %}
92
  } else {
93
  q_tile[d4] = vec4<{{ ST }}>(0.0);
94
  }
@@ -101,14 +85,8 @@ fn main(
101
  // Causal key ceiling. The key-loop bound (kvEnd) uses the workgroup's LAST query
102
  // so every lane shares a uniform trip count (subgroup ops stay reconverged);
103
  // each lane masks its own keys past myMaxKj to NEG_INF.
104
- {% if CAUSAL %}
105
- let lastQ = min(wg.x * Q_STEP + Q_STEP - 1u, params.qSeq - 1u);
106
- let kvEnd = select(params.kvSeq, min(lastQ + 1u, params.kvSeq), params.isCausal != 0u);
107
- let myMaxKj = select(params.kvSeq, min(qi + 1u, params.kvSeq), params.isCausal != 0u);
108
- {% else %}
109
  let kvEnd = params.kvSeq;
110
  let myMaxKj = params.kvSeq;
111
- {% endif %}
112
  let kvBase = b * params.kvSeq * KV_HIDDEN_V4 + hKv * HEAD_DIM_V4;
113
  let kvKeyStride = KV_HIDDEN_V4;
114
 
@@ -236,11 +214,7 @@ fn main(
236
  if (valid_q) {
237
  let outBaseV4 = (b * params.qSeq + qi) * Q_HIDDEN_V4 + h * HEAD_DIM_V4;
238
  for (var d4: u32 = 0u; d4 < HEAD_DIM_V4; d4 = d4 + 1u) {
239
- {% if hasBias %}
240
- output[outBaseV4 + d4] = vec4<{{ scalar }}>(o_tile[d4] + load_bias4(2u * {{ qHidden }}u + h * HEAD_DIM, d4));
241
- {% else %}
242
  output[outBaseV4 + d4] = vec4<{{ scalar }}>(o_tile[d4]);
243
- {% endif %}
244
  }
245
  }
246
  }
 
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
  {% if USE_SUBGROUPS %}
15
  enable subgroups;
16
  {% endif %}
 
21
  const KV_HEADS: u32 = {{ kvNumHeads }}u;
22
  const K_STEP: u32 = {{ kStep }}u;
23
  const Q_STEP: u32 = {{ Q_STEP }}u;
 
 
 
24
  const Q_HIDDEN_V4: u32 = Q_HEADS * HEAD_DIM_V4;
25
  const KV_HIDDEN_V4: u32 = KV_HEADS * HEAD_DIM_V4;
26
  const NEG_INF: f32 = -3.4028234663852886e38;
 
43
  }
44
 
45
 
 
 
 
 
 
 
46
 
47
  @compute @workgroup_size({{ Q_STEP }}, 1, 1)
48
  fn main(
 
73
  for (var d4: u32 = 0u; d4 < HEAD_DIM_V4; d4 = d4 + 1u) {
74
  if (valid_q) {
75
  q_tile[d4] = query[qBaseV4 + d4];
 
 
 
76
  } else {
77
  q_tile[d4] = vec4<{{ ST }}>(0.0);
78
  }
 
85
  // Causal key ceiling. The key-loop bound (kvEnd) uses the workgroup's LAST query
86
  // so every lane shares a uniform trip count (subgroup ops stay reconverged);
87
  // each lane masks its own keys past myMaxKj to NEG_INF.
 
 
 
 
 
88
  let kvEnd = params.kvSeq;
89
  let myMaxKj = params.kvSeq;
 
90
  let kvBase = b * params.kvSeq * KV_HIDDEN_V4 + hKv * HEAD_DIM_V4;
91
  let kvKeyStride = KV_HIDDEN_V4;
92
 
 
214
  if (valid_q) {
215
  let outBaseV4 = (b * params.qSeq + qi) * Q_HIDDEN_V4 + h * HEAD_DIM_V4;
216
  for (var d4: u32 = 0u; d4 < HEAD_DIM_V4; d4 = d4 + 1u) {
 
 
 
217
  output[outBaseV4 + d4] = vec4<{{ scalar }}>(o_tile[d4]);
 
218
  }
219
  }
220
  }
build/webgpu/attn-materialized-rowstats-combine-f32.wgsl.jinja CHANGED
@@ -43,10 +43,9 @@ fn exp_shift(value: f32, maxValue: f32) -> f32 {
43
 
44
  @compute @workgroup_size(WG, 1, 1)
45
  fn main(
46
- @builtin(global_invocation_id) gid: vec3<u32>,
47
- @builtin(num_workgroups) nwg: vec3<u32>
48
  ) {
49
- let row = gid.x + gid.y * nwg.x * WG;
50
  if (row >= params.rows) { return; }
51
 
52
  // `row` already runs over (batch, head, query) together, and the partial
 
43
 
44
  @compute @workgroup_size(WG, 1, 1)
45
  fn main(
46
+ @builtin(global_invocation_id) gid: vec3<u32>
 
47
  ) {
48
+ let row = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * WG;
49
  if (row >= params.rows) { return; }
50
 
51
  // `row` already runs over (batch, head, query) together, and the partial
build/webgpu/attn-materialized-sgmat-f32.wgsl.jinja CHANGED
@@ -9,22 +9,23 @@ 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 = source.layout | default("bsh") %}
15
  {% set headMajor = layout == "bhsd" %}
16
- {% set kvHeadMajor = (source.kvLayout | default(layout)) == "bhsd" %}
17
- {% set CAUSAL_UPPER_LEFT = source.causalUpperLeft is defined and source.causalUpperLeft %}
18
- {% set CAUSAL = (source.causalRightAlign is defined and source.causalRightAlign) or CAUSAL_UPPER_LEFT %}
19
- {% macro q_index(row, d) %}(b * params.qSeq + {{ row }}) * HIDDEN + h * HEAD_DIM + {{ d }}{% endmacro %}
20
  {% 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 %}
21
  {% set OUT_ROW_STRIDE = "HEAD_DIM" if headMajor else "HIDDEN" %}
22
  {% set KV_ROW_STRIDE = "HEAD_DIM" if kvHeadMajor else "KV_HIDDEN" %}
23
- {% set scorePhase = source.phase == "score" %}
24
- {% set SCORE_BIAS = scorePhase and source.scoreBias is defined and source.scoreBias %}
25
- {% set SCORE_WINDOW = CAUSAL and source.scoreWindow is defined and source.scoreWindow %}
26
- {% set USE_SEQLENS = source.useSeqlens is defined and source.useSeqlens %}
27
- {% set FUSED_SOFTMAX = source.fusedSoftmax is defined and source.fusedSoftmax %}
28
  {% set PRIVATE_ROW_STATS = FUSED_SOFTMAX and (materializedSgmatPrivateRowStats is defined and materializedSgmatPrivateRowStats) %}
29
  {% macro score_value(index, guard) %}
30
  {% if FUSED_SOFTMAX %}
@@ -49,7 +50,7 @@ select(0.0, scores[{{ index }}], {{ guard[1] }})
49
  {% set SUBGROUP_COUNT = SUBGROUP_ROWS * SUBGROUP_COLS %}
50
  {% set WORKGROUP_THREADS = SUBGROUP_COUNT * 32 %}
51
  {% if hasBias is not defined %}{% set hasBias = false %}{% endif %}
52
- {% set EMIT_ROW_STATS = source.emitRowStats is defined and source.emitRowStats %}
53
  {% set DIRECT_SCORE_STORE = materializedSgmatDirectScoreStore and not EMIT_ROW_STATS %}
54
  {% set DIRECT_APPLY_STORE = materializedSgmatDirectApplyStore and not hasBias and MT == "f32" %}
55
  {% set DIRECT_OUTPUT_STORE = DIRECT_SCORE_STORE if scorePhase else DIRECT_APPLY_STORE %}
@@ -58,17 +59,14 @@ select(0.0, scores[{{ index }}], {{ guard[1] }})
58
  and (scorePhase or (not hasBias and MT == "f32"))
59
  and not EMIT_ROW_STATS %}
60
  {% set ANY_DIRECT_STORE = DIRECT_OUTPUT_STORE or RUNTIME_DIRECT_STORE %}
61
- {% set SCALE_IN_Q = DIRECT_SCORE_STORE or (RUNTIME_DIRECT_STORE and scorePhase) %}
62
- {% macro q_tile_value(index) %}{% if hasBias %}(query[{{ index }}] + bias[h * HEAD_DIM + k]){% else %}query[{{ index }}]{% endif %}{% if SCALE_IN_Q %} * score_scale{% endif %}{% endmacro %}
63
 
64
  const HEADS: u32 = {{ qNumHeads }}u;
65
- {% if kvNumHeads is defined %}
66
  const KV_HEADS: u32 = {{ kvNumHeads }}u;
67
- {% else %}
68
- const KV_HEADS: u32 = HEADS;
69
- {% endif %}
70
  const HEAD_DIM: u32 = {{ headDim }}u;
 
71
  const HIDDEN: u32 = {{ qHidden }}u;
 
72
  {% if hasBias %}
73
  /* Packed [Q; K; V] bias. Q folds into the query tile before the GEMM. K is
74
  * omitted: expanding (q + bq).(k + bk) leaves a (q + bq).bk term that is
@@ -158,10 +156,10 @@ fn main(
158
  let base_A = subtile_idy * SUB_ROWS;
159
  let base_B = subtile_idx * SUB_COLS;
160
  {% if USE_SEQLENS %}
161
- // Rows the cache-update pass left resident: the survivors live in
162
- // [0, kv_active), and every causal/window bound below uses this live length
163
- // while params.kvSeq keeps the allocated capacity for strides.
164
- let kv_active = min(params.kvSeq, u32(seqlens_k[b]) + 1u);
165
  {% endif %}
166
  {% if CAUSAL and scorePhase %}
167
  // Workgroup-uniform causal skip: a key tile starting past the last row's
@@ -172,7 +170,8 @@ fn main(
172
  // the same way: later rows only move the floor further right.
173
  {% endif %}
174
  let kv_causal_off = i32({% if USE_SEQLENS %}kv_active{% else %}params.kvSeq{% endif %}) - i32(params.qSeq);
175
- if (i32(n_base) > kv_causal_off + i32(m_base + TILE_M) - 1{% if SCORE_WINDOW %}
 
176
  || i32(n_base + TILE_N) <= kv_causal_off + i32(m_base) + 1 - i32(params.windowSize){% endif %}) {
177
  for (var idx = li; idx < TILE_M * {{ SUBGROUP_COLS }}u; idx += {{ WORKGROUP_THREADS }}u) {
178
  let stat_row = m_base + idx / {{ SUBGROUP_COLS }}u;
@@ -195,11 +194,6 @@ fn main(
195
 
196
  {% if scorePhase %}
197
  let inner = HEAD_DIM;
198
- {% if SCALE_IN_Q %}
199
- // The direct store has no epilogue, so apply the score scale to Q. Direct and
200
- // guarded store paths then share one pre-scaled query tile.
201
- let score_scale = {{ attentionScaleExpression }};
202
- {% endif %}
203
  {% else %}
204
  let inner = {% if USE_SEQLENS %}kv_active{% else %}params.kvSeq{% endif %};
205
  {% if CAUSAL %}
@@ -240,7 +234,7 @@ fn main(
240
  {% endif %}
241
  {% endif %}
242
  for (var k_base = {% if SCORE_WINDOW and not scorePhase %}inner_start{% else %}0u{% endif %}; k_base < {% if CAUSAL and not scorePhase %}inner_bound{% else %}inner{% endif %}; k_base += TILE_K) {
243
- {% if source.phase == "apply" and not FUSED_SOFTMAX and MT == "f32" %}
244
  // Full interior PV tiles can be loaded directly from storage. Query,
245
  // reduction, and output-dimension tails use the guarded shared path below.
246
  if (
@@ -278,10 +272,8 @@ fn main(
278
  {% endif %}
279
 
280
  {% if TILE_M_VALUE == 64 and TILE_N_VALUE == 64 and TILE_K_VALUE == 32 and WORKGROUP_THREADS == 256 %}
281
- // BM64/BN64/WG256 is the high-throughput geometry on wide devices. Four
282
- // adjacent threads each own one contiguous eight-element segment of a
283
- // row, preserving the original coalesced load schedule while retaining
284
- // the generalized K-tail guards.
285
  let a_row = li / 4u;
286
  let a_col = (li % 4u) * 8u;
287
  for (var i = 0u; i < 8u; i++) {
@@ -353,10 +345,10 @@ fn main(
353
  let loaded =
354
  {{ "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 "" }};
355
  {% else %}
356
- var loaded = {{ "0.0h" if MT == "f16" else "0.0" }};
357
  if (row < params.qSeq && k < params.kvSeq) {
358
  let score_base = (b * HEADS + h) * params.qSeq * params.kvSeq;
359
- loaded = {{ "f16(" if MT == "f16" else "" }}scores[score_base + row * params.kvSeq + k]{{ ")" if MT == "f16" else "" }};
360
  }
361
  {% endif %}
362
  tile_A[idx] = loaded;
@@ -412,18 +404,8 @@ fn main(
412
  }
413
 
414
  {% if ANY_DIRECT_STORE %}
415
- {% if RUNTIME_DIRECT_STORE %}
416
- // Workgroup-uniform (both bases come from workgroup_id alone): an interior tile
417
- // publishes through the subgroup-matrix collectives and is done; only an edge
418
- // tile falls through to the scratch round trip and its barriers.
419
- if (
420
- m_base + TILE_M <= params.qSeq &&
421
- n_base + TILE_N <= {% if scorePhase %}params.kvSeq{% else %}HEAD_DIM{% endif %}
422
- ) {
423
- {% else %}
424
  // Every dispatched tile is interior, so the matrices can go straight to
425
  // storage without the compact scratch/readback epilogue.
426
- {% endif %}
427
  {% for row_block in range(ROW_BLOCKS) %}
428
  {% for col_block in range(COL_BLOCKS) %}
429
  {% if scorePhase %}
@@ -445,10 +427,6 @@ fn main(
445
  {% endif %}
446
  {% endfor %}
447
  {% endfor %}
448
- {% if RUNTIME_DIRECT_STORE %}
449
- return;
450
- }
451
- {% endif %}
452
  {% endif %}
453
  {% if not DIRECT_OUTPUT_STORE %}
454
  let row_in_block = lane / 4u;
@@ -489,9 +467,7 @@ fn main(
489
  + row_in_block * 8u + col_in_block + pair
490
  ];
491
  {% if scorePhase %}
492
- {% if not SCALE_IN_Q %}
493
  let scale = {{ attentionScaleExpression }};
494
- {% endif %}
495
  {% if CAUSAL %}
496
  var scored = result * scale;
497
  {% if SCORE_BIAS %}
@@ -502,11 +478,14 @@ fn main(
502
  ];
503
  {% endif %}
504
  if (i32(col) > kv_causal_off + i32(row)) { scored = -FLT_MAX; }
 
 
 
505
  {% if SCORE_WINDOW %}
506
  if (i32(col) + i32(params.windowSize) <= kv_causal_off + i32(row)) { scored = -FLT_MAX; }
507
  {% endif %}
508
  {% else %}
509
- let scored = result{% if not SCALE_IN_Q %} * scale{% endif %};
510
  {% endif %}
511
  scores[
512
  (b * HEADS + h) * params.qSeq * params.kvSeq + row * params.kvSeq + col
 
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
+ {% set CAUSAL_UPPER_LEFT = causalUpperLeft is defined and causalUpperLeft %}
19
+ {% set CAUSAL = (causalRightAlign is defined and causalRightAlign) or CAUSAL_UPPER_LEFT %}
20
+ {% 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 %}
21
  {% 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 %}
22
  {% set OUT_ROW_STRIDE = "HEAD_DIM" if headMajor else "HIDDEN" %}
23
  {% set KV_ROW_STRIDE = "HEAD_DIM" if kvHeadMajor else "KV_HIDDEN" %}
24
+ {% set scorePhase = phase == "score" %}
25
+ {% set SCORE_BIAS = scorePhase and scoreBias is defined and scoreBias %}
26
+ {% set SCORE_WINDOW = CAUSAL and scoreWindow is defined and scoreWindow %}
27
+ {% set USE_SEQLENS = useSeqlens is defined and useSeqlens %}
28
+ {% set FUSED_SOFTMAX = fusedSoftmax is defined and fusedSoftmax %}
29
  {% set PRIVATE_ROW_STATS = FUSED_SOFTMAX and (materializedSgmatPrivateRowStats is defined and materializedSgmatPrivateRowStats) %}
30
  {% macro score_value(index, guard) %}
31
  {% if FUSED_SOFTMAX %}
 
50
  {% set SUBGROUP_COUNT = SUBGROUP_ROWS * SUBGROUP_COLS %}
51
  {% set WORKGROUP_THREADS = SUBGROUP_COUNT * 32 %}
52
  {% if hasBias is not defined %}{% set hasBias = false %}{% endif %}
53
+ {% set EMIT_ROW_STATS = emitRowStats is defined and emitRowStats %}
54
  {% set DIRECT_SCORE_STORE = materializedSgmatDirectScoreStore and not EMIT_ROW_STATS %}
55
  {% set DIRECT_APPLY_STORE = materializedSgmatDirectApplyStore and not hasBias and MT == "f32" %}
56
  {% set DIRECT_OUTPUT_STORE = DIRECT_SCORE_STORE if scorePhase else DIRECT_APPLY_STORE %}
 
59
  and (scorePhase or (not hasBias and MT == "f32"))
60
  and not EMIT_ROW_STATS %}
61
  {% set ANY_DIRECT_STORE = DIRECT_OUTPUT_STORE or RUNTIME_DIRECT_STORE %}
62
+ {% macro q_tile_value(index) %}{% if hasBias %}(query[{{ index }}] + bias[h * HEAD_DIM + k]){% else %}query[{{ index }}]{% endif %}{% endmacro %}
 
63
 
64
  const HEADS: u32 = {{ qNumHeads }}u;
 
65
  const KV_HEADS: u32 = {{ kvNumHeads }}u;
 
 
 
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
 
156
  let base_A = subtile_idy * SUB_ROWS;
157
  let base_B = subtile_idx * SUB_COLS;
158
  {% if USE_SEQLENS %}
159
+ // Resident rows occupy [0, kv_active). Causal and window bounds use this live
160
+ // length while params.kvSeq remains the allocated stride. Clamp by qSeq so a
161
+ // right-padded batch still includes the complete appended query chunk.
162
+ let kv_active = min(params.kvSeq, max(params.qSeq, u32(seqlens_k[b]) + 1u));
163
  {% endif %}
164
  {% if CAUSAL and scorePhase %}
165
  // Workgroup-uniform causal skip: a key tile starting past the last row's
 
170
  // the same way: later rows only move the floor further right.
171
  {% endif %}
172
  let kv_causal_off = i32({% if USE_SEQLENS %}kv_active{% else %}params.kvSeq{% endif %}) - i32(params.qSeq);
173
+ if (i32(n_base) > kv_causal_off + i32(m_base + TILE_M) - 1{% if USE_SEQLENS %}
174
+ || n_base >= kv_active{% endif %}{% if SCORE_WINDOW %}
175
  || i32(n_base + TILE_N) <= kv_causal_off + i32(m_base) + 1 - i32(params.windowSize){% endif %}) {
176
  for (var idx = li; idx < TILE_M * {{ SUBGROUP_COLS }}u; idx += {{ WORKGROUP_THREADS }}u) {
177
  let stat_row = m_base + idx / {{ SUBGROUP_COLS }}u;
 
194
 
195
  {% if scorePhase %}
196
  let inner = HEAD_DIM;
 
 
 
 
 
197
  {% else %}
198
  let inner = {% if USE_SEQLENS %}kv_active{% else %}params.kvSeq{% endif %};
199
  {% if CAUSAL %}
 
234
  {% endif %}
235
  {% endif %}
236
  for (var k_base = {% if SCORE_WINDOW and not scorePhase %}inner_start{% else %}0u{% endif %}; k_base < {% if CAUSAL and not scorePhase %}inner_bound{% else %}inner{% endif %}; k_base += TILE_K) {
237
+ {% if phase == "apply" and not FUSED_SOFTMAX and MT == "f32" %}
238
  // Full interior PV tiles can be loaded directly from storage. Query,
239
  // reduction, and output-dimension tails use the guarded shared path below.
240
  if (
 
272
  {% endif %}
273
 
274
  {% if TILE_M_VALUE == 64 and TILE_N_VALUE == 64 and TILE_K_VALUE == 32 and WORKGROUP_THREADS == 256 %}
275
+ // In the BM64/BN64/BK32/WG256 geometry, four adjacent threads each own one
276
+ // contiguous eight-element segment of a row. K-tail guards remain active.
 
 
277
  let a_row = li / 4u;
278
  let a_col = (li % 4u) * 8u;
279
  for (var i = 0u; i < 8u; i++) {
 
345
  let loaded =
346
  {{ "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 "" }};
347
  {% else %}
348
+ var loaded = 0.0;
349
  if (row < params.qSeq && k < params.kvSeq) {
350
  let score_base = (b * HEADS + h) * params.qSeq * params.kvSeq;
351
+ loaded = scores[score_base + row * params.kvSeq + k];
352
  }
353
  {% endif %}
354
  tile_A[idx] = loaded;
 
404
  }
405
 
406
  {% if ANY_DIRECT_STORE %}
 
 
 
 
 
 
 
 
 
407
  // Every dispatched tile is interior, so the matrices can go straight to
408
  // storage without the compact scratch/readback epilogue.
 
409
  {% for row_block in range(ROW_BLOCKS) %}
410
  {% for col_block in range(COL_BLOCKS) %}
411
  {% if scorePhase %}
 
427
  {% endif %}
428
  {% endfor %}
429
  {% endfor %}
 
 
 
 
430
  {% endif %}
431
  {% if not DIRECT_OUTPUT_STORE %}
432
  let row_in_block = lane / 4u;
 
467
  + row_in_block * 8u + col_in_block + pair
468
  ];
469
  {% if scorePhase %}
 
470
  let scale = {{ attentionScaleExpression }};
 
471
  {% if CAUSAL %}
472
  var scored = result * scale;
473
  {% if SCORE_BIAS %}
 
478
  ];
479
  {% endif %}
480
  if (i32(col) > kv_causal_off + i32(row)) { scored = -FLT_MAX; }
481
+ {% if USE_SEQLENS %}
482
+ if (col >= kv_active) { scored = -FLT_MAX; }
483
+ {% endif %}
484
  {% if SCORE_WINDOW %}
485
  if (i32(col) + i32(params.windowSize) <= kv_causal_off + i32(row)) { scored = -FLT_MAX; }
486
  {% endif %}
487
  {% else %}
488
+ let scored = result * scale;
489
  {% endif %}
490
  scores[
491
  (b * HEADS + h) * params.qSeq * params.kvSeq + row * params.kvSeq + col
build/webgpu/attn-online-scalar.wgsl.jinja CHANGED
@@ -1,28 +1,14 @@
1
- {% if usesF16 %}
2
- enable f16;
3
- {% endif %}
4
  {{ env.wgsl.resourceDeclarations }}
5
- {% set MASK_BATCH = "batch * params.maskBatchStride + " if source.layout == "bsh" else "" %}
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. It is the portable
11
- // path for any workgroup size and device tier.
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 %}
26
  {% if hasMask %}
27
  // Additive per-score attention bias with broadcast strides: a stride of 0
28
  // collapses that axis (batch and/or head broadcast).
@@ -30,10 +16,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 +64,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 +110,6 @@ 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
  {% if hasWindow %}
142
  // local_window_size: the query at relative index
143
  // query_token sits at absolute position p = kvSeq - qSeq + query_token, so it
@@ -156,11 +128,6 @@ fn main(
156
  for (var d: u32 = tid; d < HEAD_DIM; d = d + WG) {
157
  var q_value = f32(query[qBase + d]);
158
  var k_value = f32(key[kRow + d]);
159
- {% if hasBias %}
160
- let channel = h * HEAD_DIM + d;
161
- q_value = q_value + f32(bias[channel]);
162
- k_value = k_value + f32(bias[Q_HIDDEN + channel]);
163
- {% endif %}
164
  partial_dot = partial_dot + q_value * k_value;
165
  }
166
 
@@ -186,15 +153,14 @@ fn main(
186
  let probability_numerator = exp(score - running_max);
187
  for (var d: u32 = tid; d < HEAD_DIM; d = d + WG) {
188
  var v_value = f32(value[kRow + d]);
189
- {% if hasBias %}
190
- v_value = v_value + f32(bias[2u * Q_HIDDEN + h * HEAD_DIM + d]);
191
- {% endif %}
192
  running_out[d] = running_out[d] * previous_scale + probability_numerator * v_value;
193
  }
194
  workgroupBarrier();
195
  }
196
 
197
- let inv_denom = 1.0 / running_denom;
 
 
198
  for (var d: u32 = tid; d < HEAD_DIM; d = d + WG) {
199
  output[qBase + d] = {{ outputScalar }}(running_out[d] * inv_denom);
200
  }
 
 
 
 
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 hasMask %}
13
  // Additive per-score attention bias with broadcast strides: a stride of 0
14
  // collapses that axis (batch and/or head broadcast).
 
16
  const HEAD_DIM: u32 = {{ headDim }}u;
17
  const Q_HIDDEN: u32 = {{ qHidden }}u;
18
  const KV_HIDDEN: u32 = {{ kvHidden }}u;
 
19
  const Q_HEADS: u32 = {{ qNumHeads }}u;
20
  const KV_HEADS: u32 = {{ kvNumHeads }}u;
 
21
  {% set qHeads = "params.qHeads" if headsFromParams else "Q_HEADS" %}
22
  {% set kvHeads = "params.kvHeads" if headsFromParams else "KV_HEADS" %}
23
  {% set scale = scale | default("0.0") %}
 
64
 
65
  {{ wgsl_tree_reduce_f32("reduce_sum", "add", "partial", "WG") }}
66
 
 
67
  {% if ATTN_SCALE_DIM is not defined %}{% set ATTN_SCALE_DIM = "HEAD_DIM" %}{% endif %}
68
  fn scale_value() -> f32 {
69
  if (params.scale != 0.0) { return params.scale; }
70
  return inverseSqrt(f32({{ ATTN_SCALE_DIM }}));
71
  }
72
 
 
 
 
 
 
73
 
74
  @compute @workgroup_size(WG, 1, 1)
75
  fn main(
 
110
 
111
  var maxKj = params.kvSeq;
112
  var minKj: u32 = 0u;
 
 
 
 
 
 
113
  {% if hasWindow %}
114
  // local_window_size: the query at relative index
115
  // query_token sits at absolute position p = kvSeq - qSeq + query_token, so it
 
128
  for (var d: u32 = tid; d < HEAD_DIM; d = d + WG) {
129
  var q_value = f32(query[qBase + d]);
130
  var k_value = f32(key[kRow + d]);
 
 
 
 
 
131
  partial_dot = partial_dot + q_value * k_value;
132
  }
133
 
 
153
  let probability_numerator = exp(score - running_max);
154
  for (var d: u32 = tid; d < HEAD_DIM; d = d + WG) {
155
  var v_value = f32(value[kRow + d]);
 
 
 
156
  running_out[d] = running_out[d] * previous_scale + probability_numerator * v_value;
157
  }
158
  workgroupBarrier();
159
  }
160
 
161
+ // A query whose every key is masked to -inf (or whose key range is empty)
162
+ // leaves running_denom at zero; emit a zero row rather than 0 * (1/0) = NaN.
163
+ let inv_denom = select(0.0, 1.0 / running_denom, running_denom > 0.0);
164
  for (var d: u32 = tid; d < HEAD_DIM; d = d + WG) {
165
  output[qBase + d] = {{ outputScalar }}(running_out[d] * inv_denom);
166
  }
build/webgpu/bench.json CHANGED
@@ -1,5 +1,4 @@
1
  {
2
- "op": "com.microsoft.GroupQueryAttention",
3
  "tunableSpace": {
4
  "PREFILL_QUERY_TILE": [16, 32],
5
  "PREFILL_LANES_PER_QUERY": [4, 8],
@@ -134,7 +133,7 @@
134
  ]
135
  },
136
  "provenance": {
137
- "notes": "Long-context decode beyond the MAX_SPLITS boundary: ceilDiv(kvSeq, QKV_SPLIT_TILE_K)=64 exceeds the cap of 16. Paired with the headDim-32 control, this case checks the tradeoff between additional split parallelism and merge overhead without assuming that one flat cap is optimal for both geometries."
138
  }
139
  },
140
  {
@@ -711,9 +710,7 @@
711
  {
712
  "name": "quant-int8-prefill-h32kv8-d128-s512-pathology",
713
  "preset": "stress",
714
- "provenance": {
715
- "notes": "Production-sized prefill for the int8 KV-cache contract, retained as a performance witness for quantized-cache attention."
716
- },
717
  "attrs": {
718
  "num_heads": 32,
719
  "kv_num_heads": 8,
@@ -768,9 +765,7 @@
768
  {
769
  "name": "quant-int4-prefill-h32kv8-d128-s512-pathology",
770
  "preset": "stress",
771
- "provenance": {
772
- "notes": "Production-sized prefill for the packed int4 KV-cache contract, retained as a performance witness for quantized-cache attention."
773
- },
774
  "attrs": {
775
  "num_heads": 32,
776
  "kv_num_heads": 8,
@@ -1048,7 +1043,7 @@
1048
  "name": "headsink-prefill-h32kv8-d128-s512-generic-pathology",
1049
  "preset": "stress",
1050
  "provenance": {
1051
- "notes": "Llama-sized GQA prefill with smooth-softmax head sinks, measuring the additional score transformation on a production attention shape."
1052
  },
1053
  "attrs": { "num_heads": 32, "kv_num_heads": 8 },
1054
  "inputs": {
@@ -1091,7 +1086,7 @@
1091
  "name": "bias-headsink-prefill-h32kv8-d128-s512-generic-pathology",
1092
  "preset": "stress",
1093
  "provenance": {
1094
- "notes": "The valid additive-bias plus smooth-softmax head-sink combination has a distinct manifest variant but no flash-prefill specialization."
1095
  },
1096
  "attrs": { "num_heads": 32, "kv_num_heads": 8 },
1097
  "inputs": {
@@ -1138,9 +1133,7 @@
1138
  {
1139
  "name": "softcap-prefill-h32kv8-d128-s512-generic-pathology",
1140
  "preset": "stress",
1141
- "provenance": {
1142
- "notes": "Production prefill with model-used score soft-capping, measuring the capped-score semantic on the past-KV contract."
1143
- },
1144
  "attrs": { "num_heads": 32, "kv_num_heads": 8, "softcap": 30 },
1145
  "inputs": {
1146
  "queryT": {
 
1
  {
 
2
  "tunableSpace": {
3
  "PREFILL_QUERY_TILE": [16, 32],
4
  "PREFILL_LANES_PER_QUERY": [4, 8],
 
133
  ]
134
  },
135
  "provenance": {
136
+ "notes": "Long-context decode yields 64 candidate K partitions, exceeding the 16-partition cap and exercising capped split parallelism plus its merge at head size 128."
137
  }
138
  },
139
  {
 
710
  {
711
  "name": "quant-int8-prefill-h32kv8-d128-s512-pathology",
712
  "preset": "stress",
713
+ "provenance": { "notes": "A production-sized prefill exercises the int8 KV-cache attention contract." },
 
 
714
  "attrs": {
715
  "num_heads": 32,
716
  "kv_num_heads": 8,
 
765
  {
766
  "name": "quant-int4-prefill-h32kv8-d128-s512-pathology",
767
  "preset": "stress",
768
+ "provenance": { "notes": "A production-sized prefill exercises the packed int4 KV-cache attention contract." },
 
 
769
  "attrs": {
770
  "num_heads": 32,
771
  "kv_num_heads": 8,
 
1043
  "name": "headsink-prefill-h32kv8-d128-s512-generic-pathology",
1044
  "preset": "stress",
1045
  "provenance": {
1046
+ "notes": "A Llama-sized GQA prefill exercises smooth-softmax head sinks on a production attention shape."
1047
  },
1048
  "attrs": { "num_heads": 32, "kv_num_heads": 8 },
1049
  "inputs": {
 
1086
  "name": "bias-headsink-prefill-h32kv8-d128-s512-generic-pathology",
1087
  "preset": "stress",
1088
  "provenance": {
1089
+ "notes": "A production prefill combining additive bias with smooth-softmax head sinks exercises the generic attention route."
1090
  },
1091
  "attrs": { "num_heads": 32, "kv_num_heads": 8 },
1092
  "inputs": {
 
1133
  {
1134
  "name": "softcap-prefill-h32kv8-d128-s512-generic-pathology",
1135
  "preset": "stress",
1136
+ "provenance": { "notes": "A production prefill exercises model-used score soft-capping on the past-KV contract." },
 
 
1137
  "attrs": { "num_heads": 32, "kv_num_heads": 8, "softcap": 30 },
1138
  "inputs": {
1139
  "queryT": {
build/webgpu/gqa-attention.wgsl.jinja CHANGED
@@ -73,9 +73,9 @@ var<workgroup> m_sh: array<f32, 32u>;
73
  var<workgroup> l_sh: array<f32, 32u>;
74
 
75
  @compute @workgroup_size(WG)
76
- fn main(@builtin(workgroup_id) wid: vec3<u32>, @builtin(num_workgroups) nwg: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
77
  // One workgroup per query; fold large query grids over x/y.
78
- let qi = wid.x + wid.y * nwg.x;
79
  let total = params.batch * Q_HEADS * params.qSeq;
80
  if (qi >= total) { return; }
81
  let lane = lid.x;
@@ -178,7 +178,10 @@ fn main(@builtin(workgroup_id) wid: vec3<u32>, @builtin(num_workgroups) nwg: vec
178
  smoothTerm = exp(sink - finalM);
179
  }
180
  let accScale = exp(gm - finalM);
181
- let invDenom = accScale / (smoothTerm + glsum * accScale);
 
 
 
182
 
183
  // Cross-lane sum of the rescaled V accumulators, written cooperatively.
184
  let oBase = (b * params.qSeq + s) * Q_HIDDEN + h * HEAD_DIM;
@@ -190,13 +193,13 @@ fn main(@builtin(workgroup_id) wid: vec3<u32>, @builtin(num_workgroups) nwg: vec
190
  }
191
  {% else %}
192
  @compute @workgroup_size(WG)
193
- fn main(@builtin(workgroup_id) wid: vec3<u32>, @builtin(num_workgroups) nwg: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
194
  // One thread per query; the grid is 2D-folded so the workgroup count stays
195
- // within maxComputeWorkgroupsPerDimension for large prefills. Reconstruct the
196
- // flat thread index from the folded workgroup grid (wid.x + wid.y*nwg.x) — the
197
- // global_invocation_id only spans nwg.x*WG and would drop the y-folded tail.
198
- // This reduces to the ordinary flat index when nwg.y == 1.
199
- let qi = (wid.x + wid.y * nwg.x) * WG + lid.x;
200
  let total = params.batch * Q_HEADS * params.qSeq;
201
  if (qi >= total) { return; }
202
  let s = qi % params.qSeq;
@@ -277,7 +280,9 @@ fn main(@builtin(workgroup_id) wid: vec3<u32>, @builtin(num_workgroups) nwg: vec
277
  smoothTerm = exp(sink - finalM);
278
  }
279
  let accScale = exp(m - finalM);
280
- let invDenom = accScale / (smoothTerm + l * accScale);
 
 
281
 
282
  let oBase = (b * params.qSeq + s) * Q_HIDDEN + h * HEAD_DIM;
283
  for (var d = 0u; d < HEAD_DIM; d = d + 1u) { output[oBase + d] = {{ IO }}(acc[d] * invDenom); }
 
73
  var<workgroup> l_sh: array<f32, 32u>;
74
 
75
  @compute @workgroup_size(WG)
76
+ fn main(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
77
  // One workgroup per query; fold large query grids over x/y.
78
+ let qi = wid.x + wid.y * {{ DISPATCH_FOLD_WIDTH }}u;
79
  let total = params.batch * Q_HEADS * params.qSeq;
80
  if (qi >= total) { return; }
81
  let lane = lid.x;
 
178
  smoothTerm = exp(sink - finalM);
179
  }
180
  let accScale = exp(gm - finalM);
181
+ // A query whose every key is masked out leaves the denominator at zero (no
182
+ // head sink to carry mass); emit a zero row instead of 0 * (1/0) = NaN.
183
+ let denom = smoothTerm + glsum * accScale;
184
+ let invDenom = select(0.0, accScale / denom, denom > 0.0);
185
 
186
  // Cross-lane sum of the rescaled V accumulators, written cooperatively.
187
  let oBase = (b * params.qSeq + s) * Q_HIDDEN + h * HEAD_DIM;
 
193
  }
194
  {% else %}
195
  @compute @workgroup_size(WG)
196
+ fn main(@builtin(workgroup_id) wid: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
197
  // One thread per query; the grid is 2D-folded so the workgroup count stays
198
+ // within the per-axis dispatch fold width for large prefills. Reconstruct the flat
199
+ // thread index from the folded workgroup grid rather than from global_invocation_id,
200
+ // which only spans the clamped axis and would drop the y-folded tail. With no fold
201
+ // wid.y is zero, so this reduces to the ordinary flat index.
202
+ let qi = (wid.x + wid.y * {{ DISPATCH_FOLD_WIDTH }}u) * WG + lid.x;
203
  let total = params.batch * Q_HEADS * params.qSeq;
204
  if (qi >= total) { return; }
205
  let s = qi % params.qSeq;
 
280
  smoothTerm = exp(sink - finalM);
281
  }
282
  let accScale = exp(m - finalM);
283
+ // Same all-masked guard as the cooperative epilogue above.
284
+ let denom = smoothTerm + l * accScale;
285
+ let invDenom = select(0.0, accScale / denom, denom > 0.0);
286
 
287
  let oBase = (b * params.qSeq + s) * Q_HIDDEN + h * HEAD_DIM;
288
  for (var d = 0u; d < HEAD_DIM; d = d + 1u) { output[oBase + d] = {{ IO }}(acc[d] * invDenom); }
build/webgpu/gqa-present.wgsl.jinja CHANGED
@@ -1,5 +1,5 @@
1
- {% if source is defined and source.mode is defined %}
2
- {% set mode = source.mode %}
3
  {% elif mode is not defined %}
4
  {% set mode = "transpose" %}
5
  {% endif %}
@@ -18,7 +18,8 @@ const WG: u32 = {{ copyWorkgroupSize }}u;
18
  {% if presentVec4 %}
19
  // Transpose copy [batch, token, head*headDim] -> present [batch, head, token,
20
  // headDim]. The headDim row is contiguous on both sides and divisible by four,
21
- // so each invocation copies a vec4 along d with coalesced stores.
 
22
  const HEAD_DIM_V4: u32 = {{ headDimV4 }}u;
23
  const KV_HIDDEN_V4: u32 = {{ kvHiddenV4 }}u;
24
  {% else %}
@@ -28,12 +29,11 @@ const KV_HIDDEN: u32 = {{ kvHidden }}u;
28
 
29
  @compute @workgroup_size(WG, 1, 1)
30
  fn main(
31
- @builtin(global_invocation_id) gid: vec3<u32>,
32
- @builtin(num_workgroups) nwg: vec3<u32>
33
  ) {
34
  // The dispatch folds oversized one-dimensional grids into x/y. Rebuild the
35
  // flat invocation index; this reduces to gid.x when no fold is needed.
36
- let index = gid.x + gid.y * nwg.x * WG;
37
  {% if presentVec4 %}
38
  let total = params.batchSize * KV_HEADS * params.kvSeq * HEAD_DIM_V4;
39
  if (index >= total) {
@@ -111,11 +111,9 @@ fn vscale(d: u32, hk: u32) -> f32 { return v_scale[select(0u, hk * HEAD_DIM + d,
111
  {% endif %}
112
 
113
  @compute @workgroup_size(WG)
114
- fn main({% if needsGid %}@builtin(global_invocation_id) gid: vec3<u32>,
115
- {% endif %}{% if cooperativeMode %}@builtin(workgroup_id) wid: vec3<u32>,
116
- @builtin(local_invocation_id) lid: vec3<u32>,
117
- {% endif %}
118
- @builtin(num_workgroups) nwg: vec3<u32>) {
119
  {% macro append_cooperative_walk() %}
120
  // Compact cooperative append walk: the thread range spans only the appended
121
  // window (batch x kvHead x keySeq rows), not the full cache capacity; the
@@ -155,7 +153,7 @@ fn main({% if needsGid %}@builtin(global_invocation_id) gid: vec3<u32>,
155
  {% endmacro %}
156
  {% macro append_per_row_walk() %}
157
  // Per-row append walk: one thread streams its row's contiguous bytes.
158
- let i = gid.x + gid.y * nwg.x * WG;
159
  if (i >= params.count) { return; }
160
  let j = i % params.keySeq;
161
  let tmp = i / params.keySeq;
@@ -195,7 +193,7 @@ fn main({% if needsGid %}@builtin(global_invocation_id) gid: vec3<u32>,
195
  // cooperatively: at step s lane l touches element s*WG + l, so adjacent
196
  // lanes hit adjacent addresses on both source and destination. Same
197
  // dispatch geometry (ceil(rows/WG) workgroups, folded into x/y).
198
- let wgFlat = wid.x + wid.y * nwg.x;
199
  let elemBase = wgFlat * (WG * HEAD_DIM);
200
  let totalElems = params.count * HEAD_DIM;
201
  {% endif %}
@@ -265,7 +263,7 @@ fn main({% if needsGid %}@builtin(global_invocation_id) gid: vec3<u32>,
265
  {% endif %}
266
  {% elif mode == "copy" %}
267
  // Thread i streams one contiguous (batch, kvHead, token) row.
268
- let i = gid.x + gid.y * nwg.x * WG;
269
  if (i >= params.count) { return; }
270
  let base = i * HEAD_DIM;
271
  for (var d = 0u; d < HEAD_DIM; d = d + 1u) {
@@ -274,7 +272,7 @@ fn main({% if needsGid %}@builtin(global_invocation_id) gid: vec3<u32>,
274
  }
275
  {% elif mode == "merge" %}
276
  // Per-row merge walk.
277
- let i = gid.x + gid.y * nwg.x * WG;
278
  if (i >= params.count) { return; }
279
  let t = i % params.seq;
280
  let tmp = i / params.seq;
@@ -298,7 +296,7 @@ fn main({% if needsGid %}@builtin(global_invocation_id) gid: vec3<u32>,
298
  {% elif mode == "merge_share" %}
299
  // Past and present share the full-capacity stride, so outside-window rows
300
  // copy at the same index.
301
- let i = gid.x + gid.y * nwg.x * WG;
302
  if (i >= params.count) { return; }
303
  let t = i % params.seq;
304
  let tmp = i / params.seq;
@@ -338,7 +336,7 @@ fn main({% if needsGid %}@builtin(global_invocation_id) gid: vec3<u32>,
338
  // arithmetic. Only RoPE would need the true absolute position, which is why
339
  // `windowShiftOk` refuses a rotary request outright rather than silently
340
  // rotating at the cache row.
341
- let i = gid.x + gid.y * nwg.x * WG;
342
  if (i >= params.count) { return; }
343
  let t = i % params.seq;
344
  let tmp = i / params.seq;
@@ -377,7 +375,7 @@ fn main({% if needsGid %}@builtin(global_invocation_id) gid: vec3<u32>,
377
  {% else %}
378
  // The dispatch folds oversized one-dimensional grids into x/y. Rebuild the
379
  // flat invocation index; this reduces to gid.x when no fold is needed.
380
- let i = gid.x + gid.y * nwg.x * WG;
381
  if (i >= params.count) { return; }
382
  let t = i % params.seq;
383
  let tmp = i / params.seq;
 
1
+ {% if modeSpec is defined %}
2
+ {% set mode = modeSpec %}
3
  {% elif mode is not defined %}
4
  {% set mode = "transpose" %}
5
  {% endif %}
 
18
  {% if presentVec4 %}
19
  // Transpose copy [batch, token, head*headDim] -> present [batch, head, token,
20
  // headDim]. The headDim row is contiguous on both sides and divisible by four,
21
+ // so each invocation copies one vec4 along d and adjacent invocations write
22
+ // adjacent destination vectors.
23
  const HEAD_DIM_V4: u32 = {{ headDimV4 }}u;
24
  const KV_HIDDEN_V4: u32 = {{ kvHiddenV4 }}u;
25
  {% else %}
 
29
 
30
  @compute @workgroup_size(WG, 1, 1)
31
  fn main(
32
+ @builtin(global_invocation_id) gid: vec3<u32>
 
33
  ) {
34
  // The dispatch folds oversized one-dimensional grids into x/y. Rebuild the
35
  // flat invocation index; this reduces to gid.x when no fold is needed.
36
+ let index = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * WG;
37
  {% if presentVec4 %}
38
  let total = params.batchSize * KV_HEADS * params.kvSeq * HEAD_DIM_V4;
39
  if (index >= total) {
 
111
  {% endif %}
112
 
113
  @compute @workgroup_size(WG)
114
+ fn main({% if needsGid %}@builtin(global_invocation_id) gid: vec3<u32>{% if cooperativeMode %},
115
+ {% endif %}{% endif %}{% if cooperativeMode %}@builtin(workgroup_id) wid: vec3<u32>,
116
+ @builtin(local_invocation_id) lid: vec3<u32>{% endif %}) {
 
 
117
  {% macro append_cooperative_walk() %}
118
  // Compact cooperative append walk: the thread range spans only the appended
119
  // window (batch x kvHead x keySeq rows), not the full cache capacity; the
 
153
  {% endmacro %}
154
  {% macro append_per_row_walk() %}
155
  // Per-row append walk: one thread streams its row's contiguous bytes.
156
+ let i = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * WG;
157
  if (i >= params.count) { return; }
158
  let j = i % params.keySeq;
159
  let tmp = i / params.keySeq;
 
193
  // cooperatively: at step s lane l touches element s*WG + l, so adjacent
194
  // lanes hit adjacent addresses on both source and destination. Same
195
  // dispatch geometry (ceil(rows/WG) workgroups, folded into x/y).
196
+ let wgFlat = wid.x + wid.y * {{ DISPATCH_FOLD_WIDTH }}u;
197
  let elemBase = wgFlat * (WG * HEAD_DIM);
198
  let totalElems = params.count * HEAD_DIM;
199
  {% endif %}
 
263
  {% endif %}
264
  {% elif mode == "copy" %}
265
  // Thread i streams one contiguous (batch, kvHead, token) row.
266
+ let i = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * WG;
267
  if (i >= params.count) { return; }
268
  let base = i * HEAD_DIM;
269
  for (var d = 0u; d < HEAD_DIM; d = d + 1u) {
 
272
  }
273
  {% elif mode == "merge" %}
274
  // Per-row merge walk.
275
+ let i = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * WG;
276
  if (i >= params.count) { return; }
277
  let t = i % params.seq;
278
  let tmp = i / params.seq;
 
296
  {% elif mode == "merge_share" %}
297
  // Past and present share the full-capacity stride, so outside-window rows
298
  // copy at the same index.
299
+ let i = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * WG;
300
  if (i >= params.count) { return; }
301
  let t = i % params.seq;
302
  let tmp = i / params.seq;
 
336
  // arithmetic. Only RoPE would need the true absolute position, which is why
337
  // `windowShiftOk` refuses a rotary request outright rather than silently
338
  // rotating at the cache row.
339
+ let i = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * WG;
340
  if (i >= params.count) { return; }
341
  let t = i % params.seq;
342
  let tmp = i / params.seq;
 
375
  {% else %}
376
  // The dispatch folds oversized one-dimensional grids into x/y. Rebuild the
377
  // flat invocation index; this reduces to gid.x when no fold is needed.
378
+ let i = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * WG;
379
  if (i >= params.count) { return; }
380
  let t = i % params.seq;
381
  let tmp = i / params.seq;
build/webgpu/gqa-qprep.wgsl.jinja CHANGED
@@ -1,18 +1,12 @@
1
  {% if usesF16 is defined and usesF16 %}enable f16;
2
  {% endif %}{{ env.wgsl.resourceDeclarations }}
3
 
4
- // Q preprocessing for the flash-prefill path. One thread per (batch, qHead, qToken)
5
- // applies optional per-head qk-norm (RMS) then NeoX half-split rotary at the query's
6
- // absolute position, and writes a transformed Q (vec4, same BSH layout) that the
7
- // flash cluster then consumes unchanged. This mirrors the cooperative/threaded path's Q-prep
8
- // exactly (the present-cache K is already norm/rotary-applied, so only Q is touched),
9
- // so the flash result is bit-identical to the cooperative/threaded path.
10
- //
11
- // f16 queries are widened before cos/sin and norm/rotary arithmetic, and the
12
- // rotation itself always runs in f32 — RoPE precision matters at large
13
- // positions. The store narrows only for a consumer whose operand tiles are
14
- // f16 anyway (the subgroup-matrix route), where a wider intermediate would be
15
- // narrowed at staging regardless; every other consumer keeps the f32 output.
16
  const HEAD_DIM: u32 = {{ headDim }}u;
17
  const HEAD_DIM_V4: u32 = {{ headDim }}u / 4u;
18
  const Q_HEADS: u32 = {{ qHeads }}u;
@@ -26,11 +20,10 @@ const QK_EPS: f32 = {{ qkEps }};
26
  {% endif %}
27
 
28
  @compute @workgroup_size(WG)
29
- fn main(@builtin(global_invocation_id) gid: vec3<u32>,
30
- @builtin(num_workgroups) nwg: vec3<u32>) {
31
- // 2D-folded flat index: gid.y carries the high bits past the maxComputeWorkgroupsPerDimension
32
- // workgroup-per-dimension dispatch limit. Reduces to gid.x when nwg.y == 1.
33
- let qi = gid.x + gid.y * nwg.x * WG;
34
  let total = params.batch * Q_HEADS * params.qSeq;
35
  if (qi >= total) { return; }
36
  let s = qi % params.qSeq;
 
1
  {% if usesF16 is defined and usesF16 %}enable f16;
2
  {% endif %}{{ env.wgsl.resourceDeclarations }}
3
 
4
+ // One invocation per (batch, query head, query token) applies optional per-head
5
+ // RMS normalization followed by NeoX half-split rotary embedding at the query's
6
+ // absolute position. Present-cache keys are already transformed, so this pass
7
+ // writes only Q in BSH layout. f16 queries are widened for normalization,
8
+ // trigonometry, and rotation. The result narrows only when the following
9
+ // attention pass consumes f16 operand tiles; other configurations store f32.
 
 
 
 
 
 
10
  const HEAD_DIM: u32 = {{ headDim }}u;
11
  const HEAD_DIM_V4: u32 = {{ headDim }}u / 4u;
12
  const Q_HEADS: u32 = {{ qHeads }}u;
 
20
  {% endif %}
21
 
22
  @compute @workgroup_size(WG)
23
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
24
+ // 2D-folded flat index: gid.y carries the high bits past the per-axis dispatch fold width.
25
+ // Reduces to gid.x when the dispatch does not fold.
26
+ let qi = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * WG;
 
27
  let total = params.batch * Q_HEADS * params.qSeq;
28
  if (qi >= total) { return; }
29
  let s = qi % params.qSeq;
build/webgpu/manifest.json CHANGED
The diff for this file is too large to render. See raw diff
 
build/webgpu/metadata.json CHANGED
@@ -1,29 +1,99 @@
1
  {
2
  "name": "com.microsoft.GroupQueryAttention",
3
- "id": "_com_microsoft_groupqueryattention_webgpu_34ecc1b",
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": "EmYf/jV5lG4hmXr5wP+Qo6hZlY3LDd1aeAQhFXtDJEc=",
11
- "attn-flash-decode-splitk-merge.wgsl.jinja": "kQDmryD3bdfkboFSLPK4pF2D40t3MX2xgJrMHx031yc=",
12
- "attn-flash-decode-splitk.wgsl.jinja": "M6AWhYqX3pJ/MHz2K6Vy1rx7RjaZktzgQqB9GoY4cj0=",
13
- "attn-flash-online.wgsl.jinja": "MStTMF1/l5L9Rzx1+rPz5T+70/46n/vO/+hZQ0lbFws=",
14
- "attn-flash-prefill-cluster.wgsl.jinja": "yk1WymJIbxCy0slbiho5DS223uZH9PQ15629yvQ9RFc=",
15
- "attn-flash-q32-broadcast.wgsl.jinja": "evSdaiYgvrikYZ8hpb5m4gxf+rIryKkHOTiKgyF7ioI=",
16
- "attn-materialized-rowstats-combine-f32.wgsl.jinja": "TtTBeeeP/ufhzeNHoeLWGLxKTfFT9fiEAstnXB79j48=",
17
- "attn-materialized-sgmat-f32.wgsl.jinja": "iQeacl5PUCl75lx78qlSBaQ/syBDNxgjZg4UPGki9U0=",
18
- "attn-online-scalar.wgsl.jinja": "X4yGNCYu9+uJX6Q3EKkxV6v28CPlIYWme0Nmf8F16L8=",
19
- "bench.json": "b4ZLhmCqTm6AgiNfIiRHYVeNp4q1mx12GMcDAextGqI=",
20
- "gqa-attention.wgsl.jinja": "0EbwmdstNZMuskS170zYEo8whbQOFtupXLnNP2PaFL0=",
21
- "gqa-present.wgsl.jinja": "J9g6oAeY2BtbmWBMlmbIXbxzo4sKHdxYqcE4EAd7BYM=",
22
- "gqa-qprep.wgsl.jinja": "RCBDy7LA/yy4ayRAsnyq+7/IaXjsDGmPWidj51c+t8k=",
23
- "manifest.json": "oxSz/yE5vrd9vU5lHkrRN2kG8sdBzWQmGP9/jjRoqNU=",
24
- "test.json": "zfjYb8H4THOE5qN8K90UPdGq0lL/B2LWarbvokAz8Cg="
25
  }
26
  },
27
- "provenance": { "kernel": { "sha": "c928d21e6cc1310861cba3bafb75f5f679ecf5f3", "dirty": false } },
28
- "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/com.microsoft.GroupQueryAttention" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  }
 
1
  {
2
  "name": "com.microsoft.GroupQueryAttention",
3
+ "id": "_com_microsoft_groupqueryattention_webgpu_1ea0022",
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": "y3ubiijNd5rGJa2KVdluBdaoKYAGeqrPVd6KSppJPHw=",
12
+ "attn-flash-decode-splitk.wgsl.jinja": "xRRcDB3IGuE25xq3sG1qVPHRsESDmfA15KFVKlq5l4w=",
13
+ "attn-flash-online.wgsl.jinja": "ontnzJw9RHN7DklqqyavsAiRuKrsnWuEiU+vllbKfGE=",
14
+ "attn-flash-prefill-cluster.wgsl.jinja": "UHb5IdDeGHB/0gsQdAHcfg9FI73nyzk3zoZiIhaWbgI=",
15
+ "attn-flash-q32-broadcast.wgsl.jinja": "U/O9TNr3pXvFP+VZ2EAJLuXaWSz62De0TQEmEiU7TLc=",
16
+ "attn-materialized-rowstats-combine-f32.wgsl.jinja": "zcR02XUVlPyYeLeWH7gLOX65BcozZ1qje9hSIZSOaBA=",
17
+ "attn-materialized-sgmat-f32.wgsl.jinja": "rNruz2BmIcsQeu5eUnQZh41ftfDt/PjqnZiy2XT+9bk=",
18
+ "attn-online-scalar.wgsl.jinja": "AJGUYoMkrTHnwCPxu1gVp2CIrdHlca5PMgr9mgqukz4=",
19
+ "bench.json": "bUGLPxdrsT20K/2ptNbqVs+28i4d2W/9vOyPld+joKs=",
20
+ "gqa-attention.wgsl.jinja": "9JBHgllUq4dekL6Y1uXkyi2NoEYKjHTkHqnhktbgdzY=",
21
+ "gqa-present.wgsl.jinja": "/63mkauF/TKCsH4dxgIVlTZnREzSd/j64WGkvu6i2ls=",
22
+ "gqa-qprep.wgsl.jinja": "kRFlYxr5SdQJlbjmcF/Ii+0w56q0xhF8jyAOKPNx47I=",
23
+ "manifest.json": "U9Xgwr8xQCIMv/BNUNjiaMDyUjEVXkawKtl5Bsnn4UA=",
24
+ "test.json": "GGjXGSZThloU2mCpXKJf9lJVgddpxc8ouDX+JRD5zTs="
25
  }
26
  },
27
+ "provenance": { "kernel": { "sha": "91d990483a174128daf7673f3f37a7c890493ae1", "dirty": false } },
28
+ "webgpu": {
29
+ "manifestSpec": "2.0",
30
+ "variants": {
31
+ "qkv_present_materialized_sgmat_f32": ["attn-materialized-rowstats-combine-f32.wgsl.jinja", "attn-materialized-sgmat-f32.wgsl.jinja", "gqa-present.wgsl.jinja"],
32
+ "past_kv_materialized_sgmat_f32": ["attn-materialized-rowstats-combine-f32.wgsl.jinja", "attn-materialized-sgmat-f32.wgsl.jinja", "gqa-present.wgsl.jinja"],
33
+ "past_kv_bias_materialized_sgmat_f32": ["attn-materialized-rowstats-combine-f32.wgsl.jinja", "attn-materialized-sgmat-f32.wgsl.jinja", "gqa-present.wgsl.jinja"],
34
+ "past_kv_materialized_sgmat_f16": ["attn-materialized-rowstats-combine-f32.wgsl.jinja", "attn-materialized-sgmat-f32.wgsl.jinja", "gqa-present.wgsl.jinja"],
35
+ "past_kv_rotary_materialized_sgmat_f32": ["attn-materialized-rowstats-combine-f32.wgsl.jinja", "attn-materialized-sgmat-f32.wgsl.jinja", "gqa-present.wgsl.jinja", "gqa-qprep.wgsl.jinja"],
36
+ "past_kv_rotary_materialized_sgmat_f16": ["attn-materialized-rowstats-combine-f32.wgsl.jinja", "attn-materialized-sgmat-f32.wgsl.jinja", "gqa-present.wgsl.jinja", "gqa-qprep.wgsl.jinja"],
37
+ "new_kv_past_materialized_sgmat_f32": ["attn-materialized-rowstats-combine-f32.wgsl.jinja", "attn-materialized-sgmat-f32.wgsl.jinja", "gqa-present.wgsl.jinja"],
38
+ "window_shift_materialized_sgmat_f32": ["attn-materialized-rowstats-combine-f32.wgsl.jinja", "attn-materialized-sgmat-f32.wgsl.jinja", "gqa-present.wgsl.jinja"],
39
+ "share_append_materialized_sgmat_f32": ["attn-materialized-rowstats-combine-f32.wgsl.jinja", "attn-materialized-sgmat-f32.wgsl.jinja", "gqa-present.wgsl.jinja"],
40
+ "new_kv_share_append_split": ["gqa-attention.wgsl.jinja", "gqa-present.wgsl.jinja"],
41
+ "new_kv_share_append_headsink_split": ["gqa-attention.wgsl.jinja", "gqa-present.wgsl.jinja"],
42
+ "new_kv_share_append_rotary_split": ["gqa-attention.wgsl.jinja", "gqa-present.wgsl.jinja"],
43
+ "qkv_present_tiled_nosg": ["attention-rank4-tiled.wgsl.jinja", "gqa-present.wgsl.jinja"],
44
+ "qkv_present_flash": ["attn-flash-online.wgsl.jinja", "gqa-present.wgsl.jinja"],
45
+ "qkv_present": ["attn-online-scalar.wgsl.jinja", "gqa-present.wgsl.jinja"],
46
+ "quant_int8": ["gqa-attention.wgsl.jinja", "gqa-present.wgsl.jinja"],
47
+ "quant_int4": ["gqa-attention.wgsl.jinja", "gqa-present.wgsl.jinja"],
48
+ "quant_int8_decode_splitk": ["attn-flash-decode-splitk-merge.wgsl.jinja", "attn-flash-decode-splitk.wgsl.jinja", "gqa-present.wgsl.jinja"],
49
+ "qkv_present_flash_splitk": ["attn-flash-decode-splitk-merge.wgsl.jinja", "attn-flash-decode-splitk.wgsl.jinja", "gqa-present.wgsl.jinja"],
50
+ "qkv_present_flash_cluster": ["attn-flash-prefill-cluster.wgsl.jinja", "gqa-present.wgsl.jinja"],
51
+ "past_kv_bias_flash_prefill": ["attn-flash-prefill-cluster.wgsl.jinja", "gqa-present.wgsl.jinja"],
52
+ "past_kv_qnorm_rotary_flash_prefill": ["attn-flash-prefill-cluster.wgsl.jinja", "gqa-present.wgsl.jinja", "gqa-qprep.wgsl.jinja"],
53
+ "quant_int8_decode_splitk_nosg": ["attn-flash-decode-splitk-merge.wgsl.jinja", "attn-flash-decode-splitk.wgsl.jinja", "gqa-present.wgsl.jinja"],
54
+ "qkv_present_flash_splitk_nosg": ["attn-flash-decode-splitk-merge.wgsl.jinja", "attn-flash-decode-splitk.wgsl.jinja", "gqa-present.wgsl.jinja"],
55
+ "qkv_present_flash_cluster_nosg": ["attn-flash-prefill-cluster.wgsl.jinja", "gqa-present.wgsl.jinja"],
56
+ "past_kv_bias_flash_prefill_nosg": ["attn-flash-prefill-cluster.wgsl.jinja", "gqa-present.wgsl.jinja"],
57
+ "past_kv_qnorm_rotary_flash_prefill_nosg": ["attn-flash-prefill-cluster.wgsl.jinja", "gqa-present.wgsl.jinja", "gqa-qprep.wgsl.jinja"],
58
+ "past_kv_decode_splitk": ["attn-flash-decode-splitk-merge.wgsl.jinja", "attn-flash-decode-splitk.wgsl.jinja", "gqa-present.wgsl.jinja"],
59
+ "new_kv_past_decode_splitk": ["attn-flash-decode-splitk-merge.wgsl.jinja", "attn-flash-decode-splitk.wgsl.jinja", "gqa-present.wgsl.jinja"],
60
+ "window_shift_decode_splitk": ["attn-flash-decode-splitk-merge.wgsl.jinja", "attn-flash-decode-splitk.wgsl.jinja", "gqa-present.wgsl.jinja"],
61
+ "past_kv_decode_splitk_nosg": ["attn-flash-decode-splitk-merge.wgsl.jinja", "attn-flash-decode-splitk.wgsl.jinja", "gqa-present.wgsl.jinja"],
62
+ "new_kv_past_decode_splitk_nosg": ["attn-flash-decode-splitk-merge.wgsl.jinja", "attn-flash-decode-splitk.wgsl.jinja", "gqa-present.wgsl.jinja"],
63
+ "window_shift_decode_splitk_nosg": ["attn-flash-decode-splitk-merge.wgsl.jinja", "attn-flash-decode-splitk.wgsl.jinja", "gqa-present.wgsl.jinja"],
64
+ "past_kv_flash_prefill": ["attn-flash-prefill-cluster.wgsl.jinja", "gqa-present.wgsl.jinja"],
65
+ "new_kv_past_flash_prefill": ["attn-flash-prefill-cluster.wgsl.jinja", "gqa-present.wgsl.jinja"],
66
+ "past_kv_rotary_flash_prefill": ["attn-flash-prefill-cluster.wgsl.jinja", "gqa-present.wgsl.jinja"],
67
+ "past_kv_softcap_flash_prefill": ["attn-flash-prefill-cluster.wgsl.jinja", "gqa-present.wgsl.jinja"],
68
+ "past_kv_headsink_flash_prefill": ["attn-flash-prefill-cluster.wgsl.jinja", "gqa-present.wgsl.jinja"],
69
+ "past_kv_bias_headsink_flash_prefill": ["attn-flash-prefill-cluster.wgsl.jinja", "gqa-present.wgsl.jinja"],
70
+ "window_shift_flash_prefill": ["attn-flash-prefill-cluster.wgsl.jinja", "gqa-present.wgsl.jinja"],
71
+ "past_kv_flash_prefill_nosg": ["attn-flash-prefill-cluster.wgsl.jinja", "gqa-present.wgsl.jinja"],
72
+ "new_kv_past_flash_prefill_nosg": ["attn-flash-prefill-cluster.wgsl.jinja", "gqa-present.wgsl.jinja"],
73
+ "past_kv_rotary_flash_prefill_nosg": ["attn-flash-prefill-cluster.wgsl.jinja", "gqa-present.wgsl.jinja"],
74
+ "past_kv_softcap_flash_prefill_nosg": ["attn-flash-prefill-cluster.wgsl.jinja", "gqa-present.wgsl.jinja"],
75
+ "past_kv_headsink_flash_prefill_nosg": ["attn-flash-prefill-cluster.wgsl.jinja", "gqa-present.wgsl.jinja"],
76
+ "past_kv_bias_headsink_flash_prefill_nosg": ["attn-flash-prefill-cluster.wgsl.jinja", "gqa-present.wgsl.jinja"],
77
+ "window_shift_flash_prefill_nosg": ["attn-flash-prefill-cluster.wgsl.jinja", "gqa-present.wgsl.jinja"],
78
+ "qkv_present_flash_q32_broadcast": ["attn-flash-q32-broadcast.wgsl.jinja", "gqa-present.wgsl.jinja"],
79
+ "qkv_present_flash_q32_shared": ["attn-flash-q32-broadcast.wgsl.jinja", "gqa-present.wgsl.jinja"],
80
+ "past_kv": ["gqa-attention.wgsl.jinja", "gqa-present.wgsl.jinja"],
81
+ "past_kv_rotary": ["gqa-attention.wgsl.jinja", "gqa-present.wgsl.jinja"],
82
+ "past_kv_qnorm_rotary": ["gqa-attention.wgsl.jinja", "gqa-present.wgsl.jinja"],
83
+ "new_kv_past": ["gqa-attention.wgsl.jinja", "gqa-present.wgsl.jinja"],
84
+ "window_shift_append": ["gqa-attention.wgsl.jinja", "gqa-present.wgsl.jinja"],
85
+ "new_kv_qnorm_rotary": ["gqa-attention.wgsl.jinja", "gqa-present.wgsl.jinja"],
86
+ "past_kv_bias": ["gqa-attention.wgsl.jinja", "gqa-present.wgsl.jinja"],
87
+ "past_kv_headsink": ["gqa-attention.wgsl.jinja", "gqa-present.wgsl.jinja"],
88
+ "past_kv_bias_headsink": ["gqa-attention.wgsl.jinja", "gqa-present.wgsl.jinja"],
89
+ "quant_int8_flash_prefill": ["attn-flash-prefill-cluster.wgsl.jinja", "gqa-present.wgsl.jinja"],
90
+ "quant_int4_flash_prefill": ["attn-flash-prefill-cluster.wgsl.jinja", "gqa-present.wgsl.jinja"],
91
+ "quant_int8_flash_prefill_nosg": ["attn-flash-prefill-cluster.wgsl.jinja", "gqa-present.wgsl.jinja"],
92
+ "quant_int4_flash_prefill_nosg": ["attn-flash-prefill-cluster.wgsl.jinja", "gqa-present.wgsl.jinja"],
93
+ "share_append_split_decode_splitk": ["attn-flash-decode-splitk-merge.wgsl.jinja", "attn-flash-decode-splitk.wgsl.jinja", "gqa-present.wgsl.jinja"],
94
+ "share_append_split_decode_splitk_nosg": ["attn-flash-decode-splitk-merge.wgsl.jinja", "attn-flash-decode-splitk.wgsl.jinja", "gqa-present.wgsl.jinja"],
95
+ "share_append_split_flash_prefill": ["attn-flash-prefill-cluster.wgsl.jinja", "gqa-present.wgsl.jinja"],
96
+ "share_append_split_flash_prefill_nosg": ["attn-flash-prefill-cluster.wgsl.jinja", "gqa-present.wgsl.jinja"]
97
+ }
98
+ }
99
  }
build/webgpu/test.json CHANGED
@@ -1,5 +1,4 @@
1
  {
2
- "op": "com.microsoft.GroupQueryAttention",
3
  "fixtureArrays": {
4
  "ort_past_key_t_pattern": [0.2, 0.4, 0.6, 0.8, 1, 0.2, 0.4, 0.6, 0.8, 1, 0.2, 0.4, 0.6, 0.8, 1, 0.2, 0.4, 0.6, 0.8, 1, 0.2, 0.4, 0.6, 0.8, 1, 0.2, 0.4, 0.6, 0.8, 1, 0.2, 0.4, 0.6, 0.8, 1, 0.2, 0.4, 0.6, 0.8, 1, 0.2, 0.4, 0.6, 0.8, 1, 0.2, 0.4, 0.6, 0.8, 1, 0.2, 0.4, 0.6, 0.8, 1, 0.2, 0.4, 0.6, 0.8, 1, 0.2, 0.4, 0.6, 0.8],
5
  "ort_past_value_t_pattern": [0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3],
@@ -247,7 +246,7 @@
247
  "valueT": {
248
  "dtype": "float32",
249
  "shape": [1, 512, 64],
250
- "data": { "kind": "fillFloat32", "scale": 0.18, "sinStep": 0.23, "cosStep": 0.17 }
251
  },
252
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [511] } },
253
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [512] } }
@@ -467,7 +466,7 @@
467
  "valueT": {
468
  "dtype": "float32",
469
  "shape": [1, 2048, 128],
470
- "data": { "kind": "fillFloat32", "scale": 0.18, "sinStep": 0.023, "cosStep": 0.017 }
471
  },
472
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [2047] } },
473
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [2048] } }
@@ -495,7 +494,7 @@
495
  "valueT": {
496
  "dtype": "float32",
497
  "shape": [1, 1024, 64],
498
- "data": { "kind": "fillFloat32", "scale": 0.18, "sinStep": 0.023, "cosStep": 0.017 }
499
  },
500
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1023] } },
501
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1024] } }
@@ -523,15 +522,15 @@
523
  "valueT": {
524
  "dtype": "float16",
525
  "shape": [1, 128, 64],
526
- "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.23, "cosStep": 0.17 }
527
  },
528
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [127] } },
529
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [128] } }
530
  },
531
  "outputs": {
532
- "outputT": { "dtype": "float16", "shape": [1, 128, 128], "tolerance": 0.03 },
533
- "presentKeyT": { "dtype": "float16", "shape": [1, 1, 128, 64], "tolerance": 0.03 },
534
- "presentValueT": { "dtype": "float16", "shape": [1, 1, 128, 64], "tolerance": 0.03 }
535
  }
536
  },
537
  {
@@ -551,15 +550,15 @@
551
  "valueT": {
552
  "dtype": "float16",
553
  "shape": [1, 512, 64],
554
- "data": { "kind": "fillFloat32", "scale": 0.18, "sinStep": 0.23, "cosStep": 0.17 }
555
  },
556
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [511] } },
557
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [512] } }
558
  },
559
  "outputs": {
560
- "outputT": { "dtype": "float16", "shape": [1, 1, 256], "tolerance": 0.03 },
561
- "presentKeyT": { "dtype": "float16", "shape": [1, 1, 512, 64], "tolerance": 0.03 },
562
- "presentValueT": { "dtype": "float16", "shape": [1, 1, 512, 64], "tolerance": 0.03 }
563
  }
564
  },
565
  {
@@ -579,15 +578,15 @@
579
  "valueT": {
580
  "dtype": "float16",
581
  "shape": [1, 2048, 128],
582
- "data": { "kind": "fillFloat32", "scale": 0.18, "sinStep": 0.023, "cosStep": 0.017 }
583
  },
584
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [2047] } },
585
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [2048] } }
586
  },
587
  "outputs": {
588
- "outputT": { "dtype": "float16", "shape": [1, 1, 512], "tolerance": 0.03 },
589
- "presentKeyT": { "dtype": "float16", "shape": [1, 2, 2048, 64], "tolerance": 0.03 },
590
- "presentValueT": { "dtype": "float16", "shape": [1, 2, 2048, 64], "tolerance": 0.03 }
591
  }
592
  },
593
  {
@@ -691,7 +690,7 @@
691
  "valueT": {
692
  "dtype": "float32",
693
  "shape": [1, 1024, 128],
694
- "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.0023, "cosStep": 0.0013 }
695
  },
696
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1023] } },
697
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1024] } }
@@ -1210,7 +1209,7 @@
1210
  "vScaleT": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [0.0038802] } }
1211
  },
1212
  "outputs": {
1213
- "outputT": { "dtype": "float32", "shape": [1, 4, 16], "tolerance": 0.05 },
1214
  "presentKeyT": { "dtype": "int8", "shape": [1, 1, 4, 8], "tolerance": 0 },
1215
  "presentValueT": { "dtype": "int8", "shape": [1, 1, 4, 8], "tolerance": 0 }
1216
  }
@@ -1276,7 +1275,7 @@
1276
  }
1277
  },
1278
  "outputs": {
1279
- "outputT": { "dtype": "float32", "shape": [1, 4, 16], "tolerance": 0.05 },
1280
  "presentKeyT": { "dtype": "int8", "shape": [1, 1, 4, 8], "tolerance": 0 },
1281
  "presentValueT": { "dtype": "int8", "shape": [1, 1, 4, 8], "tolerance": 0 }
1282
  }
@@ -1381,7 +1380,7 @@
1381
  "vScaleT": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [0.0703979] } }
1382
  },
1383
  "outputs": {
1384
- "outputT": { "dtype": "float32", "shape": [1, 4, 16], "tolerance": 0.15 },
1385
  "presentKeyT": { "dtype": "uint8", "shape": [1, 1, 4, 4], "tolerance": 0 },
1386
  "presentValueT": { "dtype": "uint8", "shape": [1, 1, 4, 4], "tolerance": 0 }
1387
  }
@@ -1447,7 +1446,7 @@
1447
  }
1448
  },
1449
  "outputs": {
1450
- "outputT": { "dtype": "float32", "shape": [1, 4, 16], "tolerance": 0.15 },
1451
  "presentKeyT": { "dtype": "uint8", "shape": [1, 1, 4, 4], "tolerance": 0 },
1452
  "presentValueT": { "dtype": "uint8", "shape": [1, 1, 4, 4], "tolerance": 0 }
1453
  }
@@ -1508,7 +1507,7 @@
1508
  "vScaleT": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [0.0039328] } }
1509
  },
1510
  "outputs": {
1511
- "outputT": { "dtype": "float32", "shape": [2, 4, 64], "tolerance": 0.05 },
1512
  "presentKeyT": { "dtype": "int8", "shape": [2, 2, 4, 16], "tolerance": 0 },
1513
  "presentValueT": { "dtype": "int8", "shape": [2, 2, 4, 16], "tolerance": 0 }
1514
  }
@@ -1563,7 +1562,7 @@
1563
  "vScaleT": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [0.0039328] } }
1564
  },
1565
  "outputs": {
1566
- "outputT": { "dtype": "float32", "shape": [1, 8, 128], "tolerance": 0.05 },
1567
  "presentKeyT": { "dtype": "int8", "shape": [1, 1, 8, 64], "tolerance": 0 },
1568
  "presentValueT": { "dtype": "int8", "shape": [1, 1, 8, 64], "tolerance": 0 }
1569
  }
@@ -1624,7 +1623,7 @@
1624
  "vScaleT": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [0.0703979] } }
1625
  },
1626
  "outputs": {
1627
- "outputT": { "dtype": "float32", "shape": [1, 4, 64], "tolerance": 0.15 },
1628
  "presentKeyT": { "dtype": "uint8", "shape": [1, 1, 4, 8], "tolerance": 0 },
1629
  "presentValueT": { "dtype": "uint8", "shape": [1, 1, 4, 8], "tolerance": 0 }
1630
  }
@@ -1803,7 +1802,7 @@
1803
  "vScaleT": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [0.0038802] } }
1804
  },
1805
  "outputs": {
1806
- "outputT": { "dtype": "float32", "shape": [1, 4, 16], "tolerance": 0.05 },
1807
  "presentKeyT": { "dtype": "int8", "shape": [1, 1, 4, 8], "tolerance": 0 },
1808
  "presentValueT": { "dtype": "int8", "shape": [1, 1, 4, 8], "tolerance": 0 }
1809
  }
@@ -1849,6 +1848,102 @@
1849
  "presentValueT": { "dtype": "float32", "shape": [1, 1, 8, 8], "tolerance": 0.0001 }
1850
  }
1851
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1852
  {
1853
  "name": "ort_bias_bcast_prompt_b1q8p8_h2kv1d8",
1854
  "attrs": { "num_heads": 2, "kv_num_heads": 1 },
@@ -2002,7 +2097,7 @@
2002
  {
2003
  "name": "flashprefill_pastzero_h8kv2_d128_q16",
2004
  "provenance": {
2005
- "notes": "Compact companion for bench case sharedkv-prefill-32h8kv-d128-s64, preserving shared-KV prefill with headDim=128."
2006
  },
2007
  "attrs": { "num_heads": 8, "kv_num_heads": 2, "scale": 0.08838834764831845 },
2008
  "inputs": {
@@ -2051,7 +2146,7 @@
2051
  "pastValueT": {
2052
  "dtype": "float32",
2053
  "shape": [1, 2, 48, 64],
2054
- "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
2055
  },
2056
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [47] } },
2057
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [48] } }
@@ -2136,7 +2231,7 @@
2136
  "name": "qnorm_rotary_scalar_h8kv2_d256_q1",
2137
  "provenance": {
2138
  "source": "scope coverage",
2139
- "test": "rendered-Jinja scope coverage",
2140
  "notes": "headDim=256 requires 34,048 bytes for the cooperative path, exceeding a 32 KiB workgroup-storage tier and exercising the thread-per-query Q-norm route."
2141
  },
2142
  "attrs": { "num_heads": 8, "kv_num_heads": 2, "scale": 0.0625, "do_rotary": 1, "qk_norm_epsilon": 0.00001 },
@@ -2191,7 +2286,7 @@
2191
  "name": "f16_qnorm_rotary_scalar_f16wts_h8kv2_d256_q1",
2192
  "provenance": {
2193
  "source": "scope coverage",
2194
- "test": "rendered-Jinja scope coverage",
2195
  "notes": "Float16 counterpart to the thread-per-query Q-norm case. It verifies explicit f32 conversion of q_norm_weight, cos_cache, and sin_cache before scalar arithmetic."
2196
  },
2197
  "attrs": { "num_heads": 8, "kv_num_heads": 2, "scale": 0.0625, "do_rotary": 1, "qk_norm_epsilon": 0.00001 },
@@ -2311,7 +2406,7 @@
2311
  "pastValueT": {
2312
  "dtype": "float32",
2313
  "shape": [1, 2, 1024, 64],
2314
- "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
2315
  },
2316
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1023] } },
2317
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1024] } }
@@ -2325,7 +2420,7 @@
2325
  {
2326
  "name": "flashdecode_splitk_h8kv2_d128_kv1024",
2327
  "provenance": {
2328
- "notes": "Compact companion for bench case sharedkv-decode-32h8kv-d128-kv2048, preserving f32 shared-past decode with headDim=128 and the split-K route."
2329
  },
2330
  "attrs": { "num_heads": 8, "kv_num_heads": 2, "scale": 0.08838834764831845 },
2331
  "inputs": {
@@ -2344,7 +2439,7 @@
2344
  "pastValueT": {
2345
  "dtype": "float32",
2346
  "shape": [1, 2, 1024, 128],
2347
- "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
2348
  },
2349
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1023] } },
2350
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1024] } }
@@ -2374,13 +2469,13 @@
2374
  "pastValueT": {
2375
  "dtype": "float16",
2376
  "shape": [1, 2, 512, 128],
2377
- "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
2378
  },
2379
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [511] } },
2380
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [512] } }
2381
  },
2382
  "outputs": {
2383
- "outputT": { "dtype": "float16", "shape": [1, 1, 1024], "tolerance": 0.03 },
2384
  "presentKeyT": { "dtype": "float16", "shape": [1, 2, 512, 128], "tolerance": 0.001 },
2385
  "presentValueT": { "dtype": "float16", "shape": [1, 2, 512, 128], "tolerance": 0.001 }
2386
  }
@@ -2404,13 +2499,13 @@
2404
  "pastValueT": {
2405
  "dtype": "float16",
2406
  "shape": [1, 2, 1024, 128],
2407
- "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
2408
  },
2409
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1023] } },
2410
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1024] } }
2411
  },
2412
  "outputs": {
2413
- "outputT": { "dtype": "float16", "shape": [1, 1, 1024], "tolerance": 0.03 },
2414
  "presentKeyT": { "dtype": "float16", "shape": [1, 2, 1024, 128], "tolerance": 0.001 },
2415
  "presentValueT": { "dtype": "float16", "shape": [1, 2, 1024, 128], "tolerance": 0.001 }
2416
  }
@@ -2464,7 +2559,7 @@
2464
  "pastValueT": {
2465
  "dtype": "float16",
2466
  "shape": [1, 2, 512, 64],
2467
- "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
2468
  },
2469
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [511] } },
2470
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [512] } },
@@ -2480,7 +2575,7 @@
2480
  }
2481
  },
2482
  "outputs": {
2483
- "outputT": { "dtype": "float16", "shape": [1, 1, 512], "tolerance": 0.03 },
2484
  "presentKeyT": { "dtype": "float16", "shape": [1, 2, 512, 64], "tolerance": 0.001 },
2485
  "presentValueT": { "dtype": "float16", "shape": [1, 2, 512, 64], "tolerance": 0.001 }
2486
  }
@@ -2504,7 +2599,7 @@
2504
  "pastValueT": {
2505
  "dtype": "float16",
2506
  "shape": [1, 2, 32, 64],
2507
- "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
2508
  },
2509
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [31] } },
2510
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [32] } },
@@ -2544,7 +2639,7 @@
2544
  "pastValueT": {
2545
  "dtype": "float16",
2546
  "shape": [1, 2, 32, 64],
2547
- "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
2548
  },
2549
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [31] } },
2550
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [32] } },
@@ -2630,7 +2725,7 @@
2630
  "valueT": {
2631
  "dtype": "float32",
2632
  "shape": [1, 32, 128],
2633
- "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.019, "cosStep": 0.023 }
2634
  },
2635
  "pastKeyT": {
2636
  "dtype": "float32",
@@ -2640,7 +2735,7 @@
2640
  "pastValueT": {
2641
  "dtype": "float32",
2642
  "shape": [1, 2, 32, 64],
2643
- "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
2644
  },
2645
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [63] } },
2646
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [64] } }
@@ -2668,7 +2763,7 @@
2668
  "valueT": {
2669
  "dtype": "float32",
2670
  "shape": [1, 1, 128],
2671
- "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.019, "cosStep": 0.023 }
2672
  },
2673
  "pastKeyT": {
2674
  "dtype": "float32",
@@ -2678,7 +2773,7 @@
2678
  "pastValueT": {
2679
  "dtype": "float32",
2680
  "shape": [1, 2, 1024, 64],
2681
- "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
2682
  },
2683
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1024] } },
2684
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1025] } }
@@ -2706,7 +2801,7 @@
2706
  "valueT": {
2707
  "dtype": "float16",
2708
  "shape": [1, 32, 128],
2709
- "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.019, "cosStep": 0.023 }
2710
  },
2711
  "pastKeyT": {
2712
  "dtype": "float16",
@@ -2716,13 +2811,13 @@
2716
  "pastValueT": {
2717
  "dtype": "float16",
2718
  "shape": [1, 2, 32, 64],
2719
- "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
2720
  },
2721
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [63] } },
2722
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [64] } }
2723
  },
2724
  "outputs": {
2725
- "outputT": { "dtype": "float16", "shape": [1, 32, 512], "tolerance": 0.03 },
2726
  "presentKeyT": { "dtype": "float16", "shape": [1, 2, 64, 64], "tolerance": 0.001 },
2727
  "presentValueT": { "dtype": "float16", "shape": [1, 2, 64, 64], "tolerance": 0.001 }
2728
  }
@@ -2872,7 +2967,7 @@
2872
  "valueT": {
2873
  "dtype": "float32",
2874
  "shape": [1, 511, 128],
2875
- "data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.007, "cosStep": 0.041 }
2876
  },
2877
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [510] } },
2878
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [511] } }
@@ -2901,7 +2996,7 @@
2901
  "valueT": {
2902
  "dtype": "float32",
2903
  "shape": [1, 64, 256],
2904
- "data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.007, "cosStep": 0.041 }
2905
  },
2906
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [63] } },
2907
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [64] } }
@@ -2932,7 +3027,7 @@
2932
  "valueT": {
2933
  "dtype": "float32",
2934
  "shape": [1, 512, 256],
2935
- "data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.029, "cosStep": 0.017 }
2936
  },
2937
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [511] } },
2938
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [512] } }
@@ -3063,7 +3158,7 @@
3063
  {
3064
  "name": "qkv_prefill_f32_h8kv2_d256_register_boundary",
3065
  "provenance": {
3066
- "notes": "Upper-head routing boundary for QKV prefill. On subgroup tiers the generic online kernel beats both register-heavy Q32 broadcast and the clustered d256 topology; no-subgroup tiers retain the shared-memory cluster as their fastest fallback."
3067
  },
3068
  "attrs": { "num_heads": 8, "kv_num_heads": 2, "scale": 0.0625, "causal": 0 },
3069
  "inputs": {
@@ -3261,7 +3356,7 @@
3261
  "provenance": {
3262
  "source": "onnxruntime/contrib_ops/webgpu/bert/flash_attention_decode_qkv.wgsl.template",
3263
  "test": "head_size=8 split-reduce OOB race class (PR #29593)",
3264
- "notes": "head_size below the flash minimum routes to the cooperative fallback; this pins the tiny-headDim decode regime where tile-width-guarded reductions have raced in other engines."
3265
  },
3266
  "attrs": { "num_heads": 4, "kv_num_heads": 2 },
3267
  "inputs": {
@@ -3280,7 +3375,7 @@
3280
  "pastValueT": {
3281
  "dtype": "float32",
3282
  "shape": [1, 2, 512, 8],
3283
- "data": { "kind": "fillFloat32", "sinStep": 0.07, "cosStep": 0.31, "scale": 0.5 }
3284
  },
3285
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [511] } },
3286
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [512] } }
@@ -3296,7 +3391,7 @@
3296
  "provenance": {
3297
  "source": "onnxruntime/contrib_ops/webgpu/bert/flash_attention_decode_qkv.wgsl.template",
3298
  "test": "head_size=16 split-reduce OOB race class (PR #29593)",
3299
- "notes": "Sibling of the head_size=8 case at the next tiny head width."
3300
  },
3301
  "attrs": { "num_heads": 4, "kv_num_heads": 2 },
3302
  "inputs": {
@@ -3315,7 +3410,7 @@
3315
  "pastValueT": {
3316
  "dtype": "float32",
3317
  "shape": [1, 2, 512, 16],
3318
- "data": { "kind": "fillFloat32", "sinStep": 0.07, "cosStep": 0.31, "scale": 0.5 }
3319
  },
3320
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [511] } },
3321
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [512] } }
@@ -3541,7 +3636,7 @@
3541
  "provenance": {
3542
  "source": "onnxruntime/contrib_ops/cpu/bert/group_query_attention.cc",
3543
  "test": "GQA head sink on the buffer-sharing append path",
3544
- "notes": "CASE: the decode step of the same geometry -- one new K/V row appended at 11 with 20 rows of capacity slack the attention bound must exclude, so seqlens_k rather than the bound capacity sets both the append offset and the window origin."
3545
  }
3546
  },
3547
  {
@@ -3627,7 +3722,7 @@
3627
  {
3628
  "name": "prefill_tiled_q31_h8kv2_d36_non_cluster_stride",
3629
  "provenance": {
3630
- "notes": "Head size 36 is a multiple of 4 but not of 4*PREFILL_LANES_PER_QUERY=16, so the cooperative cluster prefill cannot map its per-query lanes; qSeq 31 clears the prefill minimum and qSeq*heads=248 clears the flash occupancy floor. Locks the blocked tiled prefill kernel on tiers without subgroups."
3631
  },
3632
  "attrs": { "num_heads": 8, "kv_num_heads": 2, "scale": 0.16666666666666666, "causal": 0 },
3633
  "inputs": {
@@ -3685,7 +3780,7 @@
3685
  "presentValueT": { "dtype": "float32", "shape": [1, 2, 32, 64], "tolerance": 0.0001 }
3686
  },
3687
  "provenance": {
3688
- "notes": "Logit soft-capping on the past_kv prefill path. Every flash prefill variant used to gate `standardSoftmax`, so a softcap sent the whole prefill to the scalar kernel at ~3% of peak; capping is a per-score transform, so the tiled kernel takes it unchanged."
3689
  }
3690
  },
3691
  {
@@ -3772,7 +3867,7 @@
3772
  {
3773
  "name": "flashprefill_quant_int8_h4kv2_d64_q32",
3774
  "provenance": {
3775
- "notes": "INT8 KV cache on the tiled prefill path. The scalar kernel dequantized a cache element per (query, key); staging the tile dequantizes once per key and every query in the tile reads the result, so the unpack cost divides by the tile height."
3776
  },
3777
  "attrs": {
3778
  "num_heads": 4,
@@ -3883,7 +3978,7 @@
3883
  "provenance": {
3884
  "source": "onnxruntime/contrib_ops/cpu/bert/group_query_attention.cc",
3885
  "test": "GQA sliding_window_cache (ORT #29904), CPU/CUDA reference semantics",
3886
- "notes": "Windowed KV cache: the bound past/present buffer is a fixed CAPACITY holding only the most recent min(T, C) tokens contiguously at rows [0, L). Appending S tokens onto past P evicts E = max(0, min(P,C) + S - C) rows and slides survivors down by E. Attention reads keys at absolute positions translated by the cache origin T - L; RoPE keeps absolute positions. ORT's own WebGPU EP rejects this attribute (CUDA/CPU only). CASE: T=6 <= C=8, E=0, origin=0 degenerates to append; also pins the unread-row clear."
3887
  }
3888
  },
3889
  {
@@ -3918,7 +4013,7 @@
3918
  "provenance": {
3919
  "source": "onnxruntime/contrib_ops/cpu/bert/group_query_attention.cc",
3920
  "test": "GQA sliding_window_cache (ORT #29904), CPU/CUDA reference semantics",
3921
- "notes": "Windowed KV cache: the bound past/present buffer is a fixed CAPACITY holding only the most recent min(T, C) tokens contiguously at rows [0, L). Appending S tokens onto past P evicts E = max(0, min(P,C) + S - C) rows and slides survivors down by E. Attention reads keys at absolute positions translated by the cache origin T - L; RoPE keeps absolute positions. ORT's own WebGPU EP rejects this attribute (CUDA/CPU only). CASE: T=12 > C=8, E=1, appendStart=7, origin=4 — the compaction shift."
3922
  }
3923
  },
3924
  {
@@ -3953,7 +4048,7 @@
3953
  "provenance": {
3954
  "source": "onnxruntime/contrib_ops/cpu/bert/group_query_attention.cc",
3955
  "test": "GQA sliding_window_cache (ORT #29904), CPU/CUDA reference semantics",
3956
- "notes": "Windowed KV cache: the bound past/present buffer is a fixed CAPACITY holding only the most recent min(T, C) tokens contiguously at rows [0, L). Appending S tokens onto past P evicts E = max(0, min(P,C) + S - C) rows and slides survivors down by E. Attention reads keys at absolute positions translated by the cache origin T - L; RoPE keeps absolute positions. ORT's own WebGPU EP rejects this attribute (CUDA/CPU only). CASE: S=4, T=10 > C=8, E=2, appendStart=4, origin=2 — multi-row eviction."
3957
  }
3958
  },
3959
  {
@@ -3988,7 +4083,7 @@
3988
  "provenance": {
3989
  "source": "onnxruntime/contrib_ops/cpu/bert/group_query_attention.cc",
3990
  "test": "GQA sliding_window_cache (ORT #29904), CPU/CUDA reference semantics",
3991
- "notes": "Windowed KV cache: the bound past/present buffer is a fixed CAPACITY holding only the most recent min(T, C) tokens contiguously at rows [0, L). Appending S tokens onto past P evicts E = max(0, min(P,C) + S - C) rows and slides survivors down by E. Attention reads keys at absolute positions translated by the cache origin T - L; RoPE keeps absolute positions. ORT's own WebGPU EP rejects this attribute (CUDA/CPU only). CASE: b0 T=4 (E=0, origin=0), b1 T=12 (E=1, origin=4) — per-batch origin."
3992
  }
3993
  },
3994
  {
@@ -4023,7 +4118,7 @@
4023
  "provenance": {
4024
  "source": "onnxruntime/contrib_ops/cpu/bert/group_query_attention.cc",
4025
  "test": "GQA sliding_window_cache (ORT #29904), CPU/CUDA reference semantics",
4026
- "notes": "Windowed KV cache: the bound past/present buffer is a fixed CAPACITY holding only the most recent min(T, C) tokens contiguously at rows [0, L). Appending S tokens onto past P evicts E = max(0, min(P,C) + S - C) rows and slides survivors down by E. Attention reads keys at absolute positions translated by the cache origin T - L; RoPE keeps absolute positions. ORT's own WebGPU EP rejects this attribute (CUDA/CPU only). CASE: w=4 <= C=8 with E=1 — mask floor and cache origin interact."
4027
  }
4028
  },
4029
  {
@@ -4058,7 +4153,7 @@
4058
  "provenance": {
4059
  "source": "onnxruntime/contrib_ops/cpu/bert/group_query_attention.cc",
4060
  "test": "GQA sliding_window_cache (ORT #29904), CPU/CUDA reference semantics",
4061
- "notes": "Windowed KV cache: the bound past/present buffer is a fixed CAPACITY holding only the most recent min(T, C) tokens contiguously at rows [0, L). Appending S tokens onto past P evicts E = max(0, min(P,C) + S - C) rows and slides survivors down by E. Attention reads keys at absolute positions translated by the cache origin T - L; RoPE keeps absolute positions. ORT's own WebGPU EP rejects this attribute (CUDA/CPU only). CASE: C == w == 4, T=10 the shape a sliding-window layer actually allocates."
4062
  }
4063
  },
4064
  {
@@ -4093,7 +4188,7 @@
4093
  "provenance": {
4094
  "source": "onnxruntime/contrib_ops/cpu/bert/group_query_attention.cc",
4095
  "test": "GQA sliding_window_cache (ORT #29904), CPU/CUDA reference semantics",
4096
- "notes": "Windowed KV cache: the bound past/present buffer is a fixed CAPACITY holding only the most recent min(T, C) tokens contiguously at rows [0, L). Appending S tokens onto past P evicts E = max(0, min(P,C) + S - C) rows and slides survivors down by E. Attention reads keys at absolute positions translated by the cache origin T - L; RoPE keeps absolute positions. ORT's own WebGPU EP rejects this attribute (CUDA/CPU only). CASE: T=64 with C=16 origin 48, far past the buffer."
4097
  }
4098
  },
4099
  {
@@ -4115,7 +4210,7 @@
4115
  "pastValueT": {
4116
  "dtype": "float32",
4117
  "shape": [1, 1, 1024, 64],
4118
- "data": { "kind": "fillFloat32", "sinStep": 0.23, "cosStep": 0.13, "scale": 0.5 }
4119
  },
4120
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [4095] } },
4121
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [4096] } }
@@ -4128,7 +4223,7 @@
4128
  "provenance": {
4129
  "source": "onnxruntime/contrib_ops/cpu/bert/group_query_attention.cc",
4130
  "test": "GQA sliding_window_cache (ORT #29904), CPU/CUDA reference semantics",
4131
- "notes": "Windowed cache at a capacity that clears CACHED_DECODE_MIN_KV_TOKENS, so the split-K decode variant is selected rather than the scalar fallback. T=4096 > C=1024, so the step genuinely evicts and the split-K kernel reads a shifted cache."
4132
  }
4133
  },
4134
  {
@@ -4502,7 +4597,7 @@
4502
  "provenance": {
4503
  "source": "onnxruntime/contrib_ops/cpu/bert/group_query_attention.cc",
4504
  "test": "GQA buffer-sharing append chunk prefill on the flash cluster kernel",
4505
- "notes": "Share-append binds one fixed-capacity cache for past and present, so the resident length is seqlens_k + 1 rather than the bound capacity — the same split between attention bound and batch stride the windowed cache needs. headDim >= 64 with qSeq >= PREFILL_QUERY_TILE routes these to the flash prefill kernel. CASE: T=48 < C=64 — 16 rows of capacity slack the flash bound must exclude."
4506
  }
4507
  },
4508
  {
@@ -4574,7 +4669,206 @@
4574
  "provenance": {
4575
  "source": "onnxruntime/contrib_ops/cpu/bert/group_query_attention.cc",
4576
  "test": "GQA buffer-sharing append chunk prefill on the flash cluster kernel",
4577
- "notes": "Share-append binds one fixed-capacity cache for past and present, so the resident length is seqlens_k + 1 rather than the bound capacity — the same split between attention bound and batch stride the windowed cache needs. headDim >= 64 with qSeq >= PREFILL_QUERY_TILE routes these to the flash prefill kernel. CASE: b0 T=32 (append fills exactly), b1 T=48 — bound is per batch."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4578
  }
4579
  },
4580
  {
@@ -4611,7 +4905,7 @@
4611
  {
4612
  "name": "splitk_decode_h8kv2_d64_kv2048_f16_ramp_value_scale_lock",
4613
  "provenance": {
4614
- "notes": "The f16 compile of the present-KV decode split-K kernel had exactly one fixture, splitk_decode_h8kv2_d64_kv2048_f16, and its 0.03 absolute tolerance against a 2.1e-4 expected output is blind to a 146x scale error - the worst ratio in either attention op. Same shape and route with a ramped V so the f16 accumulator, its rescale and the final divide are all scale-locked."
4615
  },
4616
  "attrs": { "num_heads": 8, "kv_num_heads": 2, "scale": 0.125, "causal": 0 },
4617
  "inputs": {
@@ -4642,7 +4936,7 @@
4642
  {
4643
  "name": "flash_prefill_h2kv1_d64_s128_f16_ramp_value_scale_lock",
4644
  "provenance": {
4645
- "notes": "qkv_present_flash_q32_broadcast and qkv_present_flash_q32_shared are reached by one fixture only, flash_prefill_h2kv1_d64_s128_f16, whose zero-mean V averages to 2.3e-3 under a 0.03 absolute tolerance - blind to a 13x scale error. Ramped V makes every causal row an O(1) function of its own weighted mean key index, so the register-blocked q32 epilogue divide is under test and each of the 128 query rows carries a different expected value."
4646
  },
4647
  "attrs": { "num_heads": 2, "kv_num_heads": 1, "scale": 0.125, "causal": 0 },
4648
  "inputs": {
@@ -4673,7 +4967,7 @@
4673
  {
4674
  "name": "newkv_past_splitk_h8kv2_d64_q1p1024_ramp_value_scale_lock",
4675
  "provenance": {
4676
- "notes": "new_kv_past_decode_splitk and its _nosg twin - the append-new-KV-then-decode route - had one fixture, newkv_past_splitk_h8kv2_d64_q1p1024, whose zero-mean past and new V average to 4.7e-4 under a 0.005 tolerance: a 10x scale error passes. Ramping both value tensors makes the output an O(1) function of the weighted mean key index across the final 256 cached/new keys, so the local-window floor, split-K combine, softmax denominator, and seam between cached and new V are all scale-locked."
4677
  },
4678
  "attrs": { "num_heads": 8, "kv_num_heads": 2, "scale": 0.125, "local_window_size": 256 },
4679
  "inputs": {
@@ -4710,7 +5004,7 @@
4710
  {
4711
  "name": "newkv_past_f16_flash_h8kv2_d64_q32p32_ramp_value_scale_lock",
4712
  "provenance": {
4713
- "notes": "new_kv_past_flash_prefill and its _nosg twin are reached only by newkv_past_flash_h8kv2_d64_q32p32 and its f16 sibling; the f16 one averages zero-mean V to 0.034 under a 0.03 tolerance, so an 89% scale error passes. Ramping both value tensors makes each of the 32 chunked-prefill rows an O(1) function of its 16-key causal local window; early rows cross the past/new seam, locking the window floor, flash normalization, and cache transition on the f16 compile."
4714
  },
4715
  "attrs": { "num_heads": 8, "kv_num_heads": 2, "scale": 0.125, "local_window_size": 16 },
4716
  "inputs": {
@@ -4751,7 +5045,7 @@
4751
  {
4752
  "name": "window_cache_decode_splitk_cap1024_b1q1_h2kv1d64_ramp_value_scale_lock",
4753
  "provenance": {
4754
- "notes": "window_shift_decode_splitk and its _nosg twin are reached by one fixture, window_cache_decode_splitk_cap1024_b1q1_h2kv1d64, which already ramps the one appended V but leaves the 1024-slot shifted cache zero-mean, so the average is 1.7e-3 under a 0.005 tolerance and a 3x scale error passes. Ramping the past cache as well makes the output an O(1) function of the weighted mean slot in the shifted window, so the split-K combine and the eviction offset both move it: T=4096 > C=1024, so the cache genuinely wraps."
4755
  },
4756
  "attrs": { "num_heads": 2, "kv_num_heads": 1, "sliding_window_cache": 1, "local_window_size": 1024 },
4757
  "inputs": {
@@ -4784,7 +5078,7 @@
4784
  {
4785
  "name": "quant_int8_scalar_prompt_ramp_value_scale_lock",
4786
  "provenance": {
4787
- "notes": "The scalar INT8 KV-cache route is reached only by ORT-derived fixtures carrying a 0.05 absolute tolerance against an O(0.5) output, so a 10% uniform scale error passes every one of them; unlike the other attention holes this is loose tolerance rather than a cancelled average. A synthetic sibling on the same route reproduces the reference to under 1e-7 - the kernel and the oracle quantize identically, so the ORT tolerance was never needed here - which locks the vScale dequant, the softmax denominator and the apply divide at 2e-5. V ramps 0.5 to 2.0 so each causal row lands on a different O(1) value."
4788
  },
4789
  "attrs": {
4790
  "num_heads": 2,
@@ -4821,7 +5115,7 @@
4821
  {
4822
  "name": "quant_int4_scalar_prompt_ramp_value_scale_lock",
4823
  "provenance": {
4824
- "notes": "Same hole as the INT8 sibling but wider: the scalar INT4 KV-cache route only has ORT-derived fixtures at a 0.15 absolute tolerance over an O(0.5) output, blind to a 30% scale error. vScale 0.3 spans the ramp inside the signed 4-bit range, and the kernel matches the oracle to under 1e-7, so a 2e-5 tolerance locks the +8-biased nibble unpack, the dequant scale and the normalization."
4825
  },
4826
  "attrs": {
4827
  "num_heads": 2,
@@ -5235,6 +5529,40 @@
5235
  "presentValueT": { "dtype": "float16", "shape": [1, 1, 512, 64], "tolerance": 0.001 }
5236
  }
5237
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5238
  {
5239
  "name": "past_kv_materialized_sgmat_f16_floor_q256_p256_h2kv1_d64",
5240
  "attrs": { "num_heads": 2, "kv_num_heads": 1 },
@@ -5374,6 +5702,39 @@
5374
  "presentKeyT": { "dtype": "float32", "shape": [2, 1, 256, 64], "tolerance": 0.000001 },
5375
  "presentValueT": { "dtype": "float32", "shape": [2, 1, 256, 64], "tolerance": 0.000001 }
5376
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5377
  }
5378
  ]
5379
  }
 
1
  {
 
2
  "fixtureArrays": {
3
  "ort_past_key_t_pattern": [0.2, 0.4, 0.6, 0.8, 1, 0.2, 0.4, 0.6, 0.8, 1, 0.2, 0.4, 0.6, 0.8, 1, 0.2, 0.4, 0.6, 0.8, 1, 0.2, 0.4, 0.6, 0.8, 1, 0.2, 0.4, 0.6, 0.8, 1, 0.2, 0.4, 0.6, 0.8, 1, 0.2, 0.4, 0.6, 0.8, 1, 0.2, 0.4, 0.6, 0.8, 1, 0.2, 0.4, 0.6, 0.8, 1, 0.2, 0.4, 0.6, 0.8, 1, 0.2, 0.4, 0.6, 0.8, 1, 0.2, 0.4, 0.6, 0.8],
4
  "ort_past_value_t_pattern": [0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3, 0.6, 0.9, 0.3],
 
246
  "valueT": {
247
  "dtype": "float32",
248
  "shape": [1, 512, 64],
249
+ "data": { "kind": "fillFloat32", "scale": 0.18, "sinStep": 0.09817477042, "cosStep": 0.000383495197 }
250
  },
251
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [511] } },
252
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [512] } }
 
466
  "valueT": {
467
  "dtype": "float32",
468
  "shape": [1, 2048, 128],
469
+ "data": { "kind": "fillFloat32", "scale": 0.18, "sinStep": 0.04908738521, "cosStep": 0.00004793689962 }
470
  },
471
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [2047] } },
472
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [2048] } }
 
494
  "valueT": {
495
  "dtype": "float32",
496
  "shape": [1, 1024, 64],
497
+ "data": { "kind": "fillFloat32", "scale": 0.18, "sinStep": 0.09817477042, "cosStep": 0.0001917475985 }
498
  },
499
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1023] } },
500
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1024] } }
 
522
  "valueT": {
523
  "dtype": "float16",
524
  "shape": [1, 128, 64],
525
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.09817477042, "cosStep": 0.001533980788 }
526
  },
527
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [127] } },
528
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [128] } }
529
  },
530
  "outputs": {
531
+ "outputT": { "dtype": "float16", "shape": [1, 128, 128], "tolerance": 0.004 },
532
+ "presentKeyT": { "dtype": "float16", "shape": [1, 1, 128, 64], "tolerance": 0.004 },
533
+ "presentValueT": { "dtype": "float16", "shape": [1, 1, 128, 64], "tolerance": 0.004 }
534
  }
535
  },
536
  {
 
550
  "valueT": {
551
  "dtype": "float16",
552
  "shape": [1, 512, 64],
553
+ "data": { "kind": "fillFloat32", "scale": 0.18, "sinStep": 0.09817477042, "cosStep": 0.000383495197 }
554
  },
555
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [511] } },
556
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [512] } }
557
  },
558
  "outputs": {
559
+ "outputT": { "dtype": "float16", "shape": [1, 1, 256], "tolerance": 0.004 },
560
+ "presentKeyT": { "dtype": "float16", "shape": [1, 1, 512, 64], "tolerance": 0.004 },
561
+ "presentValueT": { "dtype": "float16", "shape": [1, 1, 512, 64], "tolerance": 0.004 }
562
  }
563
  },
564
  {
 
578
  "valueT": {
579
  "dtype": "float16",
580
  "shape": [1, 2048, 128],
581
+ "data": { "kind": "fillFloat32", "scale": 0.18, "sinStep": 0.04908738521, "cosStep": 0.00004793689962 }
582
  },
583
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [2047] } },
584
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [2048] } }
585
  },
586
  "outputs": {
587
+ "outputT": { "dtype": "float16", "shape": [1, 1, 512], "tolerance": 0.004 },
588
+ "presentKeyT": { "dtype": "float16", "shape": [1, 2, 2048, 64], "tolerance": 0.004 },
589
+ "presentValueT": { "dtype": "float16", "shape": [1, 2, 2048, 64], "tolerance": 0.004 }
590
  }
591
  },
592
  {
 
690
  "valueT": {
691
  "dtype": "float32",
692
  "shape": [1, 1024, 128],
693
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.04908738521, "cosStep": 0.00009587379924 }
694
  },
695
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1023] } },
696
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1024] } }
 
1209
  "vScaleT": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [0.0038802] } }
1210
  },
1211
  "outputs": {
1212
+ "outputT": { "dtype": "float32", "shape": [1, 4, 16], "tolerance": 0.01 },
1213
  "presentKeyT": { "dtype": "int8", "shape": [1, 1, 4, 8], "tolerance": 0 },
1214
  "presentValueT": { "dtype": "int8", "shape": [1, 1, 4, 8], "tolerance": 0 }
1215
  }
 
1275
  }
1276
  },
1277
  "outputs": {
1278
+ "outputT": { "dtype": "float32", "shape": [1, 4, 16], "tolerance": 0.01 },
1279
  "presentKeyT": { "dtype": "int8", "shape": [1, 1, 4, 8], "tolerance": 0 },
1280
  "presentValueT": { "dtype": "int8", "shape": [1, 1, 4, 8], "tolerance": 0 }
1281
  }
 
1380
  "vScaleT": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [0.0703979] } }
1381
  },
1382
  "outputs": {
1383
+ "outputT": { "dtype": "float32", "shape": [1, 4, 16], "tolerance": 0.01 },
1384
  "presentKeyT": { "dtype": "uint8", "shape": [1, 1, 4, 4], "tolerance": 0 },
1385
  "presentValueT": { "dtype": "uint8", "shape": [1, 1, 4, 4], "tolerance": 0 }
1386
  }
 
1446
  }
1447
  },
1448
  "outputs": {
1449
+ "outputT": { "dtype": "float32", "shape": [1, 4, 16], "tolerance": 0.01 },
1450
  "presentKeyT": { "dtype": "uint8", "shape": [1, 1, 4, 4], "tolerance": 0 },
1451
  "presentValueT": { "dtype": "uint8", "shape": [1, 1, 4, 4], "tolerance": 0 }
1452
  }
 
1507
  "vScaleT": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [0.0039328] } }
1508
  },
1509
  "outputs": {
1510
+ "outputT": { "dtype": "float32", "shape": [2, 4, 64], "tolerance": 0.01 },
1511
  "presentKeyT": { "dtype": "int8", "shape": [2, 2, 4, 16], "tolerance": 0 },
1512
  "presentValueT": { "dtype": "int8", "shape": [2, 2, 4, 16], "tolerance": 0 }
1513
  }
 
1562
  "vScaleT": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [0.0039328] } }
1563
  },
1564
  "outputs": {
1565
+ "outputT": { "dtype": "float32", "shape": [1, 8, 128], "tolerance": 0.01 },
1566
  "presentKeyT": { "dtype": "int8", "shape": [1, 1, 8, 64], "tolerance": 0 },
1567
  "presentValueT": { "dtype": "int8", "shape": [1, 1, 8, 64], "tolerance": 0 }
1568
  }
 
1623
  "vScaleT": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [0.0703979] } }
1624
  },
1625
  "outputs": {
1626
+ "outputT": { "dtype": "float32", "shape": [1, 4, 64], "tolerance": 0.01 },
1627
  "presentKeyT": { "dtype": "uint8", "shape": [1, 1, 4, 8], "tolerance": 0 },
1628
  "presentValueT": { "dtype": "uint8", "shape": [1, 1, 4, 8], "tolerance": 0 }
1629
  }
 
1802
  "vScaleT": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [0.0038802] } }
1803
  },
1804
  "outputs": {
1805
+ "outputT": { "dtype": "float32", "shape": [1, 4, 16], "tolerance": 0.01 },
1806
  "presentKeyT": { "dtype": "int8", "shape": [1, 1, 4, 8], "tolerance": 0 },
1807
  "presentValueT": { "dtype": "int8", "shape": [1, 1, 4, 8], "tolerance": 0 }
1808
  }
 
1848
  "presentValueT": { "dtype": "float32", "shape": [1, 1, 8, 8], "tolerance": 0.0001 }
1849
  }
1850
  },
1851
+ {
1852
+ "name": "ort_bias_decode_all_neg_inf_row_zero",
1853
+ "provenance": {
1854
+ "notes": "Cached decode uses two query heads, one KV head, head size 8, eight active cache positions, and no new K/V values. Bias for query head 1 is -Infinity at every key while head 0 remains finite. The masked head's output is zero; the finite head is its softmax-weighted V. `seqlens_k = 7` means eight active keys, not an empty row. Present K/V outputs copy the input cache unchanged."
1855
+ },
1856
+ "attrs": { "num_heads": 2, "kv_num_heads": 1 },
1857
+ "inputs": {
1858
+ "queryT": {
1859
+ "dtype": "float32",
1860
+ "shape": [1, 1, 16],
1861
+ "data": {
1862
+ "kind": "values",
1863
+ "values": { "$ref": "#/fixtureArrays/ort_sharedkv_decode_b1q1p8_h2kv1d8_input_queryT" }
1864
+ }
1865
+ },
1866
+ "keyT": { "dtype": "float32", "shape": [1, 0, 8], "data": { "kind": "values", "values": [] } },
1867
+ "valueT": { "dtype": "float32", "shape": [1, 0, 8], "data": { "kind": "values", "values": [] } },
1868
+ "pastKeyT": {
1869
+ "dtype": "float32",
1870
+ "shape": [1, 1, 8, 8],
1871
+ "data": { "kind": "values", "values": { "$ref": "#/fixtureArrays/ort_past_key_t_pattern" } }
1872
+ },
1873
+ "pastValueT": {
1874
+ "dtype": "float32",
1875
+ "shape": [1, 1, 8, 8],
1876
+ "data": { "kind": "values", "values": { "$ref": "#/fixtureArrays/ort_past_value_t_pattern" } }
1877
+ },
1878
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [7] } },
1879
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [8] } },
1880
+ "attentionBiasT": {
1881
+ "dtype": "float32",
1882
+ "shape": [1, 2, 1, 8],
1883
+ "data": {
1884
+ "kind": "values",
1885
+ "values": [-0.05, -0.0020574, 0.0341471, 0.0497495, 0.0409297, 0.0098472, -0.035888, -0.0850783, "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity"]
1886
+ }
1887
+ }
1888
+ },
1889
+ "outputs": {
1890
+ "outputT": {
1891
+ "dtype": "float32",
1892
+ "shape": [1, 1, 16],
1893
+ "tolerance": 0.0001,
1894
+ "data": {
1895
+ "kind": "values",
1896
+ "values": [0.6027476, 0.5584169, 0.6388355, 0.6027476, 0.5584169, 0.6388355, 0.6027476, 0.5584169, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
1897
+ }
1898
+ },
1899
+ "presentKeyT": { "dtype": "float32", "shape": [1, 1, 8, 8], "tolerance": 0.0001 },
1900
+ "presentValueT": { "dtype": "float32", "shape": [1, 1, 8, 8], "tolerance": 0.0001 }
1901
+ }
1902
+ },
1903
+ {
1904
+ "name": "ort_bias_decode_single_finite_key_control",
1905
+ "provenance": {
1906
+ "notes": "Attention bias has one finite key per head and -Infinity elsewhere. Each softmax therefore selects exactly one cached value: head 0 returns row 5 and head 1 returns row 3. This distinguishes a valid one-key denominator from the fully masked behavior in `ort_bias_decode_all_neg_inf_row_zero`."
1907
+ },
1908
+ "attrs": { "num_heads": 2, "kv_num_heads": 1 },
1909
+ "inputs": {
1910
+ "queryT": {
1911
+ "dtype": "float32",
1912
+ "shape": [1, 1, 16],
1913
+ "data": {
1914
+ "kind": "values",
1915
+ "values": { "$ref": "#/fixtureArrays/ort_sharedkv_decode_b1q1p8_h2kv1d8_input_queryT" }
1916
+ }
1917
+ },
1918
+ "keyT": { "dtype": "float32", "shape": [1, 0, 8], "data": { "kind": "values", "values": [] } },
1919
+ "valueT": { "dtype": "float32", "shape": [1, 0, 8], "data": { "kind": "values", "values": [] } },
1920
+ "pastKeyT": {
1921
+ "dtype": "float32",
1922
+ "shape": [1, 1, 8, 8],
1923
+ "data": { "kind": "values", "values": { "$ref": "#/fixtureArrays/ort_past_key_t_pattern" } }
1924
+ },
1925
+ "pastValueT": {
1926
+ "dtype": "float32",
1927
+ "shape": [1, 1, 8, 8],
1928
+ "data": { "kind": "values", "values": { "$ref": "#/fixtureArrays/ort_past_value_t_pattern" } }
1929
+ },
1930
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [7] } },
1931
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [8] } },
1932
+ "attentionBiasT": {
1933
+ "dtype": "float32",
1934
+ "shape": [1, 2, 1, 8],
1935
+ "data": {
1936
+ "kind": "values",
1937
+ "values": ["-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", 0.0, "-Infinity", "-Infinity", "-Infinity", "-Infinity", "-Infinity", 0.0, "-Infinity", "-Infinity", "-Infinity", "-Infinity"]
1938
+ }
1939
+ }
1940
+ },
1941
+ "outputs": {
1942
+ "outputT": { "dtype": "float32", "shape": [1, 1, 16], "tolerance": 0.0001 },
1943
+ "presentKeyT": { "dtype": "float32", "shape": [1, 1, 8, 8], "tolerance": 0.0001 },
1944
+ "presentValueT": { "dtype": "float32", "shape": [1, 1, 8, 8], "tolerance": 0.0001 }
1945
+ }
1946
+ },
1947
  {
1948
  "name": "ort_bias_bcast_prompt_b1q8p8_h2kv1d8",
1949
  "attrs": { "num_heads": 2, "kv_num_heads": 1 },
 
2097
  {
2098
  "name": "flashprefill_pastzero_h8kv2_d128_q16",
2099
  "provenance": {
2100
+ "notes": "A compact shared-KV prefill with 32 query heads, eight KV heads, head size 128, and sequence length 64 exercises grouped attention."
2101
  },
2102
  "attrs": { "num_heads": 8, "kv_num_heads": 2, "scale": 0.08838834764831845 },
2103
  "inputs": {
 
2146
  "pastValueT": {
2147
  "dtype": "float32",
2148
  "shape": [1, 2, 48, 64],
2149
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.09817477042, "cosStep": 0.002045307717 }
2150
  },
2151
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [47] } },
2152
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [48] } }
 
2231
  "name": "qnorm_rotary_scalar_h8kv2_d256_q1",
2232
  "provenance": {
2233
  "source": "scope coverage",
2234
+ "test": "headDim=256 scalar Q-norm and rotary specialization",
2235
  "notes": "headDim=256 requires 34,048 bytes for the cooperative path, exceeding a 32 KiB workgroup-storage tier and exercising the thread-per-query Q-norm route."
2236
  },
2237
  "attrs": { "num_heads": 8, "kv_num_heads": 2, "scale": 0.0625, "do_rotary": 1, "qk_norm_epsilon": 0.00001 },
 
2286
  "name": "f16_qnorm_rotary_scalar_f16wts_h8kv2_d256_q1",
2287
  "provenance": {
2288
  "source": "scope coverage",
2289
+ "test": "headDim=256 float16-weight scalar Q-norm and rotary specialization",
2290
  "notes": "Float16 counterpart to the thread-per-query Q-norm case. It verifies explicit f32 conversion of q_norm_weight, cos_cache, and sin_cache before scalar arithmetic."
2291
  },
2292
  "attrs": { "num_heads": 8, "kv_num_heads": 2, "scale": 0.0625, "do_rotary": 1, "qk_norm_epsilon": 0.00001 },
 
2406
  "pastValueT": {
2407
  "dtype": "float32",
2408
  "shape": [1, 2, 1024, 64],
2409
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.09817477042, "cosStep": 0.00009587379924 }
2410
  },
2411
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1023] } },
2412
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1024] } }
 
2420
  {
2421
  "name": "flashdecode_splitk_h8kv2_d128_kv1024",
2422
  "provenance": {
2423
+ "notes": "A compact float32 shared-past decode with 32 query heads, eight KV heads, head size 128, and KV length 2,048 exercises split-K grouped attention."
2424
  },
2425
  "attrs": { "num_heads": 8, "kv_num_heads": 2, "scale": 0.08838834764831845 },
2426
  "inputs": {
 
2439
  "pastValueT": {
2440
  "dtype": "float32",
2441
  "shape": [1, 2, 1024, 128],
2442
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.04908738521, "cosStep": 0.00004793689962 }
2443
  },
2444
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1023] } },
2445
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1024] } }
 
2469
  "pastValueT": {
2470
  "dtype": "float16",
2471
  "shape": [1, 2, 512, 128],
2472
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.04908738521, "cosStep": 0.00009587379924 }
2473
  },
2474
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [511] } },
2475
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [512] } }
2476
  },
2477
  "outputs": {
2478
+ "outputT": { "dtype": "float16", "shape": [1, 1, 1024], "tolerance": 0.004 },
2479
  "presentKeyT": { "dtype": "float16", "shape": [1, 2, 512, 128], "tolerance": 0.001 },
2480
  "presentValueT": { "dtype": "float16", "shape": [1, 2, 512, 128], "tolerance": 0.001 }
2481
  }
 
2499
  "pastValueT": {
2500
  "dtype": "float16",
2501
  "shape": [1, 2, 1024, 128],
2502
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.04908738521, "cosStep": 0.00004793689962 }
2503
  },
2504
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1023] } },
2505
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1024] } }
2506
  },
2507
  "outputs": {
2508
+ "outputT": { "dtype": "float16", "shape": [1, 1, 1024], "tolerance": 0.004 },
2509
  "presentKeyT": { "dtype": "float16", "shape": [1, 2, 1024, 128], "tolerance": 0.001 },
2510
  "presentValueT": { "dtype": "float16", "shape": [1, 2, 1024, 128], "tolerance": 0.001 }
2511
  }
 
2559
  "pastValueT": {
2560
  "dtype": "float16",
2561
  "shape": [1, 2, 512, 64],
2562
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.09817477042, "cosStep": 0.0001917475985 }
2563
  },
2564
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [511] } },
2565
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [512] } },
 
2575
  }
2576
  },
2577
  "outputs": {
2578
+ "outputT": { "dtype": "float16", "shape": [1, 1, 512], "tolerance": 0.004 },
2579
  "presentKeyT": { "dtype": "float16", "shape": [1, 2, 512, 64], "tolerance": 0.001 },
2580
  "presentValueT": { "dtype": "float16", "shape": [1, 2, 512, 64], "tolerance": 0.001 }
2581
  }
 
2599
  "pastValueT": {
2600
  "dtype": "float16",
2601
  "shape": [1, 2, 32, 64],
2602
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.09817477042, "cosStep": 0.003067961576 }
2603
  },
2604
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [31] } },
2605
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [32] } },
 
2639
  "pastValueT": {
2640
  "dtype": "float16",
2641
  "shape": [1, 2, 32, 64],
2642
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.09817477042, "cosStep": 0.003067961576 }
2643
  },
2644
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [31] } },
2645
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [32] } },
 
2725
  "valueT": {
2726
  "dtype": "float32",
2727
  "shape": [1, 32, 128],
2728
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.04908738521, "cosStep": 0.003067961576 }
2729
  },
2730
  "pastKeyT": {
2731
  "dtype": "float32",
 
2735
  "pastValueT": {
2736
  "dtype": "float32",
2737
  "shape": [1, 2, 32, 64],
2738
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.09817477042, "cosStep": 0.003067961576 }
2739
  },
2740
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [63] } },
2741
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [64] } }
 
2763
  "valueT": {
2764
  "dtype": "float32",
2765
  "shape": [1, 1, 128],
2766
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.04908738521, "cosStep": 0.09817477042 }
2767
  },
2768
  "pastKeyT": {
2769
  "dtype": "float32",
 
2773
  "pastValueT": {
2774
  "dtype": "float32",
2775
  "shape": [1, 2, 1024, 64],
2776
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.09817477042, "cosStep": 0.00009587379924 }
2777
  },
2778
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1024] } },
2779
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1025] } }
 
2801
  "valueT": {
2802
  "dtype": "float16",
2803
  "shape": [1, 32, 128],
2804
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.04908738521, "cosStep": 0.003067961576 }
2805
  },
2806
  "pastKeyT": {
2807
  "dtype": "float16",
 
2811
  "pastValueT": {
2812
  "dtype": "float16",
2813
  "shape": [1, 2, 32, 64],
2814
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.09817477042, "cosStep": 0.003067961576 }
2815
  },
2816
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [63] } },
2817
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [64] } }
2818
  },
2819
  "outputs": {
2820
+ "outputT": { "dtype": "float16", "shape": [1, 32, 512], "tolerance": 0.004 },
2821
  "presentKeyT": { "dtype": "float16", "shape": [1, 2, 64, 64], "tolerance": 0.001 },
2822
  "presentValueT": { "dtype": "float16", "shape": [1, 2, 64, 64], "tolerance": 0.001 }
2823
  }
 
2967
  "valueT": {
2968
  "dtype": "float32",
2969
  "shape": [1, 511, 128],
2970
+ "data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.04908738521, "cosStep": 0.0001921228384 }
2971
  },
2972
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [510] } },
2973
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [511] } }
 
2996
  "valueT": {
2997
  "dtype": "float32",
2998
  "shape": [1, 64, 256],
2999
+ "data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.02454369261, "cosStep": 0.0007669903939 }
3000
  },
3001
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [63] } },
3002
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [64] } }
 
3027
  "valueT": {
3028
  "dtype": "float32",
3029
  "shape": [1, 512, 256],
3030
+ "data": { "kind": "fillFloat32", "scale": 0.08, "sinStep": 0.02454369261, "cosStep": 0.00009587379924 }
3031
  },
3032
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [511] } },
3033
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [512] } }
 
3158
  {
3159
  "name": "qkv_prefill_f32_h8kv2_d256_register_boundary",
3160
  "provenance": {
3161
+ "notes": "A QKV prefill with eight query heads, two KV heads, and head size 256 exercises generic online attention on subgroup tiers and shared-memory clustered attention on tiers without subgroups."
3162
  },
3163
  "attrs": { "num_heads": 8, "kv_num_heads": 2, "scale": 0.0625, "causal": 0 },
3164
  "inputs": {
 
3356
  "provenance": {
3357
  "source": "onnxruntime/contrib_ops/webgpu/bert/flash_attention_decode_qkv.wgsl.template",
3358
  "test": "head_size=8 split-reduce OOB race class (PR #29593)",
3359
+ "notes": "Diverges from the upstream test's inputs (inputs.pastValueT fillFloat32 -> fillFloat32); the expected output is recomputed by the CPU reference for the new inputs. A head size below the flash minimum exercises the cooperative fallback and its tile-width-guarded reduction."
3360
  },
3361
  "attrs": { "num_heads": 4, "kv_num_heads": 2 },
3362
  "inputs": {
 
3375
  "pastValueT": {
3376
  "dtype": "float32",
3377
  "shape": [1, 2, 512, 8],
3378
+ "data": { "kind": "fillFloat32", "sinStep": 0.7853981634, "cosStep": 0.001533980788, "scale": 0.5 }
3379
  },
3380
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [511] } },
3381
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [512] } }
 
3391
  "provenance": {
3392
  "source": "onnxruntime/contrib_ops/webgpu/bert/flash_attention_decode_qkv.wgsl.template",
3393
  "test": "head_size=16 split-reduce OOB race class (PR #29593)",
3394
+ "notes": "Diverges from the upstream test's inputs (inputs.pastValueT fillFloat32 -> fillFloat32); the expected output is recomputed by the CPU reference for the new inputs. Head size 16 exercises the cooperative fallback at the next four-wide tiny-head boundary."
3395
  },
3396
  "attrs": { "num_heads": 4, "kv_num_heads": 2 },
3397
  "inputs": {
 
3410
  "pastValueT": {
3411
  "dtype": "float32",
3412
  "shape": [1, 2, 512, 16],
3413
+ "data": { "kind": "fillFloat32", "sinStep": 0.3926990817, "cosStep": 0.0007669903939, "scale": 0.5 }
3414
  },
3415
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [511] } },
3416
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [512] } }
 
3636
  "provenance": {
3637
  "source": "onnxruntime/contrib_ops/cpu/bert/group_query_attention.cc",
3638
  "test": "GQA head sink on the buffer-sharing append path",
3639
+ "notes": "One K/V row is appended at index 11 in a capacity-32 cache. The 20 unused rows must not enter attention; `seqlens_k` determines both the append offset and window origin."
3640
  }
3641
  },
3642
  {
 
3722
  {
3723
  "name": "prefill_tiled_q31_h8kv2_d36_non_cluster_stride",
3724
  "provenance": {
3725
+ "notes": "Head size 36 is divisible by four but not by the 16 values mapped across each query cluster. With 31 queries and eight heads, tiers without subgroups exercise blocked tiled prefill."
3726
  },
3727
  "attrs": { "num_heads": 8, "kv_num_heads": 2, "scale": 0.16666666666666666, "causal": 0 },
3728
  "inputs": {
 
3780
  "presentValueT": { "dtype": "float32", "shape": [1, 2, 32, 64], "tolerance": 0.0001 }
3781
  },
3782
  "provenance": {
3783
+ "notes": "Logit soft-capping on the past-KV prefill path exercises the tiled attention route with a per-score cap before softmax."
3784
  }
3785
  },
3786
  {
 
3867
  {
3868
  "name": "flashprefill_quant_int8_h4kv2_d64_q32",
3869
  "provenance": {
3870
+ "notes": "INT8 KV cache on the tiled prefill path. Each key is dequantized once into the staged tile and reused by every query in that tile."
3871
  },
3872
  "attrs": {
3873
  "num_heads": 4,
 
3978
  "provenance": {
3979
  "source": "onnxruntime/contrib_ops/cpu/bert/group_query_attention.cc",
3980
  "test": "GQA sliding_window_cache (ORT #29904), CPU/CUDA reference semantics",
3981
+ "notes": "A capacity-8 windowed cache reaches `T = 6` without eviction, so its origin remains zero and unused rows must be cleared."
3982
  }
3983
  },
3984
  {
 
4013
  "provenance": {
4014
  "source": "onnxruntime/contrib_ops/cpu/bert/group_query_attention.cc",
4015
  "test": "GQA sliding_window_cache (ORT #29904), CPU/CUDA reference semantics",
4016
+ "notes": "At `T = 12` with capacity 8, appending one token evicts one row, writes at row 7, and advances the absolute cache origin to 4."
4017
  }
4018
  },
4019
  {
 
4048
  "provenance": {
4049
  "source": "onnxruntime/contrib_ops/cpu/bert/group_query_attention.cc",
4050
  "test": "GQA sliding_window_cache (ORT #29904), CPU/CUDA reference semantics",
4051
+ "notes": "Appending four tokens to a capacity-8 cache at `T = 10` evicts two rows, begins the append at row 4, and advances the absolute cache origin to 2."
4052
  }
4053
  },
4054
  {
 
4083
  "provenance": {
4084
  "source": "onnxruntime/contrib_ops/cpu/bert/group_query_attention.cc",
4085
  "test": "GQA sliding_window_cache (ORT #29904), CPU/CUDA reference semantics",
4086
+ "notes": "Two batches require independent cache origins: batch 0 reaches `T = 4` without eviction, while batch 1 reaches `T = 12`, evicts one row, and uses origin 4."
4087
  }
4088
  },
4089
  {
 
4118
  "provenance": {
4119
  "source": "onnxruntime/contrib_ops/cpu/bert/group_query_attention.cc",
4120
  "test": "GQA sliding_window_cache (ORT #29904), CPU/CUDA reference semantics",
4121
+ "notes": "A local window of four within a capacity-8 cache combines one-row eviction with a nonzero attention floor derived from the absolute cache origin."
4122
  }
4123
  },
4124
  {
 
4153
  "provenance": {
4154
  "source": "onnxruntime/contrib_ops/cpu/bert/group_query_attention.cc",
4155
  "test": "GQA sliding_window_cache (ORT #29904), CPU/CUDA reference semantics",
4156
+ "notes": "Cache capacity and local-window size are both four at `T = 10`, exercising a fully occupied sliding-window allocation with a nonzero origin."
4157
  }
4158
  },
4159
  {
 
4188
  "provenance": {
4189
  "source": "onnxruntime/contrib_ops/cpu/bert/group_query_attention.cc",
4190
  "test": "GQA sliding_window_cache (ORT #29904), CPU/CUDA reference semantics",
4191
+ "notes": "At `T = 64` with capacity 16, the cache origin is 48. The four-to-one grouped-query layout and head size 16 exercise absolute-position translation far beyond the physical buffer."
4192
  }
4193
  },
4194
  {
 
4210
  "pastValueT": {
4211
  "dtype": "float32",
4212
  "shape": [1, 1, 1024, 64],
4213
+ "data": { "kind": "fillFloat32", "sinStep": 0.09817477042, "cosStep": 0.0001917475985, "scale": 0.5 }
4214
  },
4215
  "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [4095] } },
4216
  "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [4096] } }
 
4223
  "provenance": {
4224
  "source": "onnxruntime/contrib_ops/cpu/bert/group_query_attention.cc",
4225
  "test": "GQA sliding_window_cache (ORT #29904), CPU/CUDA reference semantics",
4226
+ "notes": "Diverges from the upstream test's inputs (inputs.pastValueT fillFloat32 -> fillFloat32); the expected output is recomputed by the CPU reference for the new inputs. Windowed cache at a capacity that clears CACHED_DECODE_MIN_KV_TOKENS, so the split-K decode variant is selected rather than the scalar fallback. T=4096 > C=1024, so the step genuinely evicts and the split-K kernel reads a shifted cache."
4227
  }
4228
  },
4229
  {
 
4597
  "provenance": {
4598
  "source": "onnxruntime/contrib_ops/cpu/bert/group_query_attention.cc",
4599
  "test": "GQA buffer-sharing append chunk prefill on the flash cluster kernel",
4600
+ "notes": "A fixed-capacity cache has active length 48 and capacity 64 before a 32-row prefill. The flash route must use the live length for attention and the capacity for batch strides."
4601
  }
4602
  },
4603
  {
 
4669
  "provenance": {
4670
  "source": "onnxruntime/contrib_ops/cpu/bert/group_query_attention.cc",
4671
  "test": "GQA buffer-sharing append chunk prefill on the flash cluster kernel",
4672
+ "notes": "Two batches share capacity-64 caches but have live lengths 32 and 48. The flash route must track each attention bound independently while retaining the fixed batch stride."
4673
+ }
4674
+ },
4675
+ {
4676
+ "name": "share_append_sgmat_chunk_slack_b1q128cap384_h2kv1d64",
4677
+ "attrs": { "num_heads": 2, "kv_num_heads": 1 },
4678
+ "inputs": {
4679
+ "queryT": {
4680
+ "dtype": "float32",
4681
+ "shape": [1, 128, 128],
4682
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.27, "scale": 0.5 }
4683
+ },
4684
+ "keyT": { "dtype": "float32", "shape": [1, 128, 64], "data": { "kind": "linspace", "start": -1.0, "end": 1.0 } },
4685
+ "valueT": {
4686
+ "dtype": "float32",
4687
+ "shape": [1, 128, 64],
4688
+ "data": { "kind": "linspace", "start": 0.0, "end": 2.0 }
4689
+ },
4690
+ "pastKeyT": {
4691
+ "dtype": "float32",
4692
+ "shape": [1, 1, 384, 64],
4693
+ "data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.31, "scale": 0.5 }
4694
+ },
4695
+ "pastValueT": {
4696
+ "dtype": "float32",
4697
+ "shape": [1, 1, 384, 64],
4698
+ "data": { "kind": "linspace", "start": 0.0, "end": 4.0 }
4699
+ },
4700
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [255] } },
4701
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [256] } }
4702
+ },
4703
+ "outputs": {
4704
+ "outputT": { "dtype": "float32", "shape": [1, 128, 128], "tolerance": 0.005 },
4705
+ "presentKeyT": { "dtype": "float32", "shape": [1, 1, 384, 64], "tolerance": 0.0001 },
4706
+ "presentValueT": { "dtype": "float32", "shape": [1, 1, 384, 64], "tolerance": 0.0001 }
4707
+ },
4708
+ "provenance": {
4709
+ "source": "onnxruntime/contrib_ops/cpu/bert/group_query_attention.cc",
4710
+ "test": "GQA buffer-sharing chunk append on the materialized subgroup-matrix route",
4711
+ "notes": "128 past rows plus a 128-row appended chunk in a 384-capacity shared buffer: the live length (256) is shorter than the capacity, so the causal bound and the tile skips must come from seqlens_k while every stride stays capacity-sized."
4712
+ }
4713
+ },
4714
+ {
4715
+ "name": "share_append_sgmat_chunk_full_b1q128cap256_h2kv1d128",
4716
+ "attrs": { "num_heads": 2, "kv_num_heads": 1 },
4717
+ "inputs": {
4718
+ "queryT": {
4719
+ "dtype": "float32",
4720
+ "shape": [1, 128, 256],
4721
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.27, "scale": 0.5 }
4722
+ },
4723
+ "keyT": {
4724
+ "dtype": "float32",
4725
+ "shape": [1, 128, 128],
4726
+ "data": { "kind": "linspace", "start": -1.0, "end": 1.0 }
4727
+ },
4728
+ "valueT": {
4729
+ "dtype": "float32",
4730
+ "shape": [1, 128, 128],
4731
+ "data": { "kind": "linspace", "start": 0.0, "end": 2.0 }
4732
+ },
4733
+ "pastKeyT": {
4734
+ "dtype": "float32",
4735
+ "shape": [1, 1, 256, 128],
4736
+ "data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.31, "scale": 0.5 }
4737
+ },
4738
+ "pastValueT": {
4739
+ "dtype": "float32",
4740
+ "shape": [1, 1, 256, 128],
4741
+ "data": { "kind": "linspace", "start": 0.0, "end": 4.0 }
4742
+ },
4743
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [255] } },
4744
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [256] } }
4745
+ },
4746
+ "outputs": {
4747
+ "outputT": { "dtype": "float32", "shape": [1, 128, 256], "tolerance": 0.005 },
4748
+ "presentKeyT": { "dtype": "float32", "shape": [1, 1, 256, 128], "tolerance": 0.0001 },
4749
+ "presentValueT": { "dtype": "float32", "shape": [1, 1, 256, 128], "tolerance": 0.0001 }
4750
+ },
4751
+ "provenance": {
4752
+ "source": "onnxruntime/contrib_ops/cpu/bert/group_query_attention.cc",
4753
+ "test": "GQA buffer-sharing chunk append on the materialized subgroup-matrix route",
4754
+ "notes": "The append exactly fills the shared capacity at d=128 (two apply column tiles); live length equals capacity, so the seqlens bound and the compile-time bound coincide."
4755
+ }
4756
+ },
4757
+ {
4758
+ "name": "share_append_sgmat_chunk_mixed_batch_b2q128cap384_h2kv1d64",
4759
+ "attrs": { "num_heads": 2, "kv_num_heads": 1 },
4760
+ "inputs": {
4761
+ "queryT": {
4762
+ "dtype": "float32",
4763
+ "shape": [2, 128, 128],
4764
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.27, "scale": 0.5 }
4765
+ },
4766
+ "keyT": { "dtype": "float32", "shape": [2, 128, 64], "data": { "kind": "linspace", "start": -1.0, "end": 1.0 } },
4767
+ "valueT": {
4768
+ "dtype": "float32",
4769
+ "shape": [2, 128, 64],
4770
+ "data": { "kind": "linspace", "start": 0.0, "end": 2.0 }
4771
+ },
4772
+ "pastKeyT": {
4773
+ "dtype": "float32",
4774
+ "shape": [2, 1, 384, 64],
4775
+ "data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.31, "scale": 0.5 }
4776
+ },
4777
+ "pastValueT": {
4778
+ "dtype": "float32",
4779
+ "shape": [2, 1, 384, 64],
4780
+ "data": { "kind": "linspace", "start": 0.0, "end": 4.0 }
4781
+ },
4782
+ "seqlensKT": { "dtype": "int32", "shape": [2], "data": { "kind": "values", "values": [255, 127] } },
4783
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [256] } }
4784
+ },
4785
+ "outputs": {
4786
+ "outputT": { "dtype": "float32", "shape": [2, 128, 128], "tolerance": 0.005 },
4787
+ "presentKeyT": { "dtype": "float32", "shape": [2, 1, 384, 64], "tolerance": 0.0001 },
4788
+ "presentValueT": { "dtype": "float32", "shape": [2, 1, 384, 64], "tolerance": 0.0001 }
4789
+ },
4790
+ "provenance": {
4791
+ "source": "onnxruntime/contrib_ops/cpu/bert/group_query_attention.cc",
4792
+ "test": "GQA buffer-sharing chunk append on the materialized subgroup-matrix route",
4793
+ "notes": "Per-batch live lengths from seqlens_k: batch 0 has 128 past rows, batch 1 appends into an empty cache, and the same dispatch serves both."
4794
+ }
4795
+ },
4796
+ {
4797
+ "name": "share_append_sgmat_chunk_right_padded_b2q128cap384_h2kv1d64",
4798
+ "attrs": { "num_heads": 2, "kv_num_heads": 1 },
4799
+ "inputs": {
4800
+ "queryT": {
4801
+ "dtype": "float32",
4802
+ "shape": [2, 128, 128],
4803
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.27, "scale": 0.5 }
4804
+ },
4805
+ "keyT": { "dtype": "float32", "shape": [2, 128, 64], "data": { "kind": "linspace", "start": -1.0, "end": 1.0 } },
4806
+ "valueT": {
4807
+ "dtype": "float32",
4808
+ "shape": [2, 128, 64],
4809
+ "data": { "kind": "linspace", "start": 0.0, "end": 2.0 }
4810
+ },
4811
+ "pastKeyT": {
4812
+ "dtype": "float32",
4813
+ "shape": [2, 1, 384, 64],
4814
+ "data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.31, "scale": 0.5 }
4815
+ },
4816
+ "pastValueT": {
4817
+ "dtype": "float32",
4818
+ "shape": [2, 1, 384, 64],
4819
+ "data": { "kind": "linspace", "start": 0.0, "end": 4.0 }
4820
+ },
4821
+ "seqlensKT": { "dtype": "int32", "shape": [2], "data": { "kind": "values", "values": [255, 63] } },
4822
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [256] } }
4823
+ },
4824
+ "outputs": {
4825
+ "outputT": { "dtype": "float32", "shape": [2, 128, 128], "tolerance": 0.005 },
4826
+ "presentKeyT": { "dtype": "float32", "shape": [2, 1, 384, 64], "tolerance": 0.0001 },
4827
+ "presentValueT": { "dtype": "float32", "shape": [2, 1, 384, 64], "tolerance": 0.0001 }
4828
+ },
4829
+ "provenance": {
4830
+ "source": "onnxruntime/contrib_ops/cpu/bert/group_query_attention.cc",
4831
+ "test": "GQA buffer-sharing chunk append on the materialized subgroup-matrix route",
4832
+ "notes": "Batch 1's live length (64) is shorter than the 128-row query chunk, so its queries left-align against the live region exactly as the flash routes' saturated past length does; batch 0 stays right-aligned."
4833
+ }
4834
+ },
4835
+ {
4836
+ "name": "share_append_sgmat_chunk_qtail_b1q160cap384_h2kv1d64",
4837
+ "attrs": { "num_heads": 2, "kv_num_heads": 1 },
4838
+ "inputs": {
4839
+ "queryT": {
4840
+ "dtype": "float32",
4841
+ "shape": [1, 160, 128],
4842
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.27, "scale": 0.5 }
4843
+ },
4844
+ "keyT": { "dtype": "float32", "shape": [1, 160, 64], "data": { "kind": "linspace", "start": -1.0, "end": 1.0 } },
4845
+ "valueT": {
4846
+ "dtype": "float32",
4847
+ "shape": [1, 160, 64],
4848
+ "data": { "kind": "linspace", "start": 0.0, "end": 2.0 }
4849
+ },
4850
+ "pastKeyT": {
4851
+ "dtype": "float32",
4852
+ "shape": [1, 1, 384, 64],
4853
+ "data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.31, "scale": 0.5 }
4854
+ },
4855
+ "pastValueT": {
4856
+ "dtype": "float32",
4857
+ "shape": [1, 1, 384, 64],
4858
+ "data": { "kind": "linspace", "start": 0.0, "end": 4.0 }
4859
+ },
4860
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [255] } },
4861
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [256] } }
4862
+ },
4863
+ "outputs": {
4864
+ "outputT": { "dtype": "float32", "shape": [1, 160, 128], "tolerance": 0.005 },
4865
+ "presentKeyT": { "dtype": "float32", "shape": [1, 1, 384, 64], "tolerance": 0.0001 },
4866
+ "presentValueT": { "dtype": "float32", "shape": [1, 1, 384, 64], "tolerance": 0.0001 }
4867
+ },
4868
+ "provenance": {
4869
+ "source": "onnxruntime/contrib_ops/cpu/bert/group_query_attention.cc",
4870
+ "test": "GQA buffer-sharing chunk append on the materialized subgroup-matrix route",
4871
+ "notes": "q=160 is not a query-tile multiple, so the guarded query tails run together with the live-length ceiling."
4872
  }
4873
  },
4874
  {
 
4905
  {
4906
  "name": "splitk_decode_h8kv2_d64_kv2048_f16_ramp_value_scale_lock",
4907
  "provenance": {
4908
+ "notes": "A monotone value ramp keeps float16 decode outputs at order-one magnitude, making split-K accumulation, cross-partition rescaling, and the final normalization observable."
4909
  },
4910
  "attrs": { "num_heads": 8, "kv_num_heads": 2, "scale": 0.125, "causal": 0 },
4911
  "inputs": {
 
4936
  {
4937
  "name": "flash_prefill_h2kv1_d64_s128_f16_ramp_value_scale_lock",
4938
  "provenance": {
4939
+ "notes": "A monotone value ramp gives each of 128 causal float16 prefill rows a distinct order-one output, exposing the register-blocked q32 epilogue division and row selection."
4940
  },
4941
  "attrs": { "num_heads": 2, "kv_num_heads": 1, "scale": 0.125, "causal": 0 },
4942
  "inputs": {
 
4967
  {
4968
  "name": "newkv_past_splitk_h8kv2_d64_q1p1024_ramp_value_scale_lock",
4969
  "provenance": {
4970
+ "notes": "Monotone cached and new value ramps make decode output an order-one function of the final 256 key positions. This exposes the local-window floor, split-K combine, softmax denominator, and cached/new seam."
4971
  },
4972
  "attrs": { "num_heads": 8, "kv_num_heads": 2, "scale": 0.125, "local_window_size": 256 },
4973
  "inputs": {
 
5004
  {
5005
  "name": "newkv_past_f16_flash_h8kv2_d64_q32p32_ramp_value_scale_lock",
5006
  "provenance": {
5007
+ "notes": "Monotone cached and new value ramps make each of 32 float16 prefill rows an order-one function of its 16-key causal window. Early rows cross the cached/new seam, exposing the window floor and flash normalization."
5008
  },
5009
  "attrs": { "num_heads": 8, "kv_num_heads": 2, "scale": 0.125, "local_window_size": 16 },
5010
  "inputs": {
 
5045
  {
5046
  "name": "window_cache_decode_splitk_cap1024_b1q1_h2kv1d64_ramp_value_scale_lock",
5047
  "provenance": {
5048
+ "notes": "A monotone past-cache ramp makes the decode output depend at order-one scale on the weighted slot within the shifted window. With `T = 4096` and capacity 1024, both the eviction offset and split-K combine affect the result."
5049
  },
5050
  "attrs": { "num_heads": 2, "kv_num_heads": 1, "sliding_window_cache": 1, "local_window_size": 1024 },
5051
  "inputs": {
 
5078
  {
5079
  "name": "quant_int8_scalar_prompt_ramp_value_scale_lock",
5080
  "provenance": {
5081
+ "notes": "An INT8 value ramp from 0.5 to 2.0 gives each causal row a distinct order-one output. Tight expected values exercise value-scale dequantization, the softmax denominator, and the scalar apply division."
5082
  },
5083
  "attrs": {
5084
  "num_heads": 2,
 
5115
  {
5116
  "name": "quant_int4_scalar_prompt_ramp_value_scale_lock",
5117
  "provenance": {
5118
+ "notes": "An INT4 value ramp with scale 0.3 spans the signed nibble range and gives order-one outputs. Tight expected values exercise +8-biased nibble unpacking, dequantization, and normalization."
5119
  },
5120
  "attrs": {
5121
  "num_heads": 2,
 
5529
  "presentValueT": { "dtype": "float16", "shape": [1, 1, 512, 64], "tolerance": 0.001 }
5530
  }
5531
  },
5532
+ {
5533
+ "name": "past_kv_materialized_sgmat_f16_cooperative_q512_p512_h2kv1_d64",
5534
+ "provenance": {
5535
+ "notes": "A 128-row query tile uses 512 workgroup threads and the generic cooperative loader. Both GEMM phases stage tiles with a workgroup-strided linear walk, and the fused-softmax apply narrows each normalized score to float16 when staging it."
5536
+ },
5537
+ "tunables": { "MATERIALIZED_SGMAT_QUERY_TILE": 128 },
5538
+ "attrs": { "num_heads": 2, "kv_num_heads": 1 },
5539
+ "inputs": {
5540
+ "queryT": {
5541
+ "dtype": "float16",
5542
+ "shape": [1, 512, 128],
5543
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31 }
5544
+ },
5545
+ "keyT": { "dtype": "float16", "shape": [1, 0, 64], "data": { "kind": "values", "values": [] } },
5546
+ "valueT": { "dtype": "float16", "shape": [1, 0, 64], "data": { "kind": "values", "values": [] } },
5547
+ "pastKeyT": {
5548
+ "dtype": "float16",
5549
+ "shape": [1, 1, 512, 64],
5550
+ "data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.23 }
5551
+ },
5552
+ "pastValueT": {
5553
+ "dtype": "float16",
5554
+ "shape": [1, 1, 512, 64],
5555
+ "data": { "kind": "fillFloat32", "sinStep": 0.07, "cosStep": 0.41 }
5556
+ },
5557
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [511] } },
5558
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [512] } }
5559
+ },
5560
+ "outputs": {
5561
+ "outputT": { "dtype": "float16", "shape": [1, 512, 128], "tolerance": 0.03 },
5562
+ "presentKeyT": { "dtype": "float16", "shape": [1, 1, 512, 64], "tolerance": 0.001 },
5563
+ "presentValueT": { "dtype": "float16", "shape": [1, 1, 512, 64], "tolerance": 0.001 }
5564
+ }
5565
+ },
5566
  {
5567
  "name": "past_kv_materialized_sgmat_f16_floor_q256_p256_h2kv1_d64",
5568
  "attrs": { "num_heads": 2, "kv_num_heads": 1 },
 
5702
  "presentKeyT": { "dtype": "float32", "shape": [2, 1, 256, 64], "tolerance": 0.000001 },
5703
  "presentValueT": { "dtype": "float32", "shape": [2, 1, 256, 64], "tolerance": 0.000001 }
5704
  }
5705
+ },
5706
+ {
5707
+ "name": "flashdecode_splitk_h8kv2_d16_kv1024",
5708
+ "attrs": { "num_heads": 8, "kv_num_heads": 2, "scale": 0.25 },
5709
+ "inputs": {
5710
+ "queryT": {
5711
+ "dtype": "float32",
5712
+ "shape": [1, 1, 128],
5713
+ "data": { "kind": "fillFloat32", "scale": 2.0, "sinStep": 0.011, "cosStep": 0.029 }
5714
+ },
5715
+ "keyT": { "dtype": "float32", "shape": [1, 0, 32], "data": { "kind": "values", "values": [] } },
5716
+ "valueT": { "dtype": "float32", "shape": [1, 0, 32], "data": { "kind": "values", "values": [] } },
5717
+ "pastKeyT": {
5718
+ "dtype": "float32",
5719
+ "shape": [1, 2, 1024, 16],
5720
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.017, "cosStep": 0.019 }
5721
+ },
5722
+ "pastValueT": {
5723
+ "dtype": "float32",
5724
+ "shape": [1, 2, 1024, 16],
5725
+ "data": { "kind": "fillFloat32", "scale": 3.0, "sinStep": 0.023, "cosStep": 0.013 }
5726
+ },
5727
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1023] } },
5728
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1024] } }
5729
+ },
5730
+ "outputs": {
5731
+ "outputT": { "dtype": "float32", "shape": [1, 1, 128], "tolerance": 0.0001 },
5732
+ "presentKeyT": { "dtype": "float32", "shape": [1, 2, 1024, 16], "tolerance": 0.0001 },
5733
+ "presentValueT": { "dtype": "float32", "shape": [1, 2, 1024, 16], "tolerance": 0.0001 }
5734
+ },
5735
+ "provenance": {
5736
+ "notes": "Cached decode at head size 16 exercises the split-K route exactly at its minimum head-size boundary."
5737
+ }
5738
  }
5739
  ]
5740
  }