sync 6fdf6301e2bb
Browse files- README.md +132 -0
- build/webgpu/attn-packed-varlen-cluster.wgsl.jinja +390 -0
- build/webgpu/attn-packed-varlen-scalar.wgsl.jinja +165 -0
- build/webgpu/bench.json +0 -0
- build/webgpu/gemm-epilogue-tiled-reg.wgsl.jinja +128 -0
- build/webgpu/gemm-subgroup-matrix.wgsl.jinja +345 -0
- build/webgpu/manifest.json +0 -0
- build/webgpu/metadata.json +61 -0
- build/webgpu/test.json +0 -0
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
|
|
|