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

sync 6fdf6301e2bb

Browse files
README.md CHANGED
@@ -1,3 +1,135 @@
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.PackedAttention
10
+
11
+ `com.microsoft` · ONNX Runtime contrib operator · contrib since_version 1
12
+
13
+ ## Description
14
+
15
+ Self-attention with a fused Q/K/V input projection 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; there is no mask input. The optional attention bias is indexed in PADDED coordinates. The output is 2-D in packed token order. Float32 and float16 only; bfloat16 is not implemented.
16
+
17
+ See the [ONNX Runtime `PackedAttention` contrib-operator spec](https://github.com/microsoft/onnxruntime/blob/main/docs/ContribOperators.md#com.microsoft.PackedAttention) for the reference semantics.
18
+
19
+ ## Inputs
20
+
21
+ | Name | Upstream name | Logical dtype | WebGPU storage | Rank | Shape | Description | Presence |
22
+ | --- | --- | --- | --- | --- | --- | --- | --- |
23
+ | `inputT` | `input` | `T` | same as logical dtype | `2` | — | Packed token embeddings with shape `[token_count, input_hidden_size]`. | required |
24
+ | `weightsT` | `weights` | `T` | same as logical dtype | `2` | — | Merged projection weights with shape `[input_hidden_size, q_hidden + k_hidden + v_hidden]`, column-ordered `[Q all heads \| K all heads \| V all heads]`. | required |
25
+ | `biasT` | `bias` | `T` | same as logical dtype | `1` | — | Required projection bias with shape `[q_hidden + k_hidden + v_hidden]`. | required |
26
+ | `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 |
27
+ | `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 |
28
+ | `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 |
29
+
30
+ ## Outputs
31
+
32
+ | Name | Upstream name | Logical dtype | Rank | Shape | Description | Presence |
33
+ | --- | --- | --- | --- | --- | --- | --- |
34
+ | `outputT` | `output` | `T` | `2` | derived | Attention output with shape `[token_count, v_hidden_size]`, in packed token order. | required |
35
+
36
+ ## Attributes
37
+
38
+ Attributes and default values (overridable per request):
39
+
40
+ | Attribute | Default | Description |
41
+ | --- | --- | --- |
42
+ | `num_heads` | — | Number of attention heads. Required; every Q/K/V hidden size must be divisible by it. |
43
+ | `qkv_hidden_sizes` | — | Optional `[q_hidden, k_hidden, v_hidden]`. Omission splits the bias length in three; `q_hidden` must equal `k_hidden`. |
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
+ - `matrix_direct_f32_flash` — Projects with dtype-preserving matrix tiles and direct input reads; a gated staged pass covers partial rows. Attention uses subgroup or portable tiled reductions according to its own resource and lane budget.
58
+ - `matrix_staged_f32_flash` — Projects with staged 32-row matrix tiles and float32 accumulation, then runs subgroup or portable tiled attention. Small projection grids avoid a separate tail dispatch; both passes retain independent resource guards.
59
+ - `f32_flash` — Fuses the Q/K/V projection into a register-tiled GEMM, then runs tiled flash attention over the packed token stream: one workgroup covers `CLUSTER_TILE_Q` consecutive tokens for one head and stages `CLUSTER_TILE_K` keys, masking each token to its own sequence. Cluster dot fragments combine through subgroup shuffles.
60
+ - `matrix_direct_f32_flash_nosg` — Projects with dtype-preserving matrix tiles and direct input reads; a gated staged pass covers partial rows. Attention uses subgroup or portable tiled reductions according to its own resource and lane budget.
61
+ - `matrix_staged_f32_flash_nosg` — Projects with staged 32-row matrix tiles and float32 accumulation, then runs subgroup or portable tiled attention. Small projection grids avoid a separate tail dispatch; both passes retain independent resource guards.
62
+ - `f32_flash_nosg` — Projects Q/K/V with the register-tiled GEMM, then performs packed flash attention. Portable clusters distribute dot fragments and softmax work across lanes. Wider heads use more lanes and fewer queries per workgroup; device limits bound geometry and key staging. Four-lane clusters retain the existing reduction.
63
+ - `matrix_direct_f32_serial` — Projects with dtype-preserving subgroup matrices, direct input reads and double-buffered weights. Float32 uses four accumulation streams. A gated staged pass handles partial rows; sparse grids use smaller staged tiles.
64
+ - `matrix_staged_f32_serial` — Projects with staged 32-row subgroup-matrix tiles and float32 accumulation before packed attention. Small and partial-row grids avoid an extra projection dispatch; reported matrix configurations, subgroup width and resource limits retain portable fallbacks.
65
+ - `f32_serial` — Fuses the Q/K/V projection into a register-tiled GEMM, then runs portable serial-key attention: one workgroup owns one (packed token, head) and reduces each dot through workgroup memory. Head dimensions need no alignment and the value head dimension may differ.
66
+ - `matrix_direct_f32_attn_bias_flash` — Projects with dtype-preserving matrix tiles and direct input reads; a gated staged pass covers partial rows. Attention uses subgroup or portable tiled reductions according to its own resource and lane budget.
67
+ - `matrix_staged_f32_attn_bias_flash` — Projects with staged 32-row matrix tiles and float32 accumulation, then runs subgroup or portable tiled attention. Small projection grids avoid a separate tail dispatch; both passes retain independent resource guards.
68
+ - `f32_attn_bias_flash` — Fuses the Q/K/V projection into a register-tiled GEMM, then runs tiled flash attention over the packed token stream: one workgroup covers `CLUSTER_TILE_Q` consecutive tokens for one head and stages `CLUSTER_TILE_K` keys, masking each token to its own sequence. Cluster dot fragments combine through subgroup shuffles.
69
+ - `matrix_direct_f32_attn_bias_flash_nosg` — Projects with dtype-preserving matrix tiles and direct input reads; a gated staged pass covers partial rows. Attention uses subgroup or portable tiled reductions according to its own resource and lane budget.
70
+ - `matrix_staged_f32_attn_bias_flash_nosg` — Projects with staged 32-row matrix tiles and float32 accumulation, then runs subgroup or portable tiled attention. Small projection grids avoid a separate tail dispatch; both passes retain independent resource guards.
71
+ - `f32_attn_bias_flash_nosg` — Projects Q/K/V with the register-tiled GEMM, then performs packed flash attention. Portable clusters distribute dot fragments and softmax work across lanes. Wider heads use more lanes and fewer queries per workgroup; device limits bound geometry and key staging. Four-lane clusters retain the existing reduction.
72
+ - `matrix_direct_f32_attn_bias_serial` — Projects with dtype-preserving subgroup matrices, direct input reads and double-buffered weights. Float32 uses four accumulation streams. A gated staged pass handles partial rows; sparse grids use smaller staged tiles.
73
+ - `matrix_staged_f32_attn_bias_serial` — Projects with staged 32-row subgroup-matrix tiles and float32 accumulation before packed attention. Small and partial-row grids avoid an extra projection dispatch; reported matrix configurations, subgroup width and resource limits retain portable fallbacks.
74
+ - `f32_attn_bias_serial` — Fuses the Q/K/V projection into a register-tiled GEMM, then runs portable serial-key attention: one workgroup owns one (packed token, head) and reduces each dot through workgroup memory. Head dimensions need no alignment and the value head dimension may differ.
75
+ - `matrix_direct_f16_flash` — Projects with dtype-preserving matrix tiles and direct input reads; a gated staged pass covers partial rows. Attention uses subgroup or portable tiled reductions according to its own resource and lane budget.
76
+ - `matrix_staged_f16_flash` — Projects with staged 32-row matrix tiles and float32 accumulation, then runs subgroup or portable tiled attention. Small projection grids avoid a separate tail dispatch; both passes retain independent resource guards.
77
+ - `f16_flash` — Fuses the Q/K/V projection into a register-tiled GEMM, then runs tiled flash attention over the packed token stream: one workgroup covers `CLUSTER_TILE_Q` consecutive tokens for one head and stages `CLUSTER_TILE_K` keys, masking each token to its own sequence. Cluster dot fragments combine through subgroup shuffles.
78
+ - `matrix_direct_f16_flash_nosg` — Projects with dtype-preserving matrix tiles and direct input reads; a gated staged pass covers partial rows. Attention uses subgroup or portable tiled reductions according to its own resource and lane budget.
79
+ - `matrix_staged_f16_flash_nosg` — Projects with staged 32-row matrix tiles and float32 accumulation, then runs subgroup or portable tiled attention. Small projection grids avoid a separate tail dispatch; both passes retain independent resource guards.
80
+ - `f16_flash_nosg` — Projects Q/K/V with the register-tiled GEMM, then performs packed flash attention. Portable clusters distribute dot fragments and softmax work across lanes. Wider heads use more lanes and fewer queries per workgroup; device limits bound geometry and key staging. Four-lane clusters retain the existing reduction.
81
+ - `matrix_direct_f16_serial` — Projects with dtype-preserving subgroup matrices, direct input reads and double-buffered weights. Float32 uses four accumulation streams. A gated staged pass handles partial rows; sparse grids use smaller staged tiles.
82
+ - `matrix_staged_f16_serial` — Projects with staged 32-row subgroup-matrix tiles and float32 accumulation before packed attention. Small and partial-row grids avoid an extra projection dispatch; reported matrix configurations, subgroup width and resource limits retain portable fallbacks.
83
+ - `f16_serial` — Fuses the Q/K/V projection into a register-tiled GEMM, then runs portable serial-key attention: one workgroup owns one (packed token, head) and reduces each dot through workgroup memory. Head dimensions need no alignment and the value head dimension may differ.
84
+ - `matrix_direct_f16_attn_bias_flash` — Projects with dtype-preserving matrix tiles and direct input reads; a gated staged pass covers partial rows. Attention uses subgroup or portable tiled reductions according to its own resource and lane budget.
85
+ - `matrix_staged_f16_attn_bias_flash` — Projects with staged 32-row matrix tiles and float32 accumulation, then runs subgroup or portable tiled attention. Small projection grids avoid a separate tail dispatch; both passes retain independent resource guards.
86
+ - `f16_attn_bias_flash` — Fuses the Q/K/V projection into a register-tiled GEMM, then runs tiled flash attention over the packed token stream: one workgroup covers `CLUSTER_TILE_Q` consecutive tokens for one head and stages `CLUSTER_TILE_K` keys, masking each token to its own sequence. Cluster dot fragments combine through subgroup shuffles.
87
+ - `matrix_direct_f16_attn_bias_flash_nosg` — Projects with dtype-preserving matrix tiles and direct input reads; a gated staged pass covers partial rows. Attention uses subgroup or portable tiled reductions according to its own resource and lane budget.
88
+ - `matrix_staged_f16_attn_bias_flash_nosg` — Projects with staged 32-row matrix tiles and float32 accumulation, then runs subgroup or portable tiled attention. Small projection grids avoid a separate tail dispatch; both passes retain independent resource guards.
89
+ - `f16_attn_bias_flash_nosg` — Projects Q/K/V with the register-tiled GEMM, then performs packed flash attention. Portable clusters distribute dot fragments and softmax work across lanes. Wider heads use more lanes and fewer queries per workgroup; device limits bound geometry and key staging. Four-lane clusters retain the existing reduction.
90
+ - `matrix_direct_f16_attn_bias_serial` — Projects with dtype-preserving subgroup matrices, direct input reads and double-buffered weights. Float32 uses four accumulation streams. A gated staged pass handles partial rows; sparse grids use smaller staged tiles.
91
+ - `matrix_staged_f16_attn_bias_serial` — Projects with staged 32-row subgroup-matrix tiles and float32 accumulation before packed attention. Small and partial-row grids avoid an extra projection dispatch; reported matrix configurations, subgroup width and resource limits retain portable fallbacks.
92
+ - `f16_attn_bias_serial` — Fuses the Q/K/V projection into a register-tiled GEMM, then runs portable serial-key attention: one workgroup owns one (packed token, head) and reduces each dot through workgroup memory. Head dimensions need no alignment and the value head dimension may differ.
93
+
94
+ ## Device requirements
95
+
96
+ Some implementation variants require `subgroup-matrix` and `subgroups`. These are route-specific capabilities, not package-wide requirements; availability also depends on the request shape and dtype.
97
+
98
+ ## Files
99
+
100
+ - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, per-variant templates, provenance)
101
+ - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
102
+ - [`test.json`](build/webgpu/test.json) — correctness cases
103
+ - [`bench.json`](build/webgpu/bench.json) — benchmark cases
104
+ - [`attn-packed-varlen-cluster.wgsl.jinja`](build/webgpu/attn-packed-varlen-cluster.wgsl.jinja)
105
+ - [`attn-packed-varlen-scalar.wgsl.jinja`](build/webgpu/attn-packed-varlen-scalar.wgsl.jinja)
106
+ - [`gemm-epilogue-tiled-reg.wgsl.jinja`](build/webgpu/gemm-epilogue-tiled-reg.wgsl.jinja)
107
+ - [`gemm-subgroup-matrix.wgsl.jinja`](build/webgpu/gemm-subgroup-matrix.wgsl.jinja)
108
+
109
+ ## Use with `@huggingface/kernels`
110
+
111
+ ```sh
112
+ npm install --save-exact @huggingface/kernels@0.0.1-preview.3
113
+ ```
114
+
115
+ Required output shapes and logical data types are inferred from the supplied inputs and attributes; result tensors are allocated automatically.
116
+
117
+ The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
118
+ It follows the `v1` branch as fixes land. To pin exact artifact bytes, pass a 40-character commit `revision` instead of `version`.
119
+
120
+ Replace each `*Data` placeholder with a typed array containing the corresponding input data.
121
+
122
+ ```js
123
+ import { getKernel } from "@huggingface/kernels";
124
+
125
+ const kernel = await getKernel("webgpu-kernels/com.microsoft.PackedAttention", { version: 1 });
126
+ const { outputT } = await kernel({
127
+ inputT: { data: inputTData, shape: [2, 4] },
128
+ weightsT: { data: weightsTData, shape: [4, 12] },
129
+ biasT: { data: biasTData, shape: [12] },
130
+ tokenOffsetT: { data: tokenOffsetTData, shape: [1, 2] },
131
+ cumulativeSequenceLengthT: { data: cumulativeSequenceLengthTData, shape: [2] },
132
+ }, {
133
+ attrs: { num_heads: 2 },
134
+ });
135
+ ```
build/webgpu/attn-packed-varlen-cluster.wgsl.jinja ADDED
@@ -0,0 +1,390 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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" %}
8
+ {% set VBUF = "query" %}
9
+ {% set ST = "f16" if packedStageBytes == 2 else "f32" %}
10
+ {% set SLICE_COUNT = (((headDimV4 + LPQ - 1) / LPQ) | int) %}
11
+ {% set queryTail = headDimV4 % LPQ != 0 %}
12
+ {% set VALUE_DIM_V4 = ((vHeadDim / 4) | int) %}
13
+ {% set VALUE_SLICES = (((VALUE_DIM_V4 + LPQ - 1) / LPQ) | int) %}
14
+ {% set unequalValue = vHeadDim != headDim %}
15
+ {% set valueTail = VALUE_DIM_V4 % LPQ != 0 %}
16
+ {% set valueWidth = "VALUE_HEAD_DIM_V4" if unequalValue else "HEAD_DIM_V4" %}
17
+ {% set valueLane = "valueLaneSlice" if unequalValue else "laneSlice" %}
18
+
19
+ // Tiled flash attention over a padding-removed packed token stream. One
20
+ // workgroup covers TILE_Q consecutive packed tokens for one head, with an
21
+ // LPQ-lane cluster per token; K and V are staged in workgroup memory once per
22
+ // TILE_K keys and reused by every token in the tile. Each lane owns contiguous
23
+ // query and output vec4 slices, sized independently for the Q/K and V widths.
24
+ // Dot products, online softmax and weighted value sums accumulate in f32; output
25
+ // conversion happens at the store. The staging type above governs biased K/V.
26
+
27
+ // A packed tile can straddle a sequence boundary, so the key loop runs over the
28
+ // workgroup's union range [wgStart, wgEnd) — the first token's sequence start to
29
+ // the last token's sequence end — so the trip count is uniform, and each token
30
+ // masks the keys outside its own sequence.
31
+
32
+ // Source: one packed `[T, N, 3, H]` buffer; Q, K and V differ only by offset.
33
+ {% if hasAttnBias %}
34
+
35
+ // The additive attention bias lives in PADDED coordinates: its trailing two axes
36
+ // are indexed by the query's and the key's position inside their shared
37
+ // sequence, not by their packed indices. A zero broadcast stride collapses the
38
+ // batch or head axis.
39
+ {% endif %}
40
+
41
+ {% if useSubgroups %}
42
+ // The per-lane dot fragments combine through a log2(LPQ) subgroupShuffleXor
43
+ // butterfly inside each cluster.
44
+ {% else %}
45
+ // The per-lane dot fragments combine through workgroup memory; this form
46
+ // requires no subgroup feature.
47
+ {% endif %}
48
+ const HEAD_DIM: u32 = {{ headDim }}u;
49
+ const HEAD_DIM_V4: u32 = {{ headDimV4 }}u;
50
+ {% if unequalValue %}
51
+ const VALUE_HEAD_DIM_V4: u32 = {{ VALUE_DIM_V4 }}u;
52
+ {% endif %}
53
+ const Q_HEADS: u32 = {{ qNumHeads }}u;
54
+ const Q_ROW_V4: u32 = {{ qRowStrideV4 }}u;
55
+ const Q_HEAD_V4: u32 = {{ qHeadStrideV4 }}u;
56
+ const K_ROW_V4: u32 = {{ kRowStrideV4 }}u;
57
+ const K_HEAD_V4: u32 = {{ kHeadStrideV4 }}u;
58
+ const K_OFFSET_V4: u32 = {{ kOffsetV4 }}u;
59
+ const V_ROW_V4: u32 = {{ vRowStrideV4 }}u;
60
+ const V_HEAD_V4: u32 = {{ vHeadStrideV4 }}u;
61
+ const V_OFFSET_V4: u32 = {{ vOffsetV4 }}u;
62
+ const O_ROW_V4: u32 = {{ oRowStrideV4 }}u;
63
+ const O_HEAD_V4: u32 = {{ oHeadStrideV4 }}u;
64
+ const TILE_Q: u32 = {{ TILE_Q }}u;
65
+ const TILE_K: u32 = {{ TILE_K }}u;
66
+ // Lanes per token cluster, sized by the manifest for query and value ownership.
67
+ const LPQ: u32 = {{ LPQ }}u;
68
+ const SLICE: u32 = {% if queryTail %}{{ SLICE_COUNT }}u{% else %}HEAD_DIM_V4 / LPQ{% endif %}; // query vec4s per lane slice
69
+ const WG: u32 = TILE_Q * LPQ;
70
+ const NEG_INF: f32 = -3.4028234663852886e38;
71
+
72
+ var<workgroup> k_tile: array<vec4<{{ ST }}>, TILE_K * HEAD_DIM_V4>;
73
+ var<workgroup> v_tile: array<vec4<{{ ST }}>, TILE_K * {{ valueWidth }}>;
74
+ {% if not useSubgroups %}
75
+ // Reuse scratch for dot fragments, maxima, sums and probabilities when the
76
+ // manifest admits the full tile; otherwise retain one key's reduction slots.
77
+ 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 %}>;
78
+ {% endif %}
79
+
80
+ {% set ATTN_SCALE_DIM = "HEAD_DIM" %}fn scale_value() -> f32 {
81
+ if (params.scale != 0.0) { return params.scale; }
82
+ return inverseSqrt(f32({{ ATTN_SCALE_DIM }}));
83
+ }
84
+ // Packed-token metadata. `token_offset` records, for every packed token, its
85
+ // flat index in the padded `[batch, sequence]` grid, so the sequence index and
86
+ // the position inside that sequence are one load and two integer operations.
87
+ // Packing keeps a sequence's tokens contiguous and in ascending position order,
88
+ // so the packed index of a sequence's first token is `token - position` and the
89
+ // sequence's packed key range is [keyStart, keyEnd).
90
+ struct PackedToken {
91
+ sequence: u32,
92
+ position: u32,
93
+ keyStart: u32,
94
+ keyEnd: u32,
95
+ }
96
+
97
+ fn packed_token_of(token: u32, seqPad: u32) -> PackedToken {
98
+ let flat = u32(token_offset[token]);
99
+ var info: PackedToken;
100
+ info.sequence = flat / seqPad;
101
+ info.position = flat - info.sequence * seqPad;
102
+ info.keyStart = token - info.position;
103
+ info.keyEnd = u32(cumulative_sequence_length[info.sequence + 1u]);
104
+ return info;
105
+ }
106
+
107
+ {% macro packed_dot_fragment() %}
108
+ var part: f32 = 0.0;
109
+ let kb = kk * HEAD_DIM_V4 + laneSlice;
110
+ {% for c in range(SLICE_COUNT) %}
111
+ {% if queryTail %}
112
+ if (laneSlice + {{ c }}u < HEAD_DIM_V4) {
113
+ {% endif %}
114
+ part = part + dot(qr{{ c }}, vec4<f32>(k_tile[kb + {{ c }}u]));
115
+ {% if queryTail %}
116
+ }
117
+ {% endif %}
118
+ {% endfor %}
119
+ {% endmacro %}
120
+ {% macro packed_value_accumulate() %}
121
+ let vRow = kk * {{ valueWidth }} + {{ valueLane }};
122
+ {% for c in range(VALUE_SLICES) %}
123
+ {% if valueTail %}
124
+ if ({{ valueLane }} + {{ c }}u < {{ valueWidth }}) {
125
+ {% endif %}
126
+ o{{ c }} = o{{ c }} + p * vec4<f32>(v_tile[vRow + {{ c }}u]);
127
+ {% if valueTail %}
128
+ }
129
+ {% endif %}
130
+ {% endfor %}
131
+ {% endmacro %}
132
+ {% macro packed_score_write(score_index = "kk") %}
133
+ if (kj >= minKj && kj < maxKj) {
134
+ {% if hasAttnBias %}
135
+ let maskIndex = info.sequence * params.maskBatchStride + h * params.maskHeadStride
136
+ + info.position * params.maskSeqStride + (kj - minKj);
137
+ s[{{ score_index }}] = part * SCALE + f32(attn_bias[maskIndex]);
138
+ {% else %}
139
+ s[{{ score_index }}] = part * SCALE;
140
+ {% endif %}
141
+ }
142
+ {% endmacro %}
143
+
144
+ @compute @workgroup_size(WG, 1, 1)
145
+ fn main(
146
+ @builtin(workgroup_id) wg: vec3<u32>,
147
+ @builtin(local_invocation_id) lid: vec3<u32>{% if useSubgroups %},
148
+ @builtin(subgroup_size) sgSize: u32{% endif %}
149
+ ) {
150
+ let h = wg.y;
151
+ // The query-tile axis is folded over x and z with a baked stride; z is 1
152
+ // whenever the tile count fits one dimension, so the fold term vanishes there.
153
+ let tile = wg.x + wg.z * {{ DISPATCH_FOLD_WIDTH }}u;
154
+ let firstToken = tile * TILE_Q;
155
+ if (firstToken >= params.tokenCount || h >= Q_HEADS) {
156
+ return;
157
+ }
158
+ {% if useSubgroups %}
159
+ // The subgroupShuffleXor butterfly requires every LPQ-lane cluster to fit in,
160
+ // and align with, one subgroup. Keep a uniform guard for defensive portability.
161
+ if (sgSize < LPQ || sgSize % LPQ != 0u) { return; }
162
+ {% endif %}
163
+
164
+ let tid = lid.x;
165
+ let qSub = tid / LPQ;
166
+ let lane = tid % LPQ;
167
+ let qIdx = firstToken + qSub;
168
+ let qValid = qIdx < params.tokenCount;
169
+ let qClamped = min(qIdx, params.tokenCount - 1u);
170
+ let info = packed_token_of(qClamped, params.seqPad);
171
+ let minKj = info.keyStart;
172
+ let maxKj = info.keyEnd;
173
+
174
+ // Union key range for the whole tile: packed tokens are ordered, so the first
175
+ // token's sequence start and the last token's sequence end bound every token
176
+ // in the tile.
177
+ let lastToken = min(firstToken + TILE_Q - 1u, params.tokenCount - 1u);
178
+ let wgStart = packed_token_of(firstToken, params.seqPad).keyStart;
179
+ let wgEnd = packed_token_of(lastToken, params.seqPad).keyEnd;
180
+
181
+ let SCALE = scale_value();
182
+ let laneSlice = lane * SLICE;
183
+ {% if unequalValue %}
184
+ let valueLaneSlice = lane * {{ VALUE_SLICES }}u;
185
+ {% endif %}
186
+ let qBase4 = qClamped * Q_ROW_V4 + h * Q_HEAD_V4 + laneSlice;
187
+ {% for c in range(SLICE_COUNT) %}
188
+ {% if queryTail %}
189
+ var qr{{ c }} = vec4<f32>(0.0);
190
+ if (laneSlice + {{ c }}u < HEAD_DIM_V4) {
191
+ qr{{ c }} = vec4<f32>({{ QBUF }}[qBase4 + {{ c }}u]);
192
+ }
193
+ {% else %}
194
+ var qr{{ c }} = vec4<f32>({{ QBUF }}[qBase4 + {{ c }}u]);
195
+ {% endif %}
196
+ {% if not unequalValue %}
197
+ var o{{ c }} = vec4<f32>(0.0);
198
+ {% endif %}
199
+ {% endfor %}
200
+ {% if unequalValue %}
201
+ {% for c in range(VALUE_SLICES) %}
202
+ var o{{ c }} = vec4<f32>(0.0);
203
+ {% endfor %}
204
+ {% endif %}
205
+
206
+ var m: f32 = NEG_INF;
207
+ var l: f32 = 0.0;
208
+
209
+ var kStart: u32 = wgStart;
210
+ loop {
211
+ if (kStart >= wgEnd) { break; }
212
+
213
+ // Cooperative K/V tile load with out-of-range keys zero-filled. The K and V
214
+ // biases are token-independent, so they fold in here, once per staged word.
215
+ workgroupBarrier();
216
+ {% if unequalValue %}
217
+ {% for tile, buffer, rowStride, headStride, offset, width, biasRow, biasWidth in [
218
+ ["k_tile", KBUF, "K_ROW_V4", "K_HEAD_V4", "K_OFFSET_V4", "HEAD_DIM_V4", "BIAS_K", "HEAD_DIM"],
219
+ ["v_tile", VBUF, "V_ROW_V4", "V_HEAD_V4", "V_OFFSET_V4", "VALUE_HEAD_DIM_V4", "BIAS_V", "VALUE_HEAD_DIM"]
220
+ ] %}
221
+ for (var i: u32 = tid; i < TILE_K * {{ width }}; i = i + WG) {
222
+ let slot = i / {{ width }};
223
+ let d4 = i % {{ width }};
224
+ let kj = kStart + slot;
225
+ if (kj < wgEnd) {
226
+ var word = vec4<f32>({{ buffer }}[kj * {{ rowStride }} + h * {{ headStride }} + {{ offset }} + d4]);
227
+ {{ tile }}[i] = vec4<{{ ST }}>(word);
228
+ } else {
229
+ {{ tile }}[i] = vec4<{{ ST }}>(0.0);
230
+ }
231
+ }
232
+ {% endfor %}
233
+ {% else %}
234
+ for (var i: u32 = tid; i < TILE_K * HEAD_DIM_V4; i = i + WG) {
235
+ let slot = i / HEAD_DIM_V4;
236
+ let d4 = i % HEAD_DIM_V4;
237
+ let kj = kStart + slot;
238
+ if (kj < wgEnd) {
239
+ var kWord = vec4<f32>({{ KBUF }}[kj * K_ROW_V4 + h * K_HEAD_V4 + K_OFFSET_V4 + d4]);
240
+ var vWord = vec4<f32>({{ VBUF }}[kj * V_ROW_V4 + h * V_HEAD_V4 + V_OFFSET_V4 + d4]);
241
+ k_tile[i] = vec4<{{ ST }}>(kWord);
242
+ v_tile[i] = vec4<{{ ST }}>(vWord);
243
+ } else {
244
+ k_tile[i] = vec4<{{ ST }}>(0.0);
245
+ v_tile[i] = vec4<{{ ST }}>(0.0);
246
+ }
247
+ }
248
+ {% endif %}
249
+ workgroupBarrier();
250
+
251
+ {% if parallelSoftmax %}
252
+ // Each cluster lane owns disjoint key scores and exponentials. The dot
253
+ // fragments, maxima, sums and probabilities reuse one shared allocation.
254
+ var s: array<f32, {{ ((TILE_K + LPQ - 1) / LPQ) | int }}>;
255
+ for (var kk: u32 = 0u; kk < TILE_K; kk = kk + 1u) {
256
+ {{ packed_dot_fragment() }}
257
+ red[kk * WG + tid] = part;
258
+ }
259
+ workgroupBarrier();
260
+ var localMax = NEG_INF;
261
+ for (var kk: u32 = lane; kk < TILE_K; kk = kk + LPQ) {
262
+ let kj = kStart + kk;
263
+ let key_slot = kk / LPQ;
264
+ s[key_slot] = NEG_INF;
265
+ var part: f32 = 0.0;
266
+ let cbase = kk * WG + tid - lane;
267
+ for (var j: u32 = 0u; j < LPQ; j = j + 1u) {
268
+ part = part + red[cbase + j];
269
+ }
270
+ {{ packed_score_write("key_slot") }}
271
+ localMax = max(localMax, s[key_slot]);
272
+ }
273
+ // All fragment readers finish before their storage is reused for maxima.
274
+ workgroupBarrier();
275
+ red[tid] = localMax;
276
+ workgroupBarrier();
277
+ let cbase = tid - lane;
278
+ // Keyless lanes contribute only NEG_INF or zero to max/sum. Dot fragments
279
+ // above still consume every lane, including when TILE_K is smaller than LPQ.
280
+ var tileMax = NEG_INF;
281
+ for (var j: u32 = 0u; j < {{ "LPQ" if TILE_K >= LPQ else "TILE_K" }}; j = j + 1u) {
282
+ tileMax = max(tileMax, red[cbase + j]);
283
+ }
284
+ let newMax = max(m, tileMax);
285
+ let corr = select(exp(m - newMax), 0.0, m == NEG_INF);
286
+ var localSum: f32 = 0.0;
287
+ for (var kk: u32 = lane; kk < TILE_K; kk = kk + LPQ) {
288
+ let score = s[kk / LPQ];
289
+ let pk = select(0.0, exp(score - newMax), score != NEG_INF);
290
+ red[2u * WG + qSub * TILE_K + kk] = pk;
291
+ localSum = localSum + pk;
292
+ }
293
+ // Sum slots are disjoint from the maxima still being consumed above.
294
+ red[WG + tid] = localSum;
295
+ workgroupBarrier();
296
+ var pSum: f32 = 0.0;
297
+ for (var j: u32 = 0u; j < {{ "LPQ" if TILE_K >= LPQ else "TILE_K" }}; j = j + 1u) {
298
+ pSum = pSum + red[WG + cbase + j];
299
+ }
300
+ l = l * corr + pSum;
301
+ m = newMax;
302
+ {% else %}
303
+ var s: array<f32, TILE_K>;
304
+ for (var kk: u32 = 0u; kk < TILE_K; kk = kk + 1u) {
305
+ let kj = kStart + kk;
306
+ s[kk] = NEG_INF;
307
+ {{ packed_dot_fragment() }}
308
+ {% if useSubgroups %}
309
+ // log2(LPQ) butterfly all-reduce inside the cluster: every lane ends with
310
+ // the full dot.
311
+ part = part + subgroupShuffleXor(part, 1u);
312
+ {% for shift in [2, 4, 8, 16, 32] %}
313
+ {% if LPQ > shift %}
314
+ part = part + subgroupShuffleXor(part, {{ shift }}u);
315
+ {% endif %}
316
+ {% endfor %}
317
+ {% else %}
318
+ red[tid] = part;
319
+ workgroupBarrier();
320
+ let cbase = tid - lane;
321
+ part = 0.0;
322
+ for (var j: u32 = 0u; j < LPQ; j = j + 1u) {
323
+ part = part + red[cbase + j];
324
+ }
325
+ workgroupBarrier();
326
+ {% endif %}
327
+ {{ packed_score_write() }}
328
+ }
329
+
330
+ // Per-lane online softmax over the tile. s[kk] is reused to hold the
331
+ // exponentiated probabilities for the value accumulation below.
332
+ var tileMax: f32 = s[0];
333
+ for (var kk: u32 = 1u; kk < TILE_K; kk = kk + 1u) {
334
+ tileMax = max(tileMax, s[kk]);
335
+ }
336
+ let newMax = max(m, tileMax);
337
+ let corr = select(exp(m - newMax), 0.0, m == NEG_INF);
338
+ var pSum: f32 = 0.0;
339
+ for (var kk: u32 = 0u; kk < TILE_K; kk = kk + 1u) {
340
+ let pk = select(0.0, exp(s[kk] - newMax), s[kk] != NEG_INF);
341
+ s[kk] = pk;
342
+ pSum = pSum + pk;
343
+ }
344
+ l = l * corr + pSum;
345
+ m = newMax;
346
+
347
+ {% endif %}
348
+
349
+ // A boundary tile addresses V rows outside this token's sequence. Because
350
+ // 0 * NaN is NaN, the guarded loop skips the V read entirely for a key with
351
+ // no probability mass; an interior tile runs the unguarded FMA chain. One
352
+ // range test covers the whole lane slice, and one probability load feeds
353
+ // every vec4 of it.
354
+ let tileInterior = kStart >= minKj && kStart + TILE_K <= maxKj;
355
+ {% for c in range(VALUE_SLICES) %}
356
+ o{{ c }} = o{{ c }} * corr;
357
+ {% endfor %}
358
+ if (tileInterior) {
359
+ for (var kk: u32 = 0u; kk < TILE_K; kk = kk + 1u) {
360
+ let p = {% if parallelSoftmax %}red[2u * WG + qSub * TILE_K + kk]{% else %}s[kk]{% endif %};
361
+ {{ packed_value_accumulate() }}
362
+ }
363
+ } else {
364
+ for (var kk: u32 = 0u; kk < TILE_K; kk = kk + 1u) {
365
+ let p = {% if parallelSoftmax %}red[2u * WG + qSub * TILE_K + kk]{% else %}s[kk]{% endif %};
366
+ if (p != 0.0) {
367
+ {{ packed_value_accumulate() }}
368
+ }
369
+ }
370
+ }
371
+
372
+ kStart = kStart + TILE_K;
373
+ }
374
+
375
+ if (qValid) {
376
+ // l == 0 means this token had no probability-bearing key. Emit 0 rather
377
+ // than 0/0.
378
+ let inv = select(0.0, 1.0 / l, l > 0.0);
379
+ let outBase4 = qIdx * O_ROW_V4 + h * O_HEAD_V4 + {{ valueLane }};
380
+ {% for c in range(VALUE_SLICES) %}
381
+ {% if valueTail %}
382
+ if ({{ valueLane }} + {{ c }}u < {{ valueWidth }}) {
383
+ {% endif %}
384
+ output[outBase4 + {{ c }}u] = vec4<{{ scalar }}>(o{{ c }} * inv);
385
+ {% if valueTail %}
386
+ }
387
+ {% endif %}
388
+ {% endfor %}
389
+ }
390
+ }
build/webgpu/attn-packed-varlen-scalar.wgsl.jinja ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ env.wgsl.resourceDeclarations }}
2
+ {% set QBUF = "query" %}
3
+ {% set KBUF = "query" %}
4
+ {% set VBUF = "query" %}
5
+
6
+ // Online-softmax attention over a padding-removed packed token stream, with no
7
+ // feature requirements. One workgroup owns one (packed token, query head): the
8
+ // workgroup walks its own sequence's keys serially, cooperates on each q·k dot
9
+ // through a shared-memory tree reduction, and applies the online rescale to a
10
+ // running value accumulator per key. Head dimensions need no alignment and the
11
+ // V head dimension may differ from the Q/K head dimension.
12
+ // Source: one packed `[T, N, 3, H]` buffer; Q, K and V differ only by offset.
13
+ {% if hasAttnBias %}
14
+ // The additive attention bias lives in PADDED coordinates: its trailing two
15
+ // axes are indexed by the query's and the key's position inside their shared
16
+ // sequence, not by their packed indices. A zero broadcast stride collapses the
17
+ // batch or head axis.
18
+ {% endif %}
19
+ const HEAD_DIM: u32 = {{ headDim }}u;
20
+ const V_HEAD_DIM: u32 = {{ vHeadDim }}u;
21
+ const Q_HEADS: u32 = {{ qNumHeads }}u;
22
+ const Q_ROW: u32 = {{ qRowStride }}u;
23
+ const Q_HEAD: u32 = {{ qHeadStride }}u;
24
+ const K_ROW: u32 = {{ kRowStride }}u;
25
+ const K_HEAD: u32 = {{ kHeadStride }}u;
26
+ const K_OFFSET: u32 = {{ kOffset }}u;
27
+ const V_ROW: u32 = {{ vRowStride }}u;
28
+ const V_HEAD: u32 = {{ vHeadStride }}u;
29
+ const V_OFFSET: u32 = {{ vOffset }}u;
30
+ const O_ROW: u32 = {{ oRowStride }}u;
31
+ const O_HEAD: u32 = {{ oHeadStride }}u;
32
+ const WG: u32 = {{ workgroupSize }}u;
33
+
34
+ var<workgroup> partial: array<f32, WG>;
35
+ var<workgroup> running_max: f32;
36
+ var<workgroup> running_denom: f32;
37
+ var<workgroup> running_out: array<f32, V_HEAD_DIM>;
38
+ var<workgroup> previous_scale: f32;
39
+ {% macro wgsl_tree_reduce_f32(name, mode, buffer="partial", wg="WG", trailingBarrier=true, valueType="f32") %}
40
+ fn {{ name }}(value: {{ valueType }}, tid: u32) -> {{ valueType }} {
41
+ {{ buffer }}[tid] = value;
42
+ workgroupBarrier();
43
+ // Ceil-halving keeps every lane when the workgroup size is not a power of
44
+ // two. For even n this matches the power-of-two tree order; for odd n, lanes
45
+ // [0, n-half) fold the upper tail while the middle lane carries forward.
46
+ var n: u32 = {{ wg }};
47
+ loop {
48
+ let half = (n + 1u) / 2u;
49
+ if (tid < n - half) {
50
+ {{ buffer }}[tid] = {{ buffer }}[tid] + {{ buffer }}[tid + half];
51
+ }
52
+ workgroupBarrier();
53
+ n = half;
54
+ if (n == 1u) {
55
+ break;
56
+ }
57
+ }
58
+ // The default trailing barrier makes this helper safe for back-to-back calls: every lane reads
59
+ // slot 0 here, so the next call's first store must not run until all lanes have read it.
60
+ // `trailingBarrier=false` is safe only when the buffer is never written again before kernel exit.
61
+ let reduced = {{ buffer }}[0];
62
+ workgroupBarrier();
63
+ return reduced;
64
+ }
65
+ {% endmacro %}
66
+ {{ wgsl_tree_reduce_f32("reduce_sum", "add", "partial", "WG") }}
67
+ {% set ATTN_SCALE_DIM = "HEAD_DIM" %}fn scale_value() -> f32 {
68
+ if (params.scale != 0.0) { return params.scale; }
69
+ return inverseSqrt(f32({{ ATTN_SCALE_DIM }}));
70
+ }
71
+ // Packed-token metadata. `token_offset` records, for every packed token, its
72
+ // flat index in the padded `[batch, sequence]` grid, so the sequence index and
73
+ // the position inside that sequence are one load and two integer operations.
74
+ // Packing keeps a sequence's tokens contiguous and in ascending position order,
75
+ // so the packed index of a sequence's first token is `token - position` and the
76
+ // sequence's packed key range is [keyStart, keyEnd).
77
+ struct PackedToken {
78
+ sequence: u32,
79
+ position: u32,
80
+ keyStart: u32,
81
+ keyEnd: u32,
82
+ }
83
+
84
+ fn packed_token_of(token: u32, seqPad: u32) -> PackedToken {
85
+ let flat = u32(token_offset[token]);
86
+ var info: PackedToken;
87
+ info.sequence = flat / seqPad;
88
+ info.position = flat - info.sequence * seqPad;
89
+ info.keyStart = token - info.position;
90
+ info.keyEnd = u32(cumulative_sequence_length[info.sequence + 1u]);
91
+ return info;
92
+ }
93
+
94
+ @compute @workgroup_size(WG, 1, 1)
95
+ fn main(
96
+ @builtin(workgroup_id) wg: vec3<u32>,
97
+ @builtin(local_invocation_id) lid: vec3<u32>
98
+ ) {
99
+ // The token axis is folded over x and z with a baked stride; z is 1 whenever
100
+ // the token count fits one dimension, so the fold term vanishes there.
101
+ let token = wg.x + wg.z * {{ DISPATCH_FOLD_WIDTH }}u;
102
+ let h = wg.y;
103
+ if (token >= params.tokenCount || h >= Q_HEADS) {
104
+ return;
105
+ }
106
+
107
+ let tid = lid.x;
108
+ let info = packed_token_of(token, params.seqPad);
109
+ let qBase = token * Q_ROW + h * Q_HEAD;
110
+ let oBase = token * O_ROW + h * O_HEAD;
111
+
112
+ if (tid == 0u) {
113
+ running_max = -3.4028234663852886e38;
114
+ running_denom = 0.0;
115
+ }
116
+ for (var d: u32 = tid; d < V_HEAD_DIM; d = d + WG) {
117
+ running_out[d] = 0.0;
118
+ }
119
+ workgroupBarrier();
120
+
121
+ for (var keyToken: u32 = info.keyStart; keyToken < info.keyEnd; keyToken = keyToken + 1u) {
122
+ let kBase = keyToken * K_ROW + h * K_HEAD + K_OFFSET;
123
+ var partial_dot = 0.0;
124
+ for (var d: u32 = tid; d < HEAD_DIM; d = d + WG) {
125
+ var q_value = f32({{ QBUF }}[qBase + d]);
126
+ var k_value = f32({{ KBUF }}[kBase + d]);
127
+ partial_dot = partial_dot + q_value * k_value;
128
+ }
129
+
130
+ // reduce_sum returns the same partial[0] to every lane, so `score` is
131
+ // already workgroup-uniform; tid==0 advances the running max/denominator
132
+ // (broadcast through shared memory by the barrier below).
133
+ {% if hasAttnBias %}
134
+ let maskIndex = info.sequence * params.maskBatchStride + h * params.maskHeadStride
135
+ + info.position * params.maskSeqStride + (keyToken - info.keyStart);
136
+ let score = reduce_sum(partial_dot, tid) * scale_value() + f32(attn_bias[maskIndex]);
137
+ {% else %}
138
+ let score = reduce_sum(partial_dot, tid) * scale_value();
139
+ {% endif %}
140
+ if (tid == 0u) {
141
+ let next_max = max(running_max, score);
142
+ let old_factor = exp(running_max - next_max);
143
+ let new_factor = exp(score - next_max);
144
+ previous_scale = old_factor;
145
+ running_max = next_max;
146
+ running_denom = running_denom * old_factor + new_factor;
147
+ }
148
+ workgroupBarrier();
149
+
150
+ let probability_numerator = exp(score - running_max);
151
+ let vBase = keyToken * V_ROW + h * V_HEAD + V_OFFSET;
152
+ for (var d: u32 = tid; d < V_HEAD_DIM; d = d + WG) {
153
+ var v_value = f32({{ VBUF }}[vBase + d]);
154
+ running_out[d] = running_out[d] * previous_scale + probability_numerator * v_value;
155
+ }
156
+ workgroupBarrier();
157
+ }
158
+
159
+ // A token whose sequence carries no key leaves running_denom at zero; emit a
160
+ // zero row rather than 0 * (1/0) = NaN.
161
+ let inv_denom = select(0.0, 1.0 / running_denom, running_denom > 0.0);
162
+ for (var d: u32 = tid; d < V_HEAD_DIM; d = d + WG) {
163
+ output[oBase + d] = {{ outputScalar }}(running_out[d] * inv_denom);
164
+ }
165
+ }
build/webgpu/bench.json ADDED
The diff for this file is too large to render. See raw diff
 
build/webgpu/gemm-epilogue-tiled-reg.wgsl.jinja ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% set OUT = "f16" if (usesF16 is defined and usesF16) else "f32" %}
2
+ {{ env.wgsl.resourceDeclarations }}
3
+
4
+ // Shape-driven, register-blocked f32 GEMM for all four transpose combinations.
5
+ // Runtime M/N/K share one compiled pipeline for each tile configuration.
6
+ // Register micro-tiles reuse staged K slices; transpose modes change load strides and
7
+ // keep the stored-contiguous axis coalesced. M/N/K tails are bounds-checked
8
+ // unless an authored dimension proves that its tile has no tail.
9
+ // Both tiles are indexed by their own output axis and group four K values per
10
+ // vector word, so the micro-tile accumulates through dot() and one step reads
11
+ // TM + TN words instead of 4 * (TM + TN) scalars.
12
+ {% set epiOpen = "" %}
13
+ {% set epiClose = "" %}
14
+ {% set gemmWgX = ((gemmTileCols / gemmMicroCols) | int) %}
15
+ {% set gemmWgY = ((gemmTileRows / gemmMicroRows) | int) %}
16
+ {% set gemmTileK = 16 %}
17
+ const BK: u32 = {{ gemmTileK }}u;
18
+ const BM: u32 = {{ gemmTileRows }}u;
19
+ const BN: u32 = {{ gemmTileCols }}u;
20
+ const TM: u32 = {{ gemmMicroRows }}u; // per-thread micro-tile rows
21
+ const TN: u32 = {{ gemmMicroCols }}u; // per-thread micro-tile cols
22
+
23
+ // Stage tiles at the operands' storage width. For f16 inputs this is
24
+ // bit-identical to widening on shared load, keeps f32 FMA registers, and halves
25
+ // workgroup storage.
26
+ {% set tileT = OUT %}
27
+ const K_VECS: u32 = BK / 4u;
28
+ var<workgroup> tileA: array<array<vec4<{{ tileT }}>, K_VECS>, BM>; // logical A'[m][k/4]
29
+ var<workgroup> tileB: array<array<vec4<{{ tileT }}>, K_VECS>, BN>; // logical B'[n][k/4]
30
+
31
+ @compute @workgroup_size({{ gemmWgX }}, {{ gemmWgY }}, 1)
32
+ fn main(
33
+ @builtin(workgroup_id) wg: vec3<u32>,
34
+ @builtin(local_invocation_id) lid: vec3<u32>,
35
+ @builtin(num_workgroups) nwg: vec3<u32>
36
+ ) {
37
+ let li = lid.y * {{ gemmWgX }}u + lid.x;
38
+ let M = params.M;
39
+ let N = params.N;
40
+ let K = params.K;
41
+ let n_tiles = (N + BN - 1u) / BN;
42
+ let m_tiles = (M + BM - 1u) / BM;
43
+ for (var nt: u32 = wg.x; nt < n_tiles; nt = nt + nwg.x) {
44
+ let nBase = nt * BN;
45
+ for (var mt: u32 = wg.y; mt < m_tiles; mt = mt + nwg.y) {
46
+ let mBase = mt * BM;
47
+
48
+ var acc: array<f32, TM * TN>;
49
+ for (var i: u32 = 0u; i < TM * TN; i = i + 1u) { acc[i] = 0.0; }
50
+
51
+ let numTiles = (K + BK - 1u) / BK;
52
+ for (var kt: u32 = 0u; kt < numTiles; kt = kt + 1u) {
53
+ let kBase = kt * BK;
54
+ // Cooperative load: one vector word per lane per pass. The lane grid is
55
+ // laid out so the axis the operand stores contiguously stays coalesced.
56
+ for (var idx: u32 = li; idx < BM * K_VECS; idx = idx + {{ gemmWgX * gemmWgY }}u) {
57
+ // A stored [M, K]: logical A[m,k] = a[m*K + k]. Coalesce on k (contiguous).
58
+ let ar = idx / K_VECS;
59
+ let ac4 = idx % K_VECS;
60
+ let am = mBase + ar;
61
+ let ak = kBase + ac4 * 4u;
62
+ var aWord = vec4<{{ tileT }}>({{ tileT }}(0.0));
63
+ if (am < M) {
64
+ {% for component in range(4) %}
65
+ if (ak + {{ component }}u < K) {
66
+ aWord[{{ component }}u] = {{ tileT }}(a[am * K + ak + {{ component }}u]);
67
+ }
68
+ {% endfor %}
69
+ }
70
+ tileA[ar][ac4] = aWord;
71
+ }
72
+ for (var idx: u32 = li; idx < BN * K_VECS; idx = idx + {{ gemmWgX * gemmWgY }}u) {
73
+ // B stored [K, N]: logical B[k,n] = b[k*N + n]. Coalesce on n (contiguous).
74
+ let bc = idx % BN;
75
+ let br4 = idx / BN;
76
+ let bn = nBase + bc;
77
+ let bk = kBase + br4 * 4u;
78
+ var bWord = vec4<{{ tileT }}>({{ tileT }}(0.0));
79
+ if (bn < N) {
80
+ {% for component in range(4) %}
81
+ if (bk + {{ component }}u < K) {
82
+ bWord[{{ component }}u] = {{ tileT }}(b[(bk + {{ component }}u) * N + bn]);
83
+ }
84
+ {% endfor %}
85
+ }
86
+ tileB[bc][br4] = bWord;
87
+ }
88
+ workgroupBarrier();
89
+ {% set regAccumulator = "acc" %}
90
+ {% set regT = "f32" %}{% set regCast = 1 if tileT != "f32" else 0 %}
91
+ {% filter indent(8, true) %}
92
+ {% set regAccumulator = "acc" %}
93
+ let aRow = lid.y * TM;
94
+ let bCol = lid.x * TN;
95
+ for (var kv: u32 = 0u; kv < BK / 4u; kv = kv + 1u) {
96
+ var av: array<vec4<{{ regT }}>, TM>;
97
+ var bv: array<vec4<{{ regT }}>, TN>;
98
+ for (var i: u32 = 0u; i < TM; i = i + 1u) { av[i] = {% if regCast %}vec4<{{ regT }}>(tileA[aRow + i][kv]){% else %}tileA[aRow + i][kv]{% endif %}; }
99
+ for (var j: u32 = 0u; j < TN; j = j + 1u) { bv[j] = {% if regCast %}vec4<{{ regT }}>(tileB[bCol + j][kv]){% else %}tileB[bCol + j][kv]{% endif %}; }
100
+ for (var i: u32 = 0u; i < TM; i = i + 1u) {
101
+ for (var j: u32 = 0u; j < TN; j = j + 1u) {
102
+ {{ regAccumulator }}[i * TN + j] = {{ regAccumulator }}[i * TN + j] + dot(av[i], bv[j]);
103
+ }
104
+ }
105
+ }
106
+ {% endfilter %}
107
+ workgroupBarrier();
108
+ }
109
+
110
+ let m0 = mBase + lid.y * TM;
111
+ let n0 = nBase + lid.x * TN;
112
+ for (var ti: u32 = 0u; ti < TM; ti = ti + 1u) {
113
+ let m = m0 + ti;
114
+ if (m >= M) { continue; }
115
+ for (var tj: u32 = 0u; tj < TN; tj = tj + 1u) {
116
+ let n = n0 + tj;
117
+ if (n < N) {
118
+ // alpha == 0 nulls A*B exactly and avoids Inf*0 becoming NaN.
119
+ let product = select(acc[ti * TN + tj] * params.alpha, 0.0, params.alpha == 0.0);
120
+ let biasValue = f32(bias[n]);
121
+ let biasTerm = select(biasValue * params.beta, 0.0, params.beta == 0.0);
122
+ y[m * N + n] = {{ OUT }}({{ epiOpen }}product + biasTerm{{ epiClose }});
123
+ }
124
+ }
125
+ }
126
+ }
127
+ }
128
+ }
build/webgpu/gemm-subgroup-matrix.wgsl.jinja ADDED
@@ -0,0 +1,345 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ enable subgroups;
2
+ {% if pinSubgroupSize32 %}
3
+ enable subgroup_size_control;
4
+ {% endif %}
5
+ enable chromium_experimental_subgroup_matrix;
6
+ diagnostic(off, chromium.subgroup_matrix_uniformity);
7
+
8
+ {{ env.wgsl.resourceDeclarations }}
9
+
10
+ {% set operandScalar = mmaScalar %}
11
+ {% set accScalar = "f32" %}
12
+ {% set OUT = "y" %}
13
+ {% set OUT_SCALAR = outScalar if outScalar is defined else yScalar %}
14
+ {% set tileMValue = tileM if tileM is defined else 32 %}
15
+ {% set tileNValue = tileN if tileN is defined else 64 %}
16
+ {% set tileKValue = 32 %}
17
+ {% set epiOpen = "" %}
18
+ {% set epiClose = "" %}
19
+ {% set useDirectStore = false %}
20
+ {% set useDirectAStorage = directAStorage is defined and directAStorage %}
21
+ {% set useDoubleBufferedB = doubleBufferedB is defined and doubleBufferedB %}
22
+ {% set useBalancedAccumulation = balancedMatrixAccumulation is defined and balancedMatrixAccumulation and useDoubleBufferedB and not useDirectStore and operandScalar == "f32" %}
23
+ {% set columnTileBaseValue = 0 %}
24
+ {% set rowTileBaseValue = rowTileBase if rowTileBase is defined else 0 %}
25
+ // Four subgroups cover 32xN as a 2x2 grid, or taller tiles as four row bands.
26
+ {% if tileMValue > 32 %}
27
+ {% set subtileCols = tileNValue %}
28
+ {% set subtileRows = (tileMValue / 4)|int %}
29
+ {% else %}
30
+ {% set subtileCols = (tileNValue / 2)|int %}
31
+ {% set subtileRows = 16 %}
32
+ {% endif %}
33
+ {% set aTilesPerSubgroup = (subtileRows / 8)|int %}
34
+ {% set bTilesPerSubgroup = (subtileCols / 8)|int %}
35
+ {% set subgroupCount = 4 %}
36
+ {% set scratchBanks = subgroupCount * aTilesPerSubgroup * bTilesPerSubgroup %}
37
+ {% macro matrixAccumulators() %}
38
+ {% for m in range(aTilesPerSubgroup) %}
39
+ {% for n in range(bTilesPerSubgroup) %}
40
+ var matC{{ m }}{{ n }}: subgroup_matrix_result<{{ accScalar }}, 8, 8>;
41
+ {% if useBalancedAccumulation %}
42
+ var matD{{ m }}{{ n }}: subgroup_matrix_result<{{ accScalar }}, 8, 8>;
43
+ var matE{{ m }}{{ n }}: subgroup_matrix_result<{{ accScalar }}, 8, 8>;
44
+ var matF{{ m }}{{ n }}: subgroup_matrix_result<{{ accScalar }}, 8, 8>;
45
+ {% endif %}
46
+ {% endfor %}
47
+ {% endfor %}
48
+ {% endmacro %}
49
+ {% macro storeMatrixAccumulators() %}
50
+ {% for m in range(aTilesPerSubgroup) %}
51
+ {% for n in range(bTilesPerSubgroup) %}
52
+ subgroupMatrixStore<row_major>(&scratch[bank + {{ m * bTilesPerSubgroup + n }}u], 0u, matC{{ m }}{{ n }}, 8u);
53
+ {% if useBalancedAccumulation %}
54
+ subgroupMatrixStore<row_major>(&tile_B, (bank + {{ m * bTilesPerSubgroup + n }}u) * 64u, matD{{ m }}{{ n }}, 8u);
55
+ {% endif %}
56
+ {% endfor %}
57
+ {% endfor %}
58
+ {% endmacro %}
59
+
60
+ const IN_F: u32 = {{ inFeatures }}u;
61
+ const OUT_F: u32 = {{ outFeatures }}u;
62
+ {% set kLoopEndValue = "IN_F" %}
63
+ const TILE_COLS: u32 = {{ tileNValue }}u;
64
+ const TILE_ROWS: u32 = {{ tileMValue }}u;
65
+ const TILE_K: u32 = {{ tileKValue }}u;
66
+ const SUB_COLS: u32 = {{ subtileCols }}u;
67
+ const SUB_ROWS: u32 = {{ subtileRows }}u;
68
+ const COLUMN_TILE_BASE: u32 = {{ columnTileBaseValue }}u;
69
+ const ROW_TILE_BASE: u32 = {{ rowTileBaseValue }}u;
70
+ {% if not useDirectAStorage %}
71
+
72
+ var<workgroup> tile_A: array<{{ operandScalar }}, {{ tileMValue }} * {{ tileKValue }}>;
73
+ {% endif %}
74
+ var<workgroup> tile_B: array<{{ operandScalar }}, {{ (2 if useDoubleBufferedB else 1) * tileNValue }} * {{ tileKValue }}>;
75
+ // Distinct readback banks for every result matrix, so one barrier can publish
76
+ // the full subtile without write-after-read reuse.
77
+ var<workgroup> scratch: array<array<{{ accScalar }}, 64>, {{ scratchBanks }}>;
78
+
79
+ {% if not useDirectAStorage %}
80
+ fn loadSHMA(tile_base: u32, k_idx: u32, row: u32, c_idx: u32) {
81
+ // Clamp the partial-M tail to the last valid row (M-1) instead of zero-filling
82
+ // it. Some subgroup-matrix implementations let a zero-padded left tile corrupt
83
+ // the adjacent valid row. Replicating row M-1 avoids that boundary; the
84
+ // replicated rows are discarded by storeOutput's row_limit guard, so every valid
85
+ // output row stays bit-identical. M >= 1 is guaranteed by the when-clause args.M > 0.
86
+ let col: u32 = c_idx * 8u;
87
+ for (var row_offset: u32 = 0u; row_offset < TILE_ROWS; row_offset += {{ (1024 / tileKValue)|int }}u) {
88
+ let r: u32 = row + row_offset;
89
+ let a_global: u32 = min(tile_base + r, params.M - 1u);
90
+ for (var col_offset: u32 = 0u; col_offset < 8u; col_offset++) {
91
+ let k: u32 = k_idx + col + col_offset;
92
+ tile_A[r * TILE_K + col + col_offset] =
93
+ x[a_global * IN_F + k];
94
+ }
95
+ }
96
+ }
97
+
98
+ {% endif %}
99
+ fn loadSHMB(tile_base: u32, k_idx: u32, row: u32, c_idx: u32, bank_offset: u32) {
100
+ // Generic transB=0 stores B as [K,N]. Stage that native orientation and load
101
+ // the subgroup right operand without a transpose.
102
+ let local_idx = row * {{ (tileKValue / 16)|int }}u + c_idx;
103
+ let vectors_per_k = TILE_COLS / 4u;
104
+ let total_vectors = TILE_K * vectors_per_k;
105
+ for (var vector_idx = local_idx; vector_idx < total_vectors; vector_idx += 128u) {
106
+ let k_local = vector_idx / vectors_per_k;
107
+ let n4 = (vector_idx - k_local * vectors_per_k) * 4u;
108
+ for (var component = 0u; component < 4u; component++) {
109
+ let n_local = n4 + component;
110
+ let global_n = tile_base + n_local;
111
+ let global_k = k_idx + k_local;
112
+ tile_B[bank_offset + k_local * TILE_COLS + n_local] = w[global_k * OUT_F + global_n];
113
+ }
114
+ }
115
+ }
116
+
117
+ {% set BIAS_1 = " + f32(bias[col_base + col])" %}
118
+ {% set BIAS_2 = " + f32(bias[col_base + col2])" %}
119
+ {% set ACC_1 = "(prior.x + (scratch[src_slot][row * 8u + col] + tile_B[src_slot * 64u + row * 8u + col]))" if useBalancedAccumulation else "scratch[src_slot][row * 8u + col]" %}
120
+ {% set ACC_2 = "(prior.y + (scratch[src_slot][row * 8u + col2] + tile_B[src_slot * 64u + row * 8u + col2]))" if useBalancedAccumulation else "scratch[src_slot][row * 8u + col2]" %}
121
+ fn storeOutput(offset: u32, col_base: u32, row: u32, col: u32, src_slot: u32, row_limit: i32{% if useBalancedAccumulation %}, prior: vec2<f32>{% endif %}) {
122
+ if (row_limit > 0 && row < u32(row_limit)) {
123
+ let col2: u32 = col + 1u;
124
+ {{ OUT }}[offset + row * OUT_F + col] = {{ OUT_SCALAR }}({{ epiOpen }}{{ ACC_1 }}{{ BIAS_1 }}{{ epiClose }});
125
+ {{ OUT }}[offset + row * OUT_F + col2] = {{ OUT_SCALAR }}({{ epiOpen }}{{ ACC_2 }}{{ BIAS_2 }}{{ epiClose }});
126
+ }
127
+ }
128
+
129
+ @compute @workgroup_size(128, 1, 1){{ " @subgroup_size(32)" if pinSubgroupSize32 else "" }}
130
+ fn main(
131
+ @builtin(workgroup_id) workgroup_id: vec3<u32>,
132
+ @builtin(num_workgroups) nwg: vec3<u32>,
133
+ @builtin(local_invocation_index) local_idx: u32,
134
+ @builtin(subgroup_invocation_id) sg_id: u32,
135
+ @builtin(subgroup_size) sg_size: u32
136
+ ) {
137
+ // 2D-folded M-tile (row) base: workgroup_id.z carries the high bits when the
138
+ // row tile exceeds the per-dimension dispatch limit.
139
+ // Reduces to workgroup_id.y when nwg.z == 1; the row_limit guard in storeOutput
140
+ // (i32(M) - i32(a_global_base + ...)) drops the over-dispatched tail.
141
+ let a_global_base: u32 = (ROW_TILE_BASE + workgroup_id.y + workgroup_id.z * nwg.y) * TILE_ROWS;
142
+ let w_global_base: u32 = (COLUMN_TILE_BASE + workgroup_id.x) * TILE_COLS;
143
+
144
+ let subtile_id: u32 = local_idx / sg_size;
145
+ {% if tileMValue > 32 %}
146
+ let subtile_idx: u32 = 0u;
147
+ let subtile_idy: u32 = subtile_id;
148
+ {% else %}
149
+ let subtile_idx: u32 = subtile_id / 2u;
150
+ let subtile_idy: u32 = subtile_id % 2u;
151
+ {% endif %}
152
+ let base_A: u32 = subtile_idy * SUB_ROWS;
153
+ let base_B: u32 = subtile_idx * SUB_COLS;
154
+
155
+ {{ matrixAccumulators() }}
156
+ {% if useDoubleBufferedB %}
157
+ // Large aligned f32 tiles do not benefit from staging A: each row band is
158
+ // consumed by exactly one subgroup. Load those matrices directly from x and
159
+ // ping-pong only B, which is reused by all four subgroups. The barrier that
160
+ // publishes the next B bank also proves the previous bank is no longer read,
161
+ // reducing the steady-state schedule from two barriers per K tile to one.
162
+ var b_bank: u32 = 0u;
163
+ loadSHMB(w_global_base, 0u, local_idx / {{ (tileKValue / 16)|int }}u, local_idx % {{ (tileKValue / 16)|int }}u, 0u);
164
+ workgroupBarrier();
165
+ for (var kidx: u32 = 0u; kidx < {{ kLoopEndValue }}; kidx += TILE_K) {
166
+ let b_bank_offset = b_bank * TILE_COLS * TILE_K;
167
+ for (var step: u32 = 0u; step < TILE_K; step += {{ 32 if useBalancedAccumulation else 8 }}u) {
168
+ {% if useBalancedAccumulation %}
169
+ {% for stream in range(4) %}
170
+ {
171
+ {% set matrixAccumulator = ["matC", "matD", "matE", "matF"][stream] %}
172
+ {% set matrixStepOffset = stream * 8 %}
173
+ {% set dynamicATiles = aTilesPerSubgroup if aTilesPerSubgroup is defined else 2 %}
174
+ {% set matrixAccumulator = matrixAccumulator | default("matC") %}
175
+ {% set matrixStepOffset = matrixStepOffset | default(0) %}
176
+ {% set matrixStep = "step" if matrixStepOffset == 0 else "(step + " ~ matrixStepOffset ~ "u)" %}
177
+ {% set directA = directAStorage is defined and directAStorage %}
178
+ {% set B_BANK = "b_bank_offset + " if doubleBufferedB is defined and doubleBufferedB else "" %}
179
+ {% if directA %}
180
+ {% set matrixAStorage = matrixAStorage | default("x") %}
181
+ {% set matrixAStride = matrixAStride | default("IN_F") %}
182
+ // Direct storage guarantees a full aligned M/K tile, so subgroup-matrix
183
+ // loads need no guarded workgroup staging.
184
+ let matrix_a_offset = (a_global_base + base_A) * {{ matrixAStride }} + kidx + {{ matrixStep }};
185
+ {% else %}
186
+ let matrix_a_offset = subtile_idy * SUB_ROWS * TILE_K + {{ matrixStep }};
187
+ {% endif %}
188
+ {% for m in range(dynamicATiles) %}
189
+ {% if directA %}
190
+ var matA{{ m }}: subgroup_matrix_left<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_left<{{ operandScalar }}, 8, 8>, row_major>(&{{ matrixAStorage }}, matrix_a_offset{% if m > 0 %} + {{ m * 8 }}u * {{ matrixAStride }}{% endif %}, {{ matrixAStride }});
191
+ {% else %}
192
+ var matA{{ m }}: subgroup_matrix_left<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_left<{{ operandScalar }}, 8, 8>, row_major>(&tile_A, matrix_a_offset{% if m > 0 %} + {{ m * 8 }}u * TILE_K{% endif %}, TILE_K);
193
+ {% endif %}
194
+ {% endfor %}
195
+
196
+ let matrix_b_offset = {{ B_BANK }}{{ matrixStep }} * TILE_COLS + subtile_idx * SUB_COLS;
197
+ {% for n in range(bTilesPerSubgroup) %}
198
+ var matB{{ n }}: subgroup_matrix_right<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_right<{{ operandScalar }}, 8, 8>, row_major>(&tile_B, matrix_b_offset + {{ n * 8 }}u, TILE_COLS);
199
+ {% endfor %}
200
+
201
+ {% for m in range(dynamicATiles) %}
202
+ {% for n in range(bTilesPerSubgroup) %}
203
+ {{ matrixAccumulator }}{{ m }}{{ n }} = subgroupMatrixMultiplyAccumulate(matA{{ m }}, matB{{ n }}, {{ matrixAccumulator }}{{ m }}{{ n }});
204
+ {% endfor %}
205
+ {% endfor %}
206
+ }
207
+ {% endfor %}
208
+ {% else %}
209
+ {% set dynamicATiles = aTilesPerSubgroup if aTilesPerSubgroup is defined else 2 %}
210
+ {% set matrixAccumulator = matrixAccumulator | default("matC") %}
211
+ {% set matrixStepOffset = matrixStepOffset | default(0) %}
212
+ {% set matrixStep = "step" if matrixStepOffset == 0 else "(step + " ~ matrixStepOffset ~ "u)" %}
213
+ {% set directA = directAStorage is defined and directAStorage %}
214
+ {% set B_BANK = "b_bank_offset + " if doubleBufferedB is defined and doubleBufferedB else "" %}
215
+ {% if directA %}
216
+ {% set matrixAStorage = matrixAStorage | default("x") %}
217
+ {% set matrixAStride = matrixAStride | default("IN_F") %}
218
+ // Direct storage guarantees a full aligned M/K tile, so subgroup-matrix
219
+ // loads need no guarded workgroup staging.
220
+ let matrix_a_offset = (a_global_base + base_A) * {{ matrixAStride }} + kidx + {{ matrixStep }};
221
+ {% else %}
222
+ let matrix_a_offset = subtile_idy * SUB_ROWS * TILE_K + {{ matrixStep }};
223
+ {% endif %}
224
+ {% for m in range(dynamicATiles) %}
225
+ {% if directA %}
226
+ var matA{{ m }}: subgroup_matrix_left<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_left<{{ operandScalar }}, 8, 8>, row_major>(&{{ matrixAStorage }}, matrix_a_offset{% if m > 0 %} + {{ m * 8 }}u * {{ matrixAStride }}{% endif %}, {{ matrixAStride }});
227
+ {% else %}
228
+ var matA{{ m }}: subgroup_matrix_left<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_left<{{ operandScalar }}, 8, 8>, row_major>(&tile_A, matrix_a_offset{% if m > 0 %} + {{ m * 8 }}u * TILE_K{% endif %}, TILE_K);
229
+ {% endif %}
230
+ {% endfor %}
231
+
232
+ let matrix_b_offset = {{ B_BANK }}{{ matrixStep }} * TILE_COLS + subtile_idx * SUB_COLS;
233
+ {% for n in range(bTilesPerSubgroup) %}
234
+ var matB{{ n }}: subgroup_matrix_right<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_right<{{ operandScalar }}, 8, 8>, row_major>(&tile_B, matrix_b_offset + {{ n * 8 }}u, TILE_COLS);
235
+ {% endfor %}
236
+
237
+ {% for m in range(dynamicATiles) %}
238
+ {% for n in range(bTilesPerSubgroup) %}
239
+ {{ matrixAccumulator }}{{ m }}{{ n }} = subgroupMatrixMultiplyAccumulate(matA{{ m }}, matB{{ n }}, {{ matrixAccumulator }}{{ m }}{{ n }});
240
+ {% endfor %}
241
+ {% endfor %}
242
+ {% endif %}
243
+ }
244
+ let next_kidx = kidx + TILE_K;
245
+ if (next_kidx < {{ kLoopEndValue }}) {
246
+ let next_bank = 1u - b_bank;
247
+ loadSHMB(w_global_base, next_kidx, local_idx / {{ (tileKValue / 16)|int }}u, local_idx % {{ (tileKValue / 16)|int }}u, next_bank * TILE_COLS * TILE_K);
248
+ workgroupBarrier();
249
+ b_bank = next_bank;
250
+ }
251
+ }
252
+ {% else %}
253
+ for (var kidx: u32 = 0u; kidx < {{ kLoopEndValue }}; kidx += TILE_K) {
254
+ {% if not useDirectAStorage %}
255
+ loadSHMA(a_global_base, kidx, local_idx / {{ (tileKValue / 8)|int }}u, local_idx % {{ (tileKValue / 8)|int }}u);
256
+ {% endif %}
257
+ loadSHMB(w_global_base, kidx, local_idx / {{ (tileKValue / 16)|int }}u, local_idx % {{ (tileKValue / 16)|int }}u, 0u);
258
+ workgroupBarrier();
259
+
260
+ for (var step: u32 = 0u; step < TILE_K; step += 8u) {
261
+ {% set dynamicATiles = aTilesPerSubgroup if aTilesPerSubgroup is defined else 2 %}
262
+ {% set matrixAccumulator = matrixAccumulator | default("matC") %}
263
+ {% set matrixStepOffset = matrixStepOffset | default(0) %}
264
+ {% set matrixStep = "step" if matrixStepOffset == 0 else "(step + " ~ matrixStepOffset ~ "u)" %}
265
+ {% set directA = directAStorage is defined and directAStorage %}
266
+ {% set B_BANK = "b_bank_offset + " if doubleBufferedB is defined and doubleBufferedB else "" %}
267
+ {% if directA %}
268
+ {% set matrixAStorage = matrixAStorage | default("x") %}
269
+ {% set matrixAStride = matrixAStride | default("IN_F") %}
270
+ // Direct storage guarantees a full aligned M/K tile, so subgroup-matrix
271
+ // loads need no guarded workgroup staging.
272
+ let matrix_a_offset = (a_global_base + base_A) * {{ matrixAStride }} + kidx + {{ matrixStep }};
273
+ {% else %}
274
+ let matrix_a_offset = subtile_idy * SUB_ROWS * TILE_K + {{ matrixStep }};
275
+ {% endif %}
276
+ {% for m in range(dynamicATiles) %}
277
+ {% if directA %}
278
+ var matA{{ m }}: subgroup_matrix_left<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_left<{{ operandScalar }}, 8, 8>, row_major>(&{{ matrixAStorage }}, matrix_a_offset{% if m > 0 %} + {{ m * 8 }}u * {{ matrixAStride }}{% endif %}, {{ matrixAStride }});
279
+ {% else %}
280
+ var matA{{ m }}: subgroup_matrix_left<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_left<{{ operandScalar }}, 8, 8>, row_major>(&tile_A, matrix_a_offset{% if m > 0 %} + {{ m * 8 }}u * TILE_K{% endif %}, TILE_K);
281
+ {% endif %}
282
+ {% endfor %}
283
+
284
+ let matrix_b_offset = {{ B_BANK }}{{ matrixStep }} * TILE_COLS + subtile_idx * SUB_COLS;
285
+ {% for n in range(bTilesPerSubgroup) %}
286
+ var matB{{ n }}: subgroup_matrix_right<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_right<{{ operandScalar }}, 8, 8>, row_major>(&tile_B, matrix_b_offset + {{ n * 8 }}u, TILE_COLS);
287
+ {% endfor %}
288
+
289
+ {% for m in range(dynamicATiles) %}
290
+ {% for n in range(bTilesPerSubgroup) %}
291
+ {{ matrixAccumulator }}{{ m }}{{ n }} = subgroupMatrixMultiplyAccumulate(matA{{ m }}, matB{{ n }}, {{ matrixAccumulator }}{{ m }}{{ n }});
292
+ {% endfor %}
293
+ {% endfor %}
294
+ }
295
+ workgroupBarrier();
296
+ }
297
+ {% endif %}
298
+
299
+ {% if useBalancedAccumulation %}
300
+ workgroupBarrier();
301
+ {% endif %}
302
+ let matrix_c_offset: u32 = (a_global_base + base_A) * OUT_F + w_global_base + base_B;
303
+ let row: u32 = sg_id / 4u;
304
+ let col: u32 = (sg_id % 4u) * 2u;
305
+ // Stage every result into a distinct bank before one barrier. Tail/bias paths
306
+ // retain this guarded scalar epilogue: subgroupMatrixStore scatters values
307
+ // across lanes, so its cross-lane readback must be published before partial-M
308
+ // guards diverge. The epilogue also handles output conversion and bias.
309
+ let bank: u32 = subtile_id * {{ aTilesPerSubgroup * bTilesPerSubgroup }}u;
310
+ {{ storeMatrixAccumulators() }}
311
+ workgroupBarrier();
312
+
313
+ {% if useBalancedAccumulation %}
314
+ var pairTotals: array<vec2<f32>, {{ aTilesPerSubgroup * bTilesPerSubgroup }}>;
315
+ {% for m in range(aTilesPerSubgroup) %}
316
+ {% for n in range(bTilesPerSubgroup) %}
317
+ pairTotals[{{ m * bTilesPerSubgroup + n }}] = vec2<f32>(
318
+ scratch[bank + {{ m * bTilesPerSubgroup + n }}u][row * 8u + col] + tile_B[(bank + {{ m * bTilesPerSubgroup + n }}u) * 64u + row * 8u + col],
319
+ scratch[bank + {{ m * bTilesPerSubgroup + n }}u][row * 8u + col + 1u] + tile_B[(bank + {{ m * bTilesPerSubgroup + n }}u) * 64u + row * 8u + col + 1u]);
320
+ {% endfor %}
321
+ {% endfor %}
322
+ workgroupBarrier();
323
+ {% for m in range(aTilesPerSubgroup) %}
324
+ {% for n in range(bTilesPerSubgroup) %}
325
+ subgroupMatrixStore<row_major>(&scratch[bank + {{ m * bTilesPerSubgroup + n }}u], 0u, matE{{ m }}{{ n }}, 8u);
326
+ subgroupMatrixStore<row_major>(&tile_B, (bank + {{ m * bTilesPerSubgroup + n }}u) * 64u, matF{{ m }}{{ n }}, 8u);
327
+ {% endfor %}
328
+ {% endfor %}
329
+ workgroupBarrier();
330
+
331
+ {% endif %}
332
+ {% for m in range(aTilesPerSubgroup) %}
333
+ let row_limit_{{ m }}: i32 = i32(params.M) - i32(a_global_base + base_A + {{ m * 8 }}u);
334
+ {% for n in range(bTilesPerSubgroup) %}
335
+ storeOutput(
336
+ matrix_c_offset + {{ m * 8 }}u * OUT_F + {{ n * 8 }}u,
337
+ w_global_base + base_B + {{ n * 8 }}u,
338
+ row,
339
+ col,
340
+ bank + {{ m * bTilesPerSubgroup + n }}u,
341
+ row_limit_{{ m }}{{ ", pairTotals[" ~ (m * bTilesPerSubgroup + n) ~ "]" if useBalancedAccumulation else "" }}
342
+ );
343
+ {% endfor %}
344
+ {% endfor %}
345
+ }
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,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "com.microsoft.PackedAttention",
3
+ "id": "_com_microsoft_packedattention_webgpu_2e383dd",
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": "M80JSlkb8TDwWae5JchL6XbpV286kAlQj5Vzibog8bQ=",
11
+ "attn-packed-varlen-scalar.wgsl.jinja": "tHu3bLC21Ptmb1VpMTt4ztk0sYa/zMjAqMGohlJGGgk=",
12
+ "bench.json": "8ZMv62kt75ShLjFueX/PRLS3gqcBHFs4BNOVE8RvzP8=",
13
+ "gemm-epilogue-tiled-reg.wgsl.jinja": "qQqLZ53/gvjjKWGMEydTYCqjBIX0DVYF1kGPt/tb0F4=",
14
+ "gemm-subgroup-matrix.wgsl.jinja": "nGflGDEU4lkkKKhH8SgascRjubigTw8iLGgHMUDqLV4=",
15
+ "manifest.json": "0UPOkWWzNh4GybUb6c4Nf2UNPNl6Lqb0t/mM10I6KTQ=",
16
+ "test.json": "Q/fxohWe7N1k2sg8Av4jtPf7tSvT3AEp1MERMj6KMRI="
17
+ }
18
+ },
19
+ "provenance": { "kernel": { "sha": "6fdf6301e2bbcc2f03bf1eaf493b7ad55ef33afc", "dirty": false } },
20
+ "webgpu": {
21
+ "manifestSpec": "2.1",
22
+ "variants": {
23
+ "matrix_direct_f32_flash": ["attn-packed-varlen-cluster.wgsl.jinja", "gemm-subgroup-matrix.wgsl.jinja"],
24
+ "matrix_staged_f32_flash": ["attn-packed-varlen-cluster.wgsl.jinja", "gemm-subgroup-matrix.wgsl.jinja"],
25
+ "f32_flash": ["attn-packed-varlen-cluster.wgsl.jinja", "gemm-epilogue-tiled-reg.wgsl.jinja"],
26
+ "matrix_direct_f32_flash_nosg": ["attn-packed-varlen-cluster.wgsl.jinja", "gemm-subgroup-matrix.wgsl.jinja"],
27
+ "matrix_staged_f32_flash_nosg": ["attn-packed-varlen-cluster.wgsl.jinja", "gemm-subgroup-matrix.wgsl.jinja"],
28
+ "f32_flash_nosg": ["attn-packed-varlen-cluster.wgsl.jinja", "gemm-epilogue-tiled-reg.wgsl.jinja"],
29
+ "matrix_direct_f32_serial": ["attn-packed-varlen-scalar.wgsl.jinja", "gemm-subgroup-matrix.wgsl.jinja"],
30
+ "matrix_staged_f32_serial": ["attn-packed-varlen-scalar.wgsl.jinja", "gemm-subgroup-matrix.wgsl.jinja"],
31
+ "f32_serial": ["attn-packed-varlen-scalar.wgsl.jinja", "gemm-epilogue-tiled-reg.wgsl.jinja"],
32
+ "matrix_direct_f32_attn_bias_flash": ["attn-packed-varlen-cluster.wgsl.jinja", "gemm-subgroup-matrix.wgsl.jinja"],
33
+ "matrix_staged_f32_attn_bias_flash": ["attn-packed-varlen-cluster.wgsl.jinja", "gemm-subgroup-matrix.wgsl.jinja"],
34
+ "f32_attn_bias_flash": ["attn-packed-varlen-cluster.wgsl.jinja", "gemm-epilogue-tiled-reg.wgsl.jinja"],
35
+ "matrix_direct_f32_attn_bias_flash_nosg": ["attn-packed-varlen-cluster.wgsl.jinja", "gemm-subgroup-matrix.wgsl.jinja"],
36
+ "matrix_staged_f32_attn_bias_flash_nosg": ["attn-packed-varlen-cluster.wgsl.jinja", "gemm-subgroup-matrix.wgsl.jinja"],
37
+ "f32_attn_bias_flash_nosg": ["attn-packed-varlen-cluster.wgsl.jinja", "gemm-epilogue-tiled-reg.wgsl.jinja"],
38
+ "matrix_direct_f32_attn_bias_serial": ["attn-packed-varlen-scalar.wgsl.jinja", "gemm-subgroup-matrix.wgsl.jinja"],
39
+ "matrix_staged_f32_attn_bias_serial": ["attn-packed-varlen-scalar.wgsl.jinja", "gemm-subgroup-matrix.wgsl.jinja"],
40
+ "f32_attn_bias_serial": ["attn-packed-varlen-scalar.wgsl.jinja", "gemm-epilogue-tiled-reg.wgsl.jinja"],
41
+ "matrix_direct_f16_flash": ["attn-packed-varlen-cluster.wgsl.jinja", "gemm-subgroup-matrix.wgsl.jinja"],
42
+ "matrix_staged_f16_flash": ["attn-packed-varlen-cluster.wgsl.jinja", "gemm-subgroup-matrix.wgsl.jinja"],
43
+ "f16_flash": ["attn-packed-varlen-cluster.wgsl.jinja", "gemm-epilogue-tiled-reg.wgsl.jinja"],
44
+ "matrix_direct_f16_flash_nosg": ["attn-packed-varlen-cluster.wgsl.jinja", "gemm-subgroup-matrix.wgsl.jinja"],
45
+ "matrix_staged_f16_flash_nosg": ["attn-packed-varlen-cluster.wgsl.jinja", "gemm-subgroup-matrix.wgsl.jinja"],
46
+ "f16_flash_nosg": ["attn-packed-varlen-cluster.wgsl.jinja", "gemm-epilogue-tiled-reg.wgsl.jinja"],
47
+ "matrix_direct_f16_serial": ["attn-packed-varlen-scalar.wgsl.jinja", "gemm-subgroup-matrix.wgsl.jinja"],
48
+ "matrix_staged_f16_serial": ["attn-packed-varlen-scalar.wgsl.jinja", "gemm-subgroup-matrix.wgsl.jinja"],
49
+ "f16_serial": ["attn-packed-varlen-scalar.wgsl.jinja", "gemm-epilogue-tiled-reg.wgsl.jinja"],
50
+ "matrix_direct_f16_attn_bias_flash": ["attn-packed-varlen-cluster.wgsl.jinja", "gemm-subgroup-matrix.wgsl.jinja"],
51
+ "matrix_staged_f16_attn_bias_flash": ["attn-packed-varlen-cluster.wgsl.jinja", "gemm-subgroup-matrix.wgsl.jinja"],
52
+ "f16_attn_bias_flash": ["attn-packed-varlen-cluster.wgsl.jinja", "gemm-epilogue-tiled-reg.wgsl.jinja"],
53
+ "matrix_direct_f16_attn_bias_flash_nosg": ["attn-packed-varlen-cluster.wgsl.jinja", "gemm-subgroup-matrix.wgsl.jinja"],
54
+ "matrix_staged_f16_attn_bias_flash_nosg": ["attn-packed-varlen-cluster.wgsl.jinja", "gemm-subgroup-matrix.wgsl.jinja"],
55
+ "f16_attn_bias_flash_nosg": ["attn-packed-varlen-cluster.wgsl.jinja", "gemm-epilogue-tiled-reg.wgsl.jinja"],
56
+ "matrix_direct_f16_attn_bias_serial": ["attn-packed-varlen-scalar.wgsl.jinja", "gemm-subgroup-matrix.wgsl.jinja"],
57
+ "matrix_staged_f16_attn_bias_serial": ["attn-packed-varlen-scalar.wgsl.jinja", "gemm-subgroup-matrix.wgsl.jinja"],
58
+ "f16_attn_bias_serial": ["attn-packed-varlen-scalar.wgsl.jinja", "gemm-epilogue-tiled-reg.wgsl.jinja"]
59
+ }
60
+ }
61
+ }
build/webgpu/test.json ADDED
The diff for this file is too large to render. See raw diff