sync 2e7068faf55e
Browse files- README.md +129 -0
- build/webgpu/attention-rank4-tiled.wgsl.jinja +119 -0
- build/webgpu/attn-flash-decode-splitk-merge.wgsl.jinja +131 -0
- build/webgpu/attn-flash-decode-splitk.wgsl.jinja +549 -0
- build/webgpu/attn-flash-online.wgsl.jinja +367 -0
- build/webgpu/attn-flash-prefill-cluster.wgsl.jinja +554 -0
- build/webgpu/attn-flash-q32-broadcast.wgsl.jinja +246 -0
- build/webgpu/attn-online-scalar.wgsl.jinja +205 -0
- build/webgpu/bench.json +1565 -0
- build/webgpu/gqa-attention.wgsl.jinja +285 -0
- build/webgpu/gqa-present.wgsl.jinja +440 -0
- build/webgpu/gqa-qprep.wgsl.jinja +57 -0
- build/webgpu/manifest.json +0 -0
- build/webgpu/metadata.json +27 -0
- build/webgpu/test.json +0 -0
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
|
|
|