Xenova HF Staff commited on
Commit
641e8a1
·
verified ·
1 Parent(s): 78115a9

sync 6fdf6301e2bb

Browse files
README.md CHANGED
@@ -1,3 +1,121 @@
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.PackedMultiHeadAttention
10
+
11
+ `com.microsoft` · ONNX Runtime contrib operator · contrib since_version 1
12
+
13
+ ## Description
14
+
15
+ Multi-head self-attention over a padding-removed token stream. The `token_offset` and `cumulative_sequence_length` schedule maps every packed token to its sequence, and a token attends only the keys of that sequence. Query is either packed `[T, N, 3, H]` with key and value absent, or a separate `[T, hidden]` triple. The optional attention bias is indexed in PADDED coordinates. Float32 and float16 only; bfloat16 is not implemented.
16
+
17
+ See the [ONNX Runtime `PackedMultiHeadAttention` contrib-operator spec](https://github.com/microsoft/onnxruntime/blob/main/docs/ContribOperators.md#com.microsoft.PackedMultiHeadAttention) for the reference semantics.
18
+
19
+ ## Inputs
20
+
21
+ | Name | Upstream name | Logical dtype | WebGPU storage | Rank | Shape | Description | Presence |
22
+ | --- | --- | --- | --- | --- | --- | --- | --- |
23
+ | `queryT` | `query` | `T` | same as logical dtype | — | — | Packed queries: `[token_count, num_heads, 3, head_size]` with `key` and `value` absent, or `[token_count, hidden_size]` alongside both. | required |
24
+ | `keyT` | `key` | `T` | same as logical dtype | `2` | — | Keys with shape `[token_count, hidden_size]`, matching `query` exactly. Present only in the separate form. | optional |
25
+ | `valueT` | `value` | `T` | same as logical dtype | `2` | — | Values with shape `[token_count, v_hidden_size]`. Present only in the separate form. | optional |
26
+ | `biasT` | `bias` | `T` | same as logical dtype | `1` | — | Optional packed input-projection bias with shape `[hidden_size + hidden_size + v_hidden_size]`, added to Q, K and V. | optional |
27
+ | `tokenOffsetT` | `token_offset` | `M` | `int32` | `2` | — | Shape `[batch_size, sequence_length]`. The first `token_count` entries hold each packed token's flat index in the padded grid; the rest hold the padding positions. This tensor carries the padded `sequence_length`. | required |
28
+ | `cumulativeSequenceLengthT` | `cumulative_sequence_length` | `M` | `int32` | `1` | — | Exclusive prefix sums with shape `[batch_size + 1]`, starting at 0 and ending at `token_count`. Sequence `i` owns packed tokens `[cum[i], cum[i + 1])`. Outputs are unspecified for a malformed schedule. | required |
29
+ | `attentionBiasT` | `attention_bias` | `T` | same as logical dtype | `4` | — | Optional additive score bias with shape `[batch_size or 1, num_heads or 1, sequence_length, sequence_length]`. Its trailing axes are PADDED positions inside a sequence, not packed indices. | optional |
30
+
31
+ ## Outputs
32
+
33
+ | Name | Upstream name | Logical dtype | Rank | Shape | Description | Presence |
34
+ | --- | --- | --- | --- | --- | --- | --- |
35
+ | `outputT` | `output` | `T` | `2` | derived | Attention output with shape `[token_count, v_hidden_size]`, in packed token order. | required |
36
+
37
+ ## Attributes
38
+
39
+ Attributes and default values (overridable per request):
40
+
41
+ | Attribute | Default | Description |
42
+ | --- | --- | --- |
43
+ | `num_heads` | — | Number of attention heads. Required; the hidden size must be divisible by it. |
44
+ | `scale` | — | Optional score scale. Omission, and the value `0`, both select `1 / sqrt(head_size)`. |
45
+
46
+ ## Type constraints
47
+
48
+ | Variable | Allowed dtypes |
49
+ | --- | --- |
50
+ | `T` | `float32`, `float16` |
51
+ | `M` | `int32` |
52
+
53
+ ## Implementation variants
54
+
55
+ One implementation is selected per call from the device capabilities, the request shapes and the dtypes; these notes say what each one covers.
56
+
57
+ - `packed_flash` — Tiled attention with independent Q/K and V slices and subgroup dot reductions. Equal wide heads target four `vec4` fragments per lane, bounded by subgroup/workgroup limits and native staging capacity. Unequal tiles cap work by token count and yield to serial or wider portable clusters when reuse is limited.
58
+ - `packed_flash_nosg` — Portable tiled attention with independent query/key and value slices. Lanes cover the larger width, with guarded zero Q/K fragments and shared reductions. Device limits bound lane count and staging. Unequal-width tiles yield to serial attention below the query-reuse target.
59
+ - `packed_serial` — Portable serial-key attention: one workgroup owns one (packed token, head), walks that token's own sequence and reduces each dot through workgroup memory. Head dimensions need no alignment and the value head dimension may differ.
60
+ - `packed_attn_bias_flash` — Tiled attention with independent Q/K and V slices and subgroup dot reductions. Equal wide heads target four `vec4` fragments per lane, bounded by subgroup/workgroup limits and native staging capacity. Unequal tiles cap work by token count and yield to serial or wider portable clusters when reuse is limited.
61
+ - `packed_attn_bias_flash_nosg` — Portable tiled attention with independent query/key and value slices. Lanes cover the larger width, with guarded zero Q/K fragments and shared reductions. Device limits bound lane count and staging. Unequal-width tiles yield to serial attention below the query-reuse target.
62
+ - `packed_attn_bias_serial` — Portable serial-key attention: one workgroup owns one (packed token, head), walks that token's own sequence and reduces each dot through workgroup memory. Head dimensions need no alignment and the value head dimension may differ.
63
+ - `packed_bias_flash` — Tiled attention with independent Q/K and V slices and subgroup dot reductions. Equal wide heads target four `vec4` fragments per lane, bounded by subgroup/workgroup limits and native staging capacity. Unequal tiles cap work by token count and yield to serial or wider portable clusters when reuse is limited.
64
+ - `packed_bias_flash_nosg` — Portable tiled attention with independent query/key and value slices. Lanes cover the larger width, with guarded zero Q/K fragments and shared reductions. Device limits bound lane count and staging. Unequal-width tiles yield to serial attention below the query-reuse target.
65
+ - `packed_bias_serial` — Portable serial-key attention: one workgroup owns one (packed token, head), walks that token's own sequence and reduces each dot through workgroup memory. Head dimensions need no alignment and the value head dimension may differ.
66
+ - `packed_bias_attn_bias_flash` — Tiled attention with independent Q/K and V slices and subgroup dot reductions. Equal wide heads target four `vec4` fragments per lane, bounded by subgroup/workgroup limits and native staging capacity. Unequal tiles cap work by token count and yield to serial or wider portable clusters when reuse is limited.
67
+ - `packed_bias_attn_bias_flash_nosg` — Portable tiled attention with independent query/key and value slices. Lanes cover the larger width, with guarded zero Q/K fragments and shared reductions. Device limits bound lane count and staging. Unequal-width tiles yield to serial attention below the query-reuse target.
68
+ - `packed_bias_attn_bias_serial` — Portable serial-key attention: one workgroup owns one (packed token, head), walks that token's own sequence and reduces each dot through workgroup memory. Head dimensions need no alignment and the value head dimension may differ.
69
+ - `separate_flash` — Tiled attention with independent Q/K and V slices and subgroup dot reductions. Equal wide heads target four `vec4` fragments per lane, bounded by subgroup/workgroup limits and native staging capacity. Unequal tiles cap work by token count and yield to serial or wider portable clusters when reuse is limited.
70
+ - `separate_flash_nosg` — Portable tiled attention with independent query/key and value slices. Lanes cover the larger width, with guarded zero Q/K fragments and shared reductions. Device limits bound lane count and staging. Unequal-width tiles yield to serial attention below the query-reuse target.
71
+ - `separate_serial` — Portable serial-key attention: one workgroup owns one (packed token, head), walks that token's own sequence and reduces each dot through workgroup memory. Head dimensions need no alignment and the value head dimension may differ.
72
+ - `separate_attn_bias_flash` — Tiled attention with independent Q/K and V slices and subgroup dot reductions. Equal wide heads target four `vec4` fragments per lane, bounded by subgroup/workgroup limits and native staging capacity. Unequal tiles cap work by token count and yield to serial or wider portable clusters when reuse is limited.
73
+ - `separate_attn_bias_flash_nosg` — Portable tiled attention with independent query/key and value slices. Lanes cover the larger width, with guarded zero Q/K fragments and shared reductions. Device limits bound lane count and staging. Unequal-width tiles yield to serial attention below the query-reuse target.
74
+ - `separate_attn_bias_serial` — Portable serial-key attention: one workgroup owns one (packed token, head), walks that token's own sequence and reduces each dot through workgroup memory. Head dimensions need no alignment and the value head dimension may differ.
75
+ - `separate_bias_flash` — Tiled attention with independent Q/K and V slices and subgroup dot reductions. Equal wide heads target four `vec4` fragments per lane, bounded by subgroup/workgroup limits and native staging capacity. Unequal tiles cap work by token count and yield to serial or wider portable clusters when reuse is limited.
76
+ - `separate_bias_flash_nosg` — Portable tiled attention with independent query/key and value slices. Lanes cover the larger width, with guarded zero Q/K fragments and shared reductions. Device limits bound lane count and staging. Unequal-width tiles yield to serial attention below the query-reuse target.
77
+ - `separate_bias_serial` — Portable serial-key attention: one workgroup owns one (packed token, head), walks that token's own sequence and reduces each dot through workgroup memory. Head dimensions need no alignment and the value head dimension may differ.
78
+ - `separate_bias_attn_bias_flash` — Tiled attention with independent Q/K and V slices and subgroup dot reductions. Equal wide heads target four `vec4` fragments per lane, bounded by subgroup/workgroup limits and native staging capacity. Unequal tiles cap work by token count and yield to serial or wider portable clusters when reuse is limited.
79
+ - `separate_bias_attn_bias_flash_nosg` — Portable tiled attention with independent query/key and value slices. Lanes cover the larger width, with guarded zero Q/K fragments and shared reductions. Device limits bound lane count and staging. Unequal-width tiles yield to serial attention below the query-reuse target.
80
+ - `separate_bias_attn_bias_serial` — Portable serial-key attention: one workgroup owns one (packed token, head), walks that token's own sequence and reduces each dot through workgroup memory. Head dimensions need no alignment and the value head dimension may differ.
81
+
82
+ ## Device requirements
83
+
84
+ Some implementation variants require `subgroups`. These are route-specific capabilities, not package-wide requirements; availability also depends on the request shape and dtype.
85
+
86
+ ## Files
87
+
88
+ - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, per-variant templates, provenance)
89
+ - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
90
+ - [`test.json`](build/webgpu/test.json) — correctness cases
91
+ - [`bench.json`](build/webgpu/bench.json) — benchmark cases
92
+ - [`attn-packed-varlen-cluster.wgsl.jinja`](build/webgpu/attn-packed-varlen-cluster.wgsl.jinja)
93
+ - [`attn-packed-varlen-scalar.wgsl.jinja`](build/webgpu/attn-packed-varlen-scalar.wgsl.jinja)
94
+
95
+ ## Use with `@huggingface/kernels`
96
+
97
+ ```sh
98
+ npm install --save-exact @huggingface/kernels@0.0.1-preview.3
99
+ ```
100
+
101
+ Required output shapes and logical data types are inferred from the supplied inputs and attributes; result tensors are allocated automatically.
102
+
103
+ The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
104
+ It follows the `v1` branch as fixes land. To pin exact artifact bytes, pass a 40-character commit `revision` instead of `version`.
105
+
106
+ Replace each `*Data` placeholder with a typed array containing the corresponding input data.
107
+
108
+ ```js
109
+ import { getKernel } from "@huggingface/kernels";
110
+
111
+ const kernel = await getKernel("webgpu-kernels/com.microsoft.PackedMultiHeadAttention", { version: 1 });
112
+ const { outputT } = await kernel({
113
+ queryT: { data: queryTData, shape: [1, 16] },
114
+ keyT: { data: keyTData, shape: [1, 16] },
115
+ valueT: { data: valueTData, shape: [1, 20] },
116
+ tokenOffsetT: { data: tokenOffsetTData, shape: [1, 1] },
117
+ cumulativeSequenceLengthT: { data: cumulativeSequenceLengthTData, shape: [2] },
118
+ }, {
119
+ attrs: { num_heads: 1 },
120
+ });
121
+ ```
build/webgpu/attn-packed-varlen-cluster.wgsl.jinja ADDED
@@ -0,0 +1,432 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% set parallelSoftmax = distributedSoftmax is defined and distributedSoftmax and not useSubgroups %}
2
+ {% if useSubgroups %}
3
+ enable subgroups;
4
+ {% endif %}
5
+ {{ env.wgsl.resourceDeclarations }}
6
+ {% set QBUF = "query" %}
7
+ {% set KBUF = "query" if packedQkv else "key" %}
8
+ {% set VBUF = "query" if packedQkv else "value" %}
9
+ {% set BIAS_K_ROW = biasKOffset if biasKOffset is defined else 0 %}
10
+ {% set BIAS_V_ROW = biasVOffset if biasVOffset is defined else 0 %}
11
+ {% set ST = "f16" if packedStageBytes == 2 else "f32" %}
12
+ {% set SLICE_COUNT = (((headDimV4 + LPQ - 1) / LPQ) | int) %}
13
+ {% set queryTail = headDimV4 % LPQ != 0 %}
14
+ {% set VALUE_DIM_V4 = ((vHeadDim / 4) | int) %}
15
+ {% set VALUE_SLICES = (((VALUE_DIM_V4 + LPQ - 1) / LPQ) | int) %}
16
+ {% set unequalValue = vHeadDim != headDim %}
17
+ {% set valueTail = VALUE_DIM_V4 % LPQ != 0 %}
18
+ {% set valueWidth = "VALUE_HEAD_DIM_V4" if unequalValue else "HEAD_DIM_V4" %}
19
+ {% set valueLane = "valueLaneSlice" if unequalValue else "laneSlice" %}
20
+
21
+ // Tiled flash attention over a padding-removed packed token stream. One
22
+ // workgroup covers TILE_Q consecutive packed tokens for one head, with an
23
+ // LPQ-lane cluster per token; K and V are staged in workgroup memory once per
24
+ // TILE_K keys and reused by every token in the tile. Each lane owns contiguous
25
+ // query and output vec4 slices, sized independently for the Q/K and V widths.
26
+ // Dot products, online softmax and weighted value sums accumulate in f32; output
27
+ // conversion happens at the store. The staging type above governs biased K/V.
28
+
29
+ // A packed tile can straddle a sequence boundary, so the key loop runs over the
30
+ // workgroup's union range [wgStart, wgEnd) — the first token's sequence start to
31
+ // the last token's sequence end — so the trip count is uniform, and each token
32
+ // masks the keys outside its own sequence.
33
+
34
+ {% if packedQkv %}
35
+ // Source: one packed `[T, N, 3, H]` buffer; Q, K and V differ only by offset.
36
+ {% else %}
37
+ // Source: separate token-major `[T, hidden]` query, key and value buffers.
38
+ {% endif %}
39
+ {% if hasBias %}
40
+
41
+ // Packed `[Q; K; V]` bias rows: the Q bias enters the query registers, and the
42
+ // K and V biases fold into the staged tiles, so a bias element is added once per
43
+ // tile rather than once per (token, key).
44
+ {% endif %}
45
+ {% if hasAttnBias %}
46
+
47
+ // The additive attention bias lives in PADDED coordinates: its trailing two axes
48
+ // are indexed by the query's and the key's position inside their shared
49
+ // sequence, not by their packed indices. A zero broadcast stride collapses the
50
+ // batch or head axis.
51
+ {% endif %}
52
+
53
+ {% if useSubgroups %}
54
+ // The per-lane dot fragments combine through a log2(LPQ) subgroupShuffleXor
55
+ // butterfly inside each cluster.
56
+ {% else %}
57
+ // The per-lane dot fragments combine through workgroup memory; this form
58
+ // requires no subgroup feature.
59
+ {% endif %}
60
+ const HEAD_DIM: u32 = {{ headDim }}u;
61
+ const HEAD_DIM_V4: u32 = {{ headDimV4 }}u;
62
+ {% if unequalValue %}
63
+ const VALUE_HEAD_DIM_V4: u32 = {{ VALUE_DIM_V4 }}u;
64
+ {% if hasBias %}
65
+ const VALUE_HEAD_DIM: u32 = {{ vHeadDim }}u;
66
+ {% endif %}
67
+ {% endif %}
68
+ const Q_HEADS: u32 = {{ qNumHeads }}u;
69
+ const Q_ROW_V4: u32 = {{ qRowStrideV4 }}u;
70
+ const Q_HEAD_V4: u32 = {{ qHeadStrideV4 }}u;
71
+ const K_ROW_V4: u32 = {{ kRowStrideV4 }}u;
72
+ const K_HEAD_V4: u32 = {{ kHeadStrideV4 }}u;
73
+ const K_OFFSET_V4: u32 = {{ kOffsetV4 }}u;
74
+ const V_ROW_V4: u32 = {{ vRowStrideV4 }}u;
75
+ const V_HEAD_V4: u32 = {{ vHeadStrideV4 }}u;
76
+ const V_OFFSET_V4: u32 = {{ vOffsetV4 }}u;
77
+ const O_ROW_V4: u32 = {{ oRowStrideV4 }}u;
78
+ const O_HEAD_V4: u32 = {{ oHeadStrideV4 }}u;
79
+ {% if hasBias %}
80
+ const BIAS_K: u32 = {{ BIAS_K_ROW }}u;
81
+ const BIAS_V: u32 = {{ BIAS_V_ROW }}u;
82
+ {% endif %}
83
+ const TILE_Q: u32 = {{ TILE_Q }}u;
84
+ const TILE_K: u32 = {{ TILE_K }}u;
85
+ // Lanes per token cluster, sized by the manifest for query and value ownership.
86
+ const LPQ: u32 = {{ LPQ }}u;
87
+ const SLICE: u32 = {% if queryTail %}{{ SLICE_COUNT }}u{% else %}HEAD_DIM_V4 / LPQ{% endif %}; // query vec4s per lane slice
88
+ const WG: u32 = TILE_Q * LPQ;
89
+ const NEG_INF: f32 = -3.4028234663852886e38;
90
+
91
+ var<workgroup> k_tile: array<vec4<{{ ST }}>, TILE_K * HEAD_DIM_V4>;
92
+ var<workgroup> v_tile: array<vec4<{{ ST }}>, TILE_K * {{ valueWidth }}>;
93
+ {% if not useSubgroups %}
94
+ // Reuse scratch for dot fragments, maxima, sums and probabilities when the
95
+ // manifest admits the full tile; otherwise retain one key's reduction slots.
96
+ var<workgroup> red: array<f32, {% if parallelSoftmax %}{{ TILE_Q * (LPQ * TILE_K if LPQ * TILE_K > 2 * LPQ + TILE_K else 2 * LPQ + TILE_K) }}{% else %}WG{% endif %}>;
97
+ {% endif %}
98
+
99
+ {% set ATTN_SCALE_DIM = "HEAD_DIM" %}fn scale_value() -> f32 {
100
+ if (params.scale != 0.0) { return params.scale; }
101
+ return inverseSqrt(f32({{ ATTN_SCALE_DIM }}));
102
+ }
103
+ // Packed-token metadata. `token_offset` records, for every packed token, its
104
+ // flat index in the padded `[batch, sequence]` grid, so the sequence index and
105
+ // the position inside that sequence are one load and two integer operations.
106
+ // Packing keeps a sequence's tokens contiguous and in ascending position order,
107
+ // so the packed index of a sequence's first token is `token - position` and the
108
+ // sequence's packed key range is [keyStart, keyEnd).
109
+ struct PackedToken {
110
+ sequence: u32,
111
+ position: u32,
112
+ keyStart: u32,
113
+ keyEnd: u32,
114
+ }
115
+
116
+ fn packed_token_of(token: u32, seqPad: u32) -> PackedToken {
117
+ let flat = u32(token_offset[token]);
118
+ var info: PackedToken;
119
+ info.sequence = flat / seqPad;
120
+ info.position = flat - info.sequence * seqPad;
121
+ info.keyStart = token - info.position;
122
+ info.keyEnd = u32(cumulative_sequence_length[info.sequence + 1u]);
123
+ return info;
124
+ }
125
+ {% if hasBias %}
126
+
127
+ {% set biasScalarWiden = usesF16 %}
128
+ {% set BW = "f32(" if biasScalarWiden is defined and biasScalarWiden else "" %}
129
+ {% set BC = ")" if biasScalarWiden is defined and biasScalarWiden else "" %}
130
+ fn load_bias4(base: u32, d4: u32) -> vec4<f32> {
131
+ let offset = base + d4 * 4u;
132
+ return vec4<f32>({{ BW }}bias[offset]{{ BC }}, {{ BW }}bias[offset + 1u]{{ BC }}, {{ BW }}bias[offset + 2u]{{ BC }}, {{ BW }}bias[offset + 3u]{{ BC }});
133
+ }
134
+ {% endif %}
135
+
136
+ {% macro packed_dot_fragment() %}
137
+ var part: f32 = 0.0;
138
+ let kb = kk * HEAD_DIM_V4 + laneSlice;
139
+ {% for c in range(SLICE_COUNT) %}
140
+ {% if queryTail %}
141
+ if (laneSlice + {{ c }}u < HEAD_DIM_V4) {
142
+ {% endif %}
143
+ part = part + dot(qr{{ c }}, vec4<f32>(k_tile[kb + {{ c }}u]));
144
+ {% if queryTail %}
145
+ }
146
+ {% endif %}
147
+ {% endfor %}
148
+ {% endmacro %}
149
+ {% macro packed_value_accumulate() %}
150
+ let vRow = kk * {{ valueWidth }} + {{ valueLane }};
151
+ {% for c in range(VALUE_SLICES) %}
152
+ {% if valueTail %}
153
+ if ({{ valueLane }} + {{ c }}u < {{ valueWidth }}) {
154
+ {% endif %}
155
+ o{{ c }} = o{{ c }} + p * vec4<f32>(v_tile[vRow + {{ c }}u]);
156
+ {% if valueTail %}
157
+ }
158
+ {% endif %}
159
+ {% endfor %}
160
+ {% endmacro %}
161
+ {% macro packed_score_write(score_index = "kk") %}
162
+ if (kj >= minKj && kj < maxKj) {
163
+ {% if hasAttnBias %}
164
+ let maskIndex = info.sequence * params.maskBatchStride + h * params.maskHeadStride
165
+ + info.position * params.maskSeqStride + (kj - minKj);
166
+ s[{{ score_index }}] = part * SCALE + f32(attn_bias[maskIndex]);
167
+ {% else %}
168
+ s[{{ score_index }}] = part * SCALE;
169
+ {% endif %}
170
+ }
171
+ {% endmacro %}
172
+
173
+ @compute @workgroup_size(WG, 1, 1)
174
+ fn main(
175
+ @builtin(workgroup_id) wg: vec3<u32>,
176
+ @builtin(local_invocation_id) lid: vec3<u32>{% if useSubgroups %},
177
+ @builtin(subgroup_size) sgSize: u32{% endif %}
178
+ ) {
179
+ let h = wg.y;
180
+ // The query-tile axis is folded over x and z with a baked stride; z is 1
181
+ // whenever the tile count fits one dimension, so the fold term vanishes there.
182
+ let tile = wg.x + wg.z * {{ DISPATCH_FOLD_WIDTH }}u;
183
+ let firstToken = tile * TILE_Q;
184
+ if (firstToken >= params.tokenCount || h >= Q_HEADS) {
185
+ return;
186
+ }
187
+ {% if useSubgroups %}
188
+ // The subgroupShuffleXor butterfly requires every LPQ-lane cluster to fit in,
189
+ // and align with, one subgroup. Keep a uniform guard for defensive portability.
190
+ if (sgSize < LPQ || sgSize % LPQ != 0u) { return; }
191
+ {% endif %}
192
+
193
+ let tid = lid.x;
194
+ let qSub = tid / LPQ;
195
+ let lane = tid % LPQ;
196
+ let qIdx = firstToken + qSub;
197
+ let qValid = qIdx < params.tokenCount;
198
+ let qClamped = min(qIdx, params.tokenCount - 1u);
199
+ let info = packed_token_of(qClamped, params.seqPad);
200
+ let minKj = info.keyStart;
201
+ let maxKj = info.keyEnd;
202
+
203
+ // Union key range for the whole tile: packed tokens are ordered, so the first
204
+ // token's sequence start and the last token's sequence end bound every token
205
+ // in the tile.
206
+ let lastToken = min(firstToken + TILE_Q - 1u, params.tokenCount - 1u);
207
+ let wgStart = packed_token_of(firstToken, params.seqPad).keyStart;
208
+ let wgEnd = packed_token_of(lastToken, params.seqPad).keyEnd;
209
+
210
+ let SCALE = scale_value();
211
+ let laneSlice = lane * SLICE;
212
+ {% if unequalValue %}
213
+ let valueLaneSlice = lane * {{ VALUE_SLICES }}u;
214
+ {% endif %}
215
+ let qBase4 = qClamped * Q_ROW_V4 + h * Q_HEAD_V4 + laneSlice;
216
+ {% for c in range(SLICE_COUNT) %}
217
+ {% if queryTail %}
218
+ var qr{{ c }} = vec4<f32>(0.0);
219
+ if (laneSlice + {{ c }}u < HEAD_DIM_V4) {
220
+ qr{{ c }} = vec4<f32>({{ QBUF }}[qBase4 + {{ c }}u]);
221
+ {% if hasBias %}
222
+ qr{{ c }} = qr{{ c }} + load_bias4(h * HEAD_DIM, laneSlice + {{ c }}u);
223
+ {% endif %}
224
+ }
225
+ {% else %}
226
+ var qr{{ c }} = vec4<f32>({{ QBUF }}[qBase4 + {{ c }}u]);
227
+ {% if hasBias %}
228
+ qr{{ c }} = qr{{ c }} + load_bias4(h * HEAD_DIM, laneSlice + {{ c }}u);
229
+ {% endif %}
230
+ {% endif %}
231
+ {% if not unequalValue %}
232
+ var o{{ c }} = vec4<f32>(0.0);
233
+ {% endif %}
234
+ {% endfor %}
235
+ {% if unequalValue %}
236
+ {% for c in range(VALUE_SLICES) %}
237
+ var o{{ c }} = vec4<f32>(0.0);
238
+ {% endfor %}
239
+ {% endif %}
240
+
241
+ var m: f32 = NEG_INF;
242
+ var l: f32 = 0.0;
243
+
244
+ var kStart: u32 = wgStart;
245
+ loop {
246
+ if (kStart >= wgEnd) { break; }
247
+
248
+ // Cooperative K/V tile load with out-of-range keys zero-filled. The K and V
249
+ // biases are token-independent, so they fold in here, once per staged word.
250
+ workgroupBarrier();
251
+ {% if unequalValue %}
252
+ {% for tile, buffer, rowStride, headStride, offset, width, biasRow, biasWidth in [
253
+ ["k_tile", KBUF, "K_ROW_V4", "K_HEAD_V4", "K_OFFSET_V4", "HEAD_DIM_V4", "BIAS_K", "HEAD_DIM"],
254
+ ["v_tile", VBUF, "V_ROW_V4", "V_HEAD_V4", "V_OFFSET_V4", "VALUE_HEAD_DIM_V4", "BIAS_V", "VALUE_HEAD_DIM"]
255
+ ] %}
256
+ for (var i: u32 = tid; i < TILE_K * {{ width }}; i = i + WG) {
257
+ let slot = i / {{ width }};
258
+ let d4 = i % {{ width }};
259
+ let kj = kStart + slot;
260
+ if (kj < wgEnd) {
261
+ var word = vec4<f32>({{ buffer }}[kj * {{ rowStride }} + h * {{ headStride }} + {{ offset }} + d4]);
262
+ {% if hasBias %}
263
+ word = word + load_bias4({{ biasRow }} + h * {{ biasWidth }}, d4);
264
+ {% endif %}
265
+ {{ tile }}[i] = vec4<{{ ST }}>(word);
266
+ } else {
267
+ {{ tile }}[i] = vec4<{{ ST }}>(0.0);
268
+ }
269
+ }
270
+ {% endfor %}
271
+ {% else %}
272
+ for (var i: u32 = tid; i < TILE_K * HEAD_DIM_V4; i = i + WG) {
273
+ let slot = i / HEAD_DIM_V4;
274
+ let d4 = i % HEAD_DIM_V4;
275
+ let kj = kStart + slot;
276
+ if (kj < wgEnd) {
277
+ var kWord = vec4<f32>({{ KBUF }}[kj * K_ROW_V4 + h * K_HEAD_V4 + K_OFFSET_V4 + d4]);
278
+ var vWord = vec4<f32>({{ VBUF }}[kj * V_ROW_V4 + h * V_HEAD_V4 + V_OFFSET_V4 + d4]);
279
+ {% if hasBias %}
280
+ kWord = kWord + load_bias4(BIAS_K + h * HEAD_DIM, d4);
281
+ vWord = vWord + load_bias4(BIAS_V + h * HEAD_DIM, d4);
282
+ {% endif %}
283
+ k_tile[i] = vec4<{{ ST }}>(kWord);
284
+ v_tile[i] = vec4<{{ ST }}>(vWord);
285
+ } else {
286
+ k_tile[i] = vec4<{{ ST }}>(0.0);
287
+ v_tile[i] = vec4<{{ ST }}>(0.0);
288
+ }
289
+ }
290
+ {% endif %}
291
+ workgroupBarrier();
292
+
293
+ {% if parallelSoftmax %}
294
+ // Each cluster lane owns disjoint key scores and exponentials. The dot
295
+ // fragments, maxima, sums and probabilities reuse one shared allocation.
296
+ var s: array<f32, {{ ((TILE_K + LPQ - 1) / LPQ) | int }}>;
297
+ for (var kk: u32 = 0u; kk < TILE_K; kk = kk + 1u) {
298
+ {{ packed_dot_fragment() }}
299
+ red[kk * WG + tid] = part;
300
+ }
301
+ workgroupBarrier();
302
+ var localMax = NEG_INF;
303
+ for (var kk: u32 = lane; kk < TILE_K; kk = kk + LPQ) {
304
+ let kj = kStart + kk;
305
+ let key_slot = kk / LPQ;
306
+ s[key_slot] = NEG_INF;
307
+ var part: f32 = 0.0;
308
+ let cbase = kk * WG + tid - lane;
309
+ for (var j: u32 = 0u; j < LPQ; j = j + 1u) {
310
+ part = part + red[cbase + j];
311
+ }
312
+ {{ packed_score_write("key_slot") }}
313
+ localMax = max(localMax, s[key_slot]);
314
+ }
315
+ // All fragment readers finish before their storage is reused for maxima.
316
+ workgroupBarrier();
317
+ red[tid] = localMax;
318
+ workgroupBarrier();
319
+ let cbase = tid - lane;
320
+ // Keyless lanes contribute only NEG_INF or zero to max/sum. Dot fragments
321
+ // above still consume every lane, including when TILE_K is smaller than LPQ.
322
+ var tileMax = NEG_INF;
323
+ for (var j: u32 = 0u; j < {{ "LPQ" if TILE_K >= LPQ else "TILE_K" }}; j = j + 1u) {
324
+ tileMax = max(tileMax, red[cbase + j]);
325
+ }
326
+ let newMax = max(m, tileMax);
327
+ let corr = select(exp(m - newMax), 0.0, m == NEG_INF);
328
+ var localSum: f32 = 0.0;
329
+ for (var kk: u32 = lane; kk < TILE_K; kk = kk + LPQ) {
330
+ let score = s[kk / LPQ];
331
+ let pk = select(0.0, exp(score - newMax), score != NEG_INF);
332
+ red[2u * WG + qSub * TILE_K + kk] = pk;
333
+ localSum = localSum + pk;
334
+ }
335
+ // Sum slots are disjoint from the maxima still being consumed above.
336
+ red[WG + tid] = localSum;
337
+ workgroupBarrier();
338
+ var pSum: f32 = 0.0;
339
+ for (var j: u32 = 0u; j < {{ "LPQ" if TILE_K >= LPQ else "TILE_K" }}; j = j + 1u) {
340
+ pSum = pSum + red[WG + cbase + j];
341
+ }
342
+ l = l * corr + pSum;
343
+ m = newMax;
344
+ {% else %}
345
+ var s: array<f32, TILE_K>;
346
+ for (var kk: u32 = 0u; kk < TILE_K; kk = kk + 1u) {
347
+ let kj = kStart + kk;
348
+ s[kk] = NEG_INF;
349
+ {{ packed_dot_fragment() }}
350
+ {% if useSubgroups %}
351
+ // log2(LPQ) butterfly all-reduce inside the cluster: every lane ends with
352
+ // the full dot.
353
+ part = part + subgroupShuffleXor(part, 1u);
354
+ {% for shift in [2, 4, 8, 16, 32] %}
355
+ {% if LPQ > shift %}
356
+ part = part + subgroupShuffleXor(part, {{ shift }}u);
357
+ {% endif %}
358
+ {% endfor %}
359
+ {% else %}
360
+ red[tid] = part;
361
+ workgroupBarrier();
362
+ let cbase = tid - lane;
363
+ part = 0.0;
364
+ for (var j: u32 = 0u; j < LPQ; j = j + 1u) {
365
+ part = part + red[cbase + j];
366
+ }
367
+ workgroupBarrier();
368
+ {% endif %}
369
+ {{ packed_score_write() }}
370
+ }
371
+
372
+ // Per-lane online softmax over the tile. s[kk] is reused to hold the
373
+ // exponentiated probabilities for the value accumulation below.
374
+ var tileMax: f32 = s[0];
375
+ for (var kk: u32 = 1u; kk < TILE_K; kk = kk + 1u) {
376
+ tileMax = max(tileMax, s[kk]);
377
+ }
378
+ let newMax = max(m, tileMax);
379
+ let corr = select(exp(m - newMax), 0.0, m == NEG_INF);
380
+ var pSum: f32 = 0.0;
381
+ for (var kk: u32 = 0u; kk < TILE_K; kk = kk + 1u) {
382
+ let pk = select(0.0, exp(s[kk] - newMax), s[kk] != NEG_INF);
383
+ s[kk] = pk;
384
+ pSum = pSum + pk;
385
+ }
386
+ l = l * corr + pSum;
387
+ m = newMax;
388
+
389
+ {% endif %}
390
+
391
+ // A boundary tile addresses V rows outside this token's sequence. Because
392
+ // 0 * NaN is NaN, the guarded loop skips the V read entirely for a key with
393
+ // no probability mass; an interior tile runs the unguarded FMA chain. One
394
+ // range test covers the whole lane slice, and one probability load feeds
395
+ // every vec4 of it.
396
+ let tileInterior = kStart >= minKj && kStart + TILE_K <= maxKj;
397
+ {% for c in range(VALUE_SLICES) %}
398
+ o{{ c }} = o{{ c }} * corr;
399
+ {% endfor %}
400
+ if (tileInterior) {
401
+ for (var kk: u32 = 0u; kk < TILE_K; kk = kk + 1u) {
402
+ let p = {% if parallelSoftmax %}red[2u * WG + qSub * TILE_K + kk]{% else %}s[kk]{% endif %};
403
+ {{ packed_value_accumulate() }}
404
+ }
405
+ } else {
406
+ for (var kk: u32 = 0u; kk < TILE_K; kk = kk + 1u) {
407
+ let p = {% if parallelSoftmax %}red[2u * WG + qSub * TILE_K + kk]{% else %}s[kk]{% endif %};
408
+ if (p != 0.0) {
409
+ {{ packed_value_accumulate() }}
410
+ }
411
+ }
412
+ }
413
+
414
+ kStart = kStart + TILE_K;
415
+ }
416
+
417
+ if (qValid) {
418
+ // l == 0 means this token had no probability-bearing key. Emit 0 rather
419
+ // than 0/0.
420
+ let inv = select(0.0, 1.0 / l, l > 0.0);
421
+ let outBase4 = qIdx * O_ROW_V4 + h * O_HEAD_V4 + {{ valueLane }};
422
+ {% for c in range(VALUE_SLICES) %}
423
+ {% if valueTail %}
424
+ if ({{ valueLane }} + {{ c }}u < {{ valueWidth }}) {
425
+ {% endif %}
426
+ output[outBase4 + {{ c }}u] = vec4<{{ scalar }}>(o{{ c }} * inv);
427
+ {% if valueTail %}
428
+ }
429
+ {% endif %}
430
+ {% endfor %}
431
+ }
432
+ }
build/webgpu/attn-packed-varlen-scalar.wgsl.jinja ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ env.wgsl.resourceDeclarations }}
2
+ {% set QBUF = "query" %}
3
+ {% set KBUF = "query" if packedQkv else "key" %}
4
+ {% set VBUF = "query" if packedQkv else "value" %}
5
+ {% set BIAS_K_ROW = biasKOffset if biasKOffset is defined else 0 %}
6
+ {% set BIAS_V_ROW = biasVOffset if biasVOffset is defined else 0 %}
7
+
8
+ // Online-softmax attention over a padding-removed packed token stream, with no
9
+ // feature requirements. One workgroup owns one (packed token, query head): the
10
+ // workgroup walks its own sequence's keys serially, cooperates on each q·k dot
11
+ // through a shared-memory tree reduction, and applies the online rescale to a
12
+ // running value accumulator per key. Head dimensions need no alignment and the
13
+ // V head dimension may differ from the Q/K head dimension.
14
+ {% if packedQkv %}
15
+ // Source: one packed `[T, N, 3, H]` buffer; Q, K and V differ only by offset.
16
+ {% else %}
17
+ // Source: separate token-major `[T, hidden]` query, key and value buffers.
18
+ {% endif %}
19
+ {% if hasBias %}
20
+ // Packed `[Q; K; V]` bias rows are added to Q, K and V during the key walk,
21
+ // which is where the reference adds them.
22
+ {% endif %}
23
+ {% if hasAttnBias %}
24
+ // The additive attention bias lives in PADDED coordinates: its trailing two
25
+ // axes are indexed by the query's and the key's position inside their shared
26
+ // sequence, not by their packed indices. A zero broadcast stride collapses the
27
+ // batch or head axis.
28
+ {% endif %}
29
+ const HEAD_DIM: u32 = {{ headDim }}u;
30
+ const V_HEAD_DIM: u32 = {{ vHeadDim }}u;
31
+ const Q_HEADS: u32 = {{ qNumHeads }}u;
32
+ const Q_ROW: u32 = {{ qRowStride }}u;
33
+ const Q_HEAD: u32 = {{ qHeadStride }}u;
34
+ const K_ROW: u32 = {{ kRowStride }}u;
35
+ const K_HEAD: u32 = {{ kHeadStride }}u;
36
+ const K_OFFSET: u32 = {{ kOffset }}u;
37
+ const V_ROW: u32 = {{ vRowStride }}u;
38
+ const V_HEAD: u32 = {{ vHeadStride }}u;
39
+ const V_OFFSET: u32 = {{ vOffset }}u;
40
+ const O_ROW: u32 = {{ oRowStride }}u;
41
+ const O_HEAD: u32 = {{ oHeadStride }}u;
42
+ {% if hasBias %}
43
+ const BIAS_K: u32 = {{ BIAS_K_ROW }}u;
44
+ const BIAS_V: u32 = {{ BIAS_V_ROW }}u;
45
+ {% endif %}
46
+ const WG: u32 = {{ workgroupSize }}u;
47
+
48
+ var<workgroup> partial: array<f32, WG>;
49
+ var<workgroup> running_max: f32;
50
+ var<workgroup> running_denom: f32;
51
+ var<workgroup> running_out: array<f32, V_HEAD_DIM>;
52
+ var<workgroup> previous_scale: f32;
53
+ {% macro wgsl_tree_reduce_f32(name, mode, buffer="partial", wg="WG", trailingBarrier=true, valueType="f32") %}
54
+ fn {{ name }}(value: {{ valueType }}, tid: u32) -> {{ valueType }} {
55
+ {{ buffer }}[tid] = value;
56
+ workgroupBarrier();
57
+ // Ceil-halving keeps every lane when the workgroup size is not a power of
58
+ // two. For even n this matches the power-of-two tree order; for odd n, lanes
59
+ // [0, n-half) fold the upper tail while the middle lane carries forward.
60
+ var n: u32 = {{ wg }};
61
+ loop {
62
+ let half = (n + 1u) / 2u;
63
+ if (tid < n - half) {
64
+ {{ buffer }}[tid] = {{ buffer }}[tid] + {{ buffer }}[tid + half];
65
+ }
66
+ workgroupBarrier();
67
+ n = half;
68
+ if (n == 1u) {
69
+ break;
70
+ }
71
+ }
72
+ // The default trailing barrier makes this helper safe for back-to-back calls: every lane reads
73
+ // slot 0 here, so the next call's first store must not run until all lanes have read it.
74
+ // `trailingBarrier=false` is safe only when the buffer is never written again before kernel exit.
75
+ let reduced = {{ buffer }}[0];
76
+ workgroupBarrier();
77
+ return reduced;
78
+ }
79
+ {% endmacro %}
80
+ {{ wgsl_tree_reduce_f32("reduce_sum", "add", "partial", "WG") }}
81
+ {% set ATTN_SCALE_DIM = "HEAD_DIM" %}fn scale_value() -> f32 {
82
+ if (params.scale != 0.0) { return params.scale; }
83
+ return inverseSqrt(f32({{ ATTN_SCALE_DIM }}));
84
+ }
85
+ // Packed-token metadata. `token_offset` records, for every packed token, its
86
+ // flat index in the padded `[batch, sequence]` grid, so the sequence index and
87
+ // the position inside that sequence are one load and two integer operations.
88
+ // Packing keeps a sequence's tokens contiguous and in ascending position order,
89
+ // so the packed index of a sequence's first token is `token - position` and the
90
+ // sequence's packed key range is [keyStart, keyEnd).
91
+ struct PackedToken {
92
+ sequence: u32,
93
+ position: u32,
94
+ keyStart: u32,
95
+ keyEnd: u32,
96
+ }
97
+
98
+ fn packed_token_of(token: u32, seqPad: u32) -> PackedToken {
99
+ let flat = u32(token_offset[token]);
100
+ var info: PackedToken;
101
+ info.sequence = flat / seqPad;
102
+ info.position = flat - info.sequence * seqPad;
103
+ info.keyStart = token - info.position;
104
+ info.keyEnd = u32(cumulative_sequence_length[info.sequence + 1u]);
105
+ return info;
106
+ }
107
+
108
+ @compute @workgroup_size(WG, 1, 1)
109
+ fn main(
110
+ @builtin(workgroup_id) wg: vec3<u32>,
111
+ @builtin(local_invocation_id) lid: vec3<u32>
112
+ ) {
113
+ // The token axis is folded over x and z with a baked stride; z is 1 whenever
114
+ // the token count fits one dimension, so the fold term vanishes there.
115
+ let token = wg.x + wg.z * {{ DISPATCH_FOLD_WIDTH }}u;
116
+ let h = wg.y;
117
+ if (token >= params.tokenCount || h >= Q_HEADS) {
118
+ return;
119
+ }
120
+
121
+ let tid = lid.x;
122
+ let info = packed_token_of(token, params.seqPad);
123
+ let qBase = token * Q_ROW + h * Q_HEAD;
124
+ let oBase = token * O_ROW + h * O_HEAD;
125
+
126
+ if (tid == 0u) {
127
+ running_max = -3.4028234663852886e38;
128
+ running_denom = 0.0;
129
+ }
130
+ for (var d: u32 = tid; d < V_HEAD_DIM; d = d + WG) {
131
+ running_out[d] = 0.0;
132
+ }
133
+ workgroupBarrier();
134
+
135
+ for (var keyToken: u32 = info.keyStart; keyToken < info.keyEnd; keyToken = keyToken + 1u) {
136
+ let kBase = keyToken * K_ROW + h * K_HEAD + K_OFFSET;
137
+ var partial_dot = 0.0;
138
+ for (var d: u32 = tid; d < HEAD_DIM; d = d + WG) {
139
+ var q_value = f32({{ QBUF }}[qBase + d]);
140
+ var k_value = f32({{ KBUF }}[kBase + d]);
141
+ {% if hasBias %}
142
+ let channel = h * HEAD_DIM + d;
143
+ q_value = q_value + f32(bias[channel]);
144
+ k_value = k_value + f32(bias[BIAS_K + channel]);
145
+ {% endif %}
146
+ partial_dot = partial_dot + q_value * k_value;
147
+ }
148
+
149
+ // reduce_sum returns the same partial[0] to every lane, so `score` is
150
+ // already workgroup-uniform; tid==0 advances the running max/denominator
151
+ // (broadcast through shared memory by the barrier below).
152
+ {% if hasAttnBias %}
153
+ let maskIndex = info.sequence * params.maskBatchStride + h * params.maskHeadStride
154
+ + info.position * params.maskSeqStride + (keyToken - info.keyStart);
155
+ let score = reduce_sum(partial_dot, tid) * scale_value() + f32(attn_bias[maskIndex]);
156
+ {% else %}
157
+ let score = reduce_sum(partial_dot, tid) * scale_value();
158
+ {% endif %}
159
+ if (tid == 0u) {
160
+ let next_max = max(running_max, score);
161
+ let old_factor = exp(running_max - next_max);
162
+ let new_factor = exp(score - next_max);
163
+ previous_scale = old_factor;
164
+ running_max = next_max;
165
+ running_denom = running_denom * old_factor + new_factor;
166
+ }
167
+ workgroupBarrier();
168
+
169
+ let probability_numerator = exp(score - running_max);
170
+ let vBase = keyToken * V_ROW + h * V_HEAD + V_OFFSET;
171
+ for (var d: u32 = tid; d < V_HEAD_DIM; d = d + WG) {
172
+ var v_value = f32({{ VBUF }}[vBase + d]);
173
+ {% if hasBias %}
174
+ v_value = v_value + f32(bias[BIAS_V + h * V_HEAD_DIM + d]);
175
+ {% endif %}
176
+ running_out[d] = running_out[d] * previous_scale + probability_numerator * v_value;
177
+ }
178
+ workgroupBarrier();
179
+ }
180
+
181
+ // A token whose sequence carries no key leaves running_denom at zero; emit a
182
+ // zero row rather than 0 * (1/0) = NaN.
183
+ let inv_denom = select(0.0, 1.0 / running_denom, running_denom > 0.0);
184
+ for (var d: u32 = tid; d < V_HEAD_DIM; d = d + WG) {
185
+ output[oBase + d] = {{ outputScalar }}(running_out[d] * inv_denom);
186
+ }
187
+ }
build/webgpu/bench.json ADDED
The diff for this file is too large to render. See raw diff
 
