Xenova HF Staff commited on
Commit
f96162c
·
verified ·
1 Parent(s): bbbd4e6

sync 2e7068faf55e

Browse files
README.md CHANGED
@@ -1,3 +1,132 @@
1
  ---
 
2
  license: apache-2.0
 
 
 
 
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ library_name: kernels
3
  license: apache-2.0
4
+ tags:
5
+ - kernel
6
+ - webgpu
7
+ - wgsl
8
  ---
9
+ # com.microsoft.GroupQueryAttention
10
+
11
+ `com.microsoft` · ONNX Runtime contrib operator · contrib since_version 1
12
+
13
+ ## Description
14
+
15
+ Grouped-query attention for explicit BSH Q/K/V and BNSH caches. Direct Q/K/V supports bidirectional attention or causal local windows and may store its generated float cache independently as float16 or float32; existing unquantized cache inputs match the Q/K/V dtype. Causal cache paths support rotary embeddings, sliding windows, bias, head sinks, softcap, smooth softmax, and paired Q/K RMS normalization. Int8/int4 caches require float32 Q/K/V and output; int4 is prompt-only. Packed QKV, position IDs, interleaved rotary, bfloat16/float8, and diagnostic QK output are not implemented.
16
+
17
+ See the [ONNX Runtime `GroupQueryAttention` contrib-operator spec](https://github.com/microsoft/onnxruntime/blob/main/docs/ContribOperators.md#com.microsoft.GroupQueryAttention) for the reference semantics.
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
+
49
+ Attributes and default values (overridable per request):
50
+
51
+ | Attribute | Default | Description |
52
+ | --- | --- | --- |
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
+
69
+ | Variable | Allowed dtypes |
70
+ | --- | --- |
71
+ | `T` | `float32`, `float16` |
72
+ | `T_CACHE` | `float32`, `float16`, `uint8`, `int8` |
73
+ | `T_KV_SCALE` | `float32` |
74
+ | `M` | `int32` |
75
+
76
+ ## Device requirements
77
+
78
+ Some implementation variants require `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
86
+ - [`attention-rank4-tiled.wgsl.jinja`](build/webgpu/attention-rank4-tiled.wgsl.jinja)
87
+ - [`attn-flash-decode-splitk-merge.wgsl.jinja`](build/webgpu/attn-flash-decode-splitk-merge.wgsl.jinja)
88
+ - [`attn-flash-decode-splitk.wgsl.jinja`](build/webgpu/attn-flash-decode-splitk.wgsl.jinja)
89
+ - [`attn-flash-online.wgsl.jinja`](build/webgpu/attn-flash-online.wgsl.jinja)
90
+ - [`attn-flash-prefill-cluster.wgsl.jinja`](build/webgpu/attn-flash-prefill-cluster.wgsl.jinja)
91
+ - [`attn-flash-q32-broadcast.wgsl.jinja`](build/webgpu/attn-flash-q32-broadcast.wgsl.jinja)
92
+ - [`attn-online-scalar.wgsl.jinja`](build/webgpu/attn-online-scalar.wgsl.jinja)
93
+ - [`gqa-attention.wgsl.jinja`](build/webgpu/gqa-attention.wgsl.jinja)
94
+ - [`gqa-present.wgsl.jinja`](build/webgpu/gqa-present.wgsl.jinja)
95
+ - [`gqa-qprep.wgsl.jinja`](build/webgpu/gqa-qprep.wgsl.jinja)
96
+
97
+ ## Use with `@huggingface/kernels`
98
+
99
+ The loader automatically allocates outputs whose metadata it can derive from the manifest contract and this call.
100
+
101
+ The explicit `outputs` entries provide shape and logical dtype metadata for the results listed below:
102
+
103
+ - `presentKeyT`
104
+ - `presentValueT`
105
+
106
+ Each entry either requests an optional result or supplies metadata that cannot be inferred from the inputs.
107
+
108
+ The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
109
+
110
+ Replace each `*Data` placeholder with a typed array containing the corresponding input data.
111
+
112
+ ```js
113
+ import { getKernel } from "@huggingface/kernels";
114
+
115
+ const kernel = await getKernel("webgpu-kernels/com.microsoft.GroupQueryAttention", { version: 1 });
116
+ // Explicit destinations request optional results or supply metadata that cannot be inferred.
117
+ const { outputT, presentKeyT, presentValueT } = await kernel({
118
+ queryT: { data: queryTData, shape: [2, 1, 16] },
119
+ keyT: { data: keyTData, shape: [2, 1, 8] },
120
+ valueT: { data: valueTData, shape: [2, 1, 8] },
121
+ pastKeyT: { data: pastKeyTData, shape: [2, 1, 8, 8] },
122
+ pastValueT: { data: pastValueTData, shape: [2, 1, 8, 8] },
123
+ seqlensKT: { data: seqlensKTData, shape: [2] },
124
+ totalSequenceLengthT: { data: totalSequenceLengthTData, shape: [1] },
125
+ }, {
126
+ attrs: { num_heads: 2, kv_num_heads: 1 },
127
+ outputs: {
128
+ presentKeyT: { shape: [2, 1, 8, 8], dtype: "float32" },
129
+ presentValueT: { shape: [2, 1, 8, 8], dtype: "float32" },
130
+ },
131
+ });
132
+ ```
build/webgpu/attention-rank4-tiled.wgsl.jinja ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
20
+ var<workgroup> acc: array<f32, {{ vHeadCap * blockM }}>;
21
+
22
+ {% set ATTN_SCALE_DIM = "params.headSize" %}
23
+ {% if ATTN_SCALE_DIM is not defined %}{% set ATTN_SCALE_DIM = "HEAD_DIM" %}{% endif %}
24
+ fn scale_value() -> f32 {
25
+ if (params.scale != 0.0) { return params.scale; }
26
+ return inverseSqrt(f32({{ ATTN_SCALE_DIM }}));
27
+ }
28
+
29
+
30
+ fn kv_head(q_head: u32) -> u32 {
31
+ return q_head / (params.qHeads / params.kvHeads);
32
+ }
33
+
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.
47
+ let batchHeads = params.count / (params.vHeadSize * params.qSeq);
48
+ if (tile >= batchHeads * qTiles) { return; }
49
+ let qTile = tile % qTiles;
50
+ var t = tile / qTiles;
51
+ let qh = t % params.qHeads;
52
+ let batch = t / params.qHeads;
53
+ let qs = qTile * BLOCK_M + tid;
54
+ // Threads past the q-seq tail own no query row. No barriers below, so they just
55
+ // exit (they hold no shared state the surviving threads read).
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) {
64
+ acc[d * BLOCK_M + tid] = 0.0;
65
+ }
66
+ var running_max: f32 = -3.4028234663852886e38;
67
+ var running_denom: f32 = 0.0;
68
+
69
+ // Causal: query qs attends keys [0, qs]. Per-thread bound — threads in the tile
70
+ // diverge on trip count, which is fine (no barriers).
71
+ var maxKj = params.kvSeq;
72
+ if (params.isCausal != 0u) {
73
+ maxKj = min(maxKj, qs + 1u);
74
+ }
75
+
76
+ for (var ks: u32 = 0u; ks < maxKj; ks = ks + 1u) {
77
+ var masked = false;
78
+ {% if hasMask and maskIsBool %}
79
+ let mIdxB = batch * params.maskBatchStride + qh * params.maskHeadStride + qs * params.maskSeqStride + ks;
80
+ masked = attn_mask[mIdxB] == 0u;
81
+ {% endif %}
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]);
92
+ }
93
+ score = dot * scale;
94
+ if (params.softcap != 0.0) {
95
+ score = params.softcap * tanh(score / params.softcap);
96
+ }
97
+ {% if hasMask and not maskIsBool %}
98
+ let mIdxA = batch * params.maskBatchStride + qh * params.maskHeadStride + qs * params.maskSeqStride + ks;
99
+ score = score + f32(attn_mask[mIdxA]);
100
+ {% endif %}
101
+ }
102
+
103
+ let next_max = max(running_max, score);
104
+ let prev_scale = exp(running_max - next_max);
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
+ }
119
+ }
build/webgpu/attn-flash-decode-splitk-merge.wgsl.jinja ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% if scalar == "f16" %}
2
+ 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
9
+ // states (max, denom, weighted V sum) written by the split pass into the final
10
+ // attention output for each query token, using the online softmax merge:
11
+ // globalMax = max_s partialMax[s]
12
+ // globalDen = sum_s partialDenom[s] * exp(partialMax[s] - globalMax)
13
+ // out[d] = (sum_s partialOut[s, d] * exp(partialMax[s] - globalMax)) / globalDen
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 %}
24
+ const HEAD_DIM_V4: u32 = {{ headDimV4 }}u;
25
+ const Q_HEADS: u32 = {{ qNumHeads }}u;
26
+ 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 %}
34
+ // Packed [Q; K; V] bias rows. The V bias is token-independent, so the split
35
+ // pass leaves it off and it is added here once after the final normalize,
36
+ // matching the non-split flash-decode semantics exactly.
37
+ const HEAD_DIM: u32 = {{ headDim }}u;
38
+ const Q_HIDDEN: u32 = {{ qHidden }}u;
39
+ {% endif %}
40
+ // FLT_MAX, not -inf, as the online (m, d) accumulator init: merges must keep
41
+ // `m - m` finite so an empty lane / all--inf row contributes the exact
42
+ // accumulator identity (m, d) = (-FLT_MAX, 0). Operator epilogues interpret
43
+ // a zero final denominator according to their public semantics. Using -inf
44
+ // here changes +inf-row behavior.
45
+ const FLT_MAX: f32 = 3.4028234663852886e38;
46
+
47
+ fn is_finite_f32(value: f32) -> bool {
48
+ return select(false, value <= FLT_MAX, value >= -FLT_MAX);
49
+ }
50
+
51
+ // x - m that is exactly 0 when x equals a finite m, so exp(shifted) == 1
52
+ // exactly at the row max. `x - x` on an infinite max is a legal fast-math
53
+ // fold to 0, which would silently turn +inf rows finite — the explicit
54
+ // equality test keeps the NaN propagation of the serial kernels.
55
+ fn shifted_value(value: f32, maxValue: f32) -> f32 {
56
+ let equalFiniteMax = select(false, value == maxValue, is_finite_f32(maxValue));
57
+ return select(value - maxValue, 0.0, equalFiniteMax);
58
+ }
59
+ fn exp_shift(value: f32, maxValue: f32) -> f32 {
60
+ return exp(shifted_value(value, maxValue));
61
+ }
62
+ @compute @workgroup_size(HEAD_DIM_V4, 1, 1)
63
+ fn main(
64
+ @builtin(workgroup_id) wg: vec3<u32>,
65
+ @builtin(local_invocation_id) lid: vec3<u32>
66
+ ) {
67
+ {% if splitQueries %}
68
+ let queryToken = wg.x;
69
+ {% endif %}
70
+ let h = wg.y;
71
+ let b = wg.z;
72
+ let d4 = lid.x;
73
+ if (h >= Q_HEADS || d4 >= HEAD_DIM_V4{% if splitQueries %} || queryToken >= Q_SEQ{% endif %}) {
74
+ return;
75
+ }
76
+
77
+ {% if splitQueries %}
78
+ let mdBase = ((b * Q_SEQ + queryToken) * Q_HEADS + h) * NUM_SPLITS;
79
+ {% else %}
80
+ let mdBase = (b * Q_HEADS + h) * NUM_SPLITS;
81
+ {% endif %}
82
+ var globalMax = -FLT_MAX;
83
+ for (var s: u32 = 0u; s < NUM_SPLITS; s = s + 1u) {
84
+ globalMax = max(globalMax, partial_stats[mdBase + s].x);
85
+ }
86
+ var globalDenom = 0.0;
87
+ var outV = vec4<f32>(0.0);
88
+ for (var s: u32 = 0u; s < NUM_SPLITS; s = s + 1u) {
89
+ let stats = partial_stats[mdBase + s];
90
+ let w = exp_shift(stats.x, globalMax);
91
+ globalDenom = globalDenom + stats.y * w;
92
+ {% if splitQueries %}
93
+ let pBase = (((b * Q_SEQ + queryToken) * Q_HEADS + h) * NUM_SPLITS + s) * HEAD_DIM_V4;
94
+ {% else %}
95
+ let pBase = ((b * Q_HEADS + h) * NUM_SPLITS + s) * HEAD_DIM_V4;
96
+ {% endif %}
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 %}
110
+ let qBaseV4 = ((b * Q_HEADS + h) * Q_SEQ + queryToken) * HEAD_DIM_V4;
111
+ {% else %}
112
+ let qBaseV4 = (b * Q_HEADS + h) * HEAD_DIM_V4;
113
+ {% endif %}
114
+ {% endif %}
115
+ // A split that covers no keys writes vec2(-FLT_MAX, 0), so when every split is empty —
116
+ // a query whose window admits nothing — globalMax stays -FLT_MAX, exp_shift(x, x) is 1,
117
+ // and globalDenom lands on exactly 0. Emit 0 rather than dividing so an all-empty
118
+ // row has a defined zero result.
119
+ var outValue = select(vec4<f32>(0.0), outV / globalDenom, globalDenom > 0.0);
120
+ {% if hasBias %}
121
+ // V bias row base: 2 * qHidden (skip the packed Q and K bias blocks) + this head.
122
+ let vBiasBase = 2u * Q_HIDDEN + h * HEAD_DIM + d4 * 4u;
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 %}
130
+ output[qBaseV4 + d4] = vec4<{{ scalar }}>(outValue);
131
+ }
build/webgpu/attn-flash-decode-splitk.wgsl.jinja ADDED
@@ -0,0 +1,549 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% if useSubgroups is not defined %}{% set useSubgroups = true %}{% endif %}
2
+ {% if splitQueries is not defined %}{% set splitQueries = false %}{% endif %}
3
+ {% if quantizedCache is not defined %}{% set quantizedCache = false %}{% endif %}
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
21
+ // KV sequence into `NUM_SPLITS` contiguous ranges and gives each range its own
22
+ // workgroup, so `batch * numHeads * NUM_SPLITS` workgroups run the tiled online
23
+ // softmax in parallel. Each workgroup emits the *un-normalized* online state for
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 }};
49
+ {% endif %}
50
+ const WG: u32 = {{ splitKWorkgroupSize }}u;
51
+ const NUM_SPLITS: u32 = {{ numSplits }}u;
52
+ {% if splitQueries %}
53
+ const Q_SEQ: u32 = {{ qSeq }}u;
54
+ {% endif %}
55
+ // FLT_MAX, not -inf, as the online (m, d) accumulator init: merges must keep
56
+ // `m - m` finite so an empty lane / all--inf row contributes the exact
57
+ // accumulator identity (m, d) = (-FLT_MAX, 0). Operator epilogues interpret
58
+ // a zero final denominator according to their public semantics. Using -inf
59
+ // here changes +inf-row behavior.
60
+ const FLT_MAX: f32 = 3.4028234663852886e38;
61
+
62
+ fn is_finite_f32(value: f32) -> bool {
63
+ return select(false, value <= FLT_MAX, value >= -FLT_MAX);
64
+ }
65
+
66
+ // x - m that is exactly 0 when x equals a finite m, so exp(shifted) == 1
67
+ // exactly at the row max. `x - x` on an infinite max is a legal fast-math
68
+ // fold to 0, which would silently turn +inf rows finite — the explicit
69
+ // equality test keeps the NaN propagation of the serial kernels.
70
+ fn shifted_value(value: f32, maxValue: f32) -> f32 {
71
+ let equalFiniteMax = select(false, value == maxValue, is_finite_f32(maxValue));
72
+ return select(value - maxValue, 0.0, equalFiniteMax);
73
+ }
74
+ fn exp_shift(value: f32, maxValue: f32) -> f32 {
75
+ return exp(shifted_value(value, maxValue));
76
+ }
77
+
78
+ var<workgroup> q_shared: array<vec4<f32>, HEAD_DIM_V4>;
79
+ var<workgroup> running_out: array<vec4<f32>, HEAD_DIM_V4>;
80
+ var<workgroup> probs: array<f32, WG>;
81
+ {% set coopQk = useSubgroups and headDimV4 >= 8 and not (usesF16 and headDimV4 <= 32) %}
82
+ {% set jGroups = (splitKWorkgroupSize / headDimV4)|int %}
83
+ {% set jSplitV = (splitKWorkgroupSize % headDimV4 == 0) and (jGroups >= 2) %}
84
+ {% if coopQk %}
85
+ var<workgroup> sval_sh: array<f32, WG>;
86
+ {% endif %}
87
+ {% if jSplitV %}
88
+ var<workgroup> vacc_sh: array<vec4<f32>, WG>;
89
+ {% endif %}
90
+ {% set combineSubgroups = useSubgroups %}
91
+ // Workgroup-cooperative merge of per-thread online-softmax (m, d) partials:
92
+ // mNew = max(m1, m2)
93
+ // dNew = d1 * exp(m1 - mNew) + d2 * exp(m2 - mNew)
94
+ // Both the subgroup and portable barrier-tree engines return the same merged
95
+ // pair to every invocation. Repeated merges require a workgroup barrier between
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
116
+ // element made exp(inf - inf) = NaN stick in that lane's d; a NaN element
117
+ // landed in d via exp(NaN); both survive the merge and are detected by the
118
+ // code after the reduction.
119
+ let sgD = subgroupAdd(d * exp_shift(m, sgM));
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
+ }
151
+ workgroupBarrier();
152
+ return combinedMD;
153
+ }
154
+ {% else %}
155
+ {% set mdStreamed = mdStreams is defined %}
156
+ {% set mdStreams = mdStreams if mdStreams is defined else 1 %}
157
+ {% set mdExtent = "WG" if mdStreams == 1 else "WG * " ~ mdStreams ~ "u" %}
158
+ 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;
168
+ loop {
169
+ if (stride == 0u) {
170
+ break;
171
+ }
172
+ if (lidx < stride) {
173
+ {% for s in range(mdStreams) %}
174
+ {
175
+ let slot = {{ s }}u * WG + lidx;
176
+ let m1 = partialM[slot];
177
+ let d1 = partialD[slot];
178
+ let m2 = partialM[slot + stride];
179
+ let d2 = partialD[slot + stride];
180
+ let mNew = max(m1, m2);
181
+ partialD[slot] = d1 * exp_shift(m1, mNew) + d2 * exp_shift(m2, mNew);
182
+ partialM[slot] = mNew;
183
+ }
184
+ {% endfor %}
185
+ }
186
+ workgroupBarrier();
187
+ stride = stride / 2u;
188
+ }
189
+ }
190
+ {% else %}
191
+
192
+ fn combine_partials(m: f32, d: f32, lidx: u32) -> vec2<f32> {
193
+ partialM[lidx] = m;
194
+ partialD[lidx] = d;
195
+ workgroupBarrier();
196
+ var stride = WG / 2u;
197
+ loop {
198
+ if (stride == 0u) {
199
+ break;
200
+ }
201
+ if (lidx < stride) {
202
+ let m1 = partialM[lidx];
203
+ let d1 = partialD[lidx];
204
+ let m2 = partialM[lidx + stride];
205
+ let d2 = partialD[lidx + stride];
206
+ let mNew = max(m1, m2);
207
+ partialD[lidx] = d1 * exp_shift(m1, mNew) + d2 * exp_shift(m2, mNew);
208
+ partialM[lidx] = mNew;
209
+ }
210
+ workgroupBarrier();
211
+ stride = stride / 2u;
212
+ }
213
+ let merged = vec2<f32>(partialM[0], partialD[0]);
214
+ // Trailing barrier so back-to-back calls cannot race a next call's partial
215
+ // stores against this call's reads of slot 0.
216
+ workgroupBarrier();
217
+ return merged;
218
+ }
219
+ {% endif %}
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 %}
227
+ return {{ ATTN_SCALE_OVERRIDE }};
228
+ {% else %}
229
+ if (params.scale != 0.0) { return params.scale; }
230
+ return inverseSqrt(f32({{ ATTN_SCALE_DIM }}));
231
+ {% endif %}
232
+ }
233
+
234
+
235
+ {% if quantizedCache %}
236
+ {% macro emit_quant_scale4(kind, scaleBuffer) %}
237
+ fn {{ kind }}scale4(d4: u32, hk: u32) -> vec4<f32> {
238
+ if (params.perChannel == 0u) {
239
+ return vec4<f32>({{ scaleBuffer }}[0]);
240
+ }
241
+ let base = hk * HEAD_DIM + d4 * 4u;
242
+ return vec4<f32>(
243
+ {{ scaleBuffer }}[base],
244
+ {{ scaleBuffer }}[base + 1u],
245
+ {{ scaleBuffer }}[base + 2u],
246
+ {{ scaleBuffer }}[base + 3u]
247
+ );
248
+ }
249
+ {%- endmacro %}
250
+ {%- macro emit_quant_load4(format, kind, buffer, scaleBuffer) %}
251
+ {{ emit_quant_scale4(kind, scaleBuffer) }}
252
+ fn load_{{ kind }}4(indexV4: u32, d4: u32, hk: u32) -> vec4<f32> {
253
+ {%- if format == "int8" %}
254
+ return vec4<f32>({{ buffer }}[indexV4]) * {{ kind }}scale4(d4, hk);
255
+ {%- else %}
256
+ // Two elements cover this vec4: each carries two +8-biased nibbles, low first.
257
+ let rowBase = indexV4 - d4;
258
+ let lo = {{ buffer }}[rowBase + d4 * 2u];
259
+ let hi = {{ buffer }}[rowBase + d4 * 2u + 1u];
260
+ let nibbles = vec4<i32>(
261
+ i32(lo & 0xFu), i32((lo >> 4u) & 0xFu),
262
+ i32(hi & 0xFu), i32((hi >> 4u) & 0xFu)
263
+ );
264
+ let signed = nibbles - vec4<i32>(8);
265
+ return vec4<f32>(signed) * {{ kind }}scale4(d4, hk);
266
+ {%- endif %}
267
+ }
268
+ {%- endmacro %}
269
+
270
+ {{ emit_quant_load4("int8", "key", "key", "k_scale") }}
271
+ {{ emit_quant_load4("int8", "value", "value", "v_scale") }}
272
+ {% else %}
273
+ fn load_key4(indexV4: u32) -> vec4<f32> {
274
+ return vec4<f32>(key[indexV4]);
275
+ }
276
+
277
+ fn load_value4(indexV4: u32) -> vec4<f32> {
278
+ return vec4<f32>(value[indexV4]);
279
+ }
280
+ {% endif %}
281
+
282
+ {% if hasBias %}
283
+ // Packed [Q; K; V] bias rows (token-independent). The Q bias folds into the
284
+ // query row before the Q.K dots; the K bias adds a constant to every key score
285
+ // that softmax cancels, so it is skipped; the V bias is token-independent and
286
+ // is applied once in the merge pass after the final normalize.
287
+ fn load_bias4(base: u32, d4: u32) -> vec4<f32> {
288
+ let offset = base + d4 * 4u;
289
+ return vec4<f32>(bias[offset], bias[offset + 1u], bias[offset + 2u], bias[offset + 3u]);
290
+ }
291
+
292
+ {% endif %}
293
+ @compute @workgroup_size(WG, 1, 1)
294
+ fn main(
295
+ @builtin(workgroup_id) wg: vec3<u32>,
296
+ @builtin(local_invocation_id) lid: vec3<u32>{% if useSubgroups %},
297
+ @builtin(subgroup_size) sgSize: u32{% endif %}
298
+ ) {
299
+ {% if useSubgroups %}
300
+ // Subgroup tiles partition the fixed workgroup exactly. The advertised range
301
+ // is validated before dispatch; retain this uniform guard for implementations that
302
+ // choose an intermediate width at pipeline execution time.
303
+ if (sgSize == 0u || sgSize > WG || WG % sgSize != 0u) { return; }
304
+ {% endif %}
305
+ {% if splitQueries %}
306
+ let queryToken = wg.x / NUM_SPLITS;
307
+ let split = wg.x % NUM_SPLITS;
308
+ {% else %}
309
+ let split = wg.x;
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;
322
+ {% if cacheSeqlens %}
323
+ // Buffer-sharing caches retain their capacity in the physical BNSH stride;
324
+ // seqlens_k supplies the active end independently for each batch.
325
+ let kvSeq = min(cacheSeq, u32(seqlens_k[b]) + 1u);
326
+ {% else %}
327
+ let kvSeq = cacheSeq;
328
+ {% endif %}
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 %}
336
+ let qBaseV4 = b * Q_HIDDEN_V4 + h * HEAD_DIM_V4;
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;
344
+ {% else %}
345
+ {% if splitQueries %}
346
+ let qBaseV4 = ((b * Q_HEADS + h) * Q_SEQ + queryToken) * HEAD_DIM_V4;
347
+ {% else %}
348
+ let qBaseV4 = (b * Q_HEADS + h) * HEAD_DIM_V4;
349
+ {% endif %}
350
+ let kvBaseV4 = (b * KV_HEADS + hKv) * cacheSeq * HEAD_DIM_V4;
351
+ let kvTokenStrideV4 = HEAD_DIM_V4;
352
+ {% endif %}
353
+
354
+ // Contiguous KV range owned by this split. Ceil division lets the last split
355
+ // absorb any remainder; empty ranges write identity partials and are ignored
356
+ // by the merge pass.
357
+ {% if hasWindow %}
358
+ // Sliding window on the single decode query (absolute position
359
+ // kvSeq-1): it attends only the last `windowSize` keys, so split the
360
+ // contiguous [windowStart, kvSeq) range instead of the whole cache.
361
+ var windowStart: u32 = 0u;
362
+ if (kvSeq > params.windowSize) {
363
+ windowStart = kvSeq - params.windowSize;
364
+ }
365
+ let activeKeys = kvSeq - windowStart;
366
+ let keysPerSplit = (activeKeys + NUM_SPLITS - 1u) / NUM_SPLITS;
367
+ let splitStart = windowStart + split * keysPerSplit;
368
+ {% else %}
369
+ let keysPerSplit = (kvSeq + NUM_SPLITS - 1u) / NUM_SPLITS;
370
+ let splitStart = split * keysPerSplit;
371
+ {% endif %}
372
+ var splitEnd = splitStart + keysPerSplit;
373
+ if (splitEnd > kvSeq) {
374
+ splitEnd = kvSeq;
375
+ }
376
+
377
+ {% set hasBias = hasBias is defined and hasBias %}
378
+ for (var d4 = tid; d4 < HEAD_DIM_V4; d4 = d4 + WG) {
379
+ var qv = vec4<f32>(query[qBaseV4 + d4]);
380
+ {% if hasBias %}
381
+ qv = qv + load_bias4(h * HEAD_DIM, d4);
382
+ {% endif %}
383
+ q_shared[d4] = qv;
384
+ running_out[d4] = vec4<f32>(0.0);
385
+ }
386
+ workgroupBarrier();
387
+ let scale = scale_value();
388
+ var runningMax = -FLT_MAX;
389
+ var runningDenom = 0.0;
390
+
391
+ var kjBase = splitStart;
392
+ loop {
393
+ if (kjBase >= splitEnd) {
394
+ break;
395
+ }
396
+ let kj = kjBase + tid;
397
+ var keyAllowed = kj < splitEnd;
398
+ let tileCount = min(WG, splitEnd - kjBase);
399
+
400
+ var score = -FLT_MAX;
401
+ var m = -FLT_MAX;
402
+ var dPart = 0.0;
403
+ {% if coopQk %}
404
+ // Cooperative Q.K: one subgroup per key, lanes splitting HEAD_DIM_V4, then a hardware
405
+ // subgroupAdd — turns the per-thread HEAD_DIM_V4-long dependent dot chain into a few
406
+ // strided vec4 dots + one reduce. Uniform trip count keeps subgroupAdd in uniform flow.
407
+ let sgPerWg = WG / sgSize;
408
+ let qkRounds = (tileCount + sgPerWg - 1u) / sgPerWg;
409
+ let lane = tid % sgSize;
410
+ let sgInWg = tid / sgSize;
411
+ for (var rr: u32 = 0u; rr < qkRounds; rr = rr + 1u) {
412
+ let j = rr * sgPerWg + sgInWg;
413
+ var accS: f32 = 0.0;
414
+ if (j < tileCount) {
415
+ let kRowV4 = kvBaseV4 + (kjBase + j) * kvTokenStrideV4;
416
+ for (var d4: u32 = lane; d4 < HEAD_DIM_V4; d4 = d4 + sgSize) {
417
+ accS = accS + dot(q_shared[d4], load_key4(kRowV4 + d4{% if quantizedCache %}, d4, hKv{% endif %}));
418
+ }
419
+ }
420
+ let sj = subgroupAdd(accS);
421
+ if (lane == 0u && j < tileCount) {
422
+ sval_sh[j] = sj;
423
+ }
424
+ }
425
+ workgroupBarrier();
426
+ if (keyAllowed) {
427
+ score = sval_sh[tid] * scale;
428
+ m = score;
429
+ dPart = 1.0;
430
+ }
431
+ {% else %}
432
+ if (keyAllowed) {
433
+ let kRowV4 = kvBaseV4 + kj * kvTokenStrideV4;
434
+ var acc: f32 = 0.0;
435
+ for (var d4: u32 = 0u; d4 < HEAD_DIM_V4; d4 = d4 + 1u) {
436
+ acc = acc + dot(q_shared[d4], load_key4(kRowV4 + d4{% if quantizedCache %}, d4, hKv{% endif %}));
437
+ }
438
+ score = acc * scale;
439
+ m = score;
440
+ dPart = 1.0;
441
+ }
442
+ {% endif %}
443
+ {% if hasMask %}
444
+ if (keyAllowed) {
445
+ {% if splitQueries %}
446
+ let maskQuery = queryToken;
447
+ {% else %}
448
+ let maskQuery = 0u;
449
+ {% endif %}
450
+ let maskIndex = b * params.maskBatchStride + h * params.maskHeadStride + maskQuery * params.maskSeqStride + kj;
451
+ {% if maskIsBool %}
452
+ // A rejected bool-mask key contributes no probability mass. The merge
453
+ // pass already maps a zero global denominator to an all-zero output row.
454
+ if (attn_mask[maskIndex] == 0u) {
455
+ keyAllowed = false;
456
+ score = -FLT_MAX;
457
+ dPart = 0.0;
458
+ }
459
+ {% else %}
460
+ score = score + f32(attn_mask[maskIndex]);
461
+ {% endif %}
462
+ m = score;
463
+ }
464
+ {% endif %}
465
+ let tile = combine_partials(m, dPart, tid{% if useSubgroups %}, sgSize{% endif %});
466
+
467
+ // Merge one key tile's online-softmax (maximum, denominator) partial into the
468
+ // running state, then store the per-key probabilities consumed by V accumulation.
469
+ let newMax = max(runningMax, tile.x);
470
+ let correction = exp_shift(runningMax, newMax);
471
+ runningDenom = runningDenom * correction + tile.y * exp_shift(tile.x, newMax);
472
+ runningMax = newMax;
473
+
474
+ var prob = 0.0;
475
+ if (keyAllowed) {
476
+ prob = exp_shift(score, newMax);
477
+ }
478
+ probs[tid] = prob;
479
+ workgroupBarrier();
480
+
481
+
482
+ {% if jSplitV %}
483
+ // j-split V accumulation: thread (jg, d4v) sums keys j == jg mod
484
+ // J_GROUPS for dim block d4v into a register, then the groups combine
485
+ // through shared memory so all lanes participate.
486
+ const J_GROUPS: u32 = {{ jGroups }}u;
487
+ let jg = tid / HEAD_DIM_V4;
488
+ let d4v = tid % HEAD_DIM_V4;
489
+ var vacc = vec4<f32>(0.0);
490
+ var jj = jg;
491
+ loop {
492
+ if (jj >= tileCount) { break; }
493
+ vacc = vacc + probs[jj] * load_value4(
494
+ kvBaseV4 + (kjBase + jj) * kvTokenStrideV4 + d4v{% if quantizedCache %},
495
+ d4v,
496
+ hKv{% endif %}
497
+ );
498
+ jj = jj + J_GROUPS;
499
+ }
500
+ vacc_sh[tid] = vacc;
501
+ workgroupBarrier();
502
+ for (var d4: u32 = tid; d4 < HEAD_DIM_V4; d4 = d4 + WG) {
503
+ var a4 = running_out[d4] * correction;
504
+ for (var g: u32 = 0u; g < J_GROUPS; g = g + 1u) {
505
+ a4 = a4 + vacc_sh[g * HEAD_DIM_V4 + d4];
506
+ }
507
+ running_out[d4] = a4;
508
+ }
509
+ workgroupBarrier();
510
+ {% else %}
511
+ for (var d4: u32 = tid; d4 < HEAD_DIM_V4; d4 = d4 + WG) {
512
+ var vSum = vec4<f32>(0.0);
513
+ for (var i: u32 = 0u; i < tileCount; i = i + 1u) {
514
+ vSum = vSum + probs[i] * load_value4(
515
+ kvBaseV4 + (kjBase + i) * kvTokenStrideV4 + d4{% if quantizedCache %},
516
+ d4,
517
+ hKv{% endif %}
518
+ );
519
+ }
520
+ running_out[d4] = running_out[d4] * correction + vSum;
521
+ }
522
+ workgroupBarrier();
523
+ {% endif %}
524
+
525
+ kjBase = kjBase + WG;
526
+ }
527
+
528
+ // Emit un-normalized partials for (b, h, split): the merge pass divides.
529
+ {% if splitQueries %}
530
+ let partialBase = (((b * Q_SEQ + queryToken) * Q_HEADS + h) * NUM_SPLITS + split) * HEAD_DIM_V4;
531
+ {% else %}
532
+ let partialBase = ((b * Q_HEADS + h) * NUM_SPLITS + split) * HEAD_DIM_V4;
533
+ {% endif %}
534
+ for (var d4: u32 = tid; d4 < HEAD_DIM_V4; d4 = d4 + WG) {
535
+ partial_out[partialBase + d4] = running_out[d4];
536
+ }
537
+ if (tid == 0u) {
538
+ {% if splitQueries %}
539
+ let mdBase = ((b * Q_SEQ + queryToken) * Q_HEADS + h) * NUM_SPLITS + split;
540
+ {% else %}
541
+ let mdBase = (b * Q_HEADS + h) * NUM_SPLITS + split;
542
+ {% endif %}
543
+ // (max, denom) travel together to the merge, so they share one buffer as an
544
+ // interleaved vec2 rather than costing two bindings. Interleaved, not two
545
+ // halves, so the index needs no region size — and the merge reads both
546
+ // fields of a split in a single load.
547
+ partial_stats[mdBase] = vec2<f32>(runningMax, runningDenom);
548
+ }
549
+ }
build/webgpu/attn-flash-online.wgsl.jinja ADDED
@@ -0,0 +1,367 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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)
14
+ // reduction. The running softmax state merges tiles with the stable online
15
+ // max-and-denominator rule, and the headDim output accumulator is rescaled once
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.
25
+ {% endif %}
26
+ {% if hasMask %}
27
+ // Per-score mask uses broadcast strides. A stride of 0 broadcasts that axis;
28
+ // rank-2 [q, k] masks set batch/head strides to 0. A rejected bool-mask key
29
+ // contributes no softmax mass, and a fully masked row emits zero.
30
+ {% endif %}
31
+ 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;
42
+ // FLT_MAX, not -inf, as the online (m, d) accumulator init: merges must keep
43
+ // `m - m` finite so an empty lane / all--inf row contributes the exact
44
+ // accumulator identity (m, d) = (-FLT_MAX, 0). Operator epilogues interpret
45
+ // a zero final denominator according to their public semantics. Using -inf
46
+ // here changes +inf-row behavior.
47
+ const FLT_MAX: f32 = 3.4028234663852886e38;
48
+
49
+ fn is_finite_f32(value: f32) -> bool {
50
+ return select(false, value <= FLT_MAX, value >= -FLT_MAX);
51
+ }
52
+
53
+ // x - m that is exactly 0 when x equals a finite m, so exp(shifted) == 1
54
+ // exactly at the row max. `x - x` on an infinite max is a legal fast-math
55
+ // fold to 0, which would silently turn +inf rows finite — the explicit
56
+ // equality test keeps the NaN propagation of the serial kernels.
57
+ fn shifted_value(value: f32, maxValue: f32) -> f32 {
58
+ let equalFiniteMax = select(false, value == maxValue, is_finite_f32(maxValue));
59
+ return select(value - maxValue, 0.0, equalFiniteMax);
60
+ }
61
+ fn exp_shift(value: f32, maxValue: f32) -> f32 {
62
+ return exp(shifted_value(value, maxValue));
63
+ }
64
+
65
+ var<workgroup> q_shared: array<vec4<f32>, HEAD_DIM_V4>;
66
+ var<workgroup> running_out: array<vec4<f32>, HEAD_DIM_V4>;
67
+ var<workgroup> probs: array<f32, WG>;
68
+ // Workgroup-cooperative merge of per-thread online-softmax (m, d) partials:
69
+ // mNew = max(m1, m2)
70
+ // dNew = d1 * exp(m1 - mNew) + d2 * exp(m2 - mNew)
71
+ // Both the subgroup and portable barrier-tree engines return the same merged
72
+ // pair to every invocation. Repeated merges require a workgroup barrier between
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
93
+ // element made exp(inf - inf) = NaN stick in that lane's d; a NaN element
94
+ // landed in d via exp(NaN); both survive the merge and are detected by the
95
+ // code after the reduction.
96
+ let sgD = subgroupAdd(d * exp_shift(m, sgM));
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
+ }
128
+ workgroupBarrier();
129
+ return combinedMD;
130
+ }
131
+ {% else %}
132
+ {% set mdStreamed = mdStreams is defined %}
133
+ {% set mdStreams = mdStreams if mdStreams is defined else 1 %}
134
+ {% set mdExtent = "WG" if mdStreams == 1 else "WG * " ~ mdStreams ~ "u" %}
135
+ 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;
145
+ loop {
146
+ if (stride == 0u) {
147
+ break;
148
+ }
149
+ if (lidx < stride) {
150
+ {% for s in range(mdStreams) %}
151
+ {
152
+ let slot = {{ s }}u * WG + lidx;
153
+ let m1 = partialM[slot];
154
+ let d1 = partialD[slot];
155
+ let m2 = partialM[slot + stride];
156
+ let d2 = partialD[slot + stride];
157
+ let mNew = max(m1, m2);
158
+ partialD[slot] = d1 * exp_shift(m1, mNew) + d2 * exp_shift(m2, mNew);
159
+ partialM[slot] = mNew;
160
+ }
161
+ {% endfor %}
162
+ }
163
+ workgroupBarrier();
164
+ stride = stride / 2u;
165
+ }
166
+ }
167
+ {% else %}
168
+
169
+ fn combine_partials(m: f32, d: f32, lidx: u32) -> vec2<f32> {
170
+ partialM[lidx] = m;
171
+ partialD[lidx] = d;
172
+ workgroupBarrier();
173
+ var stride = WG / 2u;
174
+ loop {
175
+ if (stride == 0u) {
176
+ break;
177
+ }
178
+ if (lidx < stride) {
179
+ let m1 = partialM[lidx];
180
+ let d1 = partialD[lidx];
181
+ let m2 = partialM[lidx + stride];
182
+ let d2 = partialD[lidx + stride];
183
+ let mNew = max(m1, m2);
184
+ partialD[lidx] = d1 * exp_shift(m1, mNew) + d2 * exp_shift(m2, mNew);
185
+ partialM[lidx] = mNew;
186
+ }
187
+ workgroupBarrier();
188
+ stride = stride / 2u;
189
+ }
190
+ let merged = vec2<f32>(partialM[0], partialD[0]);
191
+ // Trailing barrier so back-to-back calls cannot race a next call's partial
192
+ // stores against this call's reads of slot 0.
193
+ workgroupBarrier();
194
+ return merged;
195
+ }
196
+ {% endif %}
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; }
205
+ return inverseSqrt(f32({{ ATTN_SCALE_DIM }}));
206
+ }
207
+
208
+ {% if hasBias %}
209
+
210
+ fn load_bias4(base: u32, d4: u32) -> vec4<f32> {
211
+ let offset = base + d4 * 4u;
212
+ return vec4<f32>(bias[offset], bias[offset + 1u], bias[offset + 2u], bias[offset + 3u]);
213
+ }
214
+
215
+ {% endif %}
216
+ @compute @workgroup_size(WG, 1, 1)
217
+ fn main(
218
+ @builtin(workgroup_id) wg: vec3<u32>,
219
+ @builtin(local_invocation_id) lid: vec3<u32>{% if combineSubgroups %},
220
+ @builtin(subgroup_size) sgSize: u32{% endif %}
221
+ ) {
222
+ let qi = wg.x;
223
+ let h = wg.y;
224
+ let b = wg.z;
225
+ if (qi >= params.qSeq || h >= {{ qHeads }}) {
226
+ return;
227
+ }
228
+
229
+ let tid = lid.x;
230
+ let hKv = h / ({{ qHeads }} / {{ kvHeads }});
231
+
232
+ // Row bases: Q/output share one row, K/V rows are kvBase + token * stride.
233
+ let qBaseV4 = (b * params.qSeq + qi) * Q_HIDDEN_V4 + h * HEAD_DIM_V4;
234
+ let kvBaseV4 = b * params.kvSeq * KV_HIDDEN_V4 + hKv * HEAD_DIM_V4;
235
+ let kvTokenStrideV4 = KV_HIDDEN_V4;
236
+
237
+ // Cooperative vec4 Q-row load; init the output accumulator.
238
+ {% set hasBias = hasBias is defined and hasBias %}
239
+ for (var d4 = tid; d4 < HEAD_DIM_V4; d4 = d4 + WG) {
240
+ var qv = vec4<f32>(query[qBaseV4 + d4]);
241
+ {% if hasBias %}
242
+ qv = qv + load_bias4(h * HEAD_DIM, d4);
243
+ {% endif %}
244
+ q_shared[d4] = qv;
245
+ running_out[d4] = vec4<f32>(0.0);
246
+ }
247
+ workgroupBarrier();
248
+
249
+ let scale = scale_value();
250
+
251
+ // Running (max, denom) kept redundantly in every thread's registers: both
252
+ // are derived only from workgroup-uniform tile merges, so every thread holds
253
+ // identical values without shared-memory round trips or extra barriers.
254
+ var runningMax = -FLT_MAX;
255
+ var runningDenom = 0.0;
256
+
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
268
+ // + qi and attends keys [max(0, p+1-window), p+1) -- causal AND windowed.
269
+ let pastLen = select(0u, params.kvSeq - params.qSeq, params.kvSeq >= params.qSeq);
270
+ let qAbsP1 = pastLen + qi + 1u;
271
+ keyBoundV = min(keyBoundV, qAbsP1);
272
+ if (qAbsP1 > params.windowSize) { keyFloor = qAbsP1 - params.windowSize; }
273
+ {% endif %}
274
+ let keyBound = keyBoundV;
275
+ // Start at the WG-aligned tile containing keyFloor (sub-floor lanes mask out).
276
+ let keyFloorAligned = (keyFloor / WG) * WG;
277
+
278
+ var kjBase: u32 = keyFloorAligned;
279
+ loop {
280
+ if (kjBase >= keyBound) {
281
+ break;
282
+ }
283
+ let kj = kjBase + tid;
284
+
285
+ // Thread-local (m, d) for this tile: one key per thread, identity
286
+ // (-FLT_MAX, 0) for out-of-range lanes on the tail tile.
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 %}
294
+ // Broadcast-strided mask address; a 0 stride collapses that axis (rank-2
295
+ // [q, k] masks set batch/head strides to 0).
296
+ let maskIndex = b * params.maskBatchStride + h * params.maskHeadStride + qi * params.maskSeqStride + kj;
297
+ {% endif %}
298
+ {% if hasMask and maskIsBool %}
299
+ if (attn_mask[maskIndex] == 0u) {
300
+ keyAllowed = false;
301
+ } else {
302
+ var acc: f32 = 0.0;
303
+ for (var d4: u32 = 0u; d4 < HEAD_DIM_V4; d4 = d4 + 1u) {
304
+ acc = acc + dot(q_shared[d4], vec4<f32>(key[kRowV4 + d4]));
305
+ }
306
+ score = acc * scale;
307
+ }
308
+ {% else %}
309
+ var acc: f32 = 0.0;
310
+ for (var d4: u32 = 0u; d4 < HEAD_DIM_V4; d4 = d4 + 1u) {
311
+ acc = acc + dot(q_shared[d4], vec4<f32>(key[kRowV4 + d4]));
312
+ }
313
+ score = acc * scale;
314
+ {% if hasMask %}
315
+ score = score + f32(attn_mask[maskIndex]);
316
+ {% endif %}
317
+ {% endif %}
318
+ m = score;
319
+ dPart = select(0.0, 1.0, keyAllowed);
320
+ }
321
+ let tile = combine_partials(m, dPart, tid{% if combineSubgroups %}, sgSize{% endif %});
322
+
323
+ // Online merge of the tile into the running state (softmax-online rule).
324
+ // Merge one key tile's online-softmax (maximum, denominator) partial into the
325
+ // running state, then store the per-key probabilities consumed by V accumulation.
326
+ let newMax = max(runningMax, tile.x);
327
+ let correction = exp_shift(runningMax, newMax);
328
+ runningDenom = runningDenom * correction + tile.y * exp_shift(tile.x, newMax);
329
+ runningMax = newMax;
330
+
331
+ var prob = 0.0;
332
+ if (keyAllowed) {
333
+ prob = exp_shift(score, newMax);
334
+ }
335
+ probs[tid] = prob;
336
+ workgroupBarrier();
337
+
338
+
339
+ // running_out[d4] is owned by the same thread (tid ≡ d4 mod WG) across all
340
+ // tiles, so the rescale-and-accumulate below is race-free; vec4 V loads.
341
+ let tileCount = min(WG, keyBound - kjBase);
342
+ for (var d4: u32 = tid; d4 < HEAD_DIM_V4; d4 = d4 + WG) {
343
+ var vSum = vec4<f32>(0.0);
344
+ for (var i: u32 = 0u; i < tileCount; i = i + 1u) {
345
+ vSum = vSum + probs[i] * vec4<f32>(value[kvBaseV4 + (kjBase + i) * kvTokenStrideV4 + d4]);
346
+ }
347
+ running_out[d4] = running_out[d4] * correction + vSum;
348
+ }
349
+ // Orders this tile's probs reads before the next tile's probs writes.
350
+ workgroupBarrier();
351
+
352
+ kjBase = kjBase + WG;
353
+ }
354
+
355
+ // The output shares Q's layout and row base.
356
+ let hasMass = runningDenom > 0.0;
357
+ let invDenom = select(0.0, 1.0 / runningDenom, hasMass);
358
+ for (var d4: u32 = tid; d4 < HEAD_DIM_V4; d4 = d4 + WG) {
359
+ var outValue = running_out[d4] * invDenom;
360
+ {% if hasBias %}
361
+ // V bias is token-independent, so add it once after the softmax-weighted sum.
362
+ outValue = outValue + load_bias4(2u * {{ qHidden }}u + h * HEAD_DIM, d4)
363
+ * select(0.0, 1.0, hasMass);
364
+ {% endif %}
365
+ output[qBaseV4 + d4] = vec4<{{ scalar }}>(outValue);
366
+ }
367
+ }
build/webgpu/attn-flash-prefill-cluster.wgsl.jinja ADDED
@@ -0,0 +1,554 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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" %}
5
+ {% set Q_STRIDE = "QKV_STRIDE_V4" if sourceProfile == 1 else "Q_HIDDEN_V4" %}
6
+ {% set QUERY = "qkv" if sourceProfile == 1 else "query" %}
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 %}
13
+ {% if maskIsKeyKeep is not defined %}{% set maskIsKeyKeep = false %}{% endif %}
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
21
+ // min(total, capacity) rows resident. params.kvSeq then names the physical row
22
+ // count, which is still the right batch stride but the wrong attention bound, so
23
+ // the bounds read kvActive instead. Modes without seqlens use `params.kvSeq`
24
+ // in both roles.
25
+ {% set KVA = "kvActive" if useSeqlens else KVSEQ %}
26
+ {% macro score_expr(part) %}{% if hasSoftcap %}params.softcap * tanh(clamp(({{ part }} * SCALE) / params.softcap, -30.0, 30.0)){% else %}{{ part }} * SCALE{% endif %}{% endmacro %}
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
36
+ // workgroup storage; values widen to f32 when read. Other specializations stage
37
+ // them as f32. Score and weighted-value accumulation remain in f32 throughout.
38
+ {% set FUSED_ROTARY = fusedRotary is defined and fusedRotary %}
39
+ {% set STAGE_MASK = hasMask and useSubgroups and stageMask is defined and stageMask %}
40
+ {% set MASK_IS_INT = hasMask and (maskIsKeyKeep or maskIsBool) %}
41
+ {% set MASK_TILE_TYPE = "u32" if MASK_IS_INT else "f32" %}
42
+ {% set MASK_TILE_LOAD = "attn_mask[maskIndex]" if MASK_IS_INT else "f32(attn_mask[maskIndex])" %}
43
+ {% set MASK_TILE_ZERO = "0u" if MASK_IS_INT else "0.0" %}
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
60
+ // maskIsKeyKeep specialization supports 1=keep key-padding masks directly.
61
+ // hasCausal: params.isCausal masks keys after the query token (qSeq==kvSeq).
62
+ // hasBias: packed [Q; K; V] bias rows. The K bias adds the
63
+ // same dot(q, biasK) to every key score, which softmax cancels, so
64
+ // only the Q bias (into qr) and the token-independent V bias (added
65
+ // once after the softmax-weighted sum) are applied. Each lane owns
66
+ // SLICE contiguous head-dim vec4s starting at lane8*SLICE.
67
+ 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 %}
75
+ const Q_HEADS: u32 = {{ qNumHeads }}u;
76
+ const KV_HEADS: u32 = {{ kvNumHeads }}u;
77
+ const TILE_Q: u32 = {{ TILE_Q }}u;
78
+ // Lanes per query cluster: 8 for headDim%32, 4 for headDim%16, 2 for headDim%8.
79
+ const LPQ: u32 = {{ LPQ }}u;
80
+ const SLICE: u32 = HEAD_DIM / (4u * LPQ); // vec4s per lane slice
81
+ {% if FUSED_ROTARY %}
82
+ 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
90
+ // masked row has zero mass. Keeping one declaration shape for both mask modes
91
+ // lets both mask modes share the score loop below.
92
+ const NEG_INF: f32 = -3.4028234663852886e38;
93
+ const MASK_NEG: f32 = {{ "-1e38" if maskIsKeyKeep else "-3.4028234663852886e38" }};
94
+ {% else %}
95
+ const NEG_INF: f32 = -3.4028234663852886e38;
96
+ {% endif %}
97
+
98
+ var<workgroup> k_tile: array<vec4<{{ ST }}>, TILE_K * (HEAD_DIM / 4u)>;
99
+ var<workgroup> v_tile: array<vec4<{{ ST }}>, TILE_K * (HEAD_DIM / 4u)>;
100
+ {% if STAGE_MASK %}
101
+ // Each LPQ cluster consumes one mask value per (query,key), so stage the
102
+ // TILE_Q x TILE_K mask tile once instead of issuing LPQ duplicate global loads.
103
+ var<workgroup> mask_tile: array<{{ MASK_TILE_TYPE }}, TILE_Q * TILE_K>;
104
+ {% endif %}
105
+ {% if not useSubgroups %}
106
+ {% if batchNoSgReduction %}
107
+ // No-subgroups cluster reduction scratch for a whole K tile. Staging every
108
+ // lane's partial for every key lets the workgroup synchronize once per tile,
109
+ // instead of twice per key. This path requires TILE_K*WG f32 scratch elements.
110
+ var<workgroup> red: array<f32, TILE_K * WG>;
111
+ {% else %}
112
+ // One-key reduction path: reuse a single key's scratch across the tile.
113
+ var<workgroup> red: array<f32, WG>;
114
+ {% endif %}
115
+ {% endif %}
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 %}
124
+ // A quantized cache is dequantized once per key into the staged tile, then read
125
+ // by all TILE_Q queries in the workgroup. The unpack cost is amortized over the
126
+ // tile height instead of paid once per (query, key).
127
+ {% macro emit_quant_scale4(kind, scaleBuffer) %}
128
+ fn {{ kind }}scale4(d4: u32, hk: u32) -> vec4<f32> {
129
+ if (params.perChannel == 0u) {
130
+ return vec4<f32>({{ scaleBuffer }}[0]);
131
+ }
132
+ let base = hk * HEAD_DIM + d4 * 4u;
133
+ return vec4<f32>(
134
+ {{ scaleBuffer }}[base],
135
+ {{ scaleBuffer }}[base + 1u],
136
+ {{ scaleBuffer }}[base + 2u],
137
+ {{ scaleBuffer }}[base + 3u]
138
+ );
139
+ }
140
+ {%- endmacro %}
141
+ {%- macro emit_quant_load4(format, kind, buffer, scaleBuffer) %}
142
+ {{ emit_quant_scale4(kind, scaleBuffer) }}
143
+ fn load_{{ kind }}4(indexV4: u32, d4: u32, hk: u32) -> vec4<f32> {
144
+ {%- if format == "int8" %}
145
+ return vec4<f32>({{ buffer }}[indexV4]) * {{ kind }}scale4(d4, hk);
146
+ {%- else %}
147
+ // Two elements cover this vec4: each carries two +8-biased nibbles, low first.
148
+ let rowBase = indexV4 - d4;
149
+ let lo = {{ buffer }}[rowBase + d4 * 2u];
150
+ let hi = {{ buffer }}[rowBase + d4 * 2u + 1u];
151
+ let nibbles = vec4<i32>(
152
+ i32(lo & 0xFu), i32((lo >> 4u) & 0xFu),
153
+ i32(hi & 0xFu), i32((hi >> 4u) & 0xFu)
154
+ );
155
+ let signed = nibbles - vec4<i32>(8);
156
+ return vec4<f32>(signed) * {{ kind }}scale4(d4, hk);
157
+ {%- endif %}
158
+ }
159
+ {%- endmacro %}
160
+
161
+ {{ emit_quant_load4(quantCacheFormat, "key", KEY, "k_scale") }}
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>,
175
+ @builtin(local_invocation_id) lid: vec3<u32>{% if useSubgroups %},
176
+ @builtin(subgroup_size) sgSize: u32{% endif %}
177
+ ) {
178
+ let h = wg.y;
179
+ let b = wg.z;
180
+ let tid = lid.x;
181
+ {% if useSubgroups %}
182
+ // The subgroupShuffleXor butterfly requires every LPQ-lane query cluster to
183
+ // fit in, and align with, one subgroup. Keep a uniform guard for defensive
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.
258
+ {% if hasCausal or hasWindow %}
259
+ // Upper-left causal and/or sliding-window bounds. Query qIdx sits at
260
+ // absolute position p = kvSeq - qSeq + qIdx, so it attends keys [minKj, maxKj):
261
+ // the causal ceiling p+1 (clamped to kvSeq for non-square chunks) and, when
262
+ // windowed, the floor p+1-windowSize. The key loop runs over the workgroup's
263
+ // union range [wgStart, wgEnd); out-of-range (query,key) pairs mask to NEG_INF.
264
+ // pastLen = kvSeq - qSeq: query tokens are the last qSeq positions of the cache;
265
+ // saturate so an out-of-contract qSeq > kvSeq can't underflow u32. Used for the
266
+ // sliding window always, and for the causal ceiling only when right-aligned.
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 %}
283
+ var wgEndV = select({{ KVA }}, min(pastLen + lastQ + 1u, {{ KVA }}), {{ IS_CAUSAL }} != 0u);
284
+ {% else %}
285
+ var wgEndV = select({{ KVA }}, min(lastQ + 1u, {{ KVA }}), {{ IS_CAUSAL }} != 0u);
286
+ {% endif %}
287
+ {% else %}
288
+ var wgEndV = {{ KVA }};
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 %}
305
+ let maxKj = {{ KVA }};
306
+ let minKj: u32 = 0u;
307
+ let wgEnd = {{ KVA }};
308
+ let wgStart: u32 = 0u;
309
+ {% endif %}
310
+ {% if KL == "bhsd" %}
311
+ {% if quantCacheFormat == "int4" %}
312
+ let kvRowStride4 = HEAD_DIM_V4 * 2u;
313
+ {% else %}
314
+ let kvRowStride4 = HEAD_DIM_V4;
315
+ {% endif %}
316
+ let kvBatch4 = (b * KV_HEADS + hKv) * {{ KVSEQ }} * kvRowStride4;
317
+ {% else %}
318
+ let kvBatch4 = b * {{ KVSEQ }} * KV_HIDDEN_V4 + hKv * HEAD_DIM_V4;
319
+ {% endif %}
320
+
321
+ var kStart: u32 = wgStart;
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;
329
+ let d4 = i % HEAD_DIM_V4;
330
+ let kj = kStart + slot;
331
+ if (kj < wgEnd) {
332
+ {% if KL == "bhsd" %}
333
+ let base4 = kvBatch4 + kj * kvRowStride4 + d4;
334
+ {% else %}
335
+ let base4 = kvBatch4 + kj * KV_HIDDEN_V4 + d4;
336
+ {% endif %}
337
+ {% if quantCacheFormat %}
338
+ k_tile[i] = vec4<{{ ST }}>(load_key4(base4, d4, hKv));
339
+ v_tile[i] = vec4<{{ ST }}>(load_value4(base4, d4, hKv));
340
+ {% else %}
341
+ k_tile[i] = vec4<{{ ST }}>({{ KEY }}[base4]);
342
+ v_tile[i] = vec4<{{ ST }}>({{ VALUE }}[base4]);
343
+ {% endif %}
344
+ } else {
345
+ k_tile[i] = vec4<{{ ST }}>(0.0);
346
+ v_tile[i] = vec4<{{ ST }}>(0.0);
347
+ }
348
+ }
349
+ {% if STAGE_MASK %}
350
+ // The K/V load barrier also publishes this compact mask tile.
351
+ for (var i: u32 = tid; i < TILE_Q * TILE_K; i = i + WG) {
352
+ let qSlot = i / TILE_K;
353
+ let kSlot = i % TILE_K;
354
+ let maskQ = min(wg.x * TILE_Q + qSlot, {{ QSEQ }} - 1u);
355
+ let maskK = kStart + kSlot;
356
+ if (maskK < wgEnd) {
357
+ let maskIndex = b * params.maskBatchStride + h * params.maskHeadStride + maskQ * params.maskSeqStride + maskK;
358
+ mask_tile[i] = {{ MASK_TILE_LOAD }};
359
+ } else {
360
+ mask_tile[i] = {{ MASK_TILE_ZERO }};
361
+ }
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>;
370
+ for (var kk: u32 = 0u; kk < TILE_K; kk = kk + 1u) {
371
+ s[kk] = NEG_INF;
372
+ var part: f32 = 0.0;
373
+ let kb = kk * HEAD_DIM_V4 + lane8 * SLICE;
374
+ {% for c in range(SLICE_COUNT) %}
375
+ part = part + dot(qr{{ c }}, vec4<f32>(k_tile[kb + {{ c }}u]));
376
+ {% endfor %}
377
+ red[kk * WG + tid] = part;
378
+ }
379
+ workgroupBarrier();
380
+ // Distribute the tile's key reductions across the LPQ lanes, then publish
381
+ // each score at the cluster base. This replaces LPQ identical gathers and
382
+ // avoids making lane zero serially reduce the whole tile. The extra barrier
383
+ // is still paid once for the whole K tile, not once per key.
384
+ let cbase = tid - lane8;
385
+ for (var kk: u32 = lane8; kk < TILE_K; kk = kk + LPQ) {
386
+ {% for j in range(LPQ) %}
387
+ let r{{ j }} = red[kk * WG + cbase + {{ j }}u];
388
+ {% endfor %}
389
+ {% if LPQ == 8 %}
390
+ let part = ((r0 + r1) + (r2 + r3)) + ((r4 + r5) + (r6 + r7));
391
+ {% elif LPQ == 4 %}
392
+ let part = (r0 + r1) + (r2 + r3);
393
+ {% else %}
394
+ let part = r0 + r1;
395
+ {% endif %}
396
+ red[kk * WG + cbase] = part;
397
+ }
398
+ workgroupBarrier();
399
+ // The barrier at the start of the next K tile protects this scratch before
400
+ // it is overwritten.
401
+ for (var kk: u32 = 0u; kk < TILE_K; kk = kk + 1u) {
402
+ let kj = kStart + kk;
403
+ let part = red[kk * WG + tid - lane8];
404
+ if (kj >= minKj && kj < maxKj) {
405
+ {% if hasMask %}
406
+ let maskIndex = b * params.maskBatchStride + h * params.maskHeadStride + qClamped * params.maskSeqStride + kj;
407
+ {% if maskIsBool %}
408
+ if (attn_mask[maskIndex] != 0u) {
409
+ s[kk] = {{ score_expr("part") }};
410
+ } else {
411
+ s[kk] = MASK_NEG;
412
+ }
413
+ {% else %}
414
+ s[kk] = {{ score_expr("part") }} + f32(attn_mask[maskIndex]);
415
+ {% endif %}
416
+ {% else %}
417
+ s[kk] = {{ score_expr("part") }};
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) {
424
+ let kj = kStart + kk;
425
+ s[kk] = NEG_INF;
426
+ var part: f32 = 0.0;
427
+ let kb = kk * HEAD_DIM_V4 + lane8 * SLICE;
428
+ {% for c in range(SLICE_COUNT) %}
429
+ part = part + dot(qr{{ c }}, vec4<f32>(k_tile[kb + {{ c }}u]));
430
+ {% endfor %}
431
+ {% if useSubgroups %}
432
+ // log2(LPQ) butterfly all-reduce within the LPQ-lane cluster (each lane ends with the full dot).
433
+ part = part + subgroupShuffleXor(part, 1u);
434
+ {% for shift in [2, 4] %}
435
+ {% if LPQ > shift %}
436
+ part = part + subgroupShuffleXor(part, {{ shift }}u);
437
+ {% endif %}
438
+ {% endfor %}
439
+ {% else %}
440
+ red[tid] = part;
441
+ workgroupBarrier();
442
+ let cbase = tid - lane8;
443
+ part = 0.0;
444
+ for (var j: u32 = 0u; j < LPQ; j = j + 1u) {
445
+ part = part + red[cbase + j];
446
+ }
447
+ workgroupBarrier();
448
+ {% endif %}
449
+ if (kj >= minKj && kj < maxKj) {
450
+ {% if hasMask %}
451
+ {% if STAGE_MASK %}
452
+ let maskValue = mask_tile[qSub * TILE_K + kk];
453
+ {% else %}
454
+ // Broadcast-strided mask address (a 0 stride collapses that axis; rank-2
455
+ // [q, k] masks set batch/head strides to 0). qClamped keeps the seq index
456
+ // in-bounds for padding queries in the last tile (their output is dropped).
457
+ let maskIndex = b * params.maskBatchStride + h * params.maskHeadStride + qClamped * params.maskSeqStride + kj;
458
+ {% endif %}
459
+ {% if maskIsKeyKeep %}
460
+ // A broadcast key mask uses 1 for a retained key and 0 for padding.
461
+ s[kk] = {{ score_expr("part") }} + (1.0 - f32({{ MASK_ELEMENT }})) * MASK_NEG;
462
+ {% elif maskIsBool %}
463
+ // Logical bool: a rejected key contributes no softmax mass. Leaving
464
+ // the initialized NEG_INF sentinel in place makes a fully masked row
465
+ // land on the zero-denominator output guard below.
466
+ if ({{ MASK_ELEMENT }} != 0u) {
467
+ s[kk] = {{ score_expr("part") }};
468
+ } else {
469
+ s[kk] = MASK_NEG;
470
+ }
471
+ {% else %}
472
+ s[kk] = {{ score_expr("part") }} + {{ MASK_ADDITIVE }};
473
+ {% endif %}
474
+ {% else %}
475
+ s[kk] = {{ score_expr("part") }};
476
+ {% endif %}
477
+ }
478
+ }
479
+ {% endif %}
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
+ {
510
+ var acc = o{{ c }} * corr;
511
+ if (tileInterior) {
512
+ for (var kk: u32 = 0u; kk < TILE_K; kk = kk + 1u) {
513
+ acc = acc + s[kk] * vec4<f32>(v_tile[kk * HEAD_DIM_V4 + lane8 * SLICE + {{ c }}u]);
514
+ }
515
+ } else {
516
+ for (var kk: u32 = 0u; kk < TILE_K; kk = kk + 1u) {
517
+ acc = acc + s[kk] * select(vec4<f32>(), vec4<f32>(v_tile[kk * HEAD_DIM_V4 + lane8 * SLICE + {{ c }}u]), s[kk] != 0.0);
518
+ }
519
+ }
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
+ }
build/webgpu/attn-flash-q32-broadcast.wgsl.jinja ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Register-resident flash prefill uses one 32-lane subgroup per workgroup. Each
2
+ // lane owns a query row and keeps its Q slice and f32 output in registers. Lanes
3
+ // cooperatively load K/V and broadcast them with subgroupShuffle, so each query
4
+ // computes q·k and p·v without cross-lane reductions or workgroup storage.
5
+ //
6
+ // Batch, BHSD layout, grouped heads, and causal key ceilings share one uniform
7
+ // key-loop bound so subgroup operations stay reconverged. Packed Q/K/V bias
8
+ // applies Q before attention and V after normalization; key-independent K bias
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 %}
21
+ {{ env.wgsl.resourceDeclarations }}
22
+
23
+ const HEAD_DIM_V4: u32 = {{ headDimV4 }}u;
24
+ 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;
34
+
35
+ {% if not USE_SUBGROUPS %}
36
+ // No-subgroup backend: the same one-query-per-lane algorithm broadcasts a
37
+ // 32-key K/V tile through workgroup memory. The portable fallback can assign
38
+ // 64 query rows to one workgroup, so two SIMD-widths share each staged tile and
39
+ // halve K/V traffic without subgroup operations. At headDim=256 the two f16
40
+ // tiles consume exactly the portable 32 KiB workgroup-storage limit.
41
+ var<workgroup> keyTile: array<vec4<{{ ST }}>, HEAD_DIM_V4 * K_STEP>;
42
+ var<workgroup> valueTile: array<vec4<{{ ST }}>, HEAD_DIM_V4 * K_STEP>;
43
+ {% endif %}
44
+
45
+ {% set ATTN_SCALE_DIM = headDim ~ "u" %}
46
+ {% if ATTN_SCALE_DIM is not defined %}{% set ATTN_SCALE_DIM = "HEAD_DIM" %}{% endif %}
47
+ fn scale_value() -> f32 {
48
+ if (params.scale != 0.0) { return params.scale; }
49
+ return inverseSqrt(f32({{ ATTN_SCALE_DIM }}));
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(
62
+ @builtin(workgroup_id) wg: vec3<u32>,
63
+ @builtin(local_invocation_id) lid: vec3<u32>{% if USE_SUBGROUPS %},
64
+ @builtin(subgroup_invocation_id) sg_id: u32,
65
+ @builtin(subgroup_size) sg_size: u32
66
+ {% endif %}
67
+ ) {
68
+ {% if USE_SUBGROUPS %}
69
+ if (sg_size != 32u) { return; }
70
+ {% else %}
71
+ let lane = lid.x;
72
+ {% endif %}
73
+ let h = wg.y;
74
+ let b = wg.z;
75
+ let qi = wg.x * Q_STEP + lid.x;
76
+ let valid_q = qi < params.qSeq;
77
+ let hKv = h / (Q_HEADS / KV_HEADS);
78
+ let SCALE = scale_value();
79
+
80
+ // Q row. Padding lanes (qi >= qSeq) read query 0-clamped and never store, so they
81
+ // stay NaN-free while keeping the subgroup full for the cooperative K/V broadcast.
82
+ let qClamped = min(qi, params.qSeq - 1u);
83
+ let qBaseV4 = (b * params.qSeq + qClamped) * Q_HIDDEN_V4 + h * HEAD_DIM_V4;
84
+ var q_tile: array<vec4<{{ ST }}>, HEAD_DIM_V4>;
85
+ var o_tile: array<vec4<f32>, HEAD_DIM_V4>;
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
+ }
95
+ o_tile[d4] = vec4<f32>(0.0);
96
+ }
97
+
98
+ var previous_max: f32 = NEG_INF;
99
+ var previous_denom: f32 = 0.0;
100
+
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
+
115
+ for (var k_start: u32 = 0u; k_start < kvEnd; k_start = k_start + K_STEP) {
116
+ {% if not USE_SUBGROUPS %}
117
+ // Cooperatively stage K and V once, then all query lanes read the complete
118
+ // tile directly. A trailing barrier below protects the next tile overwrite.
119
+ for (var d4: u32 = 0u; d4 < HEAD_DIM_V4; d4 = d4 + 1u) {
120
+ for (var ki = lane; ki < K_STEP; ki += Q_STEP) {
121
+ let tileOffset = d4 * K_STEP + ki;
122
+ var kLocal = vec4<{{ ST }}>(0.0);
123
+ var vLocal = vec4<{{ ST }}>(0.0);
124
+ if (k_start + ki < params.kvSeq) {
125
+ let sourceOffset = kvBase + (k_start + ki) * kvKeyStride + d4;
126
+ kLocal = key[sourceOffset];
127
+ vLocal = value[sourceOffset];
128
+ }
129
+ keyTile[tileOffset] = kLocal;
130
+ valueTile[tileOffset] = vLocal;
131
+ }
132
+ }
133
+ workgroupBarrier();
134
+ {% endif %}
135
+ {% for g in range(qkGroups) %}
136
+ var qk{{ g }}: vec4<f32> = vec4<f32>(0.0);
137
+ {% endfor %}
138
+ for (var d4: u32 = 0u; d4 < HEAD_DIM_V4; d4 = d4 + 1u) {
139
+ let q_own = q_tile[d4];
140
+ {% if USE_SUBGROUPS %}
141
+ var k_local0: vec4<{{ ST }}> = vec4<{{ ST }}>(0.0);
142
+ {% if kStep == 64 %}
143
+ var k_local1: vec4<{{ ST }}> = vec4<{{ ST }}>(0.0);
144
+ {% endif %}
145
+ if (k_start + sg_id < params.kvSeq) {
146
+ k_local0 = key[kvBase + (k_start + sg_id) * kvKeyStride + d4];
147
+ }
148
+ {% if kStep == 64 %}
149
+ if (k_start + sg_id + 32u < params.kvSeq) {
150
+ k_local1 = key[kvBase + (k_start + sg_id + 32u) * kvKeyStride + d4];
151
+ }
152
+ {% endif %}
153
+ {% endif %}
154
+ {% for g in range(qkGroups) %}
155
+ {% for lane in range(4) %}
156
+ {% if USE_SUBGROUPS %}
157
+ {% if g < 8 %}
158
+ qk{{ g }}.{{ components[lane] }} = qk{{ g }}.{{ components[lane] }} + f32(dot(q_own, subgroupShuffle(k_local0, {{ g * 4 + lane }}u)));
159
+ {% else %}
160
+ qk{{ g }}.{{ components[lane] }} = qk{{ g }}.{{ components[lane] }} + f32(dot(q_own, subgroupShuffle(k_local1, {{ (g - 8) * 4 + lane }}u)));
161
+ {% endif %}
162
+ {% else %}
163
+ qk{{ g }}.{{ components[lane] }} = qk{{ g }}.{{ components[lane] }} + f32(dot(q_own, keyTile[d4 * K_STEP + {{ g * 4 + lane }}u]));
164
+ {% endif %}
165
+ {% endfor %}
166
+ {% endfor %}
167
+ }
168
+
169
+ // Scale + range/causal mask (a masked score is the NEG_INF sentinel, which the
170
+ // select below turns into a 0 weight — never an exp(NEG_INF-NEG_INF)=1 leak).
171
+ {% for g in range(qkGroups) %}
172
+ qk{{ g }} = qk{{ g }} * vec4<f32>(SCALE);
173
+ {% for lane in range(4) %}
174
+ if (k_start + {{ g * 4 + lane }}u >= myMaxKj) { qk{{ g }}.{{ components[lane] }} = NEG_INF; }
175
+ {% endfor %}
176
+ {% endfor %}
177
+
178
+ // Per-lane online softmax (this lane's query is independent — no barrier).
179
+ var local_max: f32 = NEG_INF;
180
+ {% for g in range(qkGroups) %}
181
+ local_max = max(local_max, max(max(qk{{ g }}.x, qk{{ g }}.y), max(qk{{ g }}.z, qk{{ g }}.w)));
182
+ {% endfor %}
183
+ let new_max = max(previous_max, local_max);
184
+ {% for g in range(qkGroups) %}
185
+ qk{{ g }} = select(vec4<f32>(0.0), exp(qk{{ g }} - vec4<f32>(new_max)), qk{{ g }} != vec4<f32>(NEG_INF));
186
+ {% endfor %}
187
+ var tile_sum: f32 = 0.0;
188
+ {% for g in range(qkGroups) %}
189
+ tile_sum = tile_sum + qk{{ g }}.x + qk{{ g }}.y + qk{{ g }}.z + qk{{ g }}.w;
190
+ {% endfor %}
191
+ let dleft = previous_denom * exp(previous_max - new_max);
192
+ let denom = max(dleft + tile_sum, 0.0000001);
193
+ let o_ratio = dleft / denom;
194
+ {% for g in range(qkGroups) %}
195
+ qk{{ g }} = qk{{ g }} / vec4<f32>(denom);
196
+ {% endfor %}
197
+ previous_max = new_max;
198
+ previous_denom = denom;
199
+
200
+ for (var d4: u32 = 0u; d4 < HEAD_DIM_V4; d4 = d4 + 1u) {
201
+ {% if USE_SUBGROUPS %}
202
+ var v_local0: vec4<{{ ST }}> = vec4<{{ ST }}>(0.0);
203
+ {% if kStep == 64 %}
204
+ var v_local1: vec4<{{ ST }}> = vec4<{{ ST }}>(0.0);
205
+ {% endif %}
206
+ if (k_start + sg_id < params.kvSeq) {
207
+ v_local0 = value[kvBase + (k_start + sg_id) * kvKeyStride + d4];
208
+ }
209
+ {% if kStep == 64 %}
210
+ if (k_start + sg_id + 32u < params.kvSeq) {
211
+ v_local1 = value[kvBase + (k_start + sg_id + 32u) * kvKeyStride + d4];
212
+ }
213
+ {% endif %}
214
+ {% endif %}
215
+ var acc: vec4<f32> = vec4<f32>(0.0);
216
+ {% for g in range(qkGroups) %}
217
+ {% for lane in range(4) %}
218
+ {% if USE_SUBGROUPS %}
219
+ {% if g < 8 %}
220
+ acc = acc + vec4<f32>(subgroupShuffle(v_local0, {{ g * 4 + lane }}u)) * qk{{ g }}.{{ components[lane] }};
221
+ {% else %}
222
+ acc = acc + vec4<f32>(subgroupShuffle(v_local1, {{ (g - 8) * 4 + lane }}u)) * qk{{ g }}.{{ components[lane] }};
223
+ {% endif %}
224
+ {% else %}
225
+ acc = acc + vec4<f32>(valueTile[d4 * K_STEP + {{ g * 4 + lane }}u]) * qk{{ g }}.{{ components[lane] }};
226
+ {% endif %}
227
+ {% endfor %}
228
+ {% endfor %}
229
+ o_tile[d4] = o_tile[d4] * vec4<f32>(o_ratio) + acc;
230
+ }
231
+ {% if not USE_SUBGROUPS %}
232
+ workgroupBarrier();
233
+ {% endif %}
234
+ }
235
+
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
+ }
build/webgpu/attn-online-scalar.wgsl.jinja ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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).
29
+ {% endif %}
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") %}
40
+ const WG: u32 = {{ workgroupSize }}u;
41
+
42
+ var<workgroup> partial: array<f32, WG>;
43
+ var<workgroup> running_max: f32;
44
+ var<workgroup> running_denom: f32;
45
+ var<workgroup> running_out: array<f32, HEAD_DIM>;
46
+ var<workgroup> previous_scale: f32;
47
+ {% macro wgsl_tree_reduce_f32(name, mode, buffer="partial", wg="WG", trailingBarrier=true) %}
48
+ fn {{ name }}(value: f32, tid: u32) -> f32 {
49
+ {{ buffer }}[tid] = value;
50
+ workgroupBarrier();
51
+ // Ceil-halving keeps every lane when the workgroup size is not a power of
52
+ // two. For even n this matches the power-of-two tree order; for odd n, lanes
53
+ // [0, n-half) fold the upper tail while the middle lane carries forward.
54
+ var n: u32 = {{ wg }};
55
+ loop {
56
+ let half = (n + 1u) / 2u;
57
+ if (tid < n - half) {
58
+ {% if mode == "max" %}
59
+ {{ buffer }}[tid] = max({{ buffer }}[tid], {{ buffer }}[tid + half]);
60
+ {% else %}
61
+ {{ buffer }}[tid] = {{ buffer }}[tid] + {{ buffer }}[tid + half];
62
+ {% endif %}
63
+ }
64
+ workgroupBarrier();
65
+ n = half;
66
+ if (n == 1u) {
67
+ break;
68
+ }
69
+ }
70
+ // The default trailing barrier makes this helper safe for back-to-back calls: every lane reads
71
+ // slot 0 here, so the next call's first store must not run until all lanes have read it.
72
+ // `trailingBarrier=false` is safe only when the buffer is never written again before kernel exit.
73
+ let reduced = {{ buffer }}[0];
74
+ {% if trailingBarrier %}
75
+ workgroupBarrier();
76
+ {% endif %}
77
+ return reduced;
78
+ }
79
+ {% endmacro %}
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(
98
+ @builtin(workgroup_id) wg: vec3<u32>,
99
+ @builtin(num_workgroups) nwg: vec3<u32>,
100
+ @builtin(local_invocation_id) lid: vec3<u32>
101
+ ) {
102
+ let h = wg.y;
103
+ let batch = wg.z;
104
+ if (h >= {{ qHeads }}) {
105
+ return;
106
+ }
107
+
108
+ let tid = lid.x;
109
+ let kv_head = h / ({{ qHeads }} / {{ kvHeads }});
110
+
111
+ // Grid-stride over query tokens: the dispatch clamps x to maxComputeWorkgroupsPerDimension, so for
112
+ // qSeq > maxComputeWorkgroupsPerDimension each workgroup serially handles queries wg.x, wg.x + nwg.x, …
113
+ // Every query's online-softmax state is workgroup-local and re-initialized
114
+ // each iteration, so iterations are independent; the trailing workgroupBarrier
115
+ // keeps a query's reads of running_denom ahead of the next query's writes.
116
+ // Reduces to exactly one query per workgroup (bit-identical) when nwg.x == qSeq.
117
+ for (var query_token = wg.x; query_token < params.qSeq; query_token = query_token + nwg.x) {
118
+
119
+ // Row bases: Q/output share one row, K/V rows are kvBase + token * stride.
120
+ let qBase = (batch * params.qSeq + query_token) * Q_HIDDEN + h * HEAD_DIM;
121
+ let kvBase = batch * params.kvSeq * KV_HIDDEN + kv_head * HEAD_DIM;
122
+ let kvTokenStride = KV_HIDDEN;
123
+
124
+ if (tid == 0u) {
125
+ running_max = -3.4028234663852886e38;
126
+ running_denom = 0.0;
127
+ }
128
+ for (var d: u32 = tid; d < HEAD_DIM; d = d + WG) {
129
+ running_out[d] = 0.0;
130
+ }
131
+ workgroupBarrier();
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
144
+ // attends keys [max(0, p + 1 - window), p + 1) -- causal AND left-windowed.
145
+ let pastLen = select(0u, params.kvSeq - params.qSeq, params.kvSeq >= params.qSeq);
146
+ let qAbsP1 = pastLen + query_token + 1u;
147
+ maxKj = min(maxKj, qAbsP1);
148
+ if (qAbsP1 > params.windowSize) {
149
+ minKj = qAbsP1 - params.windowSize;
150
+ }
151
+ {% endif %}
152
+
153
+ for (var key_token: u32 = minKj; key_token < maxKj; key_token = key_token + 1u) {
154
+ let kRow = kvBase + key_token * kvTokenStride;
155
+ var partial_dot = 0.0;
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
+
167
+ // reduce_sum returns the same partial[0] to every lane, so `score` is already
168
+ // workgroup-uniform; tid==0 advances the running max/denom/scale (broadcast
169
+ // through shared memory by the barrier below).
170
+ {% if hasMask %}
171
+ let maskIndex = {{ MASK_BATCH }}h * params.maskHeadStride + query_token * params.maskSeqStride + key_token;
172
+ let score = reduce_sum(partial_dot, tid) * scale_value() + f32(attn_mask[maskIndex]);
173
+ {% else %}
174
+ let score = reduce_sum(partial_dot, tid) * scale_value();
175
+ {% endif %}
176
+ if (tid == 0u) {
177
+ let next_max = max(running_max, score);
178
+ let old_factor = exp(running_max - next_max);
179
+ let new_factor = exp(score - next_max);
180
+ previous_scale = old_factor;
181
+ running_max = next_max;
182
+ running_denom = running_denom * old_factor + new_factor;
183
+ }
184
+ workgroupBarrier();
185
+
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
+ }
201
+ // Separate this query's reads of the shared running state from the next
202
+ // iteration's re-initialization (grid-stride; no-op when the loop runs once).
203
+ workgroupBarrier();
204
+ }
205
+ }
build/webgpu/bench.json ADDED
@@ -0,0 +1,1565 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "op": "com.microsoft.GroupQueryAttention",
3
+ "tunableSpace": {
4
+ "PREFILL_QUERY_TILE": [16, 32],
5
+ "PREFILL_LANES_PER_QUERY": [4, 8],
6
+ "CLUSTER_TILE_K_F32": [8, 16, 32],
7
+ "NO_SG_TILE_K_MAX": [8, 16]
8
+ },
9
+ "cases": [
10
+ {
11
+ "name": "gqa-f32-1x64x64-h4kv2d16",
12
+ "preset": "smoke",
13
+ "vars": { "batch": 1, "qSeq": 64, "kvSeq": 64, "heads": 4, "kvHeads": 2, "headDim": 16 },
14
+ "attrs": { "num_heads": 4, "kv_num_heads": 2, "scale": 0.25, "causal": 0 },
15
+ "inputs": {
16
+ "queryT": { "shape": [1, 64, 64], "dtype": "float32", "dist": "normal", "seed": 209, "scale": 0.2 },
17
+ "keyT": { "shape": [1, 64, 32], "dtype": "float32", "dist": "normal", "seed": 210, "scale": 0.2 },
18
+ "valueT": { "shape": [1, 64, 32], "dtype": "float32", "dist": "normal", "seed": 211, "scale": 0.2 },
19
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [63] } },
20
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [64] } }
21
+ },
22
+ "outputs": {
23
+ "outputT": { "shape": [1, 64, 64], "dtype": "float32" },
24
+ "presentKeyT": { "shape": [1, 2, 64, 16], "dtype": "float32" },
25
+ "presentValueT": { "shape": [1, 2, 64, 16], "dtype": "float32" }
26
+ },
27
+ "bench": {
28
+ "metrics": [
29
+ { "type": "gflops", "value": "4 * args.batch * args.qSeq * args.kvSeq * args.heads * args.headDim" }
30
+ ]
31
+ }
32
+ },
33
+ {
34
+ "name": "qwen3-prefill-32h8kv-d128-s512",
35
+ "preset": "smoke",
36
+ "vars": { "batch": 1, "qSeq": 512, "kvSeq": 512, "heads": 32, "kvHeads": 8, "headDim": 128 },
37
+ "attrs": { "num_heads": 32, "kv_num_heads": 8, "scale": 0.08838834764831845, "causal": 0 },
38
+ "inputs": {
39
+ "queryT": { "shape": [1, 512, 4096], "dtype": "float32", "dist": "normal", "seed": 370, "scale": 0.2 },
40
+ "keyT": { "shape": [1, 512, 1024], "dtype": "float32", "dist": "normal", "seed": 371, "scale": 0.2 },
41
+ "valueT": { "shape": [1, 512, 1024], "dtype": "float32", "dist": "normal", "seed": 372, "scale": 0.2 },
42
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [511] } },
43
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [512] } }
44
+ },
45
+ "outputs": {
46
+ "outputT": { "shape": [1, 512, 4096], "dtype": "float32" },
47
+ "presentKeyT": { "shape": [1, 8, 512, 128], "dtype": "float32" },
48
+ "presentValueT": { "shape": [1, 8, 512, 128], "dtype": "float32" }
49
+ },
50
+ "bench": {
51
+ "primary": true,
52
+ "metrics": [
53
+ { "type": "gflops", "value": "4 * args.batch * args.qSeq * args.kvSeq * args.heads * args.headDim" }
54
+ ]
55
+ }
56
+ },
57
+ {
58
+ "name": "decode-h4kv1-d64-kv512",
59
+ "preset": "smoke",
60
+ "vars": { "batch": 1, "qSeq": 1, "kvSeq": 512, "heads": 4, "kvHeads": 1, "headDim": 64 },
61
+ "attrs": { "num_heads": 4, "kv_num_heads": 1, "scale": 0.125, "causal": 0 },
62
+ "inputs": {
63
+ "queryT": { "shape": [1, 1, 256], "dtype": "float32", "dist": "normal", "seed": 376, "scale": 0.2 },
64
+ "keyT": { "shape": [1, 512, 64], "dtype": "float32", "dist": "normal", "seed": 377, "scale": 0.2 },
65
+ "valueT": { "shape": [1, 512, 64], "dtype": "float32", "dist": "normal", "seed": 378, "scale": 0.2 },
66
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [511] } },
67
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [512] } }
68
+ },
69
+ "outputs": {
70
+ "outputT": { "shape": [1, 1, 256], "dtype": "float32" },
71
+ "presentKeyT": { "shape": [1, 1, 512, 64], "dtype": "float32" },
72
+ "presentValueT": { "shape": [1, 1, 512, 64], "dtype": "float32" }
73
+ },
74
+ "bench": {
75
+ "metrics": [
76
+ {
77
+ "type": "bandwidth",
78
+ "value": "(4 * args.kvSeq * args.kvHeads * args.headDim + 2 * args.qSeq * args.heads * args.headDim) * 4"
79
+ }
80
+ ]
81
+ }
82
+ },
83
+ {
84
+ "name": "llama-decode-32h8kv-d128-kv2048",
85
+ "preset": "model",
86
+ "vars": { "batch": 1, "qSeq": 1, "kvSeq": 2048, "heads": 32, "kvHeads": 8, "headDim": 128 },
87
+ "attrs": { "num_heads": 32, "kv_num_heads": 8, "scale": 0.08838834764831845, "causal": 0 },
88
+ "inputs": {
89
+ "queryT": { "shape": [1, 1, 4096], "dtype": "float32", "dist": "normal", "seed": 373, "scale": 0.2 },
90
+ "keyT": { "shape": [1, 2048, 1024], "dtype": "float32", "dist": "normal", "seed": 374, "scale": 0.2 },
91
+ "valueT": { "shape": [1, 2048, 1024], "dtype": "float32", "dist": "normal", "seed": 375, "scale": 0.2 },
92
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [2047] } },
93
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [2048] } }
94
+ },
95
+ "outputs": {
96
+ "outputT": { "shape": [1, 1, 4096], "dtype": "float32" },
97
+ "presentKeyT": { "shape": [1, 8, 2048, 128], "dtype": "float32" },
98
+ "presentValueT": { "shape": [1, 8, 2048, 128], "dtype": "float32" }
99
+ },
100
+ "bench": {
101
+ "primary": true,
102
+ "metrics": [
103
+ {
104
+ "type": "bandwidth",
105
+ "value": "(4 * args.kvSeq * args.kvHeads * args.headDim + 2 * args.qSeq * args.heads * args.headDim) * 4"
106
+ }
107
+ ]
108
+ }
109
+ },
110
+ {
111
+ "name": "llama-decode-32h8kv-d128-kv8192",
112
+ "preset": "stress",
113
+ "vars": { "batch": 1, "qSeq": 1, "kvSeq": 8192, "heads": 32, "kvHeads": 8, "headDim": 128 },
114
+ "attrs": { "num_heads": 32, "kv_num_heads": 8, "scale": 0.08838834764831845, "causal": 0 },
115
+ "inputs": {
116
+ "queryT": { "shape": [1, 1, 4096], "dtype": "float32", "dist": "normal", "seed": 373, "scale": 0.2 },
117
+ "keyT": { "shape": [1, 8192, 1024], "dtype": "float32", "dist": "normal", "seed": 374, "scale": 0.2 },
118
+ "valueT": { "shape": [1, 8192, 1024], "dtype": "float32", "dist": "normal", "seed": 375, "scale": 0.2 },
119
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [8191] } },
120
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [8192] } }
121
+ },
122
+ "outputs": {
123
+ "outputT": { "shape": [1, 1, 4096], "dtype": "float32" },
124
+ "presentKeyT": { "shape": [1, 8, 8192, 128], "dtype": "float32" },
125
+ "presentValueT": { "shape": [1, 8, 8192, 128], "dtype": "float32" }
126
+ },
127
+ "bench": {
128
+ "primary": false,
129
+ "metrics": [
130
+ {
131
+ "type": "bandwidth",
132
+ "value": "(4 * args.kvSeq * args.kvHeads * args.headDim + 2 * args.qSeq * args.heads * args.headDim) * 4"
133
+ }
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
+ {
141
+ "name": "qwen3-prefill-32h8kv-d128-s512-f16",
142
+ "preset": "model",
143
+ "vars": { "batch": 1, "qSeq": 512, "kvSeq": 512, "heads": 32, "kvHeads": 8, "headDim": 128 },
144
+ "attrs": { "num_heads": 32, "kv_num_heads": 8, "scale": 0.08838834764831845, "causal": 0 },
145
+ "inputs": {
146
+ "queryT": { "shape": [1, 512, 4096], "dtype": "float16", "dist": "normal", "seed": 370, "scale": 0.2 },
147
+ "keyT": { "shape": [1, 512, 1024], "dtype": "float16", "dist": "normal", "seed": 371, "scale": 0.2 },
148
+ "valueT": { "shape": [1, 512, 1024], "dtype": "float16", "dist": "normal", "seed": 372, "scale": 0.2 },
149
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [511] } },
150
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [512] } }
151
+ },
152
+ "outputs": {
153
+ "outputT": { "shape": [1, 512, 4096], "dtype": "float16" },
154
+ "presentKeyT": { "shape": [1, 8, 512, 128], "dtype": "float16" },
155
+ "presentValueT": { "shape": [1, 8, 512, 128], "dtype": "float16" }
156
+ },
157
+ "bench": {
158
+ "metrics": [
159
+ { "type": "gflops", "value": "4 * args.batch * args.qSeq * args.kvSeq * args.heads * args.headDim" }
160
+ ]
161
+ }
162
+ },
163
+ {
164
+ "name": "llama-decode-32h8kv-d128-kv2048-f16",
165
+ "preset": "model",
166
+ "vars": { "batch": 1, "qSeq": 1, "kvSeq": 2048, "heads": 32, "kvHeads": 8, "headDim": 128 },
167
+ "attrs": { "num_heads": 32, "kv_num_heads": 8, "scale": 0.08838834764831845, "causal": 0 },
168
+ "inputs": {
169
+ "queryT": { "shape": [1, 1, 4096], "dtype": "float16", "dist": "normal", "seed": 373, "scale": 0.2 },
170
+ "keyT": { "shape": [1, 2048, 1024], "dtype": "float16", "dist": "normal", "seed": 374, "scale": 0.2 },
171
+ "valueT": { "shape": [1, 2048, 1024], "dtype": "float16", "dist": "normal", "seed": 375, "scale": 0.2 },
172
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [2047] } },
173
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [2048] } }
174
+ },
175
+ "outputs": {
176
+ "outputT": { "shape": [1, 1, 4096], "dtype": "float16" },
177
+ "presentKeyT": { "shape": [1, 8, 2048, 128], "dtype": "float16" },
178
+ "presentValueT": { "shape": [1, 8, 2048, 128], "dtype": "float16" }
179
+ },
180
+ "bench": {
181
+ "metrics": [
182
+ {
183
+ "type": "bandwidth",
184
+ "value": "(4 * args.kvSeq * args.kvHeads * args.headDim + 2 * args.qSeq * args.heads * args.headDim) * 2"
185
+ }
186
+ ]
187
+ }
188
+ },
189
+ {
190
+ "name": "sharedkv-decode-32h8kv-d128-kv2048",
191
+ "preset": "smoke",
192
+ "attrs": { "num_heads": 32, "kv_num_heads": 8 },
193
+ "inputs": {
194
+ "queryT": {
195
+ "dtype": "float32",
196
+ "shape": [1, 1, 4096],
197
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.011, "cosStep": 0.029 }
198
+ },
199
+ "keyT": { "dtype": "float32", "shape": [1, 0, 1024], "data": { "kind": "values", "values": [] } },
200
+ "valueT": { "dtype": "float32", "shape": [1, 0, 1024], "data": { "kind": "values", "values": [] } },
201
+ "pastKeyT": {
202
+ "dtype": "float32",
203
+ "shape": [1, 8, 2048, 128],
204
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.017, "cosStep": 0.019 }
205
+ },
206
+ "pastValueT": {
207
+ "dtype": "float32",
208
+ "shape": [1, 8, 2048, 128],
209
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
210
+ },
211
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [2047] } },
212
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [2048] } }
213
+ },
214
+ "outputs": {
215
+ "outputT": { "dtype": "float32", "shape": [1, 1, 4096] },
216
+ "presentKeyT": { "dtype": "float32", "shape": [1, 8, 2048, 128] },
217
+ "presentValueT": { "dtype": "float32", "shape": [1, 8, 2048, 128] }
218
+ },
219
+ "bench": {
220
+ "metrics": [
221
+ {
222
+ "type": "gflops",
223
+ "value": "4 * dim(shapes.queryT, 1) * dim(shapes.pastKeyT, 2) * (dim(shapes.queryT, 2) / attrs.num_heads) * attrs.num_heads"
224
+ }
225
+ ]
226
+ }
227
+ },
228
+ {
229
+ "name": "sharedkv-prefill-32h8kv-d128-s64",
230
+ "preset": "smoke",
231
+ "attrs": { "num_heads": 32, "kv_num_heads": 8 },
232
+ "inputs": {
233
+ "queryT": {
234
+ "dtype": "float32",
235
+ "shape": [1, 64, 4096],
236
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.011, "cosStep": 0.029 }
237
+ },
238
+ "keyT": { "dtype": "float32", "shape": [1, 0, 1024], "data": { "kind": "values", "values": [] } },
239
+ "valueT": { "dtype": "float32", "shape": [1, 0, 1024], "data": { "kind": "values", "values": [] } },
240
+ "pastKeyT": {
241
+ "dtype": "float32",
242
+ "shape": [1, 8, 64, 128],
243
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.017, "cosStep": 0.019 }
244
+ },
245
+ "pastValueT": {
246
+ "dtype": "float32",
247
+ "shape": [1, 8, 64, 128],
248
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
249
+ },
250
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [63] } },
251
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [64] } }
252
+ },
253
+ "outputs": {
254
+ "outputT": { "dtype": "float32", "shape": [1, 64, 4096] },
255
+ "presentKeyT": { "dtype": "float32", "shape": [1, 8, 64, 128] },
256
+ "presentValueT": { "dtype": "float32", "shape": [1, 8, 64, 128] }
257
+ },
258
+ "bench": {
259
+ "metrics": [
260
+ {
261
+ "type": "gflops",
262
+ "value": "4 * dim(shapes.queryT, 2) * (dim(shapes.queryT, 1) * (dim(shapes.pastKeyT, 2) - dim(shapes.queryT, 1)) + dim(shapes.queryT, 1) * (dim(shapes.queryT, 1) + 1) / 2)"
263
+ }
264
+ ]
265
+ }
266
+ },
267
+ {
268
+ "name": "sharedkv-prefill-32h8kv-d128-s128",
269
+ "preset": "smoke",
270
+ "attrs": { "num_heads": 32, "kv_num_heads": 8 },
271
+ "inputs": {
272
+ "queryT": {
273
+ "dtype": "float32",
274
+ "shape": [1, 128, 4096],
275
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.011, "cosStep": 0.029 }
276
+ },
277
+ "keyT": { "dtype": "float32", "shape": [1, 0, 1024], "data": { "kind": "values", "values": [] } },
278
+ "valueT": { "dtype": "float32", "shape": [1, 0, 1024], "data": { "kind": "values", "values": [] } },
279
+ "pastKeyT": {
280
+ "dtype": "float32",
281
+ "shape": [1, 8, 128, 128],
282
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.017, "cosStep": 0.019 }
283
+ },
284
+ "pastValueT": {
285
+ "dtype": "float32",
286
+ "shape": [1, 8, 128, 128],
287
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
288
+ },
289
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [127] } },
290
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [128] } }
291
+ },
292
+ "outputs": {
293
+ "outputT": { "dtype": "float32", "shape": [1, 128, 4096] },
294
+ "presentKeyT": { "dtype": "float32", "shape": [1, 8, 128, 128] },
295
+ "presentValueT": { "dtype": "float32", "shape": [1, 8, 128, 128] }
296
+ },
297
+ "bench": {
298
+ "metrics": [
299
+ {
300
+ "type": "gflops",
301
+ "value": "4 * dim(shapes.queryT, 2) * (dim(shapes.queryT, 1) * (dim(shapes.pastKeyT, 2) - dim(shapes.queryT, 1)) + dim(shapes.queryT, 1) * (dim(shapes.queryT, 1) + 1) / 2)"
302
+ }
303
+ ]
304
+ }
305
+ },
306
+ {
307
+ "name": "sharedkv-prefill-32h8kv-d128-s256",
308
+ "preset": "smoke",
309
+ "attrs": { "num_heads": 32, "kv_num_heads": 8 },
310
+ "inputs": {
311
+ "queryT": {
312
+ "dtype": "float32",
313
+ "shape": [1, 256, 4096],
314
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.011, "cosStep": 0.029 }
315
+ },
316
+ "keyT": { "dtype": "float32", "shape": [1, 0, 1024], "data": { "kind": "values", "values": [] } },
317
+ "valueT": { "dtype": "float32", "shape": [1, 0, 1024], "data": { "kind": "values", "values": [] } },
318
+ "pastKeyT": {
319
+ "dtype": "float32",
320
+ "shape": [1, 8, 256, 128],
321
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.017, "cosStep": 0.019 }
322
+ },
323
+ "pastValueT": {
324
+ "dtype": "float32",
325
+ "shape": [1, 8, 256, 128],
326
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
327
+ },
328
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [255] } },
329
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [256] } }
330
+ },
331
+ "outputs": {
332
+ "outputT": { "dtype": "float32", "shape": [1, 256, 4096] },
333
+ "presentKeyT": { "dtype": "float32", "shape": [1, 8, 256, 128] },
334
+ "presentValueT": { "dtype": "float32", "shape": [1, 8, 256, 128] }
335
+ },
336
+ "bench": {
337
+ "metrics": [
338
+ {
339
+ "type": "gflops",
340
+ "value": "4 * dim(shapes.queryT, 2) * (dim(shapes.queryT, 1) * (dim(shapes.pastKeyT, 2) - dim(shapes.queryT, 1)) + dim(shapes.queryT, 1) * (dim(shapes.queryT, 1) + 1) / 2)"
341
+ }
342
+ ]
343
+ }
344
+ },
345
+ {
346
+ "name": "sharedkv-prefill-32h8kv-d128-s512",
347
+ "preset": "smoke",
348
+ "attrs": { "num_heads": 32, "kv_num_heads": 8 },
349
+ "inputs": {
350
+ "queryT": {
351
+ "dtype": "float32",
352
+ "shape": [1, 512, 4096],
353
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.011, "cosStep": 0.029 }
354
+ },
355
+ "keyT": { "dtype": "float32", "shape": [1, 0, 1024], "data": { "kind": "values", "values": [] } },
356
+ "valueT": { "dtype": "float32", "shape": [1, 0, 1024], "data": { "kind": "values", "values": [] } },
357
+ "pastKeyT": {
358
+ "dtype": "float32",
359
+ "shape": [1, 8, 512, 128],
360
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.017, "cosStep": 0.019 }
361
+ },
362
+ "pastValueT": {
363
+ "dtype": "float32",
364
+ "shape": [1, 8, 512, 128],
365
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
366
+ },
367
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [511] } },
368
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [512] } }
369
+ },
370
+ "outputs": {
371
+ "outputT": { "dtype": "float32", "shape": [1, 512, 4096] },
372
+ "presentKeyT": { "dtype": "float32", "shape": [1, 8, 512, 128] },
373
+ "presentValueT": { "dtype": "float32", "shape": [1, 8, 512, 128] }
374
+ },
375
+ "bench": {
376
+ "metrics": [
377
+ {
378
+ "type": "gflops",
379
+ "value": "4 * dim(shapes.queryT, 2) * (dim(shapes.queryT, 1) * (dim(shapes.pastKeyT, 2) - dim(shapes.queryT, 1)) + dim(shapes.queryT, 1) * (dim(shapes.queryT, 1) + 1) / 2)"
380
+ }
381
+ ]
382
+ }
383
+ },
384
+ {
385
+ "name": "sharedkv-prefill-rotary-32h8kv-d128-s512",
386
+ "preset": "smoke",
387
+ "attrs": { "num_heads": 32, "kv_num_heads": 8, "scale": 0.08838834764831845, "do_rotary": 1 },
388
+ "inputs": {
389
+ "queryT": {
390
+ "dtype": "float32",
391
+ "shape": [1, 512, 4096],
392
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.011, "cosStep": 0.029 }
393
+ },
394
+ "keyT": { "dtype": "float32", "shape": [1, 0, 1024], "data": { "kind": "values", "values": [] } },
395
+ "valueT": { "dtype": "float32", "shape": [1, 0, 1024], "data": { "kind": "values", "values": [] } },
396
+ "pastKeyT": {
397
+ "dtype": "float32",
398
+ "shape": [1, 8, 512, 128],
399
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.017, "cosStep": 0.019 }
400
+ },
401
+ "pastValueT": {
402
+ "dtype": "float32",
403
+ "shape": [1, 8, 512, 128],
404
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
405
+ },
406
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [511] } },
407
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [512] } },
408
+ "cosCacheT": {
409
+ "dtype": "float32",
410
+ "shape": [512, 64],
411
+ "data": { "kind": "fillFloat32", "scale": 0.5, "sinStep": 0.031, "cosStep": 0.007 }
412
+ },
413
+ "sinCacheT": {
414
+ "dtype": "float32",
415
+ "shape": [512, 64],
416
+ "data": { "kind": "fillFloat32", "scale": 0.5, "sinStep": 0.013, "cosStep": 0.037 }
417
+ }
418
+ },
419
+ "outputs": {
420
+ "outputT": { "dtype": "float32", "shape": [1, 512, 4096] },
421
+ "presentKeyT": { "dtype": "float32", "shape": [1, 8, 512, 128] },
422
+ "presentValueT": { "dtype": "float32", "shape": [1, 8, 512, 128] }
423
+ },
424
+ "bench": {
425
+ "metrics": [
426
+ {
427
+ "type": "gflops",
428
+ "value": "4 * dim(shapes.queryT, 2) * (dim(shapes.queryT, 1) * (dim(shapes.pastKeyT, 2) - dim(shapes.queryT, 1)) + dim(shapes.queryT, 1) * (dim(shapes.queryT, 1) + 1) / 2)"
429
+ }
430
+ ]
431
+ }
432
+ },
433
+ {
434
+ "name": "sharedkv-decode-f16-32h8kv-d128-kv2048",
435
+ "preset": "smoke",
436
+ "attrs": { "num_heads": 32, "kv_num_heads": 8, "scale": 0.08838834764831845 },
437
+ "inputs": {
438
+ "queryT": {
439
+ "dtype": "float16",
440
+ "shape": [1, 1, 4096],
441
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.011, "cosStep": 0.029 }
442
+ },
443
+ "keyT": { "dtype": "float16", "shape": [1, 0, 1024], "data": { "kind": "values", "values": [] } },
444
+ "valueT": { "dtype": "float16", "shape": [1, 0, 1024], "data": { "kind": "values", "values": [] } },
445
+ "pastKeyT": {
446
+ "dtype": "float16",
447
+ "shape": [1, 8, 2048, 128],
448
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.017, "cosStep": 0.019 }
449
+ },
450
+ "pastValueT": {
451
+ "dtype": "float16",
452
+ "shape": [1, 8, 2048, 128],
453
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
454
+ },
455
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [2047] } },
456
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [2048] } }
457
+ },
458
+ "outputs": {
459
+ "outputT": { "dtype": "float16", "shape": [1, 1, 4096] },
460
+ "presentKeyT": { "dtype": "float16", "shape": [1, 8, 2048, 128] },
461
+ "presentValueT": { "dtype": "float16", "shape": [1, 8, 2048, 128] }
462
+ },
463
+ "bench": {
464
+ "metrics": [
465
+ {
466
+ "type": "gflops",
467
+ "value": "4 * dim(shapes.queryT, 1) * dim(shapes.pastKeyT, 2) * (dim(shapes.queryT, 2) / attrs.num_heads) * attrs.num_heads"
468
+ }
469
+ ]
470
+ }
471
+ },
472
+ {
473
+ "name": "sharedkv-prefill-f16-32h8kv-d128-s512",
474
+ "preset": "smoke",
475
+ "attrs": { "num_heads": 32, "kv_num_heads": 8, "scale": 0.08838834764831845 },
476
+ "inputs": {
477
+ "queryT": {
478
+ "dtype": "float16",
479
+ "shape": [1, 512, 4096],
480
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.011, "cosStep": 0.029 }
481
+ },
482
+ "keyT": { "dtype": "float16", "shape": [1, 0, 1024], "data": { "kind": "values", "values": [] } },
483
+ "valueT": { "dtype": "float16", "shape": [1, 0, 1024], "data": { "kind": "values", "values": [] } },
484
+ "pastKeyT": {
485
+ "dtype": "float16",
486
+ "shape": [1, 8, 512, 128],
487
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.017, "cosStep": 0.019 }
488
+ },
489
+ "pastValueT": {
490
+ "dtype": "float16",
491
+ "shape": [1, 8, 512, 128],
492
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
493
+ },
494
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [511] } },
495
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [512] } }
496
+ },
497
+ "outputs": {
498
+ "outputT": { "dtype": "float16", "shape": [1, 512, 4096] },
499
+ "presentKeyT": { "dtype": "float16", "shape": [1, 8, 512, 128] },
500
+ "presentValueT": { "dtype": "float16", "shape": [1, 8, 512, 128] }
501
+ },
502
+ "bench": {
503
+ "metrics": [
504
+ {
505
+ "type": "gflops",
506
+ "value": "4 * dim(shapes.queryT, 2) * (dim(shapes.queryT, 1) * (dim(shapes.pastKeyT, 2) - dim(shapes.queryT, 1)) + dim(shapes.queryT, 1) * (dim(shapes.queryT, 1) + 1) / 2)"
507
+ }
508
+ ]
509
+ }
510
+ },
511
+ {
512
+ "name": "sharedkv-prefill-rotary-f16-32h8kv-d128-s512",
513
+ "preset": "smoke",
514
+ "attrs": { "num_heads": 32, "kv_num_heads": 8, "scale": 0.08838834764831845, "do_rotary": 1 },
515
+ "inputs": {
516
+ "queryT": {
517
+ "dtype": "float16",
518
+ "shape": [1, 512, 4096],
519
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.011, "cosStep": 0.029 }
520
+ },
521
+ "keyT": { "dtype": "float16", "shape": [1, 0, 1024], "data": { "kind": "values", "values": [] } },
522
+ "valueT": { "dtype": "float16", "shape": [1, 0, 1024], "data": { "kind": "values", "values": [] } },
523
+ "pastKeyT": {
524
+ "dtype": "float16",
525
+ "shape": [1, 8, 512, 128],
526
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.017, "cosStep": 0.019 }
527
+ },
528
+ "pastValueT": {
529
+ "dtype": "float16",
530
+ "shape": [1, 8, 512, 128],
531
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
532
+ },
533
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [511] } },
534
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [512] } },
535
+ "cosCacheT": {
536
+ "dtype": "float16",
537
+ "shape": [512, 64],
538
+ "data": { "kind": "fillFloat32", "scale": 0.5, "sinStep": 0.031, "cosStep": 0.007 }
539
+ },
540
+ "sinCacheT": {
541
+ "dtype": "float16",
542
+ "shape": [512, 64],
543
+ "data": { "kind": "fillFloat32", "scale": 0.5, "sinStep": 0.013, "cosStep": 0.037 }
544
+ }
545
+ },
546
+ "outputs": {
547
+ "outputT": { "dtype": "float16", "shape": [1, 512, 4096] },
548
+ "presentKeyT": { "dtype": "float16", "shape": [1, 8, 512, 128] },
549
+ "presentValueT": { "dtype": "float16", "shape": [1, 8, 512, 128] }
550
+ },
551
+ "bench": {
552
+ "metrics": [
553
+ {
554
+ "type": "gflops",
555
+ "value": "4 * dim(shapes.queryT, 2) * (dim(shapes.queryT, 1) * (dim(shapes.pastKeyT, 2) - dim(shapes.queryT, 1)) + dim(shapes.queryT, 1) * (dim(shapes.queryT, 1) + 1) / 2)"
556
+ }
557
+ ]
558
+ }
559
+ },
560
+ {
561
+ "name": "newkv-past-decode-q1p2048",
562
+ "preset": "smoke",
563
+ "attrs": { "num_heads": 32, "kv_num_heads": 8, "scale": 0.08838834764831845 },
564
+ "inputs": {
565
+ "queryT": {
566
+ "dtype": "float32",
567
+ "shape": [1, 1, 4096],
568
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.011, "cosStep": 0.029 }
569
+ },
570
+ "keyT": {
571
+ "dtype": "float32",
572
+ "shape": [1, 1, 1024],
573
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.013, "cosStep": 0.017 }
574
+ },
575
+ "valueT": {
576
+ "dtype": "float32",
577
+ "shape": [1, 1, 1024],
578
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.019, "cosStep": 0.023 }
579
+ },
580
+ "pastKeyT": {
581
+ "dtype": "float32",
582
+ "shape": [1, 8, 2048, 128],
583
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.017, "cosStep": 0.019 }
584
+ },
585
+ "pastValueT": {
586
+ "dtype": "float32",
587
+ "shape": [1, 8, 2048, 128],
588
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
589
+ },
590
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [2048] } },
591
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [2049] } }
592
+ },
593
+ "outputs": {
594
+ "outputT": { "dtype": "float32", "shape": [1, 1, 4096] },
595
+ "presentKeyT": { "dtype": "float32", "shape": [1, 8, 2049, 128] },
596
+ "presentValueT": { "dtype": "float32", "shape": [1, 8, 2049, 128] }
597
+ },
598
+ "bench": {
599
+ "metrics": [
600
+ {
601
+ "type": "gflops",
602
+ "value": "4 * dim(shapes.queryT, 1) * dim(shapes.presentKeyT, 2) * (dim(shapes.queryT, 2) / attrs.num_heads) * attrs.num_heads"
603
+ }
604
+ ]
605
+ }
606
+ },
607
+ {
608
+ "name": "newkv-past-prefill-q512p512",
609
+ "preset": "smoke",
610
+ "attrs": { "num_heads": 32, "kv_num_heads": 8, "scale": 0.08838834764831845 },
611
+ "inputs": {
612
+ "queryT": {
613
+ "dtype": "float32",
614
+ "shape": [1, 512, 4096],
615
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.011, "cosStep": 0.029 }
616
+ },
617
+ "keyT": {
618
+ "dtype": "float32",
619
+ "shape": [1, 512, 1024],
620
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.013, "cosStep": 0.017 }
621
+ },
622
+ "valueT": {
623
+ "dtype": "float32",
624
+ "shape": [1, 512, 1024],
625
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.019, "cosStep": 0.023 }
626
+ },
627
+ "pastKeyT": {
628
+ "dtype": "float32",
629
+ "shape": [1, 8, 512, 128],
630
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.017, "cosStep": 0.019 }
631
+ },
632
+ "pastValueT": {
633
+ "dtype": "float32",
634
+ "shape": [1, 8, 512, 128],
635
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
636
+ },
637
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1023] } },
638
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1024] } }
639
+ },
640
+ "outputs": {
641
+ "outputT": { "dtype": "float32", "shape": [1, 512, 4096] },
642
+ "presentKeyT": { "dtype": "float32", "shape": [1, 8, 1024, 128] },
643
+ "presentValueT": { "dtype": "float32", "shape": [1, 8, 1024, 128] }
644
+ },
645
+ "bench": {
646
+ "metrics": [
647
+ {
648
+ "type": "gflops",
649
+ "value": "4 * dim(shapes.queryT, 2) * (dim(shapes.queryT, 1) * (dim(shapes.presentKeyT, 2) - dim(shapes.queryT, 1)) + dim(shapes.queryT, 1) * (dim(shapes.queryT, 1) + 1) / 2)"
650
+ }
651
+ ]
652
+ }
653
+ },
654
+ {
655
+ "name": "quant-int8-decode-h8kv2-d128-kv2048",
656
+ "preset": "smoke",
657
+ "provenance": {
658
+ "notes": "Buffer-sharing INT8 decode. The bandwidth metric counts the portable physical minimum: WebGPU exposes each logical INT8 cache element through i32 storage, the past cache is preserved into present K/V, and attention reads K/V once per KV head. It intentionally does not credit duplicate cache reads across grouped query heads."
659
+ },
660
+ "attrs": {
661
+ "num_heads": 8,
662
+ "kv_num_heads": 2,
663
+ "kv_cache_bit_width": 8,
664
+ "k_quant_type": "PER_TENSOR",
665
+ "v_quant_type": "PER_TENSOR"
666
+ },
667
+ "inputs": {
668
+ "queryT": {
669
+ "dtype": "float32",
670
+ "shape": [1, 1, 1024],
671
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.011, "cosStep": 0.029 }
672
+ },
673
+ "keyT": {
674
+ "dtype": "float32",
675
+ "shape": [1, 1, 256],
676
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.013, "cosStep": 0.031 }
677
+ },
678
+ "valueT": {
679
+ "dtype": "float32",
680
+ "shape": [1, 1, 256],
681
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.015, "cosStep": 0.033 }
682
+ },
683
+ "pastKeyT": { "dtype": "int8", "shape": [1, 2, 2048, 128], "dist": "uniform", "seed": 880, "min": -8, "max": 8 },
684
+ "pastValueT": {
685
+ "dtype": "int8",
686
+ "shape": [1, 2, 2048, 128],
687
+ "dist": "uniform",
688
+ "seed": 881,
689
+ "min": -8,
690
+ "max": 8
691
+ },
692
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [2047] } },
693
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [2048] } },
694
+ "kScaleT": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [0.0625] } },
695
+ "vScaleT": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [0.0625] } }
696
+ },
697
+ "outputs": {
698
+ "outputT": { "dtype": "float32", "shape": [1, 1, 1024] },
699
+ "presentKeyT": { "dtype": "int8", "shape": [1, 2, 2048, 128] },
700
+ "presentValueT": { "dtype": "int8", "shape": [1, 2, 2048, 128] }
701
+ },
702
+ "bench": {
703
+ "metrics": [
704
+ {
705
+ "type": "bandwidth",
706
+ "value": "4 * (6 * numel(shapes.pastKeyT) + 4 * numel(shapes.keyT) + 2 * numel(shapes.queryT))"
707
+ }
708
+ ]
709
+ }
710
+ },
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,
720
+ "kv_cache_bit_width": 8,
721
+ "k_quant_type": "PER_TENSOR",
722
+ "v_quant_type": "PER_TENSOR"
723
+ },
724
+ "inputs": {
725
+ "queryT": {
726
+ "dtype": "float32",
727
+ "shape": [1, 512, 4096],
728
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.011, "cosStep": 0.029 }
729
+ },
730
+ "keyT": {
731
+ "dtype": "float32",
732
+ "shape": [1, 512, 1024],
733
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.013, "cosStep": 0.031 }
734
+ },
735
+ "valueT": {
736
+ "dtype": "float32",
737
+ "shape": [1, 512, 1024],
738
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.015, "cosStep": 0.033 }
739
+ },
740
+ "pastKeyT": { "dtype": "int8", "shape": [1, 8, 512, 128], "dist": "uniform", "seed": 882, "min": -8, "max": 8 },
741
+ "pastValueT": {
742
+ "dtype": "int8",
743
+ "shape": [1, 8, 512, 128],
744
+ "dist": "uniform",
745
+ "seed": 883,
746
+ "min": -8,
747
+ "max": 8
748
+ },
749
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [511] } },
750
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [512] } },
751
+ "kScaleT": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [0.0625] } },
752
+ "vScaleT": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [0.0625] } }
753
+ },
754
+ "outputs": {
755
+ "outputT": { "dtype": "float32", "shape": [1, 512, 4096] },
756
+ "presentKeyT": { "dtype": "int8", "shape": [1, 8, 512, 128] },
757
+ "presentValueT": { "dtype": "int8", "shape": [1, 8, 512, 128] }
758
+ },
759
+ "bench": {
760
+ "metrics": [
761
+ {
762
+ "type": "gflops",
763
+ "value": "4 * dim(shapes.queryT, 2) * dim(shapes.queryT, 1) * (dim(shapes.queryT, 1) + 1) / 2"
764
+ }
765
+ ]
766
+ }
767
+ },
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,
777
+ "kv_cache_bit_width": 4,
778
+ "k_quant_type": "PER_TENSOR",
779
+ "v_quant_type": "PER_TENSOR"
780
+ },
781
+ "inputs": {
782
+ "queryT": {
783
+ "dtype": "float32",
784
+ "shape": [1, 512, 4096],
785
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.011, "cosStep": 0.029 }
786
+ },
787
+ "keyT": {
788
+ "dtype": "float32",
789
+ "shape": [1, 512, 1024],
790
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.013, "cosStep": 0.031 }
791
+ },
792
+ "valueT": {
793
+ "dtype": "float32",
794
+ "shape": [1, 512, 1024],
795
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.015, "cosStep": 0.033 }
796
+ },
797
+ "pastKeyT": { "dtype": "uint8", "shape": [1, 8, 512, 64], "dist": "uniform", "seed": 884, "min": 0, "max": 256 },
798
+ "pastValueT": {
799
+ "dtype": "uint8",
800
+ "shape": [1, 8, 512, 64],
801
+ "dist": "uniform",
802
+ "seed": 885,
803
+ "min": 0,
804
+ "max": 256
805
+ },
806
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [511] } },
807
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [512] } },
808
+ "kScaleT": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [0.0625] } },
809
+ "vScaleT": { "dtype": "float32", "shape": [1], "data": { "kind": "values", "values": [0.0625] } }
810
+ },
811
+ "outputs": {
812
+ "outputT": { "dtype": "float32", "shape": [1, 512, 4096] },
813
+ "presentKeyT": { "dtype": "uint8", "shape": [1, 8, 512, 64] },
814
+ "presentValueT": { "dtype": "uint8", "shape": [1, 8, 512, 64] }
815
+ },
816
+ "bench": {
817
+ "metrics": [
818
+ {
819
+ "type": "gflops",
820
+ "value": "4 * dim(shapes.queryT, 2) * dim(shapes.queryT, 1) * (dim(shapes.queryT, 1) + 1) / 2"
821
+ }
822
+ ]
823
+ }
824
+ },
825
+ {
826
+ "name": "bias-prefill-cliff-h32kv8-d128-s512",
827
+ "preset": "smoke",
828
+ "attrs": { "num_heads": 32, "kv_num_heads": 8 },
829
+ "inputs": {
830
+ "queryT": {
831
+ "dtype": "float32",
832
+ "shape": [1, 512, 4096],
833
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.011, "cosStep": 0.029 }
834
+ },
835
+ "keyT": { "dtype": "float32", "shape": [1, 0, 1024], "data": { "kind": "values", "values": [] } },
836
+ "valueT": { "dtype": "float32", "shape": [1, 0, 1024], "data": { "kind": "values", "values": [] } },
837
+ "pastKeyT": {
838
+ "dtype": "float32",
839
+ "shape": [1, 8, 512, 128],
840
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.017, "cosStep": 0.019 }
841
+ },
842
+ "pastValueT": {
843
+ "dtype": "float32",
844
+ "shape": [1, 8, 512, 128],
845
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
846
+ },
847
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [511] } },
848
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [512] } },
849
+ "attentionBiasT": {
850
+ "dtype": "float32",
851
+ "shape": [1, 32, 512, 512],
852
+ "data": { "kind": "fillFloat32", "scale": 0.05, "sinStep": 0.007, "cosStep": 0.009 }
853
+ }
854
+ },
855
+ "outputs": {
856
+ "outputT": { "dtype": "float32", "shape": [1, 512, 4096] },
857
+ "presentKeyT": { "dtype": "float32", "shape": [1, 8, 512, 128] },
858
+ "presentValueT": { "dtype": "float32", "shape": [1, 8, 512, 128] }
859
+ },
860
+ "bench": {
861
+ "metrics": [
862
+ {
863
+ "type": "gflops",
864
+ "value": "4 * dim(shapes.queryT, 2) * (dim(shapes.queryT, 1) * (dim(shapes.pastKeyT, 2) - dim(shapes.queryT, 1)) + dim(shapes.queryT, 1) * (dim(shapes.queryT, 1) + 1) / 2)"
865
+ }
866
+ ]
867
+ }
868
+ },
869
+ {
870
+ "name": "hd16-belowfloor-decode-h8kv2-kv2048",
871
+ "preset": "smoke",
872
+ "attrs": { "num_heads": 8, "kv_num_heads": 2 },
873
+ "inputs": {
874
+ "queryT": {
875
+ "dtype": "float32",
876
+ "shape": [1, 1, 128],
877
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.011, "cosStep": 0.029 }
878
+ },
879
+ "keyT": { "dtype": "float32", "shape": [1, 0, 32], "data": { "kind": "values", "values": [] } },
880
+ "valueT": { "dtype": "float32", "shape": [1, 0, 32], "data": { "kind": "values", "values": [] } },
881
+ "pastKeyT": {
882
+ "dtype": "float32",
883
+ "shape": [1, 2, 2048, 16],
884
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.017, "cosStep": 0.019 }
885
+ },
886
+ "pastValueT": {
887
+ "dtype": "float32",
888
+ "shape": [1, 2, 2048, 16],
889
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
890
+ },
891
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [2047] } },
892
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [2048] } }
893
+ },
894
+ "outputs": {
895
+ "outputT": { "dtype": "float32", "shape": [1, 1, 128] },
896
+ "presentKeyT": { "dtype": "float32", "shape": [1, 2, 2048, 16] },
897
+ "presentValueT": { "dtype": "float32", "shape": [1, 2, 2048, 16] }
898
+ },
899
+ "bench": {
900
+ "metrics": [
901
+ {
902
+ "type": "bandwidth",
903
+ "value": "2 * dim(shapes.pastKeyT, 1) * dim(shapes.pastKeyT, 2) * dim(shapes.pastKeyT, 3) * 4 + 2 * dim(shapes.queryT, 2) * 4"
904
+ }
905
+ ]
906
+ }
907
+ },
908
+ {
909
+ "name": "sliding-window-decode-h8kv2-d128-kv2048-w256",
910
+ "preset": "smoke",
911
+ "attrs": { "num_heads": 8, "kv_num_heads": 2, "local_window_size": 256 },
912
+ "inputs": {
913
+ "queryT": {
914
+ "dtype": "float32",
915
+ "shape": [1, 1, 1024],
916
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.011, "cosStep": 0.029 }
917
+ },
918
+ "keyT": { "dtype": "float32", "shape": [1, 0, 256], "data": { "kind": "values", "values": [] } },
919
+ "valueT": { "dtype": "float32", "shape": [1, 0, 256], "data": { "kind": "values", "values": [] } },
920
+ "pastKeyT": {
921
+ "dtype": "float32",
922
+ "shape": [1, 2, 2048, 128],
923
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.017, "cosStep": 0.019 }
924
+ },
925
+ "pastValueT": {
926
+ "dtype": "float32",
927
+ "shape": [1, 2, 2048, 128],
928
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
929
+ },
930
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [2047] } },
931
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [2048] } }
932
+ },
933
+ "outputs": {
934
+ "outputT": { "dtype": "float32", "shape": [1, 1, 1024] },
935
+ "presentKeyT": { "dtype": "float32", "shape": [1, 2, 2048, 128] },
936
+ "presentValueT": { "dtype": "float32", "shape": [1, 2, 2048, 128] }
937
+ },
938
+ "bench": {
939
+ "metrics": [
940
+ {
941
+ "type": "bandwidth",
942
+ "value": "2 * dim(shapes.pastKeyT, 1) * dim(shapes.pastKeyT, 2) * dim(shapes.pastKeyT, 3) * 4 + 2 * dim(shapes.queryT, 2) * 4"
943
+ }
944
+ ]
945
+ }
946
+ },
947
+ {
948
+ "name": "scalar-fallback-decode-hd32-h8kv8-kv4096",
949
+ "preset": "stress",
950
+ "vars": { "batch": 1, "qSeq": 1, "kvSeq": 4096, "heads": 8, "kvHeads": 8, "headDim": 32 },
951
+ "attrs": { "num_heads": 8, "kv_num_heads": 8, "scale": 0.17677669529663687, "causal": 0 },
952
+ "inputs": {
953
+ "queryT": { "shape": [1, 1, 256], "dtype": "float32", "dist": "normal", "seed": 911, "scale": 0.2 },
954
+ "keyT": { "shape": [1, 4096, 256], "dtype": "float32", "dist": "normal", "seed": 912, "scale": 0.2 },
955
+ "valueT": { "shape": [1, 4096, 256], "dtype": "float32", "dist": "normal", "seed": 913, "scale": 0.2 },
956
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [4095] } },
957
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [4096] } }
958
+ },
959
+ "outputs": {
960
+ "outputT": { "shape": [1, 1, 256], "dtype": "float32", "dist": "empty" },
961
+ "presentKeyT": { "shape": [1, 8, 4096, 32], "dtype": "float32", "dist": "empty" },
962
+ "presentValueT": { "shape": [1, 8, 4096, 32], "dtype": "float32", "dist": "empty" }
963
+ },
964
+ "bench": {
965
+ "metrics": [
966
+ {
967
+ "type": "bandwidth",
968
+ "value": "(4 * dim(shapes.keyT, 1) * attrs.kv_num_heads * (dim(shapes.queryT, 2) / attrs.num_heads) + 2 * dim(shapes.queryT, 2)) * 4"
969
+ }
970
+ ]
971
+ }
972
+ },
973
+ {
974
+ "name": "scalar-fallback-prefill-hd32-h8kv8-s512",
975
+ "preset": "stress",
976
+ "vars": { "batch": 1, "qSeq": 512, "kvSeq": 512, "heads": 8, "kvHeads": 8, "headDim": 32 },
977
+ "attrs": { "num_heads": 8, "kv_num_heads": 8, "scale": 0.17677669529663687, "causal": 0 },
978
+ "inputs": {
979
+ "queryT": { "shape": [1, 512, 256], "dtype": "float32", "dist": "normal", "seed": 901, "scale": 0.2 },
980
+ "keyT": { "shape": [1, 512, 256], "dtype": "float32", "dist": "normal", "seed": 902, "scale": 0.2 },
981
+ "valueT": { "shape": [1, 512, 256], "dtype": "float32", "dist": "normal", "seed": 903, "scale": 0.2 },
982
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [511] } },
983
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [512] } }
984
+ },
985
+ "outputs": {
986
+ "outputT": { "shape": [1, 512, 256], "dtype": "float32", "dist": "empty" },
987
+ "presentKeyT": { "shape": [1, 8, 512, 32], "dtype": "float32", "dist": "empty" },
988
+ "presentValueT": { "shape": [1, 8, 512, 32], "dtype": "float32", "dist": "empty" }
989
+ },
990
+ "bench": {
991
+ "metrics": [
992
+ {
993
+ "type": "gflops",
994
+ "value": "4 * dim(shapes.queryT, 0) * dim(shapes.queryT, 1) * dim(shapes.keyT, 1) * attrs.num_heads * (dim(shapes.queryT, 2) / attrs.num_heads)"
995
+ }
996
+ ]
997
+ }
998
+ },
999
+ {
1000
+ "name": "gqa-f32-q31-kv511-h8-kv2-d64-double-threshold-cliff",
1001
+ "preset": "stress",
1002
+ "vars": { "batch": 1, "qSeq": 31, "kvSeq": 511, "heads": 8, "kvHeads": 2, "headDim": 64 },
1003
+ "attrs": { "num_heads": 8, "kv_num_heads": 2, "scale": 0.125, "causal": 0 },
1004
+ "inputs": {
1005
+ "queryT": { "shape": [1, 31, 512], "dtype": "float32", "dist": "normal", "seed": 921, "scale": 0.2 },
1006
+ "keyT": { "shape": [1, 511, 128], "dtype": "float32", "dist": "normal", "seed": 922, "scale": 0.2 },
1007
+ "valueT": { "shape": [1, 511, 128], "dtype": "float32", "dist": "normal", "seed": 923, "scale": 0.2 },
1008
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [510] } },
1009
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [511] } }
1010
+ },
1011
+ "outputs": {
1012
+ "outputT": { "shape": [1, 31, 512], "dtype": "float32", "dist": "empty" },
1013
+ "presentKeyT": { "shape": [1, 2, 511, 64], "dtype": "float32", "dist": "empty" },
1014
+ "presentValueT": { "shape": [1, 2, 511, 64], "dtype": "float32", "dist": "empty" }
1015
+ },
1016
+ "bench": {
1017
+ "primary": true,
1018
+ "metrics": [
1019
+ { "type": "gflops", "value": "4 * args.batch * args.qSeq * args.kvSeq * args.heads * args.headDim" }
1020
+ ]
1021
+ }
1022
+ },
1023
+ {
1024
+ "name": "gqa-f32-q32-kv512-h8-kv2-d64-threshold-control",
1025
+ "preset": "stress",
1026
+ "vars": { "batch": 1, "qSeq": 32, "kvSeq": 512, "heads": 8, "kvHeads": 2, "headDim": 64 },
1027
+ "attrs": { "num_heads": 8, "kv_num_heads": 2, "scale": 0.125, "causal": 0 },
1028
+ "inputs": {
1029
+ "queryT": { "shape": [1, 32, 512], "dtype": "float32", "dist": "normal", "seed": 924, "scale": 0.2 },
1030
+ "keyT": { "shape": [1, 512, 128], "dtype": "float32", "dist": "normal", "seed": 925, "scale": 0.2 },
1031
+ "valueT": { "shape": [1, 512, 128], "dtype": "float32", "dist": "normal", "seed": 926, "scale": 0.2 },
1032
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [511] } },
1033
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [512] } }
1034
+ },
1035
+ "outputs": {
1036
+ "outputT": { "shape": [1, 32, 512], "dtype": "float32", "dist": "empty" },
1037
+ "presentKeyT": { "shape": [1, 2, 512, 64], "dtype": "float32", "dist": "empty" },
1038
+ "presentValueT": { "shape": [1, 2, 512, 64], "dtype": "float32", "dist": "empty" }
1039
+ },
1040
+ "bench": {
1041
+ "primary": true,
1042
+ "metrics": [
1043
+ { "type": "gflops", "value": "4 * args.batch * args.qSeq * args.kvSeq * args.heads * args.headDim" }
1044
+ ]
1045
+ }
1046
+ },
1047
+ {
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": {
1055
+ "queryT": {
1056
+ "dtype": "float32",
1057
+ "shape": [1, 512, 4096],
1058
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.011, "cosStep": 0.029 }
1059
+ },
1060
+ "keyT": { "dtype": "float32", "shape": [1, 0, 1024], "data": { "kind": "values", "values": [] } },
1061
+ "valueT": { "dtype": "float32", "shape": [1, 0, 1024], "data": { "kind": "values", "values": [] } },
1062
+ "pastKeyT": {
1063
+ "dtype": "float32",
1064
+ "shape": [1, 8, 512, 128],
1065
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.017, "cosStep": 0.019 }
1066
+ },
1067
+ "pastValueT": {
1068
+ "dtype": "float32",
1069
+ "shape": [1, 8, 512, 128],
1070
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
1071
+ },
1072
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [511] } },
1073
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [512] } },
1074
+ "headSinkT": { "dtype": "float32", "shape": [32], "dist": "normal", "seed": 927, "scale": 0.2 }
1075
+ },
1076
+ "outputs": {
1077
+ "outputT": { "dtype": "float32", "shape": [1, 512, 4096] },
1078
+ "presentKeyT": { "dtype": "float32", "shape": [1, 8, 512, 128] },
1079
+ "presentValueT": { "dtype": "float32", "shape": [1, 8, 512, 128] }
1080
+ },
1081
+ "bench": {
1082
+ "metrics": [
1083
+ {
1084
+ "type": "gflops",
1085
+ "value": "4 * dim(shapes.queryT, 2) * (dim(shapes.queryT, 1) * (dim(shapes.pastKeyT, 2) - dim(shapes.queryT, 1)) + dim(shapes.queryT, 1) * (dim(shapes.queryT, 1) + 1) / 2)"
1086
+ }
1087
+ ]
1088
+ }
1089
+ },
1090
+ {
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": {
1098
+ "queryT": {
1099
+ "dtype": "float32",
1100
+ "shape": [1, 512, 4096],
1101
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.011, "cosStep": 0.029 }
1102
+ },
1103
+ "keyT": { "dtype": "float32", "shape": [1, 0, 1024], "data": { "kind": "values", "values": [] } },
1104
+ "valueT": { "dtype": "float32", "shape": [1, 0, 1024], "data": { "kind": "values", "values": [] } },
1105
+ "pastKeyT": {
1106
+ "dtype": "float32",
1107
+ "shape": [1, 8, 512, 128],
1108
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.017, "cosStep": 0.019 }
1109
+ },
1110
+ "pastValueT": {
1111
+ "dtype": "float32",
1112
+ "shape": [1, 8, 512, 128],
1113
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
1114
+ },
1115
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [511] } },
1116
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [512] } },
1117
+ "attentionBiasT": {
1118
+ "dtype": "float32",
1119
+ "shape": [1, 32, 512, 512],
1120
+ "data": { "kind": "fillFloat32", "scale": 0.05, "sinStep": 0.007, "cosStep": 0.009 }
1121
+ },
1122
+ "headSinkT": { "dtype": "float32", "shape": [32], "dist": "normal", "seed": 928, "scale": 0.2 }
1123
+ },
1124
+ "outputs": {
1125
+ "outputT": { "dtype": "float32", "shape": [1, 512, 4096] },
1126
+ "presentKeyT": { "dtype": "float32", "shape": [1, 8, 512, 128] },
1127
+ "presentValueT": { "dtype": "float32", "shape": [1, 8, 512, 128] }
1128
+ },
1129
+ "bench": {
1130
+ "metrics": [
1131
+ {
1132
+ "type": "gflops",
1133
+ "value": "4 * dim(shapes.queryT, 2) * (dim(shapes.queryT, 1) * (dim(shapes.pastKeyT, 2) - dim(shapes.queryT, 1)) + dim(shapes.queryT, 1) * (dim(shapes.queryT, 1) + 1) / 2)"
1134
+ }
1135
+ ]
1136
+ }
1137
+ },
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": {
1147
+ "dtype": "float32",
1148
+ "shape": [1, 512, 4096],
1149
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.011, "cosStep": 0.029 }
1150
+ },
1151
+ "keyT": { "dtype": "float32", "shape": [1, 0, 1024], "data": { "kind": "values", "values": [] } },
1152
+ "valueT": { "dtype": "float32", "shape": [1, 0, 1024], "data": { "kind": "values", "values": [] } },
1153
+ "pastKeyT": {
1154
+ "dtype": "float32",
1155
+ "shape": [1, 8, 512, 128],
1156
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.017, "cosStep": 0.019 }
1157
+ },
1158
+ "pastValueT": {
1159
+ "dtype": "float32",
1160
+ "shape": [1, 8, 512, 128],
1161
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
1162
+ },
1163
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [511] } },
1164
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [512] } }
1165
+ },
1166
+ "outputs": {
1167
+ "outputT": { "dtype": "float32", "shape": [1, 512, 4096] },
1168
+ "presentKeyT": { "dtype": "float32", "shape": [1, 8, 512, 128] },
1169
+ "presentValueT": { "dtype": "float32", "shape": [1, 8, 512, 128] }
1170
+ },
1171
+ "bench": {
1172
+ "metrics": [
1173
+ {
1174
+ "type": "gflops",
1175
+ "value": "4 * dim(shapes.queryT, 2) * (dim(shapes.queryT, 1) * (dim(shapes.pastKeyT, 2) - dim(shapes.queryT, 1)) + dim(shapes.queryT, 1) * (dim(shapes.queryT, 1) + 1) / 2)"
1176
+ }
1177
+ ]
1178
+ }
1179
+ },
1180
+ {
1181
+ "name": "qkv-prefill-f32-h4kv1-d64-s128",
1182
+ "preset": "model",
1183
+ "provenance": {
1184
+ "notes": "Lower f32 register-topology boundary comparing the Q32-broadcast and subgroup-cluster routes."
1185
+ },
1186
+ "vars": { "batch": 1, "qSeq": 128, "kvSeq": 128, "heads": 4, "kvHeads": 1, "headDim": 64 },
1187
+ "attrs": { "num_heads": 4, "kv_num_heads": 1, "scale": 0.125, "causal": 0 },
1188
+ "inputs": {
1189
+ "queryT": { "shape": [1, 128, 256], "dtype": "float32", "dist": "normal", "seed": 1075, "scale": 0.2 },
1190
+ "keyT": { "shape": [1, 128, 64], "dtype": "float32", "dist": "normal", "seed": 1076, "scale": 0.2 },
1191
+ "valueT": { "shape": [1, 128, 64], "dtype": "float32", "dist": "normal", "seed": 1077, "scale": 0.2 },
1192
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [127] } },
1193
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [128] } }
1194
+ },
1195
+ "outputs": {
1196
+ "outputT": { "shape": [1, 128, 256], "dtype": "float32" },
1197
+ "presentKeyT": { "shape": [1, 1, 128, 64], "dtype": "float32" },
1198
+ "presentValueT": { "shape": [1, 1, 128, 64], "dtype": "float32" }
1199
+ },
1200
+ "bench": {
1201
+ "metrics": [
1202
+ { "type": "gflops", "value": "4 * args.batch * args.qSeq * args.kvSeq * args.heads * args.headDim" }
1203
+ ]
1204
+ }
1205
+ },
1206
+ {
1207
+ "name": "qkv-prefill-f32-h4kv1-d256-s128",
1208
+ "preset": "model",
1209
+ "provenance": {
1210
+ "notes": "Upper f32 register-topology boundary comparing Q32-broadcast, subgroup-cluster, and generic online routes."
1211
+ },
1212
+ "vars": { "batch": 1, "qSeq": 128, "kvSeq": 128, "heads": 4, "kvHeads": 1, "headDim": 256 },
1213
+ "attrs": { "num_heads": 4, "kv_num_heads": 1, "scale": 0.0625, "causal": 0 },
1214
+ "inputs": {
1215
+ "queryT": { "shape": [1, 128, 1024], "dtype": "float32", "dist": "normal", "seed": 1081, "scale": 0.2 },
1216
+ "keyT": { "shape": [1, 128, 256], "dtype": "float32", "dist": "normal", "seed": 1082, "scale": 0.2 },
1217
+ "valueT": { "shape": [1, 128, 256], "dtype": "float32", "dist": "normal", "seed": 1083, "scale": 0.2 },
1218
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [127] } },
1219
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [128] } }
1220
+ },
1221
+ "outputs": {
1222
+ "outputT": { "shape": [1, 128, 1024], "dtype": "float32" },
1223
+ "presentKeyT": { "shape": [1, 1, 128, 256], "dtype": "float32" },
1224
+ "presentValueT": { "shape": [1, 1, 128, 256], "dtype": "float32" }
1225
+ },
1226
+ "bench": {
1227
+ "metrics": [
1228
+ { "type": "gflops", "value": "4 * args.batch * args.qSeq * args.kvSeq * args.heads * args.headDim" }
1229
+ ]
1230
+ }
1231
+ },
1232
+ {
1233
+ "name": "window-decode-32h8kv-d128-cap1024-w1024",
1234
+ "preset": "smoke",
1235
+ "attrs": { "num_heads": 32, "kv_num_heads": 8, "sliding_window_cache": 1, "local_window_size": 1024 },
1236
+ "inputs": {
1237
+ "queryT": {
1238
+ "dtype": "float32",
1239
+ "shape": [1, 1, 4096],
1240
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.011, "cosStep": 0.029 }
1241
+ },
1242
+ "keyT": {
1243
+ "dtype": "float32",
1244
+ "shape": [1, 1, 1024],
1245
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.031, "cosStep": 0.007 }
1246
+ },
1247
+ "valueT": {
1248
+ "dtype": "float32",
1249
+ "shape": [1, 1, 1024],
1250
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.037, "cosStep": 0.005 }
1251
+ },
1252
+ "pastKeyT": {
1253
+ "dtype": "float32",
1254
+ "shape": [1, 8, 1024, 128],
1255
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.017, "cosStep": 0.019 }
1256
+ },
1257
+ "pastValueT": {
1258
+ "dtype": "float32",
1259
+ "shape": [1, 8, 1024, 128],
1260
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
1261
+ },
1262
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [65535] } },
1263
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [65536] } }
1264
+ },
1265
+ "outputs": {
1266
+ "outputT": { "dtype": "float32", "shape": [1, 1, 4096] },
1267
+ "presentKeyT": { "dtype": "float32", "shape": [1, 8, 1024, 128] },
1268
+ "presentValueT": { "dtype": "float32", "shape": [1, 8, 1024, 128] }
1269
+ },
1270
+ "bench": {
1271
+ "metrics": [
1272
+ {
1273
+ "type": "bandwidth",
1274
+ "value": "2 * 2 * numel(shapes.pastKeyT) * 4 + numel(shapes.queryT) * 4 + numel(shapes.outputT) * 4"
1275
+ }
1276
+ ]
1277
+ }
1278
+ },
1279
+ {
1280
+ "name": "window-decode-32h8kv-d128-cap1024-unfilled",
1281
+ "preset": "smoke",
1282
+ "attrs": { "num_heads": 32, "kv_num_heads": 8, "sliding_window_cache": 1, "local_window_size": 1024 },
1283
+ "inputs": {
1284
+ "queryT": {
1285
+ "dtype": "float32",
1286
+ "shape": [1, 1, 4096],
1287
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.011, "cosStep": 0.029 }
1288
+ },
1289
+ "keyT": {
1290
+ "dtype": "float32",
1291
+ "shape": [1, 1, 1024],
1292
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.031, "cosStep": 0.007 }
1293
+ },
1294
+ "valueT": {
1295
+ "dtype": "float32",
1296
+ "shape": [1, 1, 1024],
1297
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.037, "cosStep": 0.005 }
1298
+ },
1299
+ "pastKeyT": {
1300
+ "dtype": "float32",
1301
+ "shape": [1, 8, 1024, 128],
1302
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.017, "cosStep": 0.019 }
1303
+ },
1304
+ "pastValueT": {
1305
+ "dtype": "float32",
1306
+ "shape": [1, 8, 1024, 128],
1307
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
1308
+ },
1309
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [511] } },
1310
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [512] } }
1311
+ },
1312
+ "outputs": {
1313
+ "outputT": { "dtype": "float32", "shape": [1, 1, 4096] },
1314
+ "presentKeyT": { "dtype": "float32", "shape": [1, 8, 1024, 128] },
1315
+ "presentValueT": { "dtype": "float32", "shape": [1, 8, 1024, 128] }
1316
+ },
1317
+ "bench": {
1318
+ "metrics": [
1319
+ {
1320
+ "type": "bandwidth",
1321
+ "value": "2 * 2 * numel(shapes.pastKeyT) * 4 + numel(shapes.queryT) * 4 + numel(shapes.outputT) * 4"
1322
+ }
1323
+ ]
1324
+ }
1325
+ },
1326
+ {
1327
+ "name": "window-decode-32h8kv-d128-cap256-w256",
1328
+ "preset": "smoke",
1329
+ "attrs": { "num_heads": 32, "kv_num_heads": 8, "sliding_window_cache": 1, "local_window_size": 256 },
1330
+ "inputs": {
1331
+ "queryT": {
1332
+ "dtype": "float32",
1333
+ "shape": [1, 1, 4096],
1334
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.011, "cosStep": 0.029 }
1335
+ },
1336
+ "keyT": {
1337
+ "dtype": "float32",
1338
+ "shape": [1, 1, 1024],
1339
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.031, "cosStep": 0.007 }
1340
+ },
1341
+ "valueT": {
1342
+ "dtype": "float32",
1343
+ "shape": [1, 1, 1024],
1344
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.037, "cosStep": 0.005 }
1345
+ },
1346
+ "pastKeyT": {
1347
+ "dtype": "float32",
1348
+ "shape": [1, 8, 256, 128],
1349
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.017, "cosStep": 0.019 }
1350
+ },
1351
+ "pastValueT": {
1352
+ "dtype": "float32",
1353
+ "shape": [1, 8, 256, 128],
1354
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
1355
+ },
1356
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [65535] } },
1357
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [65536] } }
1358
+ },
1359
+ "outputs": {
1360
+ "outputT": { "dtype": "float32", "shape": [1, 1, 4096] },
1361
+ "presentKeyT": { "dtype": "float32", "shape": [1, 8, 256, 128] },
1362
+ "presentValueT": { "dtype": "float32", "shape": [1, 8, 256, 128] }
1363
+ },
1364
+ "bench": {
1365
+ "metrics": [
1366
+ {
1367
+ "type": "bandwidth",
1368
+ "value": "2 * 2 * numel(shapes.pastKeyT) * 4 + numel(shapes.queryT) * 4 + numel(shapes.outputT) * 4"
1369
+ }
1370
+ ]
1371
+ }
1372
+ },
1373
+ {
1374
+ "name": "window-chunk-prefill-32h8kv-d128-cap1024-q128",
1375
+ "preset": "smoke",
1376
+ "attrs": { "num_heads": 32, "kv_num_heads": 8, "sliding_window_cache": 1, "local_window_size": 1024 },
1377
+ "inputs": {
1378
+ "queryT": {
1379
+ "dtype": "float32",
1380
+ "shape": [1, 128, 4096],
1381
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.011, "cosStep": 0.029 }
1382
+ },
1383
+ "keyT": {
1384
+ "dtype": "float32",
1385
+ "shape": [1, 128, 1024],
1386
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.031, "cosStep": 0.007 }
1387
+ },
1388
+ "valueT": {
1389
+ "dtype": "float32",
1390
+ "shape": [1, 128, 1024],
1391
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.037, "cosStep": 0.005 }
1392
+ },
1393
+ "pastKeyT": {
1394
+ "dtype": "float32",
1395
+ "shape": [1, 8, 1024, 128],
1396
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.017, "cosStep": 0.019 }
1397
+ },
1398
+ "pastValueT": {
1399
+ "dtype": "float32",
1400
+ "shape": [1, 8, 1024, 128],
1401
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
1402
+ },
1403
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [65535] } },
1404
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [65536] } }
1405
+ },
1406
+ "outputs": {
1407
+ "outputT": { "dtype": "float32", "shape": [1, 128, 4096] },
1408
+ "presentKeyT": { "dtype": "float32", "shape": [1, 8, 1024, 128] },
1409
+ "presentValueT": { "dtype": "float32", "shape": [1, 8, 1024, 128] }
1410
+ },
1411
+ "bench": {
1412
+ "metrics": [
1413
+ { "type": "gflops", "value": "4 * args.batch * args.qSeq * args.kvSeq * args.heads * args.headDim" },
1414
+ {
1415
+ "type": "bandwidth",
1416
+ "name": "cache traffic",
1417
+ "value": "2 * 2 * numel(shapes.pastKeyT) * 4 + numel(shapes.queryT) * 4 + numel(shapes.outputT) * 4"
1418
+ }
1419
+ ]
1420
+ },
1421
+ "vars": { "batch": 1, "qSeq": 128, "kvSeq": 1024, "heads": 32, "headDim": 128 }
1422
+ },
1423
+ {
1424
+ "name": "window-decode-32h8kv-d128-cap4096-w4096",
1425
+ "preset": "model",
1426
+ "attrs": { "num_heads": 32, "kv_num_heads": 8, "sliding_window_cache": 1, "local_window_size": 4096 },
1427
+ "inputs": {
1428
+ "queryT": {
1429
+ "dtype": "float32",
1430
+ "shape": [1, 1, 4096],
1431
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.011, "cosStep": 0.029 }
1432
+ },
1433
+ "keyT": {
1434
+ "dtype": "float32",
1435
+ "shape": [1, 1, 1024],
1436
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.031, "cosStep": 0.007 }
1437
+ },
1438
+ "valueT": {
1439
+ "dtype": "float32",
1440
+ "shape": [1, 1, 1024],
1441
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.037, "cosStep": 0.005 }
1442
+ },
1443
+ "pastKeyT": {
1444
+ "dtype": "float32",
1445
+ "shape": [1, 8, 4096, 128],
1446
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.017, "cosStep": 0.019 }
1447
+ },
1448
+ "pastValueT": {
1449
+ "dtype": "float32",
1450
+ "shape": [1, 8, 4096, 128],
1451
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
1452
+ },
1453
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [65535] } },
1454
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [65536] } }
1455
+ },
1456
+ "outputs": {
1457
+ "outputT": { "dtype": "float32", "shape": [1, 1, 4096] },
1458
+ "presentKeyT": { "dtype": "float32", "shape": [1, 8, 4096, 128] },
1459
+ "presentValueT": { "dtype": "float32", "shape": [1, 8, 4096, 128] }
1460
+ },
1461
+ "bench": {
1462
+ "metrics": [
1463
+ {
1464
+ "type": "bandwidth",
1465
+ "value": "2 * 2 * numel(shapes.pastKeyT) * 4 + numel(shapes.queryT) * 4 + numel(shapes.outputT) * 4"
1466
+ }
1467
+ ]
1468
+ }
1469
+ },
1470
+ {
1471
+ "name": "sharedkv-chunk-append-32h8kv-d128-cap1024-q128",
1472
+ "preset": "smoke",
1473
+ "vars": { "batch": 1, "qSeq": 128, "kvSeq": 1024, "heads": 32, "headDim": 128 },
1474
+ "attrs": { "num_heads": 32, "kv_num_heads": 8 },
1475
+ "inputs": {
1476
+ "queryT": {
1477
+ "dtype": "float32",
1478
+ "shape": [1, 128, 4096],
1479
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.011, "cosStep": 0.029 }
1480
+ },
1481
+ "keyT": {
1482
+ "dtype": "float32",
1483
+ "shape": [1, 128, 1024],
1484
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.031, "cosStep": 0.007 }
1485
+ },
1486
+ "valueT": {
1487
+ "dtype": "float32",
1488
+ "shape": [1, 128, 1024],
1489
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.037, "cosStep": 0.005 }
1490
+ },
1491
+ "pastKeyT": {
1492
+ "dtype": "float32",
1493
+ "shape": [1, 8, 1024, 128],
1494
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.017, "cosStep": 0.019 }
1495
+ },
1496
+ "pastValueT": {
1497
+ "dtype": "float32",
1498
+ "shape": [1, 8, 1024, 128],
1499
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
1500
+ },
1501
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1023] } },
1502
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1024] } }
1503
+ },
1504
+ "outputs": {
1505
+ "outputT": { "dtype": "float32", "shape": [1, 128, 4096] },
1506
+ "presentKeyT": { "dtype": "float32", "shape": [1, 8, 1024, 128] },
1507
+ "presentValueT": { "dtype": "float32", "shape": [1, 8, 1024, 128] }
1508
+ },
1509
+ "bench": {
1510
+ "metrics": [
1511
+ { "type": "gflops", "value": "4 * args.batch * args.qSeq * args.kvSeq * args.heads * args.headDim" }
1512
+ ]
1513
+ }
1514
+ },
1515
+ {
1516
+ "name": "sharedkv-chunk-append-8h2kv-d256-cap1024-q128",
1517
+ "preset": "smoke",
1518
+ "vars": { "batch": 1, "qSeq": 128, "kvSeq": 1024, "heads": 8, "headDim": 256 },
1519
+ "attrs": { "num_heads": 8, "kv_num_heads": 2 },
1520
+ "inputs": {
1521
+ "queryT": {
1522
+ "dtype": "float32",
1523
+ "shape": [1, 128, 2048],
1524
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.011, "cosStep": 0.029 }
1525
+ },
1526
+ "keyT": {
1527
+ "dtype": "float32",
1528
+ "shape": [1, 128, 512],
1529
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.031, "cosStep": 0.007 }
1530
+ },
1531
+ "valueT": {
1532
+ "dtype": "float32",
1533
+ "shape": [1, 128, 512],
1534
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.037, "cosStep": 0.005 }
1535
+ },
1536
+ "pastKeyT": {
1537
+ "dtype": "float32",
1538
+ "shape": [1, 2, 1024, 256],
1539
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.017, "cosStep": 0.019 }
1540
+ },
1541
+ "pastValueT": {
1542
+ "dtype": "float32",
1543
+ "shape": [1, 2, 1024, 256],
1544
+ "data": { "kind": "fillFloat32", "scale": 0.2, "sinStep": 0.023, "cosStep": 0.013 }
1545
+ },
1546
+ "seqlensKT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1023] } },
1547
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1024] } }
1548
+ },
1549
+ "outputs": {
1550
+ "outputT": { "dtype": "float32", "shape": [1, 128, 2048] },
1551
+ "presentKeyT": { "dtype": "float32", "shape": [1, 2, 1024, 256] },
1552
+ "presentValueT": { "dtype": "float32", "shape": [1, 2, 1024, 256] }
1553
+ },
1554
+ "bench": {
1555
+ "metrics": [
1556
+ { "type": "gflops", "value": "4 * args.batch * args.qSeq * args.kvSeq * args.heads * args.headDim" }
1557
+ ]
1558
+ },
1559
+ "provenance": {
1560
+ "source": "register-geometry gate asymmetry",
1561
+ "notes": "Cached f32 prefill at headDim 256, above the shared-memory cluster's register-geometry boundary. This guards consistent route admission between no-past and cached prefill families."
1562
+ }
1563
+ }
1564
+ ]
1565
+ }
build/webgpu/gqa-attention.wgsl.jinja ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% set usesF16 = usesF16 | default(false) %}
2
+ {% set quantized = quantized | default(false) %}
3
+ {% set bits = bits | default(0) %}
4
+ {% if useSeqlens is not defined %}{% set useSeqlens = false %}{% endif %}
5
+ {% if usesF16 %}enable f16;
6
+ {% endif %}{{ env.wgsl.resourceDeclarations }}
7
+ // f16 inputs widen to f32 on load; computation and shared memory remain f32,
8
+ // and results narrow only on store. The casts are identities for f32.
9
+ {% set IO = "f16" if usesF16 else "f32" %}
10
+
11
+ {% if cooperative %}
12
+ // One workgroup owns each (batch, qHead, qToken). Lanes partition the key range,
13
+ // run local online softmax states, then merge max/rescale/sum through workgroup
14
+ // memory. This exposes parallelism when the query count is small.
15
+ {% else %}
16
+ // One invocation owns each (batch, qHead, qToken) and walks the key range. The
17
+ // larger query grid supplies parallelism without a workgroup-level merge.
18
+ {% endif %}
19
+ // The attention path supports optional per-head qk-norm and
20
+ // rotary on Q, causal (+ sliding-window) masking, logit soft-capping, additive
21
+ // attention bias, and smooth-softmax / head-sink. The cache is read directly
22
+ // (f32), or dequantized from standard INT8 / +8-biased packed INT4 storage when
23
+ // `quantized` is enabled.
24
+ const HEAD_DIM: u32 = {{ headDim }}u;
25
+ {% if hasRotary %}const HALF: u32 = {{ half }}u;{% endif %}
26
+ const Q_HEADS: u32 = {{ qHeads }}u;
27
+ const KV_HEADS: u32 = {{ kvHeads }}u;
28
+ const Q_HIDDEN: u32 = {{ qHidden }}u;
29
+ const GROUP: u32 = {{ qHeads }}u / {{ kvHeads }}u;
30
+ const PACKED: u32 = {{ packed }}u;
31
+ {% if hasQNorm %}const QK_EPS: f32 = {{ qkEps }};{% endif %}
32
+ {% if cooperative %}const WG: u32 = {{ tunables.COOPERATIVE_WORKGROUP_SIZE }}u;{% else %}const WG: u32 = {{ scalarWorkgroupSize }}u;{% endif %}
33
+ const NEG_INF: f32 = -3.4028234663852886e38;
34
+ {% if ATTN_SCALE_DIM is not defined %}{% set ATTN_SCALE_DIM = "HEAD_DIM" %}{% endif %}
35
+ fn scale_value() -> f32 {
36
+ if (params.scale != 0.0) { return params.scale; }
37
+ return inverseSqrt(f32({{ ATTN_SCALE_DIM }}));
38
+ }
39
+
40
+
41
+ {% if quantized %}
42
+ fn kscale(d: u32, hk: u32) -> f32 { return k_scale[select(0u, hk * HEAD_DIM + d, params.perChannel != 0u)]; }
43
+ fn vscale(d: u32, hk: u32) -> f32 { return v_scale[select(0u, hk * HEAD_DIM + d, params.perChannel != 0u)]; }
44
+ {% endif %}
45
+
46
+ fn read_k(base: u32, d: u32{% if quantized %}, hk: u32{% endif %}) -> f32 {
47
+ {% if not quantized %}
48
+ return f32(kcache[base + d]);
49
+ {% elif bits == 8 %}
50
+ return f32(kcache[base + d]) * kscale(d, hk);
51
+ {% else %}
52
+ let byte = kcache[base + (d >> 1u)];
53
+ let nib = i32((byte >> ((d & 1u) * 4u)) & 0xFu) - 8;
54
+ return f32(nib) * kscale(d, hk);
55
+ {% endif %}
56
+ }
57
+ fn read_v(base: u32, d: u32{% if quantized %}, hk: u32{% endif %}) -> f32 {
58
+ {% if not quantized %}
59
+ return f32(vcache[base + d]);
60
+ {% elif bits == 8 %}
61
+ return f32(vcache[base + d]) * vscale(d, hk);
62
+ {% else %}
63
+ let byte = vcache[base + (d >> 1u)];
64
+ let nib = i32((byte >> ((d & 1u) * 4u)) & 0xFu) - 8;
65
+ return f32(nib) * vscale(d, hk);
66
+ {% endif %}
67
+ }
68
+
69
+ {% if cooperative %}
70
+ var<workgroup> qsh: array<f32, HEAD_DIM>; // query head-vector (post norm/rotary)
71
+ var<workgroup> acc_sh: array<f32, HEAD_DIM * 32u>; // per-lane V accumulator [lane*HEAD_DIM + d]
72
+ 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;
82
+ let s = qi % params.qSeq;
83
+ let tmp = qi / params.qSeq;
84
+ let h = tmp % Q_HEADS;
85
+ let b = tmp / Q_HEADS;
86
+ let hk = h / GROUP;
87
+ let totalSeq = params.totalSeq;
88
+ {% if useSeqlens %}
89
+ // Buffer-sharing caches keep capacity in the BNSH dim2 stride; seqlens_k[b]+1
90
+ // is the per-batch active end and rows beyond it are untouched storage.
91
+ let activeEnd = min(totalSeq, max(params.qSeq, u32(seqlens_k[b]) + 1u));
92
+ {% else %}
93
+ let activeEnd = totalSeq;
94
+ {% endif %}
95
+ let absPos = activeEnd - params.qSeq + s;
96
+
97
+ // Cooperative query-vector prep (post norm/rotary) into shared memory.
98
+ let qBase = (b * params.qSeq + s) * Q_HIDDEN + h * HEAD_DIM;
99
+ for (var d = lane; d < HEAD_DIM; d = d + WG) { qsh[d] = f32(query[qBase + d]); }
100
+ workgroupBarrier();
101
+ {% if hasQNorm %}
102
+ var part = 0.0;
103
+ for (var d = lane; d < HEAD_DIM; d = d + WG) { part = part + qsh[d] * qsh[d]; }
104
+ m_sh[lane] = part;
105
+ workgroupBarrier();
106
+ var ms = 0.0;
107
+ for (var L = 0u; L < WG; L = L + 1u) { ms = ms + m_sh[L]; }
108
+ let invRms = inverseSqrt(ms / f32(HEAD_DIM) + QK_EPS);
109
+ for (var d = lane; d < HEAD_DIM; d = d + WG) { qsh[d] = qsh[d] * invRms * f32(q_norm_weight[d]); }
110
+ workgroupBarrier();
111
+ {% endif %}
112
+ {% if hasRotary %}
113
+ // Each lane owns disjoint pairs (d, d+HALF), so the in-place rotate is safe.
114
+ for (var d = lane; d < HALF; d = d + WG) {
115
+ let cs = f32(cos_cache[absPos * HALF + d]);
116
+ let sn = f32(sin_cache[absPos * HALF + d]);
117
+ let x0 = qsh[d];
118
+ let x1 = qsh[d + HALF];
119
+ qsh[d] = x0 * cs - x1 * sn;
120
+ qsh[d + HALF] = x1 * cs + x0 * sn;
121
+ }
122
+ workgroupBarrier();
123
+ {% endif %}
124
+ let scale = scale_value();
125
+ let maxKj = absPos + 1u;
126
+ var minKj = 0u;
127
+ if (params.windowSize > 0u && maxKj > params.windowSize) { minKj = maxKj - params.windowSize; }
128
+
129
+ let accBase = lane * HEAD_DIM;
130
+ for (var d = 0u; d < HEAD_DIM; d = d + 1u) { acc_sh[accBase + d] = 0.0; }
131
+ var m = NEG_INF;
132
+ var l = 0.0;
133
+
134
+ // Each lane sweeps a strided slice of the key range.
135
+ for (var j = minKj + lane; j < maxKj; j = j + WG) {
136
+ let base = ((b * KV_HEADS + hk) * totalSeq + j) * PACKED;
137
+ var dot = 0.0;
138
+ for (var d = 0u; d < HEAD_DIM; d = d + 1u) { dot = dot + qsh[d] * read_k(base, d{% if quantized %}, hk{% endif %}); }
139
+ var score = dot * scale;
140
+ if (params.softcap > 0.0) { score = params.softcap * tanh(clamp(score / params.softcap, -30.0, 30.0)); }
141
+ {% if hasBias %}
142
+ let bb = select(b, 0u, params.biasBatch == 1u);
143
+ let bh = select(h, 0u, params.biasHeads == 1u);
144
+ score = score + f32(attn_bias[((bb * params.biasHeads + bh) * params.qSeq + s) * totalSeq + j]);
145
+ {% endif %}
146
+ let newM = max(m, score);
147
+ let corr = exp(m - newM);
148
+ let p = exp(score - newM);
149
+ l = l * corr + p;
150
+ for (var d = 0u; d < HEAD_DIM; d = d + 1u) { acc_sh[accBase + d] = acc_sh[accBase + d] * corr + p * read_v(base, d{% if quantized %}, hk{% endif %}); }
151
+ m = newM;
152
+ }
153
+
154
+ // Flash merge across the WG lanes: global max, rescale, summed denom.
155
+ m_sh[lane] = m;
156
+ workgroupBarrier();
157
+ var gm = NEG_INF;
158
+ for (var L = 0u; L < WG; L = L + 1u) { gm = max(gm, m_sh[L]); }
159
+ let fctr = exp(m - gm);
160
+ l = l * fctr;
161
+ for (var d = 0u; d < HEAD_DIM; d = d + 1u) { acc_sh[accBase + d] = acc_sh[accBase + d] * fctr; }
162
+ l_sh[lane] = l;
163
+ workgroupBarrier();
164
+ var glsum = 0.0;
165
+ for (var L = 0u; L < WG; L = L + 1u) { glsum = glsum + l_sh[L]; }
166
+
167
+ var sink = 0.0;
168
+ {% if hasHeadSink %}
169
+ sink = f32(head_sink[h]);
170
+ let useSmooth = true;
171
+ {% else %}
172
+ let useSmooth = params.smoothSoftmax != 0u;
173
+ {% endif %}
174
+ var finalM = gm;
175
+ var smoothTerm = 0.0;
176
+ if (useSmooth) {
177
+ finalM = max(gm, sink);
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;
185
+ for (var d = lane; d < HEAD_DIM; d = d + WG) {
186
+ var sumacc = 0.0;
187
+ for (var L = 0u; L < WG; L = L + 1u) { sumacc = sumacc + acc_sh[L * HEAD_DIM + d]; }
188
+ output[oBase + d] = {{ IO }}(sumacc * invDenom);
189
+ }
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;
203
+ let tmp = qi / params.qSeq;
204
+ let h = tmp % Q_HEADS;
205
+ let b = tmp / Q_HEADS;
206
+ let hk = h / GROUP;
207
+ let totalSeq = params.totalSeq;
208
+ {% if useSeqlens %}
209
+ // Buffer-sharing caches keep capacity in the BNSH dim2 stride; seqlens_k[b]+1
210
+ // is the per-batch active end and rows beyond it are untouched storage.
211
+ let activeEnd = min(totalSeq, max(params.qSeq, u32(seqlens_k[b]) + 1u));
212
+ {% else %}
213
+ let activeEnd = totalSeq;
214
+ {% endif %}
215
+ let absPos = activeEnd - params.qSeq + s;
216
+
217
+ // Private query-vector prep (post norm/rotary).
218
+ var q: array<f32, HEAD_DIM>;
219
+ let qBase = (b * params.qSeq + s) * Q_HIDDEN + h * HEAD_DIM;
220
+ for (var d = 0u; d < HEAD_DIM; d = d + 1u) { q[d] = f32(query[qBase + d]); }
221
+ {% if hasQNorm %}
222
+ var ss = 0.0;
223
+ for (var d = 0u; d < HEAD_DIM; d = d + 1u) { ss = ss + q[d] * q[d]; }
224
+ let invRms = inverseSqrt(ss / f32(HEAD_DIM) + QK_EPS);
225
+ for (var d = 0u; d < HEAD_DIM; d = d + 1u) { q[d] = q[d] * invRms * f32(q_norm_weight[d]); }
226
+ {% endif %}
227
+ {% if hasRotary %}
228
+ for (var d = 0u; d < HALF; d = d + 1u) {
229
+ let cs = f32(cos_cache[absPos * HALF + d]);
230
+ let sn = f32(sin_cache[absPos * HALF + d]);
231
+ let x0 = q[d];
232
+ let x1 = q[d + HALF];
233
+ q[d] = x0 * cs - x1 * sn;
234
+ q[d + HALF] = x1 * cs + x0 * sn;
235
+ }
236
+ {% endif %}
237
+ let scale = scale_value();
238
+ let maxKj = absPos + 1u;
239
+ var minKj = 0u;
240
+ if (params.windowSize > 0u && maxKj > params.windowSize) { minKj = maxKj - params.windowSize; }
241
+
242
+ var acc: array<f32, HEAD_DIM>;
243
+ for (var d = 0u; d < HEAD_DIM; d = d + 1u) { acc[d] = 0.0; }
244
+ var m = NEG_INF;
245
+ var l = 0.0;
246
+
247
+ for (var j = minKj; j < maxKj; j = j + 1u) {
248
+ let base = ((b * KV_HEADS + hk) * totalSeq + j) * PACKED;
249
+ var dot = 0.0;
250
+ for (var d = 0u; d < HEAD_DIM; d = d + 1u) { dot = dot + q[d] * read_k(base, d{% if quantized %}, hk{% endif %}); }
251
+ var score = dot * scale;
252
+ if (params.softcap > 0.0) { score = params.softcap * tanh(clamp(score / params.softcap, -30.0, 30.0)); }
253
+ {% if hasBias %}
254
+ let bb = select(b, 0u, params.biasBatch == 1u);
255
+ let bh = select(h, 0u, params.biasHeads == 1u);
256
+ score = score + f32(attn_bias[((bb * params.biasHeads + bh) * params.qSeq + s) * totalSeq + j]);
257
+ {% endif %}
258
+ let newM = max(m, score);
259
+ let corr = exp(m - newM);
260
+ let p = exp(score - newM);
261
+ l = l * corr + p;
262
+ for (var d = 0u; d < HEAD_DIM; d = d + 1u) { acc[d] = acc[d] * corr + p * read_v(base, d{% if quantized %}, hk{% endif %}); }
263
+ m = newM;
264
+ }
265
+
266
+ var sink = 0.0;
267
+ {% if hasHeadSink %}
268
+ sink = f32(head_sink[h]);
269
+ let useSmooth = true;
270
+ {% else %}
271
+ let useSmooth = params.smoothSoftmax != 0u;
272
+ {% endif %}
273
+ var finalM = m;
274
+ var smoothTerm = 0.0;
275
+ if (useSmooth) {
276
+ finalM = max(m, sink);
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); }
284
+ }
285
+ {% endif %}
build/webgpu/gqa-present.wgsl.jinja ADDED
@@ -0,0 +1,440 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 %}
6
+ {% set presentScalar = presentScalar | default(inputScalar | default("f32")) %}
7
+ {% set presentElement = presentElement | default(presentScalar) %}
8
+ {% set packed = packed | default(0) %}
9
+ {% set bits = bits | default(0) %}
10
+ {% set qmax = qmax | default(0) %}
11
+ {% set qmin = qmin | default(0) %}
12
+ {% if usesF16 is defined and usesF16 %}enable f16;
13
+ {% endif %}{{ env.wgsl.resourceDeclarations }}
14
+ {% if mode == "transpose" %}
15
+
16
+ const KV_HEADS: u32 = {{ kvNumHeads }}u;
17
+ 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 %}
25
+ const HEAD_DIM: u32 = {{ headDim }}u;
26
+ const KV_HIDDEN: u32 = {{ kvHidden }}u;
27
+ {% endif %}
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) {
40
+ return;
41
+ }
42
+ let d4 = index % HEAD_DIM_V4;
43
+ let token = (index / HEAD_DIM_V4) % params.kvSeq;
44
+ let head = (index / (HEAD_DIM_V4 * params.kvSeq)) % KV_HEADS;
45
+ let batch = index / (HEAD_DIM_V4 * params.kvSeq * KV_HEADS);
46
+ let packed = (batch * params.kvSeq + token) * KV_HIDDEN_V4 + head * HEAD_DIM_V4 + d4;
47
+ present_key[index] = {{ presentElement }}(key[packed]);
48
+ present_value[index] = {{ presentElement }}(value[packed]);
49
+ {% else %}
50
+ let total = params.batchSize * KV_HEADS * params.kvSeq * HEAD_DIM;
51
+ if (index >= total) {
52
+ return;
53
+ }
54
+ let d = index % HEAD_DIM;
55
+ let token = (index / HEAD_DIM) % params.kvSeq;
56
+ let head = (index / (HEAD_DIM * params.kvSeq)) % KV_HEADS;
57
+ let batch = index / (HEAD_DIM * params.kvSeq * KV_HEADS);
58
+ let packed_index = (batch * params.kvSeq + token) * KV_HIDDEN + head * HEAD_DIM + d;
59
+ present_key[index] = {{ presentScalar }}(key[packed_index]);
60
+ present_value[index] = {{ presentScalar }}(value[packed_index]);
61
+ {% endif %}
62
+ }
63
+ {%- else %}
64
+
65
+ {% set shareAppend = mode == "merge_share" and shareRegion == "append" %}
66
+ {% set cooperativeCopy = device.adapterInfo.vendor != "apple" %}
67
+ {% set cooperativeMerge = cooperativeCopy or shareAppend %}
68
+ {% set cooperativeMode = cooperativeMerge and (mode == "copy" or mode == "merge" or mode == "merge_share") %}
69
+ {% set needsGid = not cooperativeMode or (shareAppend and not cooperativeCopy) %}
70
+ {% if mode == "copy" %}
71
+ // Copy the existing BNSH cache unchanged. Compression changes only the number
72
+ // of stored words per row.
73
+ {% elif mode == "merge" %}
74
+ // Copy the past BNSH cache, then append new BSH K/V tokens into the present cache.
75
+ {% elif mode == "merge_share" %}
76
+ // Buffer-sharing append: past and present share the full-capacity BNSH stride.
77
+ // Rows outside each batch's append window [seqlens_k[b]+1-keySeq, seqlens_k[b]+1)
78
+ // are an identity copy of the past cache; rows inside it come from the new BSH
79
+ // K/V (K optionally rotated at its absolute position).
80
+ // The append and retain passes own disjoint ranges, so their order is immaterial.
81
+ {% elif mode == "build" %}
82
+ // Build a BNSH cache from new BSH K/V, optionally applying K RMSNorm and rotary.
83
+ {% elif mode == "window_shift" %}
84
+ // Windowed cache: compact the surviving past rows down by the eviction count and
85
+ // append the new BSH K/V, keeping the most recent min(T, capacity) tokens
86
+ // contiguous at rows [0, L). Reads past, writes present (distinct buffers).
87
+ {% elif mode == "append_quant" %}
88
+ // Quantize new BSH K/V into an existing buffer-sharing INT8 BNSH cache. The
89
+ // active sequence end comes from seqlens_k; the physical cache stride remains
90
+ // the full present-cache capacity.
91
+ {% else %}
92
+ // Quantize new BSH K/V into an INT8 or packed INT4 BNSH cache.
93
+ {% endif %}
94
+ const HEAD_DIM: u32 = {{ headDim }}u;
95
+ {% if mode != "copy" %}const KV_HEADS: u32 = {{ kvHeads }}u;
96
+ {% endif %}
97
+ const WG: u32 = {{ tunables.COPY_WORKGROUP_SIZE }}u;
98
+ {% if mode != "copy" and (mode != "merge_share" or shareAppend) %}const KV_HIDDEN: u32 = {{ kvHeads }}u * {{ headDim }}u;
99
+ {% endif %}
100
+ {% if mode == "build_quant" or mode == "append_quant" %}const PACKED: u32 = {{ packed }}u;
101
+ {% endif %}
102
+ {% if hasRotary and mode != "copy" and (mode != "merge_share" or shareAppend) %}const HALF: u32 = {{ half }}u;
103
+ {% endif %}
104
+ {% if hasKNorm and mode != "copy" and (mode != "merge_share" or shareAppend) %}const QK_EPS: f32 = {{ qkEps }};
105
+ {% endif %}
106
+ {% if mode == "build_quant" or mode == "append_quant" %}
107
+ const QMAX: f32 = {{ qmax }};
108
+ const QMIN: f32 = {{ qmin }};
109
+ fn kscale(d: u32, hk: u32) -> f32 { return k_scale[select(0u, hk * HEAD_DIM + d, params.perChannel != 0u)]; }
110
+ fn vscale(d: u32, hk: u32) -> f32 { return v_scale[select(0u, hk * HEAD_DIM + d, params.perChannel != 0u)]; }
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
122
+ // destination row is the dynamic append offset plus the local row.
123
+ // The loop is bounded by the elements this workgroup owns rather than a fixed
124
+ // HEAD_DIM trip count.
125
+ let wgEnd = min(elemBase + WG * HEAD_DIM, totalElems);
126
+ for (var e = elemBase + lid.x; e < wgEnd; e = e + WG) {
127
+ {
128
+ let d = e % HEAD_DIM;
129
+ let et = e / HEAD_DIM;
130
+ let j = et % params.keySeq;
131
+ let etmp = et / params.keySeq;
132
+ let hk = etmp % KV_HEADS;
133
+ let b = etmp / KV_HEADS;
134
+ // Clamping below by keySeq maps a right-padded first prompt
135
+ // (seqlens_k[b]+1 < keySeq) to append offset 0.
136
+ let activeEnd = min(params.seq, max(params.keySeq, u32(seqlens_k[b]) + 1u));
137
+ let t = activeEnd - params.keySeq + j;
138
+ let dst = ((b * KV_HEADS + hk) * params.seq + t) * HEAD_DIM + d;
139
+ let nSrc = (b * params.keySeq + j) * KV_HIDDEN + hk * HEAD_DIM;
140
+ {% if hasRotary %}
141
+ // Element-wise NeoX rotation: the pair (dr, dr+HALF) shares one
142
+ // cos/sin entry, and row t is its own absolute position.
143
+ let dr = select(d - HALF, d, d < HALF);
144
+ let cs = cos_cache[t * HALF + dr];
145
+ let sn = sin_cache[t * HALF + dr];
146
+ let x0 = new_k[nSrc + dr];
147
+ let x1 = new_k[nSrc + dr + HALF];
148
+ present_key[dst] = select(x1 * cs + x0 * sn, x0 * cs - x1 * sn, d < HALF);
149
+ {% else %}
150
+ present_key[dst] = new_k[nSrc + d];
151
+ {% endif %}
152
+ present_value[dst] = new_v[nSrc + d];
153
+ }
154
+ }
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;
162
+ let hk = tmp % KV_HEADS;
163
+ let b = tmp / KV_HEADS;
164
+ // Clamping below by keySeq maps a right-padded first prompt
165
+ // (seqlens_k[b]+1 < keySeq) to append offset 0.
166
+ let activeEnd = min(params.seq, max(params.keySeq, u32(seqlens_k[b]) + 1u));
167
+ let t = activeEnd - params.keySeq + j;
168
+ let dstBase = ((b * KV_HEADS + hk) * params.seq + t) * HEAD_DIM;
169
+ let nSrc = (b * params.keySeq + j) * KV_HIDDEN + hk * HEAD_DIM;
170
+ {% if hasRotary %}
171
+ var k: array<f32, HEAD_DIM>;
172
+ for (var d = 0u; d < HEAD_DIM; d = d + 1u) { k[d] = f32(new_k[nSrc + d]); }
173
+ // Row t is its own absolute position in the shared cache.
174
+ for (var d = 0u; d < HALF; d = d + 1u) {
175
+ let cs = cos_cache[t * HALF + d];
176
+ let sn = sin_cache[t * HALF + d];
177
+ let x0 = k[d];
178
+ let x1 = k[d + HALF];
179
+ k[d] = x0 * cs - x1 * sn;
180
+ k[d + HALF] = x1 * cs + x0 * sn;
181
+ }
182
+ for (var d = 0u; d < HEAD_DIM; d = d + 1u) {
183
+ present_key[dstBase + d] = k[d];
184
+ present_value[dstBase + d] = new_v[nSrc + d];
185
+ }
186
+ {% else %}
187
+ for (var d = 0u; d < HEAD_DIM; d = d + 1u) {
188
+ present_key[dstBase + d] = new_k[nSrc + d];
189
+ present_value[dstBase + d] = new_v[nSrc + d];
190
+ }
191
+ {% endif %}
192
+ {% endmacro %}
193
+ {% if cooperativeMerge and (mode == "copy" or mode == "merge" or mode == "merge_share") %}
194
+ // Each workgroup copies the contiguous element span of its WG rows
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 %}
202
+ {% if mode == "copy" and cooperativeCopy %}
203
+ // Copy mode is an identity copy over the whole BNSH range: dst index == src index.
204
+ for (var s = 0u; s < HEAD_DIM; s = s + 1u) {
205
+ let e = elemBase + s * WG + lid.x;
206
+ if (e < totalElems) {
207
+ present_key[e] = src_k[e];
208
+ present_value[e] = src_v[e];
209
+ }
210
+ }
211
+ {% elif mode == "merge" and cooperativeCopy %}
212
+ // present[0:pastSeq] = past (BNSH copy); present[pastSeq:totalSeq] = new K/V (BSH appended).
213
+ for (var s = 0u; s < HEAD_DIM; s = s + 1u) {
214
+ let e = elemBase + s * WG + lid.x;
215
+ if (e < totalElems) {
216
+ let d = e % HEAD_DIM;
217
+ let et = e / HEAD_DIM;
218
+ let t = et % params.seq;
219
+ let etmp = et / params.seq;
220
+ let hk = etmp % KV_HEADS;
221
+ let b = etmp / KV_HEADS;
222
+ if (t < params.pastSeq) {
223
+ let src = ((b * KV_HEADS + hk) * params.pastSeq + t) * HEAD_DIM + d;
224
+ present_key[e] = past_k[src];
225
+ present_value[e] = past_v[src];
226
+ } else {
227
+ let nSrc = (b * params.keySeq + (t - params.pastSeq)) * KV_HIDDEN + hk * HEAD_DIM + d;
228
+ present_key[e] = new_k[nSrc];
229
+ present_value[e] = new_v[nSrc];
230
+ }
231
+ }
232
+ }
233
+ {% elif mode == "merge_share" and cooperativeMerge %}
234
+ {% if shareAppend %}
235
+ {% if cooperativeCopy %}
236
+ {{ append_cooperative_walk() }}
237
+ {% else %}
238
+ if (params.count < WG) {
239
+ {{ append_cooperative_walk() }}
240
+ return;
241
+ }
242
+ {{ append_per_row_walk() }}
243
+ {% endif %}
244
+ {% else %}
245
+ // Cooperative element walk (see the copy branch); the same-stride layout
246
+ // makes the outside-window rows an identity copy (dst index == src index).
247
+ for (var s = 0u; s < HEAD_DIM; s = s + 1u) {
248
+ let e = elemBase + s * WG + lid.x;
249
+ if (e < totalElems) {
250
+ let et = e / HEAD_DIM;
251
+ let t = et % params.seq;
252
+ let b = et / (params.seq * KV_HEADS);
253
+ // Clamping below by keySeq maps a right-padded first prompt
254
+ // (seqlens_k[b]+1 < keySeq) to append offset 0.
255
+ let activeEnd = min(params.seq, max(params.keySeq, u32(seqlens_k[b]) + 1u));
256
+ let appendStart = activeEnd - params.keySeq;
257
+ // Everything outside the append window. The append pass owns the window itself,
258
+ // and the shared full-capacity stride makes this an identity copy (dst == src).
259
+ if (t < appendStart || t >= activeEnd) {
260
+ present_key[e] = past_k[e];
261
+ present_value[e] = past_v[e];
262
+ }
263
+ }
264
+ }
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) {
272
+ present_key[base + d] = src_k[base + d];
273
+ present_value[base + d] = src_v[base + d];
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;
281
+ let hk = tmp % KV_HEADS;
282
+ let b = tmp / KV_HEADS;
283
+ // present[0:pastSeq] = past (BNSH copy); present[pastSeq:totalSeq] = new K/V (BSH appended).
284
+ let dstBase = ((b * KV_HEADS + hk) * params.seq + t) * HEAD_DIM;
285
+ if (t < params.pastSeq) {
286
+ let srcBase = ((b * KV_HEADS + hk) * params.pastSeq + t) * HEAD_DIM;
287
+ for (var d = 0u; d < HEAD_DIM; d = d + 1u) {
288
+ present_key[dstBase + d] = past_k[srcBase + d];
289
+ present_value[dstBase + d] = past_v[srcBase + d];
290
+ }
291
+ } else {
292
+ let nSrc = (b * params.keySeq + (t - params.pastSeq)) * KV_HIDDEN + hk * HEAD_DIM;
293
+ for (var d = 0u; d < HEAD_DIM; d = d + 1u) {
294
+ present_key[dstBase + d] = new_k[nSrc + d];
295
+ present_value[dstBase + d] = new_v[nSrc + d];
296
+ }
297
+ }
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;
305
+ let hk = tmp % KV_HEADS;
306
+ let b = tmp / KV_HEADS;
307
+ // Clamping below by keySeq maps a right-padded first prompt
308
+ // (seqlens_k[b]+1 < keySeq) to append offset 0.
309
+ let activeEnd = min(params.seq, max(params.keySeq, u32(seqlens_k[b]) + 1u));
310
+ let appendStart = activeEnd - params.keySeq;
311
+ let dstBase = ((b * KV_HEADS + hk) * params.seq + t) * HEAD_DIM;
312
+ // Everything outside the append window. The append pass owns the window itself,
313
+ // and the shared full-capacity stride makes this an identity copy (dst == src).
314
+ if (t < appendStart || t >= activeEnd) {
315
+ for (var d = 0u; d < HEAD_DIM; d = d + 1u) {
316
+ present_key[dstBase + d] = past_k[dstBase + d];
317
+ present_value[dstBase + d] = past_v[dstBase + d];
318
+ }
319
+ }
320
+ {% elif mode == "window_shift" %}
321
+ // Windowed cache: `params.seq` is a fixed capacity C, and the invariant is that
322
+ // the L = min(T, C) most recent tokens live contiguously at rows [0, L).
323
+ //
324
+ // residentBefore = min(T - S, C) E = max(0, residentBefore + S - C)
325
+ // rows [0, appendStart) <- past rows shifted down by E
326
+ // rows [appendStart, append+S) <- the new tokens
327
+ // rows beyond that <- outside the window, cleared
328
+ //
329
+ // The shift reads past and writes present, which must be distinct buffers: an
330
+ // in-place compaction would have one invocation overwrite row t while another
331
+ // still needs it as the source for row t-E, with no ordering between them.
332
+ //
333
+ // Attention needs no change for this layout. It derives its key range from
334
+ // `min(capacity, seqlens_k[b] + 1)`, which is exactly L, and both the causal
335
+ // and local-window masks depend only on the query/key distance:
336
+ // q_abs - k_abs = (T - qSeq + s) - (origin + t) = (L - qSeq + s) - t
337
+ // so scoring a windowed cache as if it were a full L-length one is the same
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;
345
+ let hk = tmp % KV_HEADS;
346
+ let b = tmp / KV_HEADS;
347
+ let absTotal = max(params.keySeq, u32(seqlens_k[b]) + 1u);
348
+ let residentBefore = min(absTotal - params.keySeq, params.seq);
349
+ // `max(0u, a + b - c)` does not clamp in u32: the subtraction wraps first, so a
350
+ // step that evicts nothing reads back ~2^32 instead of 0. Compare before
351
+ // subtracting.
352
+ let filled = residentBefore + params.keySeq;
353
+ let evicted = select(0u, filled - params.seq, filled > params.seq);
354
+ let appendStart = residentBefore - evicted;
355
+ let dstBase = ((b * KV_HEADS + hk) * params.seq + t) * HEAD_DIM;
356
+ if (t < appendStart) {
357
+ // Surviving past rows slide down by the eviction count.
358
+ let srcBase = ((b * KV_HEADS + hk) * params.seq + (t + evicted)) * HEAD_DIM;
359
+ for (var d = 0u; d < HEAD_DIM; d = d + 1u) {
360
+ present_key[dstBase + d] = past_k[srcBase + d];
361
+ present_value[dstBase + d] = past_v[srcBase + d];
362
+ }
363
+ } else if (t < appendStart + params.keySeq) {
364
+ let nSrc = (b * params.keySeq + (t - appendStart)) * KV_HIDDEN + hk * HEAD_DIM;
365
+ for (var d = 0u; d < HEAD_DIM; d = d + 1u) {
366
+ present_key[dstBase + d] = new_k[nSrc + d];
367
+ present_value[dstBase + d] = new_v[nSrc + d];
368
+ }
369
+ } else {
370
+ // Outside the resident window. Cleared rather than left stale so the present
371
+ // buffer does not expose stale contents from its distinct allocation.
372
+ for (var d = 0u; d < HEAD_DIM; d = d + 1u) {
373
+ present_key[dstBase + d] = {{ zeroScalar }}(0.0);
374
+ present_value[dstBase + d] = {{ zeroScalar }}(0.0);
375
+ }
376
+ }
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;
384
+ let hk = tmp % KV_HEADS;
385
+ let b = tmp / KV_HEADS;
386
+ let srcBase = (b * params.seq + t) * KV_HIDDEN + hk * HEAD_DIM; // BSH new K/V
387
+ var k: array<f32, HEAD_DIM>;
388
+ for (var d = 0u; d < HEAD_DIM; d = d + 1u) { k[d] = src_k[srcBase + d]; }
389
+ {% if hasKNorm %}
390
+ var ms = 0.0;
391
+ for (var d = 0u; d < HEAD_DIM; d = d + 1u) { ms = ms + k[d] * k[d]; }
392
+ let invRms = inverseSqrt(ms / f32(HEAD_DIM) + QK_EPS);
393
+ for (var d = 0u; d < HEAD_DIM; d = d + 1u) { k[d] = k[d] * invRms * k_norm_weight[d]; }
394
+ {% endif %}
395
+ {% if hasRotary %}
396
+ let pos = params.pastSeq + t;
397
+ for (var d = 0u; d < HALF; d = d + 1u) {
398
+ let cs = cos_cache[pos * HALF + d];
399
+ let sn = sin_cache[pos * HALF + d];
400
+ let x0 = k[d];
401
+ let x1 = k[d + HALF];
402
+ k[d] = x0 * cs - x1 * sn;
403
+ k[d + HALF] = x1 * cs + x0 * sn;
404
+ }
405
+ {% endif %}
406
+ {% if mode == "build" %}
407
+ let dstBase = ((b * KV_HEADS + hk) * params.seq + t) * HEAD_DIM;
408
+ for (var d = 0u; d < HEAD_DIM; d = d + 1u) {
409
+ present_key[dstBase + d] = k[d];
410
+ present_value[dstBase + d] = src_v[srcBase + d];
411
+ }
412
+ {% else %}
413
+ {% if mode == "append_quant" %}
414
+ let activeEnd = u32(seqlens_k[b]) + 1u;
415
+ let appendStart = activeEnd - params.seq;
416
+ let dstBase = ((b * KV_HEADS + hk) * params.totalSeq + appendStart + t) * PACKED;
417
+ {% else %}
418
+ let dstBase = ((b * KV_HEADS + hk) * params.seq + t) * PACKED;
419
+ {% endif %}
420
+ {% if bits == 8 %}
421
+ for (var d = 0u; d < HEAD_DIM; d = d + 1u) {
422
+ present_key[dstBase + d] = i32(clamp(round(k[d] / kscale(d, hk)), QMIN, QMAX));
423
+ present_value[dstBase + d] = i32(clamp(round(src_v[srcBase + d] / vscale(d, hk)), QMIN, QMAX));
424
+ }
425
+ {% else %}
426
+ for (var dd = 0u; dd < PACKED; dd = dd + 1u) {
427
+ let d0 = dd * 2u;
428
+ let d1 = dd * 2u + 1u;
429
+ let qk0 = i32(clamp(round(k[d0] / kscale(d0, hk)), QMIN, QMAX));
430
+ let qk1 = i32(clamp(round(k[d1] / kscale(d1, hk)), QMIN, QMAX));
431
+ let qv0 = i32(clamp(round(src_v[srcBase + d0] / vscale(d0, hk)), QMIN, QMAX));
432
+ let qv1 = i32(clamp(round(src_v[srcBase + d1] / vscale(d1, hk)), QMIN, QMAX));
433
+ present_key[dstBase + dd] = (u32(qk0 + 8) & 0xFu) | ((u32(qk1 + 8) & 0xFu) << 4u);
434
+ present_value[dstBase + dd] = (u32(qv0 + 8) & 0xFu) | ((u32(qv1 + 8) & 0xFu) << 4u);
435
+ }
436
+ {% endif %}
437
+ {% endif %}
438
+ {% endif %}
439
+ }
440
+ {%- endif %}
build/webgpu/gqa-qprep.wgsl.jinja ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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;
12
+ // the qPrep output also stays f32 — RoPE precision matters at large positions, so
13
+ // the rotated Q enters attention in full f32 (the cluster reads f32 Q + f16 K/V).
14
+ const HEAD_DIM: u32 = {{ headDim }}u;
15
+ const HEAD_DIM_V4: u32 = {{ headDim }}u / 4u;
16
+ const Q_HEADS: u32 = {{ qHeads }}u;
17
+ const Q_HIDDEN: u32 = {{ qHidden }}u;
18
+ const Q_HIDDEN_V4: u32 = {{ qHidden }}u / 4u;
19
+ const WG: u32 = {{ copyWorkgroupSize }}u;
20
+ const HALF: u32 = {{ half }}u;
21
+ const QK_EPS: f32 = {{ qkEps }};
22
+
23
+ @compute @workgroup_size(WG)
24
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>,
25
+ @builtin(num_workgroups) nwg: vec3<u32>) {
26
+ // 2D-folded flat index: gid.y carries the high bits past the maxComputeWorkgroupsPerDimension
27
+ // workgroup-per-dimension dispatch limit. Reduces to gid.x when nwg.y == 1.
28
+ let qi = gid.x + gid.y * nwg.x * WG;
29
+ let total = params.batch * Q_HEADS * params.qSeq;
30
+ if (qi >= total) { return; }
31
+ let s = qi % params.qSeq;
32
+ let tmp = qi / params.qSeq;
33
+ let h = tmp % Q_HEADS;
34
+ let b = tmp / Q_HEADS;
35
+ let absPos = params.totalSeq - params.qSeq + s;
36
+ let base = (b * params.qSeq + s) * Q_HIDDEN + h * HEAD_DIM;
37
+
38
+ var q: array<f32, HEAD_DIM>;
39
+ for (var d = 0u; d < HEAD_DIM; d = d + 1u) { q[d] = f32(query[base + d]); }
40
+ var ss = 0.0;
41
+ for (var d = 0u; d < HEAD_DIM; d = d + 1u) { ss = ss + q[d] * q[d]; }
42
+ let invRms = inverseSqrt(ss / f32(HEAD_DIM) + QK_EPS);
43
+ for (var d = 0u; d < HEAD_DIM; d = d + 1u) { q[d] = q[d] * invRms * f32(q_norm_weight[d]); }
44
+ for (var d = 0u; d < HALF; d = d + 1u) {
45
+ let cs = f32(cos_cache[absPos * HALF + d]);
46
+ let sn = f32(sin_cache[absPos * HALF + d]);
47
+ let x0 = q[d];
48
+ let x1 = q[d + HALF];
49
+ q[d] = x0 * cs - x1 * sn;
50
+ q[d + HALF] = x1 * cs + x0 * sn;
51
+ }
52
+
53
+ let base4 = (b * params.qSeq + s) * Q_HIDDEN_V4 + h * HEAD_DIM_V4;
54
+ for (var c = 0u; c < HEAD_DIM_V4; c = c + 1u) {
55
+ qout[base4 + c] = vec4<f32>(q[c * 4u], q[c * 4u + 1u], q[c * 4u + 2u], q[c * 4u + 3u]);
56
+ }
57
+ }
build/webgpu/manifest.json ADDED
The diff for this file is too large to render. See raw diff
 
build/webgpu/metadata.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "com.microsoft.GroupQueryAttention",
3
+ "id": "_com_microsoft_groupqueryattention_webgpu_75fbc9b",
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-online-scalar.wgsl.jinja": "X4yGNCYu9+uJX6Q3EKkxV6v28CPlIYWme0Nmf8F16L8=",
17
+ "bench.json": "rGPC3cQInjJZjL+u+MzVXCOGu4L+G0v+/0UGDI9DEuQ=",
18
+ "gqa-attention.wgsl.jinja": "0EbwmdstNZMuskS170zYEo8whbQOFtupXLnNP2PaFL0=",
19
+ "gqa-present.wgsl.jinja": "J9g6oAeY2BtbmWBMlmbIXbxzo4sKHdxYqcE4EAd7BYM=",
20
+ "gqa-qprep.wgsl.jinja": "SvA19/AQoN2FXduwBtMI5QzBeeth2y9g8EnaIkV3V/0=",
21
+ "manifest.json": "qODDtl2Snem6J/K6QJwIL5i6CnGQ898t0a/EEka4JnY=",
22
+ "test.json": "iZAuQQINr2i9Q2hwXzNhKO+UT+ByfRwPsNc+paVhR20="
23
+ }
24
+ },
25
+ "provenance": { "kernel": { "sha": "2e7068faf55e7f43df740015f6d1ee49391a41c5", "dirty": false } },
26
+ "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/com.microsoft.GroupQueryAttention" }
27
+ }
build/webgpu/test.json ADDED
The diff for this file is too large to render. See raw diff