build/webgpu/manifest.json ADDED
@@ -0,0 +1,667 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "domain": "com.microsoft",
3
+ "name": "PackedMultiHeadAttention",
4
+ "sinceVersion": 1,
5
+ "inputs": {
6
+ "queryT": { "onnx": "query", "dtype": "T" },
7
+ "keyT": { "onnx": "key", "dtype": "T", "rank": 2, "optional": true },
8
+ "valueT": { "onnx": "value", "dtype": "T", "rank": 2, "optional": true },
9
+ "biasT": { "onnx": "bias", "dtype": "T", "rank": 1, "optional": true },
10
+ "tokenOffsetT": { "onnx": "token_offset", "dtype": "M", "rank": 2, "storage": "int32" },
11
+ "cumulativeSequenceLengthT": { "onnx": "cumulative_sequence_length", "dtype": "M", "rank": 1, "storage": "int32" },
12
+ "attentionBiasT": { "onnx": "attention_bias", "dtype": "T", "rank": 4, "optional": true }
13
+ },
14
+ "outputs": { "outputT": { "onnx": "output", "dtype": "T", "rank": 2, "shape": "[tokenCount, vHiddenSize]" } },
15
+ "attributes": { "num_heads": {}, "scale": {} },
16
+ "attributeConstraints": { "num_heads": { "required": true } },
17
+ "typeConstraints": { "T": ["float32", "float16"], "M": ["int32"] },
18
+ "tunables": { "CLUSTER_TILE_K": { "default": 16 }, "SCALAR_WORKGROUP_SIZE": { "default": 64 } },
19
+ "derive": {
20
+ "numHeads": "attrs.num_heads",
21
+ "qNumHeads": "numHeads",
22
+ "packedQkv": "ranks.queryT == 4",
23
+ "tokenCount": "dim(shapes.queryT, 0)",
24
+ "batchSize": "dim(shapes.tokenOffsetT, 0) if ranks.tokenOffsetT == 2 else 0",
25
+ "seqPad": "dim(shapes.tokenOffsetT, 1) if ranks.tokenOffsetT == 2 else 0",
26
+ "packedHeadDim": "dim(shapes.queryT, 3) if packedQkv else 0",
27
+ "hiddenSize": "numHeads * packedHeadDim if packedQkv else (dim(shapes.queryT, 1) if ranks.queryT == 2 else 0)",
28
+ "vHiddenSize": "hiddenSize if packedQkv else (dim(shapes.valueT, 1) if present.valueT and ranks.valueT == 2 else 0)",
29
+ "headDim": "hiddenSize / numHeads if numHeads > 0 and hiddenSize % numHeads == 0 else 0",
30
+ "vHeadDim": "vHiddenSize / numHeads if numHeads > 0 and vHiddenSize % numHeads == 0 else 0",
31
+ "attentionScale": "attrs.scale if has(attrs, \"scale\") else 0",
32
+ "qRowStride": "numHeads * 3 * headDim if packedQkv else hiddenSize",
33
+ "qHeadStride": "3 * headDim if packedQkv else headDim",
34
+ "kRowStride": "qRowStride if packedQkv else hiddenSize",
35
+ "kHeadStride": "qHeadStride if packedQkv else headDim",
36
+ "kOffset": "headDim if packedQkv else 0",
37
+ "vRowStride": "qRowStride if packedQkv else vHiddenSize",
38
+ "vHeadStride": "qHeadStride if packedQkv else vHeadDim",
39
+ "vOffset": "2 * headDim if packedQkv else 0",
40
+ "oRowStride": "vHiddenSize",
41
+ "oHeadStride": "vHeadDim",
42
+ "biasKOffset": "hiddenSize",
43
+ "biasVOffset": "2 * hiddenSize",
44
+ "attnBiasHeads": "dim(shapes.attentionBiasT, 1) if present.attentionBiasT and ranks.attentionBiasT == 4 else 0",
45
+ "maskSeqStride": "seqPad",
46
+ "maskHeadStride": "0 if attnBiasHeads == 1 else seqPad * seqPad",
47
+ "maskBatchStride": "0 if (not present.attentionBiasT) or ranks.attentionBiasT != 4 or dim(shapes.attentionBiasT, 0) == 1 else attnBiasHeads * seqPad * seqPad",
48
+ "dtypeOk": "tensorDtypes.outputT == tensorDtypes.queryT and (tensorDtypes.queryT == \"float32\" or tensorDtypes.queryT == \"float16\") and f16Ok(dtypes.T)",
49
+ "packedShapeOk": "packedQkv and not present.keyT and not present.valueT and dim(shapes.queryT, 1) == numHeads and dim(shapes.queryT, 2) == 3 and packedHeadDim > 0",
50
+ "separateShapeOk": "ranks.queryT == 2 and present.keyT and present.valueT and ranks.keyT == 2 and ranks.valueT == 2 and sameShape(shapes.keyT, shapes.queryT) and dim(shapes.valueT, 0) == tokenCount and tensorDtypes.keyT == tensorDtypes.queryT and tensorDtypes.valueT == tensorDtypes.queryT",
51
+ "scheduleOk": "ranks.tokenOffsetT == 2 and ranks.cumulativeSequenceLengthT == 1 and dim(shapes.cumulativeSequenceLengthT, 0) == batchSize + 1 and batchSize > 0 and seqPad > 0 and numel(shapes.tokenOffsetT) >= tokenCount",
52
+ "outputShapeOk": "ranks.outputT == 2 and dim(shapes.outputT, 0) == tokenCount and dim(shapes.outputT, 1) == vHiddenSize",
53
+ "biasOk": "present.biasT and ranks.biasT == 1 and dim(shapes.biasT, 0) == 2 * hiddenSize + vHiddenSize and tensorDtypes.biasT == tensorDtypes.queryT",
54
+ "attnBiasOk": "present.attentionBiasT and ranks.attentionBiasT == 4 and (dim(shapes.attentionBiasT, 0) == 1 or dim(shapes.attentionBiasT, 0) == batchSize) and (attnBiasHeads == 1 or attnBiasHeads == numHeads) and dim(shapes.attentionBiasT, 2) == seqPad and dim(shapes.attentionBiasT, 3) == seqPad and tensorDtypes.attentionBiasT == tensorDtypes.queryT",
55
+ "hasBias": "present.biasT",
56
+ "hasAttnBias": "present.attentionBiasT",
57
+ "baseContract": "dtypeOk and scheduleOk and outputShapeOk and numHeads > 0 and headDim > 0 and vHeadDim > 0 and tokenCount > 0 and (packedShapeOk or separateShapeOk)",
58
+ "usesF16": "tensorDtypes.queryT == \"float16\"",
59
+ "scalar": "\"f16\" if usesF16 else \"f32\"",
60
+ "outputScalar": "scalar",
61
+ "inputVec4": "\"vec4<f16>\" if usesF16 else \"vec4<f32>\"",
62
+ "qkvBytes": "2 if usesF16 else 4",
63
+ "packedStageBytes": "4 if usesF16 and present.biasT and vHeadDim != headDim else qkvBytes",
64
+ "clusterWidthAligned": "headDim % 16 == 0 and vHeadDim % 4 == 0",
65
+ "headDimV4": "headDim / 4",
66
+ "qRowStrideV4": "qRowStride / 4",
67
+ "qHeadStrideV4": "qHeadStride / 4",
68
+ "kRowStrideV4": "kRowStride / 4",
69
+ "kHeadStrideV4": "kHeadStride / 4",
70
+ "kOffsetV4": "kOffset / 4",
71
+ "vRowStrideV4": "vRowStride / 4",
72
+ "vHeadStrideV4": "vHeadStride / 4",
73
+ "vOffsetV4": "vOffset / 4",
74
+ "oRowStrideV4": "oRowStride / 4",
75
+ "oHeadStrideV4": "oHeadStride / 4",
76
+ "scalarWg": "tunables.SCALAR_WORKGROUP_SIZE",
77
+ "scalarBytes": "4 * scalarWg + 4 * vHeadDim + 12",
78
+ "scalarOk": "scalarWg <= device.limits.maxComputeInvocationsPerWorkgroup and scalarWg <= device.limits.maxComputeWorkgroupSizeX and scalarBytes <= device.limits.maxComputeWorkgroupStorageSize and numHeads <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535) and ceilDiv(tokenCount, DISPATCH_FOLD_WIDTH) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535)",
79
+ "clusterTileQ": "min(16, pow2ceil(tokenCount)) if vHeadDim != headDim else 16",
80
+ "clusterBaseLpq": "8 if headDim % 32 == 0 else 4",
81
+ "clusterTileK": "min(tunables.CLUSTER_TILE_K, pow2ceil(tokenCount)) if vHeadDim != headDim else tunables.CLUSTER_TILE_K",
82
+ "clusterTileBytes": "clusterTileK * (headDim + vHeadDim) * packedStageBytes",
83
+ "clusterLaneTarget": "min(32, max(clusterBaseLpq, pow2ceil(headDim / 16)), device.adapterInfo.subgroupMinSize, pow2ceil(floor(min(device.limits.maxComputeInvocationsPerWorkgroup, device.limits.maxComputeWorkgroupSizeX) / clusterTileQ) + 1) / 2) if vHeadDim == headDim and clusterWidthAligned and clusterTileBytes <= device.limits.maxComputeWorkgroupStorageSize and device.features.has(\"subgroups\") and has(device.adapterInfo, \"subgroupMinSize\") and device.adapterInfo.subgroupMinSize >= clusterBaseLpq else clusterBaseLpq",
84
+ "clusterLpq": "clusterLaneTarget if ceilDiv(headDim, 4 * clusterBaseLpq) >= 2 * ceilDiv(headDim, 4 * clusterLaneTarget) else clusterBaseLpq",
85
+ "clusterWg": "clusterTileQ * clusterLpq",
86
+ "clusterNosgBytes": "clusterTileBytes + 4 * clusterWg",
87
+ "clusterTiles": "ceilDiv(tokenCount, clusterTileQ)",
88
+ "clusterShapeOk": "clusterWidthAligned and clusterWg <= device.limits.maxComputeInvocationsPerWorkgroup and clusterWg <= device.limits.maxComputeWorkgroupSizeX and numHeads <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535) and ceilDiv(clusterTiles, DISPATCH_FOLD_WIDTH) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535)",
89
+ "clusterOk": "clusterShapeOk and clusterTileBytes <= device.limits.maxComputeWorkgroupStorageSize",
90
+ "clusterNosgOk": "clusterShapeOk and clusterNosgBytes <= device.limits.maxComputeWorkgroupStorageSize",
91
+ "clusterSubgroupsOk": "has(device.adapterInfo, \"subgroupMinSize\") and has(device.adapterInfo, \"subgroupMaxSize\") and device.adapterInfo.subgroupMinSize >= clusterLpq and device.adapterInfo.subgroupMinSize % clusterLpq == 0 and device.adapterInfo.subgroupMaxSize >= device.adapterInfo.subgroupMinSize and device.adapterInfo.subgroupMaxSize % clusterLpq == 0",
92
+ "portableLaneTarget": "min(64, pow2ceil(max(headDimV4, vHeadDim / 4) / 4), device.limits.maxComputeInvocationsPerWorkgroup, device.limits.maxComputeWorkgroupSizeX)",
93
+ "portableLpq": "max(clusterLpq, pow2ceil(portableLaneTarget + 1) / 2) if vHeadDim != headDim else (64 if portableLaneTarget >= 64 and headDimV4 % 64 == 0 else (32 if portableLaneTarget >= 32 and headDimV4 % 32 == 0 else (16 if portableLaneTarget >= 16 and headDimV4 % 16 == 0 else clusterLpq)))",
94
+ "portableTileQ": "min(clusterTileQ, pow2ceil(tokenCount), max(1, floor(min(clusterWg, device.limits.maxComputeInvocationsPerWorkgroup, device.limits.maxComputeWorkgroupSizeX) / portableLpq)))",
95
+ "portableWg": "portableTileQ * portableLpq",
96
+ "portableKeyCapacity": "max(1, floor(device.limits.maxComputeWorkgroupStorageSize / ((headDim + vHeadDim) * packedStageBytes + 4 * portableWg)))",
97
+ "portableTileK": "min(clusterTileK, pow2ceil(tokenCount), pow2ceil(portableKeyCapacity + 1) / 2)",
98
+ "clusterDistributedBytes": "portableTileK * (headDim + vHeadDim) * packedStageBytes + 4 * max(portableWg * portableTileK, 2 * portableWg + portableTileQ * portableTileK)",
99
+ "clusterDistributedOk": "clusterDistributedBytes <= device.limits.maxComputeWorkgroupStorageSize",
100
+ "clusterUseDistributed": "clusterDistributedOk and (clusterLpq > 4 or vHeadDim != headDim)",
101
+ "portableTiles": "ceilDiv(tokenCount, portableTileQ if clusterUseDistributed else clusterTileQ)",
102
+ "clusterPortableOk": "clusterShapeOk and (clusterUseDistributed or clusterNosgOk) and portableWg <= device.limits.maxComputeInvocationsPerWorkgroup and portableWg <= device.limits.maxComputeWorkgroupSizeX and ceilDiv(portableTiles, DISPATCH_FOLD_WIDTH) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535)"
103
+ },
104
+ "bindings": {
105
+ "query": { "arg": "queryT", "name": "query" },
106
+ "key": { "arg": "keyT", "name": "key" },
107
+ "value": { "arg": "valueT", "name": "value" },
108
+ "bias": { "arg": "biasT", "name": "bias" },
109
+ "tokenOffset": { "arg": "tokenOffsetT", "name": "token_offset" },
110
+ "cumSeqLen": { "arg": "cumulativeSequenceLengthT", "name": "cumulative_sequence_length" },
111
+ "attnBias": { "arg": "attentionBiasT", "name": "attn_bias" },
112
+ "output": { "arg": "outputT", "name": "output" },
113
+ "query4": { "arg": "queryT", "name": "query", "elementType": "$inputVec4" },
114
+ "key4": { "arg": "keyT", "name": "key", "elementType": "$inputVec4" },
115
+ "value4": { "arg": "valueT", "name": "value", "elementType": "$inputVec4" },
116
+ "output4": { "arg": "outputT", "name": "output", "elementType": "$inputVec4" },
117
+ "params": {
118
+ "name": "params",
119
+ "struct": [
120
+ { "name": "tokenCount", "type": "u32", "value": "tokenCount" },
121
+ { "name": "seqPad", "type": "u32", "value": "seqPad" },
122
+ { "name": "scale", "type": "f32", "value": "attentionScale" }
123
+ ]
124
+ },
125
+ "paramsMask": {
126
+ "name": "params",
127
+ "struct": [
128
+ { "name": "tokenCount", "type": "u32", "value": "tokenCount" },
129
+ { "name": "seqPad", "type": "u32", "value": "seqPad" },
130
+ { "name": "scale", "type": "f32", "value": "attentionScale" },
131
+ { "name": "maskBatchStride", "type": "u32", "value": "maskBatchStride" },
132
+ { "name": "maskHeadStride", "type": "u32", "value": "maskHeadStride" },
133
+ { "name": "maskSeqStride", "type": "u32", "value": "maskSeqStride" }
134
+ ]
135
+ }
136
+ },
137
+ "variants": [
138
+ {
139
+ "id": "packed_flash",
140
+ "priority": 40,
141
+ "when": ["baseContract", "packedShapeOk", "not present.biasT", "not present.attentionBiasT", "clusterOk", "clusterSubgroupsOk"],
142
+ "derive": { "useSubgroups": true, "LPQ": "clusterLpq", "TILE_Q": "clusterTileQ", "TILE_K": "clusterTileK" },
143
+ "passes": [
144
+ {
145
+ "id": "main",
146
+ "name": "PackedMultiHeadAttention.Cluster",
147
+ "shader": "attn-packed-varlen-cluster.wgsl.jinja",
148
+ "bindings": ["query4", "tokenOffset", "cumSeqLen", "output4", "params"],
149
+ "dispatch": {
150
+ "x": "min(clusterTiles, DISPATCH_FOLD_WIDTH)",
151
+ "y": "numHeads",
152
+ "z": "ceilDiv(clusterTiles, DISPATCH_FOLD_WIDTH)"
153
+ }
154
+ }
155
+ ],
156
+ "requires": { "features": ["subgroups"] },
157
+ "demoteWhen": ["vHeadDim != headDim and (tokenCount < clusterLpq or (clusterUseDistributed and portableLpq > clusterLpq))"]
158
+ },
159
+ {
160
+ "id": "packed_flash_nosg",
161
+ "priority": 20,
162
+ "when": ["baseContract", "packedShapeOk", "not present.biasT", "not present.attentionBiasT", "clusterPortableOk"],
163
+ "derive": {
164
+ "useSubgroups": false,
165
+ "LPQ": "portableLpq if clusterUseDistributed else clusterLpq",
166
+ "TILE_Q": "portableTileQ if clusterUseDistributed else clusterTileQ",
167
+ "TILE_K": "portableTileK if clusterUseDistributed else clusterTileK",
168
+ "distributedSoftmax": "clusterUseDistributed"
169
+ },
170
+ "passes": [
171
+ {
172
+ "id": "main",
173
+ "name": "PackedMultiHeadAttention.Cluster",
174
+ "shader": "attn-packed-varlen-cluster.wgsl.jinja",
175
+ "bindings": ["query4", "tokenOffset", "cumSeqLen", "output4", "params"],
176
+ "dispatch": {
177
+ "x": "min(portableTiles, DISPATCH_FOLD_WIDTH)",
178
+ "y": "numHeads",
179
+ "z": "ceilDiv(portableTiles, DISPATCH_FOLD_WIDTH)"
180
+ }
181
+ }
182
+ ],
183
+ "demoteWhen": ["vHeadDim != headDim and tokenCount < clusterLpq"]
184
+ },
185
+ {
186
+ "id": "packed_serial",
187
+ "priority": 0,
188
+ "when": ["baseContract", "packedShapeOk", "not present.biasT", "not present.attentionBiasT", "scalarOk"],
189
+ "derive": { "workgroupSize": "scalarWg" },
190
+ "passes": [
191
+ {
192
+ "id": "main",
193
+ "name": "PackedMultiHeadAttention.Serial",
194
+ "shader": "attn-packed-varlen-scalar.wgsl.jinja",
195
+ "bindings": ["query", "tokenOffset", "cumSeqLen", "output", "params"],
196
+ "dispatch": {
197
+ "x": "min(tokenCount, DISPATCH_FOLD_WIDTH)",
198
+ "y": "numHeads",
199
+ "z": "ceilDiv(tokenCount, DISPATCH_FOLD_WIDTH)"
200
+ }
201
+ }
202
+ ]
203
+ },
204
+ {
205
+ "id": "packed_attn_bias_flash",
206
+ "priority": 40,
207
+ "when": ["baseContract", "packedShapeOk", "not present.biasT", "attnBiasOk", "clusterOk", "clusterSubgroupsOk"],
208
+ "derive": { "useSubgroups": true, "LPQ": "clusterLpq", "TILE_Q": "clusterTileQ", "TILE_K": "clusterTileK" },
209
+ "passes": [
210
+ {
211
+ "id": "main",
212
+ "name": "PackedMultiHeadAttention.Cluster",
213
+ "shader": "attn-packed-varlen-cluster.wgsl.jinja",
214
+ "bindings": ["query4", "tokenOffset", "cumSeqLen", "attnBias", "output4", "paramsMask"],
215
+ "dispatch": {
216
+ "x": "min(clusterTiles, DISPATCH_FOLD_WIDTH)",
217
+ "y": "numHeads",
218
+ "z": "ceilDiv(clusterTiles, DISPATCH_FOLD_WIDTH)"
219
+ }
220
+ }
221
+ ],
222
+ "requires": { "features": ["subgroups"] },
223
+ "demoteWhen": ["vHeadDim != headDim and (tokenCount < clusterLpq or (clusterUseDistributed and portableLpq > clusterLpq))"]
224
+ },
225
+ {
226
+ "id": "packed_attn_bias_flash_nosg",
227
+ "priority": 20,
228
+ "when": ["baseContract", "packedShapeOk", "not present.biasT", "attnBiasOk", "clusterPortableOk"],
229
+ "derive": {
230
+ "useSubgroups": false,
231
+ "LPQ": "portableLpq if clusterUseDistributed else clusterLpq",
232
+ "TILE_Q": "portableTileQ if clusterUseDistributed else clusterTileQ",
233
+ "TILE_K": "portableTileK if clusterUseDistributed else clusterTileK",
234
+ "distributedSoftmax": "clusterUseDistributed"
235
+ },
236
+ "passes": [
237
+ {
238
+ "id": "main",
239
+ "name": "PackedMultiHeadAttention.Cluster",
240
+ "shader": "attn-packed-varlen-cluster.wgsl.jinja",
241
+ "bindings": ["query4", "tokenOffset", "cumSeqLen", "attnBias", "output4", "paramsMask"],
242
+ "dispatch": {
243
+ "x": "min(portableTiles, DISPATCH_FOLD_WIDTH)",
244
+ "y": "numHeads",
245
+ "z": "ceilDiv(portableTiles, DISPATCH_FOLD_WIDTH)"
246
+ }
247
+ }
248
+ ],
249
+ "demoteWhen": ["vHeadDim != headDim and tokenCount < clusterLpq"]
250
+ },
251
+ {
252
+ "id": "packed_attn_bias_serial",
253
+ "priority": 0,
254
+ "when": ["baseContract", "packedShapeOk", "not present.biasT", "attnBiasOk", "scalarOk"],
255
+ "derive": { "workgroupSize": "scalarWg" },
256
+ "passes": [
257
+ {
258
+ "id": "main",
259
+ "name": "PackedMultiHeadAttention.Serial",
260
+ "shader": "attn-packed-varlen-scalar.wgsl.jinja",
261
+ "bindings": ["query", "tokenOffset", "cumSeqLen", "attnBias", "output", "paramsMask"],
262
+ "dispatch": {
263
+ "x": "min(tokenCount, DISPATCH_FOLD_WIDTH)",
264
+ "y": "numHeads",
265
+ "z": "ceilDiv(tokenCount, DISPATCH_FOLD_WIDTH)"
266
+ }
267
+ }
268
+ ]
269
+ },
270
+ {
271
+ "id": "packed_bias_flash",
272
+ "priority": 40,
273
+ "when": ["baseContract", "packedShapeOk", "biasOk", "not present.attentionBiasT", "clusterOk", "clusterSubgroupsOk"],
274
+ "derive": { "useSubgroups": true, "LPQ": "clusterLpq", "TILE_Q": "clusterTileQ", "TILE_K": "clusterTileK" },
275
+ "passes": [
276
+ {
277
+ "id": "main",
278
+ "name": "PackedMultiHeadAttention.Cluster",
279
+ "shader": "attn-packed-varlen-cluster.wgsl.jinja",
280
+ "bindings": ["query4", "bias", "tokenOffset", "cumSeqLen", "output4", "params"],
281
+ "dispatch": {
282
+ "x": "min(clusterTiles, DISPATCH_FOLD_WIDTH)",
283
+ "y": "numHeads",
284
+ "z": "ceilDiv(clusterTiles, DISPATCH_FOLD_WIDTH)"
285
+ }
286
+ }
287
+ ],
288
+ "requires": { "features": ["subgroups"] },
289
+ "demoteWhen": ["vHeadDim != headDim and (tokenCount < clusterLpq or (clusterUseDistributed and portableLpq > clusterLpq))"]
290
+ },
291
+ {
292
+ "id": "packed_bias_flash_nosg",
293
+ "priority": 20,
294
+ "when": ["baseContract", "packedShapeOk", "biasOk", "not present.attentionBiasT", "clusterPortableOk"],
295
+ "derive": {
296
+ "useSubgroups": false,
297
+ "LPQ": "portableLpq if clusterUseDistributed else clusterLpq",
298
+ "TILE_Q": "portableTileQ if clusterUseDistributed else clusterTileQ",
299
+ "TILE_K": "portableTileK if clusterUseDistributed else clusterTileK",
300
+ "distributedSoftmax": "clusterUseDistributed"
301
+ },
302
+ "passes": [
303
+ {
304
+ "id": "main",
305
+ "name": "PackedMultiHeadAttention.Cluster",
306
+ "shader": "attn-packed-varlen-cluster.wgsl.jinja",
307
+ "bindings": ["query4", "bias", "tokenOffset", "cumSeqLen", "output4", "params"],
308
+ "dispatch": {
309
+ "x": "min(portableTiles, DISPATCH_FOLD_WIDTH)",
310
+ "y": "numHeads",
311
+ "z": "ceilDiv(portableTiles, DISPATCH_FOLD_WIDTH)"
312
+ }
313
+ }
314
+ ],
315
+ "demoteWhen": ["vHeadDim != headDim and tokenCount < clusterLpq"]
316
+ },
317
+ {
318
+ "id": "packed_bias_serial",
319
+ "priority": 0,
320
+ "when": ["baseContract", "packedShapeOk", "biasOk", "not present.attentionBiasT", "scalarOk"],
321
+ "derive": { "workgroupSize": "scalarWg" },
322
+ "passes": [
323
+ {
324
+ "id": "main",
325
+ "name": "PackedMultiHeadAttention.Serial",
326
+ "shader": "attn-packed-varlen-scalar.wgsl.jinja",
327
+ "bindings": ["query", "bias", "tokenOffset", "cumSeqLen", "output", "params"],
328
+ "dispatch": {
329
+ "x": "min(tokenCount, DISPATCH_FOLD_WIDTH)",
330
+ "y": "numHeads",
331
+ "z": "ceilDiv(tokenCount, DISPATCH_FOLD_WIDTH)"
332
+ }
333
+ }
334
+ ]
335
+ },
336
+ {
337
+ "id": "packed_bias_attn_bias_flash",
338
+ "priority": 40,
339
+ "when": ["baseContract", "packedShapeOk", "biasOk", "attnBiasOk", "clusterOk", "clusterSubgroupsOk"],
340
+ "derive": { "useSubgroups": true, "LPQ": "clusterLpq", "TILE_Q": "clusterTileQ", "TILE_K": "clusterTileK" },
341
+ "passes": [
342
+ {
343
+ "id": "main",
344
+ "name": "PackedMultiHeadAttention.Cluster",
345
+ "shader": "attn-packed-varlen-cluster.wgsl.jinja",
346
+ "bindings": ["query4", "bias", "tokenOffset", "cumSeqLen", "attnBias", "output4", "paramsMask"],
347
+ "dispatch": {
348
+ "x": "min(clusterTiles, DISPATCH_FOLD_WIDTH)",
349
+ "y": "numHeads",
350
+ "z": "ceilDiv(clusterTiles, DISPATCH_FOLD_WIDTH)"
351
+ }
352
+ }
353
+ ],
354
+ "requires": { "features": ["subgroups"] },
355
+ "demoteWhen": ["vHeadDim != headDim and (tokenCount < clusterLpq or (clusterUseDistributed and portableLpq > clusterLpq))"]
356
+ },
357
+ {
358
+ "id": "packed_bias_attn_bias_flash_nosg",
359
+ "priority": 20,
360
+ "when": ["baseContract", "packedShapeOk", "biasOk", "attnBiasOk", "clusterPortableOk"],
361
+ "derive": {
362
+ "useSubgroups": false,
363
+ "LPQ": "portableLpq if clusterUseDistributed else clusterLpq",
364
+ "TILE_Q": "portableTileQ if clusterUseDistributed else clusterTileQ",
365
+ "TILE_K": "portableTileK if clusterUseDistributed else clusterTileK",
366
+ "distributedSoftmax": "clusterUseDistributed"
367
+ },
368
+ "passes": [
369
+ {
370
+ "id": "main",
371
+ "name": "PackedMultiHeadAttention.Cluster",
372
+ "shader": "attn-packed-varlen-cluster.wgsl.jinja",
373
+ "bindings": ["query4", "bias", "tokenOffset", "cumSeqLen", "attnBias", "output4", "paramsMask"],
374
+ "dispatch": {
375
+ "x": "min(portableTiles, DISPATCH_FOLD_WIDTH)",
376
+ "y": "numHeads",
377
+ "z": "ceilDiv(portableTiles, DISPATCH_FOLD_WIDTH)"
378
+ }
379
+ }
380
+ ],
381
+ "demoteWhen": ["vHeadDim != headDim and tokenCount < clusterLpq"]
382
+ },
383
+ {
384
+ "id": "packed_bias_attn_bias_serial",
385
+ "priority": 0,
386
+ "when": ["baseContract", "packedShapeOk", "biasOk", "attnBiasOk", "scalarOk"],
387
+ "derive": { "workgroupSize": "scalarWg" },
388
+ "passes": [
389
+ {
390
+ "id": "main",
391
+ "name": "PackedMultiHeadAttention.Serial",
392
+ "shader": "attn-packed-varlen-scalar.wgsl.jinja",
393
+ "bindings": ["query", "bias", "tokenOffset", "cumSeqLen", "attnBias", "output", "paramsMask"],
394
+ "dispatch": {
395
+ "x": "min(tokenCount, DISPATCH_FOLD_WIDTH)",
396
+ "y": "numHeads",
397
+ "z": "ceilDiv(tokenCount, DISPATCH_FOLD_WIDTH)"
398
+ }
399
+ }
400
+ ]
401
+ },
402
+ {
403
+ "id": "separate_flash",
404
+ "priority": 40,
405
+ "when": ["baseContract", "separateShapeOk", "not present.biasT", "not present.attentionBiasT", "clusterOk", "clusterSubgroupsOk"],
406
+ "derive": { "useSubgroups": true, "LPQ": "clusterLpq", "TILE_Q": "clusterTileQ", "TILE_K": "clusterTileK" },
407
+ "passes": [
408
+ {
409
+ "id": "main",
410
+ "name": "PackedMultiHeadAttention.Cluster",
411
+ "shader": "attn-packed-varlen-cluster.wgsl.jinja",
412
+ "bindings": ["query4", "key4", "value4", "tokenOffset", "cumSeqLen", "output4", "params"],
413
+ "dispatch": {
414
+ "x": "min(clusterTiles, DISPATCH_FOLD_WIDTH)",
415
+ "y": "numHeads",
416
+ "z": "ceilDiv(clusterTiles, DISPATCH_FOLD_WIDTH)"
417
+ }
418
+ }
419
+ ],
420
+ "requires": { "features": ["subgroups"] },
421
+ "demoteWhen": ["vHeadDim != headDim and (tokenCount < clusterLpq or (clusterUseDistributed and portableLpq > clusterLpq))"]
422
+ },
423
+ {
424
+ "id": "separate_flash_nosg",
425
+ "priority": 20,
426
+ "when": ["baseContract", "separateShapeOk", "not present.biasT", "not present.attentionBiasT", "clusterPortableOk"],
427
+ "derive": {
428
+ "useSubgroups": false,
429
+ "LPQ": "portableLpq if clusterUseDistributed else clusterLpq",
430
+ "TILE_Q": "portableTileQ if clusterUseDistributed else clusterTileQ",
431
+ "TILE_K": "portableTileK if clusterUseDistributed else clusterTileK",
432
+ "distributedSoftmax": "clusterUseDistributed"
433
+ },
434
+ "passes": [
435
+ {
436
+ "id": "main",
437
+ "name": "PackedMultiHeadAttention.Cluster",
438
+ "shader": "attn-packed-varlen-cluster.wgsl.jinja",
439
+ "bindings": ["query4", "key4", "value4", "tokenOffset", "cumSeqLen", "output4", "params"],
440
+ "dispatch": {
441
+ "x": "min(portableTiles, DISPATCH_FOLD_WIDTH)",
442
+ "y": "numHeads",
443
+ "z": "ceilDiv(portableTiles, DISPATCH_FOLD_WIDTH)"
444
+ }
445
+ }
446
+ ],
447
+ "demoteWhen": ["vHeadDim != headDim and tokenCount < clusterLpq"]
448
+ },
449
+ {
450
+ "id": "separate_serial",
451
+ "priority": 0,
452
+ "when": ["baseContract", "separateShapeOk", "not present.biasT", "not present.attentionBiasT", "scalarOk"],
453
+ "derive": { "workgroupSize": "scalarWg" },
454
+ "passes": [
455
+ {
456
+ "id": "main",
457
+ "name": "PackedMultiHeadAttention.Serial",
458
+ "shader": "attn-packed-varlen-scalar.wgsl.jinja",
459
+ "bindings": ["query", "key", "value", "tokenOffset", "cumSeqLen", "output", "params"],
460
+ "dispatch": {
461
+ "x": "min(tokenCount, DISPATCH_FOLD_WIDTH)",
462
+ "y": "numHeads",
463
+ "z": "ceilDiv(tokenCount, DISPATCH_FOLD_WIDTH)"
464
+ }
465
+ }
466
+ ]
467
+ },
468
+ {
469
+ "id": "separate_attn_bias_flash",
470
+ "priority": 40,
471
+ "when": ["baseContract", "separateShapeOk", "not present.biasT", "attnBiasOk", "clusterOk", "clusterSubgroupsOk"],
472
+ "derive": { "useSubgroups": true, "LPQ": "clusterLpq", "TILE_Q": "clusterTileQ", "TILE_K": "clusterTileK" },
473
+ "passes": [
474
+ {
475
+ "id": "main",
476
+ "name": "PackedMultiHeadAttention.Cluster",
477
+ "shader": "attn-packed-varlen-cluster.wgsl.jinja",
478
+ "bindings": ["query4", "key4", "value4", "tokenOffset", "cumSeqLen", "attnBias", "output4", "paramsMask"],
479
+ "dispatch": {
480
+ "x": "min(clusterTiles, DISPATCH_FOLD_WIDTH)",
481
+ "y": "numHeads",
482
+ "z": "ceilDiv(clusterTiles, DISPATCH_FOLD_WIDTH)"
483
+ }
484
+ }
485
+ ],
486
+ "requires": { "features": ["subgroups"] },
487
+ "demoteWhen": ["vHeadDim != headDim and (tokenCount < clusterLpq or (clusterUseDistributed and portableLpq > clusterLpq))"]
488
+ },
489
+ {
490
+ "id": "separate_attn_bias_flash_nosg",
491
+ "priority": 20,
492
+ "when": ["baseContract", "separateShapeOk", "not present.biasT", "attnBiasOk", "clusterPortableOk"],
493
+ "derive": {
494
+ "useSubgroups": false,
495
+ "LPQ": "portableLpq if clusterUseDistributed else clusterLpq",
496
+ "TILE_Q": "portableTileQ if clusterUseDistributed else clusterTileQ",
497
+ "TILE_K": "portableTileK if clusterUseDistributed else clusterTileK",
498
+ "distributedSoftmax": "clusterUseDistributed"
499
+ },
500
+ "passes": [
501
+ {
502
+ "id": "main",
503
+ "name": "PackedMultiHeadAttention.Cluster",
504
+ "shader": "attn-packed-varlen-cluster.wgsl.jinja",
505
+ "bindings": ["query4", "key4", "value4", "tokenOffset", "cumSeqLen", "attnBias", "output4", "paramsMask"],
506
+ "dispatch": {
507
+ "x": "min(portableTiles, DISPATCH_FOLD_WIDTH)",
508
+ "y": "numHeads",
509
+ "z": "ceilDiv(portableTiles, DISPATCH_FOLD_WIDTH)"
510
+ }
511
+ }
512
+ ],
513
+ "demoteWhen": ["vHeadDim != headDim and tokenCount < clusterLpq"]
514
+ },
515
+ {
516
+ "id": "separate_attn_bias_serial",
517
+ "priority": 0,
518
+ "when": ["baseContract", "separateShapeOk", "not present.biasT", "attnBiasOk", "scalarOk"],
519
+ "derive": { "workgroupSize": "scalarWg" },
520
+ "passes": [
521
+ {
522
+ "id": "main",
523
+ "name": "PackedMultiHeadAttention.Serial",
524
+ "shader": "attn-packed-varlen-scalar.wgsl.jinja",
525
+ "bindings": ["query", "key", "value", "tokenOffset", "cumSeqLen", "attnBias", "output", "paramsMask"],
526
+ "dispatch": {
527
+ "x": "min(tokenCount, DISPATCH_FOLD_WIDTH)",
528
+ "y": "numHeads",
529
+ "z": "ceilDiv(tokenCount, DISPATCH_FOLD_WIDTH)"
530
+ }
531
+ }
532
+ ]
533
+ },
534
+ {
535
+ "id": "separate_bias_flash",
536
+ "priority": 40,
537
+ "when": ["baseContract", "separateShapeOk", "biasOk", "not present.attentionBiasT", "clusterOk", "clusterSubgroupsOk"],
538
+ "derive": { "useSubgroups": true, "LPQ": "clusterLpq", "TILE_Q": "clusterTileQ", "TILE_K": "clusterTileK" },
539
+ "passes": [
540
+ {
541
+ "id": "main",
542
+ "name": "PackedMultiHeadAttention.Cluster",
543
+ "shader": "attn-packed-varlen-cluster.wgsl.jinja",
544
+ "bindings": ["query4", "key4", "value4", "bias", "tokenOffset", "cumSeqLen", "output4", "params"],
545
+ "dispatch": {
546
+ "x": "min(clusterTiles, DISPATCH_FOLD_WIDTH)",
547
+ "y": "numHeads",
548
+ "z": "ceilDiv(clusterTiles, DISPATCH_FOLD_WIDTH)"
549
+ }
550
+ }
551
+ ],
552
+ "requires": { "features": ["subgroups"] },
553
+ "demoteWhen": ["vHeadDim != headDim and (tokenCount < clusterLpq or (clusterUseDistributed and portableLpq > clusterLpq))"]
554
+ },
555
+ {
556
+ "id": "separate_bias_flash_nosg",
557
+ "priority": 20,
558
+ "when": ["baseContract", "separateShapeOk", "biasOk", "not present.attentionBiasT", "clusterPortableOk"],
559
+ "derive": {
560
+ "useSubgroups": false,
561
+ "LPQ": "portableLpq if clusterUseDistributed else clusterLpq",
562
+ "TILE_Q": "portableTileQ if clusterUseDistributed else clusterTileQ",
563
+ "TILE_K": "portableTileK if clusterUseDistributed else clusterTileK",
564
+ "distributedSoftmax": "clusterUseDistributed"
565
+ },
566
+ "passes": [
567
+ {
568
+ "id": "main",
569
+ "name": "PackedMultiHeadAttention.Cluster",
570
+ "shader": "attn-packed-varlen-cluster.wgsl.jinja",
571
+ "bindings": ["query4", "key4", "value4", "bias", "tokenOffset", "cumSeqLen", "output4", "params"],
572
+ "dispatch": {
573
+ "x": "min(portableTiles, DISPATCH_FOLD_WIDTH)",
574
+ "y": "numHeads",
575
+ "z": "ceilDiv(portableTiles, DISPATCH_FOLD_WIDTH)"
576
+ }
577
+ }
578
+ ],
579
+ "demoteWhen": ["vHeadDim != headDim and tokenCount < clusterLpq"]
580
+ },
581
+ {
582
+ "id": "separate_bias_serial",
583
+ "priority": 0,
584
+ "when": ["baseContract", "separateShapeOk", "biasOk", "not present.attentionBiasT", "scalarOk"],
585
+ "derive": { "workgroupSize": "scalarWg" },
586
+ "passes": [
587
+ {
588
+ "id": "main",
589
+ "name": "PackedMultiHeadAttention.Serial",
590
+ "shader": "attn-packed-varlen-scalar.wgsl.jinja",
591
+ "bindings": ["query", "key", "value", "bias", "tokenOffset", "cumSeqLen", "output", "params"],
592
+ "dispatch": {
593
+ "x": "min(tokenCount, DISPATCH_FOLD_WIDTH)",
594
+ "y": "numHeads",
595
+ "z": "ceilDiv(tokenCount, DISPATCH_FOLD_WIDTH)"
596
+ }
597
+ }
598
+ ]
599
+ },
600
+ {
601
+ "id": "separate_bias_attn_bias_flash",
602
+ "priority": 40,
603
+ "when": ["baseContract", "separateShapeOk", "biasOk", "attnBiasOk", "clusterOk", "clusterSubgroupsOk"],
604
+ "derive": { "useSubgroups": true, "LPQ": "clusterLpq", "TILE_Q": "clusterTileQ", "TILE_K": "clusterTileK" },
605
+ "passes": [
606
+ {
607
+ "id": "main",
608
+ "name": "PackedMultiHeadAttention.Cluster",
609
+ "shader": "attn-packed-varlen-cluster.wgsl.jinja",
610
+ "bindings": ["query4", "key4", "value4", "bias", "tokenOffset", "cumSeqLen", "attnBias", "output4", "paramsMask"],
611
+ "dispatch": {
612
+ "x": "min(clusterTiles, DISPATCH_FOLD_WIDTH)",
613
+ "y": "numHeads",
614
+ "z": "ceilDiv(clusterTiles, DISPATCH_FOLD_WIDTH)"
615
+ }
616
+ }
617
+ ],
618
+ "requires": { "features": ["subgroups"] },
619
+ "demoteWhen": ["vHeadDim != headDim and (tokenCount < clusterLpq or (clusterUseDistributed and portableLpq > clusterLpq))"]
620
+ },
621
+ {
622
+ "id": "separate_bias_attn_bias_flash_nosg",
623
+ "priority": 20,
624
+ "when": ["baseContract", "separateShapeOk", "biasOk", "attnBiasOk", "clusterPortableOk"],
625
+ "derive": {
626
+ "useSubgroups": false,
627
+ "LPQ": "portableLpq if clusterUseDistributed else clusterLpq",
628
+ "TILE_Q": "portableTileQ if clusterUseDistributed else clusterTileQ",
629
+ "TILE_K": "portableTileK if clusterUseDistributed else clusterTileK",
630
+ "distributedSoftmax": "clusterUseDistributed"
631
+ },
632
+ "passes": [
633
+ {
634
+ "id": "main",
635
+ "name": "PackedMultiHeadAttention.Cluster",
636
+ "shader": "attn-packed-varlen-cluster.wgsl.jinja",
637
+ "bindings": ["query4", "key4", "value4", "bias", "tokenOffset", "cumSeqLen", "attnBias", "output4", "paramsMask"],
638
+ "dispatch": {
639
+ "x": "min(portableTiles, DISPATCH_FOLD_WIDTH)",
640
+ "y": "numHeads",
641
+ "z": "ceilDiv(portableTiles, DISPATCH_FOLD_WIDTH)"
642
+ }
643
+ }
644
+ ],
645
+ "demoteWhen": ["vHeadDim != headDim and tokenCount < clusterLpq"]
646
+ },
647
+ {
648
+ "id": "separate_bias_attn_bias_serial",
649
+ "priority": 0,
650
+ "when": ["baseContract", "separateShapeOk", "biasOk", "attnBiasOk", "scalarOk"],
651
+ "derive": { "workgroupSize": "scalarWg" },
652
+ "passes": [
653
+ {
654
+ "id": "main",
655
+ "name": "PackedMultiHeadAttention.Serial",
656
+ "shader": "attn-packed-varlen-scalar.wgsl.jinja",
657
+ "bindings": ["query", "key", "value", "bias", "tokenOffset", "cumSeqLen", "attnBias", "output", "paramsMask"],
658
+ "dispatch": {
659
+ "x": "min(tokenCount, DISPATCH_FOLD_WIDTH)",
660
+ "y": "numHeads",
661
+ "z": "ceilDiv(tokenCount, DISPATCH_FOLD_WIDTH)"
662
+ }
663
+ }
664
+ ]
665
+ }
666
+ ]
667
+ }
build/webgpu/metadata.json ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "com.microsoft.PackedMultiHeadAttention",
3
+ "id": "_com_microsoft_packedmultiheadattention_webgpu_96b8ba0",
4
+ "version": 1,
5
+ "license": "Apache-2.0",
6
+ "backend": { "type": "webgpu" },
7
+ "digest": {
8
+ "algorithm": "sha256",
9
+ "files": {
10
+ "attn-packed-varlen-cluster.wgsl.jinja": "p9O8Y9FODuK+3sS5XCmt34ubccsQ1rHO8KyYdKFbox8=",
11
+ "attn-packed-varlen-scalar.wgsl.jinja": "6gm8Q90Uq/Fwm53KBabaxD39SZDE/XPt93lhHeOKEKo=",
12
+ "bench.json": "QU1tbtEXCd2xke0vrPrixj1rlwWltGZXLr8StqGlVuM=",
13
+ "manifest.json": "CImYmm6QB0MSplEddT1DpForRg78IeIsHWzBijr+yks=",
14
+ "test.json": "Y3VjrtppOZI+cmCpPGhsEdWnF8qd981T9VIMTNux648="
15
+ }
16
+ },
17
+ "provenance": { "kernel": { "sha": "6fdf6301e2bbcc2f03bf1eaf493b7ad55ef33afc", "dirty": false } },
18
+ "webgpu": {
19
+ "manifestSpec": "2.1",
20
+ "variants": {
21
+ "packed_flash": ["attn-packed-varlen-cluster.wgsl.jinja"],
22
+ "packed_flash_nosg": ["attn-packed-varlen-cluster.wgsl.jinja"],
23
+ "packed_serial": ["attn-packed-varlen-scalar.wgsl.jinja"],
24
+ "packed_attn_bias_flash": ["attn-packed-varlen-cluster.wgsl.jinja"],
25
+ "packed_attn_bias_flash_nosg": ["attn-packed-varlen-cluster.wgsl.jinja"],
26
+ "packed_attn_bias_serial": ["attn-packed-varlen-scalar.wgsl.jinja"],
27
+ "packed_bias_flash": ["attn-packed-varlen-cluster.wgsl.jinja"],
28
+ "packed_bias_flash_nosg": ["attn-packed-varlen-cluster.wgsl.jinja"],
29
+ "packed_bias_serial": ["attn-packed-varlen-scalar.wgsl.jinja"],
30
+ "packed_bias_attn_bias_flash": ["attn-packed-varlen-cluster.wgsl.jinja"],
31
+ "packed_bias_attn_bias_flash_nosg": ["attn-packed-varlen-cluster.wgsl.jinja"],
32
+ "packed_bias_attn_bias_serial": ["attn-packed-varlen-scalar.wgsl.jinja"],
33
+ "separate_flash": ["attn-packed-varlen-cluster.wgsl.jinja"],
34
+ "separate_flash_nosg": ["attn-packed-varlen-cluster.wgsl.jinja"],
35
+ "separate_serial": ["attn-packed-varlen-scalar.wgsl.jinja"],
36
+ "separate_attn_bias_flash": ["attn-packed-varlen-cluster.wgsl.jinja"],
37
+ "separate_attn_bias_flash_nosg": ["attn-packed-varlen-cluster.wgsl.jinja"],
38
+ "separate_attn_bias_serial": ["attn-packed-varlen-scalar.wgsl.jinja"],
39
+ "separate_bias_flash": ["attn-packed-varlen-cluster.wgsl.jinja"],
40
+ "separate_bias_flash_nosg": ["attn-packed-varlen-cluster.wgsl.jinja"],
41
+ "separate_bias_serial": ["attn-packed-varlen-scalar.wgsl.jinja"],
42
+ "separate_bias_attn_bias_flash": ["attn-packed-varlen-cluster.wgsl.jinja"],
43
+ "separate_bias_attn_bias_flash_nosg": ["attn-packed-varlen-cluster.wgsl.jinja"],
44
+ "separate_bias_attn_bias_serial": ["attn-packed-varlen-scalar.wgsl.jinja"]
45
+ }
46
+ }
47
+ }
build/webgpu/test.json ADDED
The diff for this file is too large to render. See raw diff