Xenova HF Staff commited on
Commit
f18314a
·
verified ·
1 Parent(s): 07147ca

sync 2e7068faf55e

Browse files
README.md CHANGED
@@ -1,3 +1,99 @@
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.SparseAttention
10
+
11
+ `com.microsoft` · ONNX Runtime contrib operator · contrib since_version 1
12
+
13
+ ## Description
14
+
15
+ Block-sparse causal attention used by Phi-3-small. `block_row_indices` and `block_col_indices` encode one or more CSR block masks, and layouts cycle over query heads. Grouped-query heads, separate or packed `[Q|K|V]`, explicit scaling, partial or full rotary embedding in NeoX or interleaved layout, and float16 are supported. The past/present key and value tensors share allocations and are updated in place. Head sizes must be non-zero multiples of 8; bfloat16 is not implemented.
16
+
17
+ See the [ONNX Runtime `SparseAttention` contrib-operator spec](https://github.com/microsoft/onnxruntime/blob/main/docs/ContribOperators.md#com.microsoft.SparseAttention) for the reference semantics.
18
+
19
+ ## Inputs
20
+
21
+ | Name | Bind key | Logical dtype | WebGPU storage | Rank | Shape | Description | Presence |
22
+ | --- | --- | --- | --- | --- | --- | --- | --- |
23
+ | `query` | `queryT` | `T` | same as logical dtype | `3` | — | Query `(batch_size, sequence_length, num_heads * head_size)`, or packed `[Q\|K\|V]` `(batch_size, sequence_length, (num_heads + 2 * kv_num_heads) * head_size)` when `key` and `value` are omitted. | required |
24
+ | `key` | `keyT` | `T` | same as logical dtype | `3` | — | Key `(batch_size, sequence_length, kv_num_heads * head_size)`. Omitted for packed QKV. | optional |
25
+ | `value` | `valueT` | `T` | same as logical dtype | `3` | — | Value `(batch_size, sequence_length, kv_num_heads * head_size)`. Omitted for packed QKV. | optional |
26
+ | `past_key` | `pastKeyT` | `T` | same as logical dtype | `4` | — | Key cache `(batch_size, kv_num_heads, max_cache_sequence_length, head_size)`, updated in place. | required |
27
+ | `past_value` | `pastValueT` | `T` | same as logical dtype | `4` | — | Value cache with the same shape as `past_key`, updated in place. | required |
28
+ | `block_row_indices` | `blockRowIndicesT` | `M` | `int32` | `2` | — | CSR row pointers `(num_layout, max_blocks + 1)`. Each layout starts at zero, is monotonically non-decreasing, and ends no later than that layout's `block_col_indices` width. | required |
29
+ | `block_col_indices` | `blockColIndicesT` | `M` | `int32` | `2` | — | CSR column indices `(num_layout, max_nnz_blocks)`, right-padded past each layout's non-zero count. Every active entry is in `[0, max_blocks)`. | required |
30
+ | `total_sequence_length` | `totalSequenceLengthT` | `M` | `int32` | — | — | Scalar or one-element vector holding the maximum total key length. Equal to `sequence_length` exactly in the prompt case, which is how the past length is decided. The value fits the cache, the sparse layout's `max_blocks * sparse_block_size` capacity, and the rotary-cache row count when rotary is enabled. | required |
31
+ | `key_total_sequence_lengths` | `keyTotalSequenceLengthsT` | `M` | `int32` | `1` | — | Per-batch total key length excluding padding, shape `(batch_size)`. Each value is at most `total_sequence_length` and is at least 1 for a prompt or at least `sequence_length` otherwise. | required |
32
+ | `cos_cache` | `cosCacheT` | `T` | same as logical dtype | `2` | — | Rotary cosine cache `(max_rotary_sequence_length, rotary_dimension / 2)`, where the width is a multiple of 8 no larger than `head_size / 2`. Required with `sin_cache` when `do_rotary` is 1. | optional |
33
+ | `sin_cache` | `sinCacheT` | `T` | same as logical dtype | `2` | — | Rotary sine cache with the same shape as `cos_cache`; required with it when `do_rotary` is 1. | optional |
34
+
35
+ ## Outputs
36
+
37
+ | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
38
+ | --- | --- | --- | --- | --- | --- | --- |
39
+ | `output` | `outputT` | `T` | `3` | derived; see description | Attention output `(batch_size, sequence_length, num_heads * head_size)`. | required |
40
+ | `past_key` | `pastKeyT` | `T` | `4` | same as `past_key` | The key cache tensor itself after the in-place append; ONNX names this output `present_key`. | required |
41
+ | `past_value` | `pastValueT` | `T` | `4` | same as `past_value` | The value cache tensor itself after the in-place append; ONNX names this output `present_value`. | required |
42
+
43
+ ## Attributes
44
+
45
+ Attributes and default values (overridable per request):
46
+
47
+ | Attribute | Default | Description |
48
+ | --- | --- | --- |
49
+ | `do_rotary` | `0` | Set to 1 to apply rotary embedding to Q and to K before it enters the cache; every other value disables rotary embedding. |
50
+ | `rotary_interleaved` | `0` | Set to 1 to rotate adjacent pairs instead of using the NeoX half-split; every other value selects the NeoX layout. |
51
+ | `num_heads` | — | Number of query heads. |
52
+ | `kv_num_heads` | — | Number of key/value heads; must divide `num_heads`. |
53
+ | `sparse_block_size` | — | Tokens per sparse block; one of 16, 32, 64, 128. |
54
+ | `scale` | — | Scale applied to query-key products; omitted or zero uses `1 / sqrt(head_size)`. |
55
+
56
+ ## Type constraints
57
+
58
+ | Variable | Allowed dtypes |
59
+ | --- | --- |
60
+ | `T` | `float32`, `float16` |
61
+ | `M` | `int32` |
62
+
63
+ ## Files
64
+
65
+ - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, provenance)
66
+ - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
67
+ - [`test.json`](build/webgpu/test.json) — correctness cases
68
+ - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
69
+ - [`sparse-attention.wgsl.jinja`](build/webgpu/sparse-attention.wgsl.jinja)
70
+ - [`sparse-kv-append.wgsl.jinja`](build/webgpu/sparse-kv-append.wgsl.jinja)
71
+ - [`sparse-q-rotary.wgsl.jinja`](build/webgpu/sparse-q-rotary.wgsl.jinja)
72
+
73
+ ## Use with `@huggingface/kernels`
74
+
75
+ The loader derives every required output's shape and logical dtype from the manifest contract and this call.
76
+ It then allocates the result tensors automatically.
77
+
78
+ The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
79
+
80
+ Replace each `*Data` placeholder with a typed array containing the corresponding input data.
81
+
82
+ ```js
83
+ import { getKernel } from "@huggingface/kernels";
84
+
85
+ const kernel = await getKernel("webgpu-kernels/com.microsoft.SparseAttention", { version: 1 });
86
+ const { pastKeyT, pastValueT, outputT } = await kernel({
87
+ queryT: { data: queryTData, shape: [1, 32, 8] },
88
+ keyT: { data: keyTData, shape: [1, 32, 8] },
89
+ valueT: { data: valueTData, shape: [1, 32, 8] },
90
+ pastKeyT: { data: pastKeyTData, shape: [1, 1, 32, 8] },
91
+ pastValueT: { data: pastValueTData, shape: [1, 1, 32, 8] },
92
+ blockRowIndicesT: { data: blockRowIndicesTData, shape: [1, 3] },
93
+ blockColIndicesT: { data: blockColIndicesTData, shape: [1, 3] },
94
+ totalSequenceLengthT: { data: totalSequenceLengthTData, shape: [1] },
95
+ keyTotalSequenceLengthsT: { data: keyTotalSequenceLengthsTData, shape: [1] },
96
+ }, {
97
+ attrs: { num_heads: 1, kv_num_heads: 1, sparse_block_size: 16 },
98
+ });
99
+ ```
build/webgpu/bench.json ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "op": "com.microsoft.SparseAttention",
3
+ "fixtureArrays": {
4
+ "block_column_indices_t_pattern": [0, 0, 1, 0, 1, 2, 0, 1, 2, 3, 0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3, 4, 5, 6, 7, 8, 0, 2, 3, 4, 5, 6, 7, 8, 9, 0, 3, 4, 5, 6, 7, 8, 9, 10, 0, 4, 5, 6, 7, 8, 9, 10, 11, 0, 4, 5, 6, 7, 8, 9, 10, 11, 12, 0, 4, 6, 7, 8, 9, 10, 11, 12, 13, 0, 4, 7, 8, 9, 10, 11, 12, 13, 14, 0, 4, 8, 9, 10, 11, 12, 13, 14, 15, 0, 0, 1, 0, 1, 2, 0, 1, 2, 3, 0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 7, 1, 2, 3, 4, 5, 6, 7, 8, 2, 3, 4, 5, 6, 7, 8, 9, 3, 4, 5, 6, 7, 8, 9, 10, 3, 4, 5, 6, 7, 8, 9, 10, 11, 3, 5, 6, 7, 8, 9, 10, 11, 12, 3, 6, 7, 8, 9, 10, 11, 12, 13, 3, 7, 8, 9, 10, 11, 12, 13, 14, 3, 7, 8, 9, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, 0, 0, 1, 0, 1, 2, 0, 1, 2, 3, 0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 7, 1, 2, 3, 4, 5, 6, 7, 8, 2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5, 6, 7, 8, 9, 10, 2, 4, 5, 6, 7, 8, 9, 10, 11, 2, 5, 6, 7, 8, 9, 10, 11, 12, 2, 6, 7, 8, 9, 10, 11, 12, 13, 2, 6, 7, 8, 9, 10, 11, 12, 13, 14, 2, 6, 8, 9, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, 0, 0, 1, 0, 1, 2, 0, 1, 2, 3, 0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 7, 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 3, 4, 5, 6, 7, 8, 9, 10, 1, 4, 5, 6, 7, 8, 9, 10, 11, 1, 5, 6, 7, 8, 9, 10, 11, 12, 1, 5, 6, 7, 8, 9, 10, 11, 12, 13, 1, 5, 7, 8, 9, 10, 11, 12, 13, 14, 1, 5, 8, 9, 10, 11, 12, 13, 14, 15, -1, -1],
5
+ "sparse-prompt-b1-s1024-h32kv8-d128-blk64_input_blockRowIndicesT": [0, 1, 3, 6, 10, 15, 21, 28, 36, 45, 54, 63, 72, 82, 92, 102, 112, 0, 1, 3, 6, 10, 15, 21, 28, 36, 44, 52, 60, 69, 78, 87, 96, 106, 0, 1, 3, 6, 10, 15, 21, 28, 36, 44, 52, 61, 70, 79, 88, 98, 108, 0, 1, 3, 6, 10, 15, 21, 28, 36, 44, 53, 62, 71, 80, 90, 100, 110]
6
+ },
7
+ "tunableSpace": { "WORKGROUP_SIZE": [32, 64, 128, 256], "APPEND_WORKGROUP_SIZE": [64, 128, 256] },
8
+ "cases": [
9
+ {
10
+ "name": "sparse-prompt-b1-s1024-h32kv8-d128-blk64",
11
+ "preset": "smoke",
12
+ "vars": {
13
+ "dtype": "float32",
14
+ "batch": 1,
15
+ "seq": 1024,
16
+ "heads": 32,
17
+ "kvHeads": 8,
18
+ "headDim": 128,
19
+ "qkPairs": 13254656,
20
+ "attendedKeys": 1024,
21
+ "dtypeBytes": 4
22
+ },
23
+ "attrs": { "num_heads": 32, "kv_num_heads": 8, "sparse_block_size": 64 },
24
+ "inputs": {
25
+ "queryT": { "shape": [1, 1024, 4096], "dtype": "float32", "dist": "normal", "seed": 9300, "scale": 1 },
26
+ "keyT": { "shape": [1, 1024, 1024], "dtype": "float32", "dist": "normal", "seed": 9301, "scale": 1 },
27
+ "valueT": { "shape": [1, 1024, 1024], "dtype": "float32", "dist": "normal", "seed": 9302, "scale": 1 },
28
+ "pastKeyT": { "shape": [1, 8, 1024, 128], "dtype": "float32", "dist": "normal", "seed": 9303, "scale": 1 },
29
+ "pastValueT": { "shape": [1, 8, 1024, 128], "dtype": "float32", "dist": "normal", "seed": 9304, "scale": 1 },
30
+ "blockRowIndicesT": {
31
+ "shape": [4, 17],
32
+ "dtype": "int32",
33
+ "data": {
34
+ "kind": "values",
35
+ "values": { "$ref": "#/fixtureArrays/sparse-prompt-b1-s1024-h32kv8-d128-blk64_input_blockRowIndicesT" }
36
+ }
37
+ },
38
+ "blockColIndicesT": {
39
+ "shape": [4, 112],
40
+ "dtype": "int32",
41
+ "data": { "kind": "values", "values": { "$ref": "#/fixtureArrays/block_column_indices_t_pattern" } }
42
+ },
43
+ "totalSequenceLengthT": { "shape": [1], "dtype": "int32", "data": { "kind": "values", "values": [1024] } },
44
+ "keyTotalSequenceLengthsT": { "shape": [1], "dtype": "int32", "dist": "constant", "value": 1024 }
45
+ },
46
+ "outputs": { "outputT": { "shape": [1, 1024, 4096], "dtype": "float32" } },
47
+ "bench": { "metrics": [{ "type": "gflops", "value": "4 * args.qkPairs * args.headDim" }] }
48
+ },
49
+ {
50
+ "name": "sparse-decode-b8-past1023-h32kv8-d128-blk64",
51
+ "preset": "smoke",
52
+ "vars": {
53
+ "dtype": "float32",
54
+ "batch": 8,
55
+ "seq": 1,
56
+ "heads": 32,
57
+ "kvHeads": 8,
58
+ "headDim": 128,
59
+ "qkPairs": 163840,
60
+ "attendedKeys": 640,
61
+ "dtypeBytes": 4
62
+ },
63
+ "attrs": { "num_heads": 32, "kv_num_heads": 8, "sparse_block_size": 64 },
64
+ "inputs": {
65
+ "queryT": { "shape": [8, 1, 4096], "dtype": "float32", "dist": "normal", "seed": 9310, "scale": 1 },
66
+ "keyT": { "shape": [8, 1, 1024], "dtype": "float32", "dist": "normal", "seed": 9311, "scale": 1 },
67
+ "valueT": { "shape": [8, 1, 1024], "dtype": "float32", "dist": "normal", "seed": 9312, "scale": 1 },
68
+ "pastKeyT": { "shape": [8, 8, 1024, 128], "dtype": "float32", "dist": "normal", "seed": 9313, "scale": 1 },
69
+ "pastValueT": { "shape": [8, 8, 1024, 128], "dtype": "float32", "dist": "normal", "seed": 9314, "scale": 1 },
70
+ "blockRowIndicesT": {
71
+ "shape": [4, 17],
72
+ "dtype": "int32",
73
+ "data": {
74
+ "kind": "values",
75
+ "values": { "$ref": "#/fixtureArrays/sparse-prompt-b1-s1024-h32kv8-d128-blk64_input_blockRowIndicesT" }
76
+ }
77
+ },
78
+ "blockColIndicesT": {
79
+ "shape": [4, 112],
80
+ "dtype": "int32",
81
+ "data": { "kind": "values", "values": { "$ref": "#/fixtureArrays/block_column_indices_t_pattern" } }
82
+ },
83
+ "totalSequenceLengthT": { "shape": [1], "dtype": "int32", "data": { "kind": "values", "values": [1024] } },
84
+ "keyTotalSequenceLengthsT": { "shape": [8], "dtype": "int32", "dist": "constant", "value": 1024 }
85
+ },
86
+ "outputs": { "outputT": { "shape": [8, 1, 4096], "dtype": "float32" } },
87
+ "bench": {
88
+ "metrics": [
89
+ {
90
+ "type": "bandwidth",
91
+ "value": "2 * args.batch * args.kvHeads * args.attendedKeys * args.headDim * args.dtypeBytes"
92
+ }
93
+ ]
94
+ }
95
+ },
96
+ {
97
+ "name": "sparse-decode-f16-b8-past1023",
98
+ "preset": "model",
99
+ "vars": {
100
+ "dtype": "float16",
101
+ "batch": 8,
102
+ "seq": 1,
103
+ "heads": 32,
104
+ "kvHeads": 8,
105
+ "headDim": 128,
106
+ "qkPairs": 163840,
107
+ "attendedKeys": 640,
108
+ "dtypeBytes": 2
109
+ },
110
+ "attrs": { "num_heads": 32, "kv_num_heads": 8, "sparse_block_size": 64 },
111
+ "inputs": {
112
+ "queryT": { "shape": [8, 1, 4096], "dtype": "float16", "dist": "normal", "seed": 9320, "scale": 1 },
113
+ "keyT": { "shape": [8, 1, 1024], "dtype": "float16", "dist": "normal", "seed": 9321, "scale": 1 },
114
+ "valueT": { "shape": [8, 1, 1024], "dtype": "float16", "dist": "normal", "seed": 9322, "scale": 1 },
115
+ "pastKeyT": { "shape": [8, 8, 1024, 128], "dtype": "float16", "dist": "normal", "seed": 9323, "scale": 1 },
116
+ "pastValueT": { "shape": [8, 8, 1024, 128], "dtype": "float16", "dist": "normal", "seed": 9324, "scale": 1 },
117
+ "blockRowIndicesT": {
118
+ "shape": [4, 17],
119
+ "dtype": "int32",
120
+ "data": {
121
+ "kind": "values",
122
+ "values": { "$ref": "#/fixtureArrays/sparse-prompt-b1-s1024-h32kv8-d128-blk64_input_blockRowIndicesT" }
123
+ }
124
+ },
125
+ "blockColIndicesT": {
126
+ "shape": [4, 112],
127
+ "dtype": "int32",
128
+ "data": { "kind": "values", "values": { "$ref": "#/fixtureArrays/block_column_indices_t_pattern" } }
129
+ },
130
+ "totalSequenceLengthT": { "shape": [1], "dtype": "int32", "data": { "kind": "values", "values": [1024] } },
131
+ "keyTotalSequenceLengthsT": { "shape": [8], "dtype": "int32", "dist": "constant", "value": 1024 }
132
+ },
133
+ "outputs": { "outputT": { "shape": [8, 1, 4096], "dtype": "float16" } },
134
+ "bench": {
135
+ "metrics": [
136
+ {
137
+ "type": "bandwidth",
138
+ "value": "2 * args.batch * args.kvHeads * args.attendedKeys * args.headDim * args.dtypeBytes"
139
+ }
140
+ ]
141
+ }
142
+ },
143
+ {
144
+ "name": "sparse-decode-packed-rotary-b8-past1023",
145
+ "preset": "model",
146
+ "vars": {
147
+ "dtype": "float32",
148
+ "batch": 8,
149
+ "seq": 1,
150
+ "heads": 32,
151
+ "kvHeads": 8,
152
+ "headDim": 128,
153
+ "qkPairs": 163840,
154
+ "attendedKeys": 640,
155
+ "dtypeBytes": 4
156
+ },
157
+ "attrs": { "num_heads": 32, "kv_num_heads": 8, "sparse_block_size": 64, "do_rotary": 1 },
158
+ "inputs": {
159
+ "queryT": { "shape": [8, 1, 6144], "dtype": "float32", "dist": "normal", "seed": 9330, "scale": 1 },
160
+ "pastKeyT": { "shape": [8, 8, 1024, 128], "dtype": "float32", "dist": "normal", "seed": 9333, "scale": 1 },
161
+ "pastValueT": { "shape": [8, 8, 1024, 128], "dtype": "float32", "dist": "normal", "seed": 9334, "scale": 1 },
162
+ "blockRowIndicesT": {
163
+ "shape": [4, 17],
164
+ "dtype": "int32",
165
+ "data": {
166
+ "kind": "values",
167
+ "values": { "$ref": "#/fixtureArrays/sparse-prompt-b1-s1024-h32kv8-d128-blk64_input_blockRowIndicesT" }
168
+ }
169
+ },
170
+ "blockColIndicesT": {
171
+ "shape": [4, 112],
172
+ "dtype": "int32",
173
+ "data": { "kind": "values", "values": { "$ref": "#/fixtureArrays/block_column_indices_t_pattern" } }
174
+ },
175
+ "totalSequenceLengthT": { "shape": [1], "dtype": "int32", "data": { "kind": "values", "values": [1024] } },
176
+ "keyTotalSequenceLengthsT": { "shape": [8], "dtype": "int32", "dist": "constant", "value": 1024 },
177
+ "cosCacheT": { "shape": [1024, 64], "dtype": "float32", "dist": "normal", "seed": 9335, "scale": 1 },
178
+ "sinCacheT": { "shape": [1024, 64], "dtype": "float32", "dist": "normal", "seed": 9336, "scale": 1 }
179
+ },
180
+ "outputs": { "outputT": { "shape": [8, 1, 4096], "dtype": "float32" } },
181
+ "bench": {
182
+ "metrics": [
183
+ {
184
+ "type": "bandwidth",
185
+ "value": "2 * args.batch * args.kvHeads * args.attendedKeys * args.headDim * args.dtypeBytes"
186
+ }
187
+ ]
188
+ }
189
+ },
190
+ {
191
+ "name": "sparse-chunked-b2-s128-past896",
192
+ "preset": "model",
193
+ "vars": {
194
+ "dtype": "float32",
195
+ "batch": 2,
196
+ "seq": 128,
197
+ "heads": 32,
198
+ "kvHeads": 8,
199
+ "headDim": 128,
200
+ "qkPairs": 4919296,
201
+ "attendedKeys": 704,
202
+ "dtypeBytes": 4
203
+ },
204
+ "attrs": { "num_heads": 32, "kv_num_heads": 8, "sparse_block_size": 64 },
205
+ "inputs": {
206
+ "queryT": { "shape": [2, 128, 4096], "dtype": "float32", "dist": "normal", "seed": 9340, "scale": 1 },
207
+ "keyT": { "shape": [2, 128, 1024], "dtype": "float32", "dist": "normal", "seed": 9341, "scale": 1 },
208
+ "valueT": { "shape": [2, 128, 1024], "dtype": "float32", "dist": "normal", "seed": 9342, "scale": 1 },
209
+ "pastKeyT": { "shape": [2, 8, 1024, 128], "dtype": "float32", "dist": "normal", "seed": 9343, "scale": 1 },
210
+ "pastValueT": { "shape": [2, 8, 1024, 128], "dtype": "float32", "dist": "normal", "seed": 9344, "scale": 1 },
211
+ "blockRowIndicesT": {
212
+ "shape": [4, 17],
213
+ "dtype": "int32",
214
+ "data": {
215
+ "kind": "values",
216
+ "values": { "$ref": "#/fixtureArrays/sparse-prompt-b1-s1024-h32kv8-d128-blk64_input_blockRowIndicesT" }
217
+ }
218
+ },
219
+ "blockColIndicesT": {
220
+ "shape": [4, 112],
221
+ "dtype": "int32",
222
+ "data": { "kind": "values", "values": { "$ref": "#/fixtureArrays/block_column_indices_t_pattern" } }
223
+ },
224
+ "totalSequenceLengthT": { "shape": [1], "dtype": "int32", "data": { "kind": "values", "values": [1024] } },
225
+ "keyTotalSequenceLengthsT": { "shape": [2], "dtype": "int32", "dist": "constant", "value": 1024 }
226
+ },
227
+ "outputs": { "outputT": { "shape": [2, 128, 4096], "dtype": "float32" } },
228
+ "bench": { "metrics": [{ "type": "gflops", "value": "4 * args.qkPairs * args.headDim" }] }
229
+ }
230
+ ]
231
+ }
build/webgpu/manifest.json ADDED
@@ -0,0 +1,752 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "domain": "com.microsoft",
3
+ "name": "SparseAttention",
4
+ "sinceVersion": 1,
5
+ "description": "Block-sparse causal attention used by Phi-3-small. `block_row_indices` and `block_col_indices` encode one or more CSR block masks, and layouts cycle over query heads. Grouped-query heads, separate or packed `[Q|K|V]`, explicit scaling, partial or full rotary embedding in NeoX or interleaved layout, and float16 are supported. The past/present key and value tensors share allocations and are updated in place. Head sizes must be non-zero multiples of 8; bfloat16 is not implemented.",
6
+ "inputs": [
7
+ {
8
+ "role": "query",
9
+ "dtype": "T",
10
+ "rank": 3,
11
+ "description": "Query `(batch_size, sequence_length, num_heads * head_size)`, or packed `[Q|K|V]` `(batch_size, sequence_length, (num_heads + 2 * kv_num_heads) * head_size)` when `key` and `value` are omitted."
12
+ },
13
+ {
14
+ "role": "key",
15
+ "dtype": "T",
16
+ "rank": 3,
17
+ "optional": true,
18
+ "description": "Key `(batch_size, sequence_length, kv_num_heads * head_size)`. Omitted for packed QKV."
19
+ },
20
+ {
21
+ "role": "value",
22
+ "dtype": "T",
23
+ "rank": 3,
24
+ "optional": true,
25
+ "description": "Value `(batch_size, sequence_length, kv_num_heads * head_size)`. Omitted for packed QKV."
26
+ },
27
+ {
28
+ "role": "past_key",
29
+ "dtype": "T",
30
+ "rank": 4,
31
+ "description": "Key cache `(batch_size, kv_num_heads, max_cache_sequence_length, head_size)`, updated in place."
32
+ },
33
+ {
34
+ "role": "past_value",
35
+ "dtype": "T",
36
+ "rank": 4,
37
+ "description": "Value cache with the same shape as `past_key`, updated in place."
38
+ },
39
+ {
40
+ "role": "block_row_indices",
41
+ "dtype": "M",
42
+ "rank": 2,
43
+ "description": "CSR row pointers `(num_layout, max_blocks + 1)`. Each layout starts at zero, is monotonically non-decreasing, and ends no later than that layout's `block_col_indices` width."
44
+ },
45
+ {
46
+ "role": "block_col_indices",
47
+ "dtype": "M",
48
+ "rank": 2,
49
+ "description": "CSR column indices `(num_layout, max_nnz_blocks)`, right-padded past each layout's non-zero count. Every active entry is in `[0, max_blocks)`."
50
+ },
51
+ {
52
+ "role": "total_sequence_length",
53
+ "dtype": "M",
54
+ "description": "Scalar or one-element vector holding the maximum total key length. Equal to `sequence_length` exactly in the prompt case, which is how the past length is decided. The value fits the cache, the sparse layout's `max_blocks * sparse_block_size` capacity, and the rotary-cache row count when rotary is enabled."
55
+ },
56
+ {
57
+ "role": "key_total_sequence_lengths",
58
+ "dtype": "M",
59
+ "rank": 1,
60
+ "description": "Per-batch total key length excluding padding, shape `(batch_size)`. Each value is at most `total_sequence_length` and is at least 1 for a prompt or at least `sequence_length` otherwise."
61
+ },
62
+ {
63
+ "role": "cos_cache",
64
+ "dtype": "T",
65
+ "rank": 2,
66
+ "optional": true,
67
+ "description": "Rotary cosine cache `(max_rotary_sequence_length, rotary_dimension / 2)`, where the width is a multiple of 8 no larger than `head_size / 2`. Required with `sin_cache` when `do_rotary` is 1."
68
+ },
69
+ {
70
+ "role": "sin_cache",
71
+ "dtype": "T",
72
+ "rank": 2,
73
+ "optional": true,
74
+ "description": "Rotary sine cache with the same shape as `cos_cache`; required with it when `do_rotary` is 1."
75
+ }
76
+ ],
77
+ "outputs": [
78
+ {
79
+ "role": "output",
80
+ "dtype": "T",
81
+ "rank": 3,
82
+ "shape": "[batchSize, seqLen, numHeads * headSize]",
83
+ "description": "Attention output `(batch_size, sequence_length, num_heads * head_size)`."
84
+ },
85
+ {
86
+ "role": "past_key",
87
+ "dtype": "T",
88
+ "rank": 4,
89
+ "shape": "shapes.pastKeyT",
90
+ "description": "The key cache tensor itself after the in-place append; ONNX names this output `present_key`."
91
+ },
92
+ {
93
+ "role": "past_value",
94
+ "dtype": "T",
95
+ "rank": 4,
96
+ "shape": "shapes.pastValueT",
97
+ "description": "The value cache tensor itself after the in-place append; ONNX names this output `present_value`."
98
+ }
99
+ ],
100
+ "attributes": { "do_rotary": 0, "rotary_interleaved": 0 },
101
+ "attributeDescriptions": {
102
+ "num_heads": "Number of query heads.",
103
+ "kv_num_heads": "Number of key/value heads; must divide `num_heads`.",
104
+ "sparse_block_size": "Tokens per sparse block; one of 16, 32, 64, 128.",
105
+ "do_rotary": "Set to 1 to apply rotary embedding to Q and to K before it enters the cache; every other value disables rotary embedding.",
106
+ "rotary_interleaved": "Set to 1 to rotate adjacent pairs instead of using the NeoX half-split; every other value selects the NeoX layout.",
107
+ "scale": "Scale applied to query-key products; omitted or zero uses `1 / sqrt(head_size)`."
108
+ },
109
+ "attributeConstraints": {
110
+ "num_heads": { "required": true },
111
+ "kv_num_heads": { "required": true },
112
+ "sparse_block_size": { "required": true }
113
+ },
114
+ "typeConstraints": { "T": ["float32", "float16"], "M": ["int32"] },
115
+ "args": {
116
+ "queryT": { "kind": "tensor", "semantic": "query", "role": "input" },
117
+ "keyT": { "kind": "tensor", "semantic": "key", "role": "input", "required": false },
118
+ "valueT": { "kind": "tensor", "semantic": "value", "role": "input", "required": false },
119
+ "pastKeyT": { "kind": "tensor", "semantic": "past_key", "role": "inout" },
120
+ "pastValueT": { "kind": "tensor", "semantic": "past_value", "role": "inout" },
121
+ "blockRowIndicesT": { "kind": "tensor", "semantic": "block_row_indices", "role": "input", "dtype": "int32" },
122
+ "blockColIndicesT": { "kind": "tensor", "semantic": "block_col_indices", "role": "input", "dtype": "int32" },
123
+ "totalSequenceLengthT": { "kind": "tensor", "semantic": "total_sequence_length", "role": "input", "dtype": "int32" },
124
+ "keyTotalSequenceLengthsT": {
125
+ "kind": "tensor",
126
+ "semantic": "key_total_sequence_lengths",
127
+ "role": "input",
128
+ "dtype": "int32"
129
+ },
130
+ "cosCacheT": { "kind": "tensor", "semantic": "cos_cache", "role": "input", "required": false },
131
+ "sinCacheT": { "kind": "tensor", "semantic": "sin_cache", "role": "input", "required": false },
132
+ "outputT": { "kind": "tensor", "semantic": "output", "role": "output" }
133
+ },
134
+ "tunables": { "WORKGROUP_SIZE": 128, "APPEND_WORKGROUP_SIZE": 256, "NARROW_MIN_WORKGROUPS": 1024, "QUERY_TILE": 4 },
135
+ "derive": {
136
+ "batchSize": "dim(shapes.queryT, 0)",
137
+ "seqLen": "dim(shapes.queryT, 1)",
138
+ "numHeads": "attrs.num_heads",
139
+ "kvNumHeads": "attrs.kv_num_heads",
140
+ "sparseBlockSize": "attrs.sparse_block_size",
141
+ "headSize": "dim(shapes.pastKeyT, 3)",
142
+ "headVec": "headSize / 4",
143
+ "sparseWidthBound": "max(256, tunables.WORKGROUP_SIZE)",
144
+ "sparseQueryTileCap": "max(1, floor((device.limits.maxComputeWorkgroupStorageSize / 4 - sparseWidthBound) / (2 * headSize + 3 * sparseWidthBound)))",
145
+ "sparseQueryTileWant": "min(tunables.QUERY_TILE, min(sparseBlockSize, sparseQueryTileCap))",
146
+ "sparseQueryTile": "1 if seqLen <= 1 else (16 if sparseQueryTileWant >= 16 and seqLen >= 16 else (8 if sparseQueryTileWant >= 8 and seqLen >= 8 else (4 if sparseQueryTileWant >= 4 and seqLen >= 4 else (2 if sparseQueryTileWant >= 2 and seqLen >= 2 else 1))))",
147
+ "sparseQueryTiles": "ceilDiv(seqLen, sparseQueryTile)",
148
+ "sparseAttnWorkgroups": "sparseQueryTiles * batchSize * numHeads",
149
+ "sparseAttnWorkgroup": "min(256, max(32, pow2ceil(headVec))) if sparseAttnWorkgroups >= tunables.NARROW_MIN_WORKGROUPS else tunables.WORKGROUP_SIZE",
150
+ "maxCacheSeq": "dim(shapes.pastKeyT, 2)",
151
+ "numLayout": "dim(shapes.blockRowIndicesT, 0)",
152
+ "maxBlocks": "dim(shapes.blockRowIndicesT, 1) - 1",
153
+ "maxNnz": "dim(shapes.blockColIndicesT, 1)",
154
+ "packedQkv": "not present.keyT",
155
+ "qHidden": "numHeads * headSize",
156
+ "kvHidden": "kvNumHeads * headSize",
157
+ "packedStride": "(numHeads + 2 * kvNumHeads) * headSize",
158
+ "doRotary": "attrs.do_rotary == 1",
159
+ "rotaryHalf": "dim(shapes.cosCacheT, 1) if doRotary and present.cosCacheT and ranks.cosCacheT == 2 else 0",
160
+ "rotaryDim": "2 * rotaryHalf",
161
+ "useRotary": "doRotary and rotaryDim > 0",
162
+ "rotaryInterleaved": "attrs.rotary_interleaved == 1",
163
+ "qRotaryElements": "batchSize * numHeads * seqLen * headSize",
164
+ "cacheShapeOk": "ranks.pastKeyT == 4 and ranks.pastValueT == 4 and dim(shapes.pastKeyT, 0) == batchSize and dim(shapes.pastKeyT, 1) == kvNumHeads and sameShape(shapes.pastValueT, shapes.pastKeyT)",
165
+ "queryShapeOk": "dim(shapes.queryT, 2) == (packedStride if packedQkv else qHidden)",
166
+ "kvShapeOk": "packedQkv or (present.valueT and ranks.keyT == 3 and ranks.valueT == 3 and dim(shapes.keyT, 0) == batchSize and dim(shapes.keyT, 1) == seqLen and dim(shapes.keyT, 2) == kvHidden and sameShape(shapes.valueT, shapes.keyT) and tensorDtypes.keyT == tensorDtypes.queryT and tensorDtypes.valueT == tensorDtypes.queryT)",
167
+ "kvPairOk": "present.keyT == present.valueT",
168
+ "rotaryPairOk": "not doRotary or (present.cosCacheT and present.sinCacheT and ranks.cosCacheT == 2 and ranks.sinCacheT == 2 and rotaryHalf % 8 == 0 and rotaryDim <= headSize and sameShape(shapes.sinCacheT, shapes.cosCacheT) and tensorDtypes.cosCacheT == tensorDtypes.queryT and tensorDtypes.sinCacheT == tensorDtypes.queryT)",
169
+ "blockIndexShapeOk": "ranks.blockRowIndicesT == 2 and ranks.blockColIndicesT == 2 and dim(shapes.blockColIndicesT, 0) == numLayout and maxBlocks >= 1 and maxNnz >= 0 and maxNnz <= maxBlocks * maxBlocks and tensorDtypes.blockRowIndicesT == \"int32\" and tensorDtypes.blockColIndicesT == \"int32\"",
170
+ "scheduleShapeOk": "(ranks.totalSequenceLengthT == 0 or ranks.totalSequenceLengthT == 1) and numel(shapes.totalSequenceLengthT) == 1 and ranks.keyTotalSequenceLengthsT == 1 and dim(shapes.keyTotalSequenceLengthsT, 0) == batchSize and tensorDtypes.totalSequenceLengthT == \"int32\" and tensorDtypes.keyTotalSequenceLengthsT == \"int32\"",
171
+ "geometryOk": "tunables.WORKGROUP_SIZE >= 1 and floor(tunables.WORKGROUP_SIZE) == tunables.WORKGROUP_SIZE and pow2ceil(tunables.WORKGROUP_SIZE) == tunables.WORKGROUP_SIZE and tunables.WORKGROUP_SIZE <= device.limits.maxComputeInvocationsPerWorkgroup and tunables.WORKGROUP_SIZE <= device.limits.maxComputeWorkgroupSizeX and tunables.APPEND_WORKGROUP_SIZE >= 1 and floor(tunables.APPEND_WORKGROUP_SIZE) == tunables.APPEND_WORKGROUP_SIZE and tunables.APPEND_WORKGROUP_SIZE <= device.limits.maxComputeInvocationsPerWorkgroup and tunables.APPEND_WORKGROUP_SIZE <= device.limits.maxComputeWorkgroupSizeX and sparseQueryTiles <= device.limits.maxComputeWorkgroupsPerDimension and batchSize * numHeads <= device.limits.maxComputeWorkgroupsPerDimension and ceilDiv(ceilDiv(qRotaryElements, tunables.APPEND_WORKGROUP_SIZE), device.limits.maxComputeWorkgroupsPerDimension) <= device.limits.maxComputeWorkgroupsPerDimension and (2 * sparseQueryTile * headSize + (3 * sparseQueryTile + 1) * sparseAttnWorkgroup) * 4 <= device.limits.maxComputeWorkgroupStorageSize and sparseAttnWorkgroup <= device.limits.maxComputeInvocationsPerWorkgroup and sparseAttnWorkgroup <= device.limits.maxComputeWorkgroupSizeX",
172
+ "contract": "ranks.queryT == 3 and ranks.outputT == 3 and (tensorDtypes.queryT == \"float32\" or tensorDtypes.queryT == \"float16\") and f16Ok(dtypes.T) and tensorDtypes.pastKeyT == tensorDtypes.queryT and tensorDtypes.pastValueT == tensorDtypes.queryT and tensorDtypes.outputT == tensorDtypes.queryT and numHeads >= 1 and kvNumHeads >= 1 and numHeads % kvNumHeads == 0 and headSize >= 8 and headSize % 8 == 0 and (not doRotary or headSize % 16 == 0) and numLayout >= 1 and numHeads % numLayout == 0 and (sparseBlockSize == 16 or sparseBlockSize == 32 or sparseBlockSize == 64 or sparseBlockSize == 128) and cacheShapeOk and queryShapeOk and kvShapeOk and kvPairOk and rotaryPairOk and blockIndexShapeOk and scheduleShapeOk and dim(shapes.outputT, 0) == batchSize and dim(shapes.outputT, 1) == seqLen and dim(shapes.outputT, 2) == qHidden",
173
+ "packedContract": "contract and packedQkv and not useRotary",
174
+ "packedRotaryContract": "contract and packedQkv and useRotary",
175
+ "separateContract": "contract and not packedQkv and not useRotary",
176
+ "separateRotaryContract": "contract and not packedQkv and useRotary"
177
+ },
178
+ "constants": {
179
+ "scalar": "dtypes.T",
180
+ "usesF16": "dtypes.T == \"f16\"",
181
+ "numHeads": "numHeads",
182
+ "kvNumHeads": "kvNumHeads",
183
+ "headSize": "headSize",
184
+ "headVec": "headVec",
185
+ "cacheVec": "\"vec4<f16>\" if dtypes.T == \"f16\" else \"vec4<f32>\"",
186
+ "attnWorkgroup": "sparseAttnWorkgroup",
187
+ "maxCacheSeq": "maxCacheSeq",
188
+ "sparseBlockSize": "sparseBlockSize",
189
+ "numLayout": "numLayout",
190
+ "maxBlocks": "maxBlocks",
191
+ "maxNnz": "maxNnz",
192
+ "packedStride": "packedStride",
193
+ "packedQkv": "packedQkv",
194
+ "usesRotary": "useRotary",
195
+ "rotaryHalf": "rotaryHalf",
196
+ "rotaryDim": "rotaryDim",
197
+ "rotaryInterleaved": "rotaryInterleaved",
198
+ "appendWorkgroupSize": "tunables.APPEND_WORKGROUP_SIZE"
199
+ },
200
+ "bindingSets": {
201
+ "appendSeparate": [
202
+ {
203
+ "name": "new_key",
204
+ "arg": "keyT",
205
+ "semantic": "key",
206
+ "buffer": { "type": "read-only-storage" },
207
+ "elementType": "$scalar"
208
+ },
209
+ {
210
+ "name": "new_value",
211
+ "arg": "valueT",
212
+ "semantic": "value",
213
+ "buffer": { "type": "read-only-storage" },
214
+ "elementType": "$scalar"
215
+ },
216
+ {
217
+ "name": "present_key",
218
+ "arg": "pastKeyT",
219
+ "semantic": "past_key",
220
+ "buffer": { "type": "storage" },
221
+ "elementType": "$scalar"
222
+ },
223
+ {
224
+ "name": "present_value",
225
+ "arg": "pastValueT",
226
+ "semantic": "past_value",
227
+ "buffer": { "type": "storage" },
228
+ "elementType": "$scalar"
229
+ },
230
+ {
231
+ "name": "key_total_sequence_lengths",
232
+ "arg": "keyTotalSequenceLengthsT",
233
+ "semantic": "key_total_sequence_lengths",
234
+ "buffer": { "type": "read-only-storage" },
235
+ "elementType": "i32"
236
+ },
237
+ {
238
+ "name": "total_sequence_length",
239
+ "arg": "totalSequenceLengthT",
240
+ "semantic": "total_sequence_length",
241
+ "buffer": { "type": "read-only-storage" },
242
+ "elementType": "i32"
243
+ },
244
+ {
245
+ "name": "params",
246
+ "semantic": "kernel.params",
247
+ "buffer": { "type": "uniform" },
248
+ "struct": {
249
+ "name": "Params",
250
+ "fields": [
251
+ { "name": "batchSize", "type": "u32", "value": "batchSize" },
252
+ { "name": "seqLen", "type": "u32", "value": "seqLen" }
253
+ ]
254
+ }
255
+ }
256
+ ],
257
+ "appendSeparateRotary": [
258
+ {
259
+ "name": "new_key",
260
+ "arg": "keyT",
261
+ "semantic": "key",
262
+ "buffer": { "type": "read-only-storage" },
263
+ "elementType": "$scalar"
264
+ },
265
+ {
266
+ "name": "new_value",
267
+ "arg": "valueT",
268
+ "semantic": "value",
269
+ "buffer": { "type": "read-only-storage" },
270
+ "elementType": "$scalar"
271
+ },
272
+ {
273
+ "name": "present_key",
274
+ "arg": "pastKeyT",
275
+ "semantic": "past_key",
276
+ "buffer": { "type": "storage" },
277
+ "elementType": "$scalar"
278
+ },
279
+ {
280
+ "name": "present_value",
281
+ "arg": "pastValueT",
282
+ "semantic": "past_value",
283
+ "buffer": { "type": "storage" },
284
+ "elementType": "$scalar"
285
+ },
286
+ {
287
+ "name": "key_total_sequence_lengths",
288
+ "arg": "keyTotalSequenceLengthsT",
289
+ "semantic": "key_total_sequence_lengths",
290
+ "buffer": { "type": "read-only-storage" },
291
+ "elementType": "i32"
292
+ },
293
+ {
294
+ "name": "total_sequence_length",
295
+ "arg": "totalSequenceLengthT",
296
+ "semantic": "total_sequence_length",
297
+ "buffer": { "type": "read-only-storage" },
298
+ "elementType": "i32"
299
+ },
300
+ {
301
+ "name": "cos_cache",
302
+ "arg": "cosCacheT",
303
+ "semantic": "cos_cache",
304
+ "buffer": { "type": "read-only-storage" },
305
+ "elementType": "$scalar"
306
+ },
307
+ {
308
+ "name": "sin_cache",
309
+ "arg": "sinCacheT",
310
+ "semantic": "sin_cache",
311
+ "buffer": { "type": "read-only-storage" },
312
+ "elementType": "$scalar"
313
+ },
314
+ {
315
+ "name": "params",
316
+ "semantic": "kernel.params",
317
+ "buffer": { "type": "uniform" },
318
+ "struct": {
319
+ "name": "Params",
320
+ "fields": [
321
+ { "name": "batchSize", "type": "u32", "value": "batchSize" },
322
+ { "name": "seqLen", "type": "u32", "value": "seqLen" }
323
+ ]
324
+ }
325
+ }
326
+ ],
327
+ "appendPacked": [
328
+ {
329
+ "name": "packed_qkv",
330
+ "arg": "queryT",
331
+ "semantic": "query",
332
+ "buffer": { "type": "read-only-storage" },
333
+ "elementType": "$scalar"
334
+ },
335
+ {
336
+ "name": "present_key",
337
+ "arg": "pastKeyT",
338
+ "semantic": "past_key",
339
+ "buffer": { "type": "storage" },
340
+ "elementType": "$scalar"
341
+ },
342
+ {
343
+ "name": "present_value",
344
+ "arg": "pastValueT",
345
+ "semantic": "past_value",
346
+ "buffer": { "type": "storage" },
347
+ "elementType": "$scalar"
348
+ },
349
+ {
350
+ "name": "key_total_sequence_lengths",
351
+ "arg": "keyTotalSequenceLengthsT",
352
+ "semantic": "key_total_sequence_lengths",
353
+ "buffer": { "type": "read-only-storage" },
354
+ "elementType": "i32"
355
+ },
356
+ {
357
+ "name": "total_sequence_length",
358
+ "arg": "totalSequenceLengthT",
359
+ "semantic": "total_sequence_length",
360
+ "buffer": { "type": "read-only-storage" },
361
+ "elementType": "i32"
362
+ },
363
+ {
364
+ "name": "params",
365
+ "semantic": "kernel.params",
366
+ "buffer": { "type": "uniform" },
367
+ "struct": {
368
+ "name": "Params",
369
+ "fields": [
370
+ { "name": "batchSize", "type": "u32", "value": "batchSize" },
371
+ { "name": "seqLen", "type": "u32", "value": "seqLen" }
372
+ ]
373
+ }
374
+ }
375
+ ],
376
+ "appendPackedRotary": [
377
+ {
378
+ "name": "packed_qkv",
379
+ "arg": "queryT",
380
+ "semantic": "query",
381
+ "buffer": { "type": "read-only-storage" },
382
+ "elementType": "$scalar"
383
+ },
384
+ {
385
+ "name": "present_key",
386
+ "arg": "pastKeyT",
387
+ "semantic": "past_key",
388
+ "buffer": { "type": "storage" },
389
+ "elementType": "$scalar"
390
+ },
391
+ {
392
+ "name": "present_value",
393
+ "arg": "pastValueT",
394
+ "semantic": "past_value",
395
+ "buffer": { "type": "storage" },
396
+ "elementType": "$scalar"
397
+ },
398
+ {
399
+ "name": "key_total_sequence_lengths",
400
+ "arg": "keyTotalSequenceLengthsT",
401
+ "semantic": "key_total_sequence_lengths",
402
+ "buffer": { "type": "read-only-storage" },
403
+ "elementType": "i32"
404
+ },
405
+ {
406
+ "name": "total_sequence_length",
407
+ "arg": "totalSequenceLengthT",
408
+ "semantic": "total_sequence_length",
409
+ "buffer": { "type": "read-only-storage" },
410
+ "elementType": "i32"
411
+ },
412
+ {
413
+ "name": "cos_cache",
414
+ "arg": "cosCacheT",
415
+ "semantic": "cos_cache",
416
+ "buffer": { "type": "read-only-storage" },
417
+ "elementType": "$scalar"
418
+ },
419
+ {
420
+ "name": "sin_cache",
421
+ "arg": "sinCacheT",
422
+ "semantic": "sin_cache",
423
+ "buffer": { "type": "read-only-storage" },
424
+ "elementType": "$scalar"
425
+ },
426
+ {
427
+ "name": "params",
428
+ "semantic": "kernel.params",
429
+ "buffer": { "type": "uniform" },
430
+ "struct": {
431
+ "name": "Params",
432
+ "fields": [
433
+ { "name": "batchSize", "type": "u32", "value": "batchSize" },
434
+ { "name": "seqLen", "type": "u32", "value": "seqLen" }
435
+ ]
436
+ }
437
+ }
438
+ ],
439
+ "qRotary": [
440
+ {
441
+ "name": "query",
442
+ "arg": "queryT",
443
+ "semantic": "query",
444
+ "buffer": { "type": "read-only-storage" },
445
+ "elementType": "$scalar"
446
+ },
447
+ {
448
+ "name": "cos_cache",
449
+ "arg": "cosCacheT",
450
+ "semantic": "cos_cache",
451
+ "buffer": { "type": "read-only-storage" },
452
+ "elementType": "$scalar"
453
+ },
454
+ {
455
+ "name": "sin_cache",
456
+ "arg": "sinCacheT",
457
+ "semantic": "sin_cache",
458
+ "buffer": { "type": "read-only-storage" },
459
+ "elementType": "$scalar"
460
+ },
461
+ { "name": "q_rotary", "semantic": "QRotary", "buffer": { "type": "storage" }, "elementType": "f32" },
462
+ {
463
+ "name": "key_total_sequence_lengths",
464
+ "arg": "keyTotalSequenceLengthsT",
465
+ "semantic": "key_total_sequence_lengths",
466
+ "buffer": { "type": "read-only-storage" },
467
+ "elementType": "i32"
468
+ },
469
+ {
470
+ "name": "total_sequence_length",
471
+ "arg": "totalSequenceLengthT",
472
+ "semantic": "total_sequence_length",
473
+ "buffer": { "type": "read-only-storage" },
474
+ "elementType": "i32"
475
+ },
476
+ {
477
+ "name": "params",
478
+ "semantic": "kernel.params",
479
+ "buffer": { "type": "uniform" },
480
+ "struct": {
481
+ "name": "Params",
482
+ "fields": [
483
+ { "name": "batchSize", "type": "u32", "value": "batchSize" },
484
+ { "name": "seqLen", "type": "u32", "value": "seqLen" }
485
+ ]
486
+ }
487
+ }
488
+ ],
489
+ "attentionDirect": [
490
+ {
491
+ "name": "query",
492
+ "arg": "queryT",
493
+ "semantic": "query",
494
+ "buffer": { "type": "read-only-storage" },
495
+ "elementType": "$scalar"
496
+ },
497
+ {
498
+ "name": "present_key",
499
+ "arg": "pastKeyT",
500
+ "semantic": "past_key",
501
+ "buffer": { "type": "read-only-storage" },
502
+ "elementType": "$cacheVec"
503
+ },
504
+ {
505
+ "name": "present_value",
506
+ "arg": "pastValueT",
507
+ "semantic": "past_value",
508
+ "buffer": { "type": "read-only-storage" },
509
+ "elementType": "$cacheVec"
510
+ },
511
+ {
512
+ "name": "block_row_indices",
513
+ "arg": "blockRowIndicesT",
514
+ "semantic": "block_row_indices",
515
+ "buffer": { "type": "read-only-storage" },
516
+ "elementType": "i32"
517
+ },
518
+ {
519
+ "name": "block_col_indices",
520
+ "arg": "blockColIndicesT",
521
+ "semantic": "block_col_indices",
522
+ "buffer": { "type": "read-only-storage" },
523
+ "elementType": "i32"
524
+ },
525
+ {
526
+ "name": "key_total_sequence_lengths",
527
+ "arg": "keyTotalSequenceLengthsT",
528
+ "semantic": "key_total_sequence_lengths",
529
+ "buffer": { "type": "read-only-storage" },
530
+ "elementType": "i32"
531
+ },
532
+ {
533
+ "name": "total_sequence_length",
534
+ "arg": "totalSequenceLengthT",
535
+ "semantic": "total_sequence_length",
536
+ "buffer": { "type": "read-only-storage" },
537
+ "elementType": "i32"
538
+ },
539
+ {
540
+ "name": "output",
541
+ "arg": "outputT",
542
+ "semantic": "output",
543
+ "buffer": { "type": "storage" },
544
+ "elementType": "$scalar"
545
+ },
546
+ {
547
+ "name": "params",
548
+ "semantic": "kernel.params",
549
+ "buffer": { "type": "uniform" },
550
+ "struct": {
551
+ "name": "Params",
552
+ "fields": [
553
+ { "name": "seqLen", "type": "u32", "value": "seqLen" },
554
+ { "name": "scale", "type": "f32", "value": "attrs.scale if has(attrs, \"scale\") else 0" }
555
+ ]
556
+ }
557
+ }
558
+ ],
559
+ "attentionRotary": [
560
+ { "name": "q_rotary", "semantic": "QRotary", "buffer": { "type": "read-only-storage" }, "elementType": "f32" },
561
+ {
562
+ "name": "present_key",
563
+ "arg": "pastKeyT",
564
+ "semantic": "past_key",
565
+ "buffer": { "type": "read-only-storage" },
566
+ "elementType": "$cacheVec"
567
+ },
568
+ {
569
+ "name": "present_value",
570
+ "arg": "pastValueT",
571
+ "semantic": "past_value",
572
+ "buffer": { "type": "read-only-storage" },
573
+ "elementType": "$cacheVec"
574
+ },
575
+ {
576
+ "name": "block_row_indices",
577
+ "arg": "blockRowIndicesT",
578
+ "semantic": "block_row_indices",
579
+ "buffer": { "type": "read-only-storage" },
580
+ "elementType": "i32"
581
+ },
582
+ {
583
+ "name": "block_col_indices",
584
+ "arg": "blockColIndicesT",
585
+ "semantic": "block_col_indices",
586
+ "buffer": { "type": "read-only-storage" },
587
+ "elementType": "i32"
588
+ },
589
+ {
590
+ "name": "key_total_sequence_lengths",
591
+ "arg": "keyTotalSequenceLengthsT",
592
+ "semantic": "key_total_sequence_lengths",
593
+ "buffer": { "type": "read-only-storage" },
594
+ "elementType": "i32"
595
+ },
596
+ {
597
+ "name": "total_sequence_length",
598
+ "arg": "totalSequenceLengthT",
599
+ "semantic": "total_sequence_length",
600
+ "buffer": { "type": "read-only-storage" },
601
+ "elementType": "i32"
602
+ },
603
+ {
604
+ "name": "output",
605
+ "arg": "outputT",
606
+ "semantic": "output",
607
+ "buffer": { "type": "storage" },
608
+ "elementType": "$scalar"
609
+ },
610
+ {
611
+ "name": "params",
612
+ "semantic": "kernel.params",
613
+ "buffer": { "type": "uniform" },
614
+ "struct": {
615
+ "name": "Params",
616
+ "fields": [
617
+ { "name": "seqLen", "type": "u32", "value": "seqLen" },
618
+ { "name": "scale", "type": "f32", "value": "attrs.scale if has(attrs, \"scale\") else 0" }
619
+ ]
620
+ }
621
+ }
622
+ ]
623
+ },
624
+ "variants": [
625
+ {
626
+ "id": "separate",
627
+ "priority": 0,
628
+ "when": ["separateContract", "geometryOk"],
629
+ "passes": [
630
+ {
631
+ "id": "append",
632
+ "name": "SparseAttention.Append",
633
+ "shader": "sparse-kv-append.wgsl.jinja",
634
+ "bindings": "appendSeparate",
635
+ "constants": { "kvSource": "\"new_key\"", "vSource": "\"new_value\"" },
636
+ "dispatch": {
637
+ "threads": "batchSize * kvNumHeads * seqLen * headSize",
638
+ "workgroupSize": "constants.appendWorkgroupSize"
639
+ }
640
+ },
641
+ {
642
+ "id": "attention",
643
+ "name": "SparseAttention.Attention",
644
+ "shader": "sparse-attention.wgsl.jinja",
645
+ "bindings": "attentionDirect",
646
+ "dispatch": { "x": "sparseQueryTiles", "y": "batchSize * numHeads" },
647
+ "constants": { "qTile": "sparseQueryTile" }
648
+ }
649
+ ]
650
+ },
651
+ {
652
+ "id": "separate_rotary",
653
+ "priority": 10,
654
+ "when": ["separateRotaryContract", "geometryOk"],
655
+ "intermediates": [{ "id": "QRotary", "dtype": "float32", "shape": "[qRotaryElements]" }],
656
+ "passes": [
657
+ {
658
+ "id": "append",
659
+ "name": "SparseAttention.Append",
660
+ "shader": "sparse-kv-append.wgsl.jinja",
661
+ "bindings": "appendSeparateRotary",
662
+ "constants": { "kvSource": "\"new_key\"", "vSource": "\"new_value\"" },
663
+ "dispatch": {
664
+ "threads": "batchSize * kvNumHeads * seqLen * headSize",
665
+ "workgroupSize": "constants.appendWorkgroupSize"
666
+ }
667
+ },
668
+ {
669
+ "id": "qrotary",
670
+ "name": "SparseAttention.QueryRotary",
671
+ "shader": "sparse-q-rotary.wgsl.jinja",
672
+ "bindings": "qRotary",
673
+ "dispatch": {
674
+ "threads": "batchSize * numHeads * seqLen * headSize",
675
+ "workgroupSize": "constants.appendWorkgroupSize"
676
+ }
677
+ },
678
+ {
679
+ "id": "attention",
680
+ "name": "SparseAttention.Attention",
681
+ "shader": "sparse-attention.wgsl.jinja",
682
+ "bindings": "attentionRotary",
683
+ "dispatch": { "x": "sparseQueryTiles", "y": "batchSize * numHeads" },
684
+ "constants": { "qTile": "sparseQueryTile" }
685
+ }
686
+ ]
687
+ },
688
+ {
689
+ "id": "packed",
690
+ "priority": 0,
691
+ "when": ["packedContract", "geometryOk"],
692
+ "passes": [
693
+ {
694
+ "id": "append",
695
+ "name": "SparseAttention.Append",
696
+ "shader": "sparse-kv-append.wgsl.jinja",
697
+ "bindings": "appendPacked",
698
+ "constants": { "kvSource": "\"packed_qkv\"", "vSource": "\"packed_qkv\"" },
699
+ "dispatch": {
700
+ "threads": "batchSize * kvNumHeads * seqLen * headSize",
701
+ "workgroupSize": "constants.appendWorkgroupSize"
702
+ }
703
+ },
704
+ {
705
+ "id": "attention",
706
+ "name": "SparseAttention.Attention",
707
+ "shader": "sparse-attention.wgsl.jinja",
708
+ "bindings": "attentionDirect",
709
+ "dispatch": { "x": "sparseQueryTiles", "y": "batchSize * numHeads" },
710
+ "constants": { "qTile": "sparseQueryTile" }
711
+ }
712
+ ]
713
+ },
714
+ {
715
+ "id": "packed_rotary",
716
+ "priority": 10,
717
+ "when": ["packedRotaryContract", "geometryOk"],
718
+ "intermediates": [{ "id": "QRotary", "dtype": "float32", "shape": "[qRotaryElements]" }],
719
+ "passes": [
720
+ {
721
+ "id": "append",
722
+ "name": "SparseAttention.Append",
723
+ "shader": "sparse-kv-append.wgsl.jinja",
724
+ "bindings": "appendPackedRotary",
725
+ "constants": { "kvSource": "\"packed_qkv\"", "vSource": "\"packed_qkv\"" },
726
+ "dispatch": {
727
+ "threads": "batchSize * kvNumHeads * seqLen * headSize",
728
+ "workgroupSize": "constants.appendWorkgroupSize"
729
+ }
730
+ },
731
+ {
732
+ "id": "qrotary",
733
+ "name": "SparseAttention.QueryRotary",
734
+ "shader": "sparse-q-rotary.wgsl.jinja",
735
+ "bindings": "qRotary",
736
+ "dispatch": {
737
+ "threads": "batchSize * numHeads * seqLen * headSize",
738
+ "workgroupSize": "constants.appendWorkgroupSize"
739
+ }
740
+ },
741
+ {
742
+ "id": "attention",
743
+ "name": "SparseAttention.Attention",
744
+ "shader": "sparse-attention.wgsl.jinja",
745
+ "bindings": "attentionRotary",
746
+ "dispatch": { "x": "sparseQueryTiles", "y": "batchSize * numHeads" },
747
+ "constants": { "qTile": "sparseQueryTile" }
748
+ }
749
+ ]
750
+ }
751
+ ]
752
+ }
build/webgpu/metadata.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "com.microsoft.SparseAttention",
3
+ "id": "_com_microsoft_sparseattention_webgpu_07e93c9",
4
+ "version": 1,
5
+ "license": "Apache-2.0",
6
+ "backend": { "type": "webgpu" },
7
+ "digest": {
8
+ "algorithm": "sha256",
9
+ "files": {
10
+ "bench.json": "E0HHOiKzV0TA0/6aDZXLjmxd2z/Q5iYFBFGNYeQoQM0=",
11
+ "manifest.json": "L9ljQObhgAxTXE7AuOhOeG1E7zQwFIDFC+FnIgrU864=",
12
+ "sparse-attention.wgsl.jinja": "zYSxDV01i/wcjq9YJjdWDzRHkAbZA6Ctx7Fi/3xdrF4=",
13
+ "sparse-kv-append.wgsl.jinja": "RavNXJuGESPqe/Jzt5Ek6+844s5q2w/ocyUgGP3cWec=",
14
+ "sparse-q-rotary.wgsl.jinja": "hI7d9cx6RqRO079h4xPBMTMj+qKqIUOKW3pF3vdeWdg=",
15
+ "test.json": "aOQD21sQAgUaJwht0fYeGHtHCmUc7VPdhXjgKLH9YEU="
16
+ }
17
+ },
18
+ "provenance": { "kernel": { "sha": "2e7068faf55e7f43df740015f6d1ee49391a41c5", "dirty": false } },
19
+ "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/com.microsoft.SparseAttention" }
20
+ }
build/webgpu/sparse-attention.wgsl.jinja ADDED
@@ -0,0 +1,425 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% macro sparse_schedule() %}
2
+ // How much history precedes this call's tokens. Prompt mode is determined from
3
+ // scalar total_sequence_length rather than per-batch values and forces the past
4
+ // length to zero; a padded prompt row must not be read as if it had history.
5
+ fn past_sequence_length(batch: u32) -> u32 {
6
+ if (u32(total_sequence_length[0]) == params.seqLen) {
7
+ return 0u;
8
+ }
9
+ let total = u32(key_total_sequence_lengths[batch]);
10
+ return select(0u, total - params.seqLen, total >= params.seqLen);
11
+ }
12
+ {%- endmacro %}
13
+
14
+ {% if usesF16 %}
15
+ enable f16;
16
+ {% endif %}
17
+ {{ env.wgsl.resourceDeclarations }}
18
+
19
+ // com.microsoft.SparseAttention, attention pass.
20
+ // One workgroup per (batch, query-token tile, query head). The workgroup sweeps only the key
21
+ // blocks this query's layout row selects, in tiles of WG keys with one key per thread,
22
+ // merging each tile into a running online softmax so the score row is never
23
+ // materialized. Repeated CSR columns are suppressed because the CSR data represents a
24
+ // boolean block mask.
25
+ //
26
+ // The block mask is CSR: row q_abs / SPARSE_BLOCK of layout (head % NUM_LAYOUT) spans
27
+ // block_col_indices[start .. end), and each of those columns names SPARSE_BLOCK
28
+ // contiguous keys. Sweeping (end - start) * SPARSE_BLOCK virtual slots and resolving
29
+ // each through the column list is what makes the kernel cost the SELECTED blocks rather
30
+ // than the whole history -- the reason the operator exists.
31
+ //
32
+ // Causality bounds every visited row at this query's absolute position. A layout
33
+ // with the triangular dense count and a sparse row with row + 1 entries take the
34
+ // dense shortcut without inspecting their column values.
35
+ const Q_HEADS: u32 = {{ numHeads }}u;
36
+ const KV_HEADS: u32 = {{ kvNumHeads }}u;
37
+ const HEAD_DIM: u32 = {{ headSize }}u;
38
+ // The cache is read four elements at a time. The sweep was one narrow dependent load
39
+ // per head dimension per key, and the score phase alone issues HEAD_DIM of them for
40
+ // every key a thread owns; a vec4 view quarters that count. The operator's contract
41
+ // requires head_size to be a multiple of eight, so there is no shape where the
42
+ // quartering does not divide and no scalar form to fall back to.
43
+ const HEAD_VEC: u32 = {{ headVec }}u;
44
+ const MAX_CACHE_SEQ: u32 = {{ maxCacheSeq }}u;
45
+ const SPARSE_BLOCK: u32 = {{ sparseBlockSize }}u;
46
+ const NUM_LAYOUT: u32 = {{ numLayout }}u;
47
+ const ROW_STRIDE: u32 = {{ maxBlocks + 1 }}u;
48
+ const COL_STRIDE: u32 = {{ maxNnz }}u;
49
+ const Q_HIDDEN: u32 = {{ numHeads * headSize }}u;
50
+ {% if not usesRotary %}
51
+ const Q_STRIDE: u32 = {{ packedStride if packedQkv else numHeads * headSize }}u;
52
+ {% endif %}
53
+ const WG: u32 = {{ attnWorkgroup }}u;
54
+ const Q_TILE: u32 = {{ qTile }}u;
55
+
56
+ // FLT_MAX, not -inf, as the online (m, d) accumulator init: merges must keep
57
+ // `m - m` finite so an empty lane / all--inf row contributes the exact
58
+ // accumulator identity (m, d) = (-FLT_MAX, 0). Operator epilogues interpret
59
+ // a zero final denominator according to their public semantics. Using -inf
60
+ // here changes +inf-row behavior.
61
+ const FLT_MAX: f32 = 3.4028234663852886e38;
62
+
63
+ fn is_finite_f32(value: f32) -> bool {
64
+ return select(false, value <= FLT_MAX, value >= -FLT_MAX);
65
+ }
66
+
67
+ // x - m that is exactly 0 when x equals a finite m, so exp(shifted) == 1
68
+ // exactly at the row max. `x - x` on an infinite max is a legal fast-math
69
+ // fold to 0, which would silently turn +inf rows finite — the explicit
70
+ // equality test keeps the NaN propagation of the serial kernels.
71
+ fn shifted_value(value: f32, maxValue: f32) -> f32 {
72
+ let equalFiniteMax = select(false, value == maxValue, is_finite_f32(maxValue));
73
+ return select(value - maxValue, 0.0, equalFiniteMax);
74
+ }
75
+ fn exp_shift(value: f32, maxValue: f32) -> f32 {
76
+ return exp(shifted_value(value, maxValue));
77
+ }
78
+
79
+ var<workgroup> q_shared: array<vec4<f32>, Q_TILE * HEAD_VEC>;
80
+ var<workgroup> running_out: array<vec4<f32>, Q_TILE * HEAD_VEC>;
81
+ var<workgroup> probs: array<f32, Q_TILE * WG>;
82
+ // One resolved cache row base per key of the current tile, so the value accumulation
83
+ // re-reads a base instead of re-walking the column list per head dimension.
84
+ var<workgroup> key_rows: array<u32, WG>;
85
+
86
+ {% set mdStreams = qTile %}
87
+ // Workgroup-cooperative merge of per-thread online-softmax (m, d) partials:
88
+ // mNew = max(m1, m2)
89
+ // dNew = d1 * exp(m1 - mNew) + d2 * exp(m2 - mNew)
90
+ // Both the subgroup and portable barrier-tree engines return the same merged
91
+ // pair to every invocation. Repeated merges require a workgroup barrier between
92
+ // calls before their shared partial storage is reused.
93
+ {% set combineSubgroups = combineSubgroups is defined and combineSubgroups %}
94
+ {% if combineSubgroups %}
95
+ // Per-subgroup partials are published into a deterministic slot: the subgroup's
96
+ // ordinal index within the workgroup (lidx / sgSize). The online (m, d) merge
97
+ // is not float-associative, so thread 0 must fold partials in a fixed order.
98
+ // Subgroups partition a workgroup into contiguous ordinal ranges on supported
99
+ // backends, so the ordinal slot is unique per subgroup and every slot in
100
+ // [0, subgroupCount) is written (each subgroup elects one leader).
101
+ // Sized for the worst case of one partial per invocation.
102
+ var<workgroup> partialM: array<f32, WG>;
103
+ var<workgroup> partialD: array<f32, WG>;
104
+ var<workgroup> combinedMD: vec2<f32>;
105
+
106
+ // When the whole workgroup is one subgroup the subgroup reduce already covers
107
+ // it (no barriers, no shared state); otherwise subgroup leaders publish
108
+ // partials through shared memory and thread 0 folds them in ordinal order.
109
+ fn combine_partials(m: f32, d: f32, lidx: u32, sgSize: u32) -> vec2<f32> {
110
+ let sgM = subgroupMax(m);
111
+ // A lane with no elements contributes d == 0 (exact identity). A +inf
112
+ // element made exp(inf - inf) = NaN stick in that lane's d; a NaN element
113
+ // landed in d via exp(NaN); both survive the merge and are detected by the
114
+ // code after the reduction.
115
+ let sgD = subgroupAdd(d * exp_shift(m, sgM));
116
+ if (sgSize == WG) {
117
+ return vec2<f32>(sgM, sgD);
118
+ }
119
+ let subgroupCount = (WG + sgSize - 1u) / sgSize;
120
+ // Pre-seed every fold slot with the (max, denom) identity. The fold below reads a
121
+ // fixed subgroupCount slots in ordinal order (for determinism), but a slot whose
122
+ // subgroup elects no leader this call — e.g. a fully out-of-window key tile in the
123
+ // flash-attention loop that re-uses this shared memory each iteration — would
124
+ // otherwise read stale shared memory. Identity makes such a slot a no-op.
125
+ // (max identity = -FLT_MAX, denom identity = 0.)
126
+ if (lidx < subgroupCount) {
127
+ partialM[lidx] = -FLT_MAX;
128
+ partialD[lidx] = 0.0;
129
+ }
130
+ workgroupBarrier();
131
+ if (subgroupElect()) {
132
+ let slot = lidx / sgSize;
133
+ partialM[slot] = sgM;
134
+ partialD[slot] = sgD;
135
+ }
136
+ workgroupBarrier();
137
+ if (lidx == 0u) {
138
+ var accM = -FLT_MAX;
139
+ var accD = 0.0;
140
+ for (var i = 0u; i < subgroupCount; i = i + 1u) {
141
+ let mNew = max(accM, partialM[i]);
142
+ accD = accD * exp_shift(accM, mNew) + partialD[i] * exp_shift(partialM[i], mNew);
143
+ accM = mNew;
144
+ }
145
+ combinedMD = vec2<f32>(accM, accD);
146
+ }
147
+ workgroupBarrier();
148
+ return combinedMD;
149
+ }
150
+ {% else %}
151
+ {% set mdStreamed = mdStreams is defined %}
152
+ {% set mdStreams = mdStreams if mdStreams is defined else 1 %}
153
+ {% set mdExtent = "WG" if mdStreams == 1 else "WG * " ~ mdStreams ~ "u" %}
154
+ var<workgroup> partialM: array<f32, {{ mdExtent }}>;
155
+ var<workgroup> partialD: array<f32, {{ mdExtent }}>;
156
+ {% if mdStreamed %}
157
+
158
+ // In-place fold of {{ mdStreams }} streams. The caller stores its per-thread
159
+ // partials into partialM/partialD first and reads the merged pair of stream s
160
+ // from slot s * WG afterwards.
161
+ fn combine_partials_streams(lidx: u32) {
162
+ workgroupBarrier();
163
+ var stride = WG / 2u;
164
+ loop {
165
+ if (stride == 0u) {
166
+ break;
167
+ }
168
+ if (lidx < stride) {
169
+ {% for s in range(mdStreams) %}
170
+ {
171
+ let slot = {{ s }}u * WG + lidx;
172
+ let m1 = partialM[slot];
173
+ let d1 = partialD[slot];
174
+ let m2 = partialM[slot + stride];
175
+ let d2 = partialD[slot + stride];
176
+ let mNew = max(m1, m2);
177
+ partialD[slot] = d1 * exp_shift(m1, mNew) + d2 * exp_shift(m2, mNew);
178
+ partialM[slot] = mNew;
179
+ }
180
+ {% endfor %}
181
+ }
182
+ workgroupBarrier();
183
+ stride = stride / 2u;
184
+ }
185
+ }
186
+ {% else %}
187
+
188
+ fn combine_partials(m: f32, d: f32, lidx: u32) -> vec2<f32> {
189
+ partialM[lidx] = m;
190
+ partialD[lidx] = d;
191
+ workgroupBarrier();
192
+ var stride = WG / 2u;
193
+ loop {
194
+ if (stride == 0u) {
195
+ break;
196
+ }
197
+ if (lidx < stride) {
198
+ let m1 = partialM[lidx];
199
+ let d1 = partialD[lidx];
200
+ let m2 = partialM[lidx + stride];
201
+ let d2 = partialD[lidx + stride];
202
+ let mNew = max(m1, m2);
203
+ partialD[lidx] = d1 * exp_shift(m1, mNew) + d2 * exp_shift(m2, mNew);
204
+ partialM[lidx] = mNew;
205
+ }
206
+ workgroupBarrier();
207
+ stride = stride / 2u;
208
+ }
209
+ let merged = vec2<f32>(partialM[0], partialD[0]);
210
+ // Trailing barrier so back-to-back calls cannot race a next call's partial
211
+ // stores against this call's reads of slot 0.
212
+ workgroupBarrier();
213
+ return merged;
214
+ }
215
+ {% endif %}
216
+ {% endif %}
217
+
218
+
219
+ {% if ATTN_SCALE_DIM is not defined %}{% set ATTN_SCALE_DIM = "HEAD_DIM" %}{% endif %}
220
+ fn scale_value() -> f32 {
221
+ if (params.scale != 0.0) { return params.scale; }
222
+ return inverseSqrt(f32({{ ATTN_SCALE_DIM }}));
223
+ }
224
+
225
+
226
+ {{ sparse_schedule() }}
227
+
228
+ @compute @workgroup_size(WG, 1, 1)
229
+ fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
230
+ let tile0 = wg.x * Q_TILE;
231
+ let head = wg.y % Q_HEADS;
232
+ let batch = wg.y / Q_HEADS;
233
+ let tid = lid.x;
234
+
235
+ let past = past_sequence_length(batch);
236
+ let kv_head = head / (Q_HEADS / KV_HEADS);
237
+ let scale = scale_value();
238
+
239
+ // Layouts cycle over the heads: four layouts and eight heads give S0 S1 S2 S3 S0 ...
240
+ let layout_id = head % NUM_LAYOUT;
241
+ let row_base = layout_id * ROW_STRIDE;
242
+ // A layout at or above the triangular dense-block count is treated as dense.
243
+ let dense_nnz = ROW_STRIDE * (ROW_STRIDE - 1u) / 2u;
244
+ let layout_is_dense = u32(block_row_indices[row_base + ROW_STRIDE - 1u]) >= dense_nnz;
245
+
246
+ {% for j in range(qTile) %}
247
+ // Keys 0 .. q_abs are visible; the +1 makes the bound exclusive. A tile that runs past
248
+ // the last token carries inactive queries; they take no mask row and store nothing.
249
+ let live_{{ j }} = tile0 + {{ j }}u < params.seqLen;
250
+ let q_abs_{{ j }} = past + tile0 + {{ j }}u;
251
+ let key_bound_{{ j }} = q_abs_{{ j }} + 1u;
252
+ let mask_row_{{ j }} = q_abs_{{ j }} / SPARSE_BLOCK;
253
+ {% endfor %}
254
+
255
+ {% for j in range(qTile) %}
256
+ {
257
+ {% if usesRotary %}
258
+ let q_base = ((batch * Q_HEADS + head) * params.seqLen + min(tile0 + {{ j }}u, params.seqLen - 1u)) * HEAD_DIM;
259
+ {% else %}
260
+ let q_base = (batch * params.seqLen + min(tile0 + {{ j }}u, params.seqLen - 1u)) * Q_STRIDE + head * HEAD_DIM;
261
+ {% endif %}
262
+ for (var dv: u32 = tid; dv < HEAD_VEC; dv = dv + WG) {
263
+ let qb = q_base + dv * 4u;
264
+ {% if usesRotary %}
265
+ q_shared[{{ j }}u * HEAD_VEC + dv] = vec4<f32>(q_rotary[qb], q_rotary[qb + 1u], q_rotary[qb + 2u], q_rotary[qb + 3u]);
266
+ {% else %}
267
+ q_shared[{{ j }}u * HEAD_VEC + dv] = vec4<f32>(f32(query[qb]), f32(query[qb + 1u]), f32(query[qb + 2u]), f32(query[qb + 3u]));
268
+ {% endif %}
269
+ running_out[{{ j }}u * HEAD_VEC + dv] = vec4<f32>(0.0);
270
+ }
271
+ }
272
+ {% endfor %}
273
+ workgroupBarrier();
274
+
275
+ {% for j in range(qTile) %}
276
+ var runningMax_{{ j }} = -FLT_MAX;
277
+ var runningDenom_{{ j }} = 0.0;
278
+ {% endfor %}
279
+
280
+ // Consecutive tokens span at most two mask rows, and every query of a row selects the
281
+ // same blocks, so one sweep per row covers the tile. A query contributes only to the
282
+ // sweep of its own row, which is why its online state is never merged across rows.
283
+ {% if qTile > 1 %}
284
+ let row_first = q_abs_0 / SPARSE_BLOCK;
285
+ let row_last = mask_row_{{ qTile - 1 }};
286
+ for (var mask_row = row_first; mask_row <= row_last; mask_row = mask_row + 1u) {
287
+ {% else %}
288
+ {
289
+ let mask_row = mask_row_0;
290
+ {% endif %}
291
+ let start = u32(block_row_indices[row_base + mask_row]);
292
+ let end = u32(block_row_indices[row_base + mask_row + 1u]);
293
+ // A row listing every causal block is also dense, so its column values are ignored.
294
+ let dense_row = layout_is_dense || (end - start == mask_row + 1u);
295
+ // The dense sweep runs to the furthest causal bound of the queries on this row; each
296
+ // query still drops the keys past its own.
297
+ var bound_max = 0u;
298
+ {% for j in range(qTile) %}
299
+ if (live_{{ j }} && mask_row_{{ j }} == mask_row) { bound_max = max(bound_max, key_bound_{{ j }}); }
300
+ {% endfor %}
301
+ let slot_count = select((end - start) * SPARSE_BLOCK, bound_max, dense_row);
302
+
303
+ var tileBase: u32 = 0u;
304
+ loop {
305
+ if (tileBase >= slot_count) {
306
+ break;
307
+ }
308
+ let slot = tileBase + tid;
309
+
310
+ // Resolving the slot is query-independent: the block list belongs to the row, and
311
+ // only the causal bound below differs between the queries sharing it.
312
+ var key = 0u;
313
+ var keyResolved = false;
314
+ if (slot < slot_count) {
315
+ if (!dense_row) {
316
+ // CSR is a boolean mask. Repeating a column therefore cannot repeat its
317
+ // probability mass, even though malformed-but-valid CSR may contain duplicates.
318
+ let entry = slot / SPARSE_BLOCK;
319
+ let block = u32(block_col_indices[layout_id * COL_STRIDE + start + entry]);
320
+ var duplicate = false;
321
+ for (var previous = 0u; previous < entry; previous = previous + 1u) {
322
+ duplicate = duplicate ||
323
+ u32(block_col_indices[layout_id * COL_STRIDE + start + previous]) == block;
324
+ }
325
+ key = block * SPARSE_BLOCK + slot % SPARSE_BLOCK;
326
+ keyResolved = !duplicate;
327
+ } else {
328
+ key = slot;
329
+ keyResolved = true;
330
+ }
331
+ }
332
+ // Always a row this cache holds, so an unresolved slot leaves a readable base rather
333
+ // than a stale or uninitialized one and the accumulation below needs no guard.
334
+ let base = ((batch * KV_HEADS + kv_head) * MAX_CACHE_SEQ + select(0u, key, keyResolved)) * HEAD_DIM;
335
+ key_rows[tid] = base;
336
+
337
+ {% for j in range(qTile) %}
338
+ let allowed_{{ j }} = keyResolved && live_{{ j }} && mask_row_{{ j }} == mask_row && key < key_bound_{{ j }};
339
+ var acc_{{ j }} = 0.0;
340
+ {% endfor %}
341
+ if (keyResolved) {
342
+ // One key vector, every query's score: this is the reuse the tile exists for.
343
+ let row_v = base / 4u;
344
+ for (var dv: u32 = 0u; dv < HEAD_VEC; dv = dv + 1u) {
345
+ let kv = vec4<f32>(present_key[row_v + dv]);
346
+ {% for j in range(qTile) %}
347
+ acc_{{ j }} = acc_{{ j }} + dot(q_shared[{{ j }}u * HEAD_VEC + dv], kv);
348
+ {% endfor %}
349
+ }
350
+ }
351
+
352
+ // One barrier tree folds all Q_TILE online-softmax partials.
353
+ {% for j in range(qTile) %}
354
+ let score_{{ j }} = acc_{{ j }} * scale;
355
+ partialM[{{ j }}u * WG + tid] = select(-FLT_MAX, score_{{ j }}, allowed_{{ j }});
356
+ partialD[{{ j }}u * WG + tid] = select(0.0, 1.0, allowed_{{ j }});
357
+ {% endfor %}
358
+ combine_partials_streams(tid);
359
+ {% for j in range(qTile) %}
360
+ let tile_{{ j }} = vec2<f32>(partialM[{{ j }}u * WG], partialD[{{ j }}u * WG]);
361
+ {% endfor %}
362
+
363
+ {% for j in range(qTile) %}
364
+ let newMax_{{ j }} = max(runningMax_{{ j }}, tile_{{ j }}.x);
365
+ let correction_{{ j }} = exp_shift(runningMax_{{ j }}, newMax_{{ j }});
366
+ runningDenom_{{ j }} = runningDenom_{{ j }} * correction_{{ j }}
367
+ + tile_{{ j }}.y * exp_shift(tile_{{ j }}.x, newMax_{{ j }});
368
+ runningMax_{{ j }} = newMax_{{ j }};
369
+ probs[{{ j }}u * WG + tid] = select(0.0, exp_shift(score_{{ j }}, newMax_{{ j }}), allowed_{{ j }});
370
+ {% endfor %}
371
+ workgroupBarrier();
372
+
373
+ // running_out[j][d] is owned by the same thread across every tile (tid = d mod WG),
374
+ // so this rescale-and-accumulate needs no further synchronization. One value vector
375
+ // serves every query, which is the other half of the tile's reuse; a key outside a
376
+ // query's causal bound carries prob 0 and is multiplied away.
377
+ let tileCount = min(WG, slot_count - tileBase);
378
+ for (var dv: u32 = tid; dv < HEAD_VEC; dv = dv + WG) {
379
+ {% for j in range(qTile) %}
380
+ var vSum_{{ j }} = vec4<f32>(0.0);
381
+ {% endfor %}
382
+ for (var i: u32 = 0u; i < tileCount; i = i + 1u) {
383
+ let vv = vec4<f32>(present_value[key_rows[i] / 4u + dv]);
384
+ {% for j in range(qTile) %}
385
+ vSum_{{ j }} = vSum_{{ j }} + probs[{{ j }}u * WG + i] * vv;
386
+ {% endfor %}
387
+ }
388
+ {% for j in range(qTile) %}
389
+ running_out[{{ j }}u * HEAD_VEC + dv] = running_out[{{ j }}u * HEAD_VEC + dv] * correction_{{ j }} + vSum_{{ j }};
390
+ {% endfor %}
391
+ }
392
+ // Orders this tile's probs and key_rows reads before the next tile writes them.
393
+ workgroupBarrier();
394
+
395
+ tileBase = tileBase + WG;
396
+ }
397
+ }
398
+
399
+ // An all-masked row becomes a uniform average over causal history rather than zero.
400
+ {% for j in range(qTile) %}
401
+ if (live_{{ j }}) {
402
+ let hasMass_{{ j }} = runningDenom_{{ j }} > 0.0;
403
+ let invDenom_{{ j }} = select(0.0, 1.0 / runningDenom_{{ j }}, hasMass_{{ j }});
404
+ let out_base_{{ j }} = (batch * params.seqLen + tile0 + {{ j }}u) * Q_HIDDEN + head * HEAD_DIM;
405
+ for (var dv: u32 = tid; dv < HEAD_VEC; dv = dv + WG) {
406
+ var out_v: vec4<f32>;
407
+ if (hasMass_{{ j }}) {
408
+ out_v = running_out[{{ j }}u * HEAD_VEC + dv] * invDenom_{{ j }};
409
+ } else {
410
+ var total = vec4<f32>(0.0);
411
+ for (var key = 0u; key < key_bound_{{ j }}; key = key + 1u) {
412
+ let row_v = (((batch * KV_HEADS + kv_head) * MAX_CACHE_SEQ + key) * HEAD_DIM) / 4u;
413
+ total = total + vec4<f32>(present_value[row_v + dv]);
414
+ }
415
+ out_v = total / f32(key_bound_{{ j }});
416
+ }
417
+ let ob = out_base_{{ j }} + dv * 4u;
418
+ output[ob] = {{ scalar }}(out_v.x);
419
+ output[ob + 1u] = {{ scalar }}(out_v.y);
420
+ output[ob + 2u] = {{ scalar }}(out_v.z);
421
+ output[ob + 3u] = {{ scalar }}(out_v.w);
422
+ }
423
+ }
424
+ {% endfor %}
425
+ }
build/webgpu/sparse-kv-append.wgsl.jinja ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% macro sparse_schedule() %}
2
+ // How much history precedes this call's tokens. Prompt mode is determined from
3
+ // scalar total_sequence_length rather than per-batch values and forces the past
4
+ // length to zero; a padded prompt row must not be read as if it had history.
5
+ fn past_sequence_length(batch: u32) -> u32 {
6
+ if (u32(total_sequence_length[0]) == params.seqLen) {
7
+ return 0u;
8
+ }
9
+ let total = u32(key_total_sequence_lengths[batch]);
10
+ return select(0u, total - params.seqLen, total >= params.seqLen);
11
+ }
12
+ {%- endmacro %}
13
+ {% macro sparse_rotary(interleaved) %}
14
+ // Which cos/sin entry a component uses, and which member of its rotation pair it is.
15
+ // The two layouts differ only here: the NeoX split pairs d with d + ROTARY_HALF, and the
16
+ // interleaved layout pairs the even lane with the odd one beside it.
17
+ fn rotary_slot(d: u32) -> u32 {
18
+ {% if interleaved %}
19
+ return d / 2u;
20
+ {% else %}
21
+ return select(d - ROTARY_HALF, d, d < ROTARY_HALF);
22
+ {% endif %}
23
+ }
24
+
25
+ fn rotary_partner(d: u32) -> u32 {
26
+ {% if interleaved %}
27
+ return select(d - 1u, d + 1u, (d % 2u) == 0u);
28
+ {% else %}
29
+ return select(d - ROTARY_HALF, d + ROTARY_HALF, d < ROTARY_HALF);
30
+ {% endif %}
31
+ }
32
+
33
+ fn rotary_is_first(d: u32) -> bool {
34
+ {% if interleaved %}
35
+ return (d % 2u) == 0u;
36
+ {% else %}
37
+ return d < ROTARY_HALF;
38
+ {% endif %}
39
+ }
40
+
41
+ // One component of the rotation, written from the component's own point of view so a
42
+ // single expression covers both members of the pair: the leading one subtracts its
43
+ // partner's sine term, the trailing one adds it.
44
+ fn rotary_value(own: f32, partner: f32, cs: f32, sn: f32, first: bool) -> f32 {
45
+ return select(own * cs + partner * sn, own * cs - partner * sn, first);
46
+ }
47
+ {%- endmacro %}
48
+
49
+ {% if usesF16 %}
50
+ enable f16;
51
+ {% endif %}
52
+ {{ env.wgsl.resourceDeclarations }}
53
+
54
+ // com.microsoft.SparseAttention, KV append pass.
55
+ // past_key and present_key are the same allocation, so this writes only the new rows:
56
+ // one invocation per (batch, kv head, new token, component) of the BNSH cache, landing
57
+ // at absolute position past + t. Nothing rewrites the retained history.
58
+ const KV_HEADS: u32 = {{ kvNumHeads }}u;
59
+ const HEAD_DIM: u32 = {{ headSize }}u;
60
+ const MAX_CACHE_SEQ: u32 = {{ maxCacheSeq }}u;
61
+ {% if packedQkv %}
62
+ // Packed layout: query carries [Q | K | V] on one row, so K starts after the query
63
+ // heads and V after the key heads.
64
+ const Q_STRIDE: u32 = {{ packedStride }}u;
65
+ const PACKED_K_OFFSET: u32 = {{ numHeads * headSize }}u;
66
+ const PACKED_V_OFFSET: u32 = {{ (numHeads + kvNumHeads) * headSize }}u;
67
+ {% else %}
68
+ const KV_HIDDEN: u32 = {{ kvNumHeads * headSize }}u;
69
+ {% endif %}
70
+ const WG: u32 = {{ appendWorkgroupSize }}u;
71
+ {% if usesRotary %}
72
+ const ROTARY_HALF: u32 = {{ rotaryHalf }}u;
73
+ const ROTARY_DIM: u32 = {{ rotaryDim }}u;
74
+ {% endif %}
75
+
76
+ {{ sparse_schedule() }}
77
+ {% if usesRotary %}
78
+
79
+ {{ sparse_rotary(rotaryInterleaved) }}
80
+ {% endif %}
81
+
82
+ @compute @workgroup_size(WG, 1, 1)
83
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>,
84
+ @builtin(num_workgroups) nwg: vec3<u32>) {
85
+ // 2D-folded flat index: gid.y carries the high bits past the
86
+ // maxComputeWorkgroupsPerDimension limit. Reduces to gid.x when nwg.y == 1.
87
+ let index = gid.x + gid.y * nwg.x * WG;
88
+ let count = params.batchSize * KV_HEADS * params.seqLen * HEAD_DIM;
89
+ if (index >= count) {
90
+ return;
91
+ }
92
+ let d = index % HEAD_DIM;
93
+ let t = (index / HEAD_DIM) % params.seqLen;
94
+ let kv_head = (index / (HEAD_DIM * params.seqLen)) % KV_HEADS;
95
+ let batch = index / (HEAD_DIM * params.seqLen * KV_HEADS);
96
+
97
+ let position = past_sequence_length(batch) + t;
98
+ let dst = ((batch * KV_HEADS + kv_head) * MAX_CACHE_SEQ + position) * HEAD_DIM + d;
99
+ {% if packedQkv %}
100
+ let row = (batch * params.seqLen + t) * Q_STRIDE;
101
+ let k_base = row + PACKED_K_OFFSET + kv_head * HEAD_DIM;
102
+ let v_base = row + PACKED_V_OFFSET + kv_head * HEAD_DIM;
103
+ {% else %}
104
+ let k_base = ((batch * params.seqLen + t) * KV_HIDDEN) + kv_head * HEAD_DIM;
105
+ let v_base = k_base;
106
+ {% endif %}
107
+
108
+ {% if usesRotary %}
109
+ // The key is rotated at its own absolute position before it enters the cache, so the
110
+ // cache holds post-rotary keys and the attention pass never rotates again.
111
+ if (d < ROTARY_DIM) {
112
+ let slot = rotary_slot(d);
113
+ let cs = f32(cos_cache[position * ROTARY_HALF + slot]);
114
+ let sn = f32(sin_cache[position * ROTARY_HALF + slot]);
115
+ let own = f32({{ kvSource }}[k_base + d]);
116
+ let partner = f32({{ kvSource }}[k_base + rotary_partner(d)]);
117
+ present_key[dst] = {{ scalar }}(rotary_value(own, partner, cs, sn, rotary_is_first(d)));
118
+ } else {
119
+ present_key[dst] = {{ scalar }}({{ kvSource }}[k_base + d]);
120
+ }
121
+ {% else %}
122
+ present_key[dst] = {{ scalar }}({{ kvSource }}[k_base + d]);
123
+ {% endif %}
124
+ present_value[dst] = {{ scalar }}({{ vSource }}[v_base + d]);
125
+ }
build/webgpu/sparse-q-rotary.wgsl.jinja ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% macro sparse_schedule() %}
2
+ // How much history precedes this call's tokens. Prompt mode is determined from
3
+ // scalar total_sequence_length rather than per-batch values and forces the past
4
+ // length to zero; a padded prompt row must not be read as if it had history.
5
+ fn past_sequence_length(batch: u32) -> u32 {
6
+ if (u32(total_sequence_length[0]) == params.seqLen) {
7
+ return 0u;
8
+ }
9
+ let total = u32(key_total_sequence_lengths[batch]);
10
+ return select(0u, total - params.seqLen, total >= params.seqLen);
11
+ }
12
+ {%- endmacro %}
13
+ {% macro sparse_rotary(interleaved) %}
14
+ // Which cos/sin entry a component uses, and which member of its rotation pair it is.
15
+ // The two layouts differ only here: the NeoX split pairs d with d + ROTARY_HALF, and the
16
+ // interleaved layout pairs the even lane with the odd one beside it.
17
+ fn rotary_slot(d: u32) -> u32 {
18
+ {% if interleaved %}
19
+ return d / 2u;
20
+ {% else %}
21
+ return select(d - ROTARY_HALF, d, d < ROTARY_HALF);
22
+ {% endif %}
23
+ }
24
+
25
+ fn rotary_partner(d: u32) -> u32 {
26
+ {% if interleaved %}
27
+ return select(d - 1u, d + 1u, (d % 2u) == 0u);
28
+ {% else %}
29
+ return select(d - ROTARY_HALF, d + ROTARY_HALF, d < ROTARY_HALF);
30
+ {% endif %}
31
+ }
32
+
33
+ fn rotary_is_first(d: u32) -> bool {
34
+ {% if interleaved %}
35
+ return (d % 2u) == 0u;
36
+ {% else %}
37
+ return d < ROTARY_HALF;
38
+ {% endif %}
39
+ }
40
+
41
+ // One component of the rotation, written from the component's own point of view so a
42
+ // single expression covers both members of the pair: the leading one subtracts its
43
+ // partner's sine term, the trailing one adds it.
44
+ fn rotary_value(own: f32, partner: f32, cs: f32, sn: f32, first: bool) -> f32 {
45
+ return select(own * cs + partner * sn, own * cs - partner * sn, first);
46
+ }
47
+ {%- endmacro %}
48
+
49
+ {% if usesF16 %}
50
+ enable f16;
51
+ {% endif %}
52
+ {{ env.wgsl.resourceDeclarations }}
53
+
54
+ // com.microsoft.SparseAttention, query rotary pass.
55
+ // Rotates Q into a (batch, heads, seq, dim) scratch buffer so the attention pass binds
56
+ // one query tensor instead of the query plus both caches. That keeps the attention pass
57
+ // at the eight storage buffers WebGPU guarantees; folding the rotation in would need
58
+ // ten.
59
+ const Q_HEADS: u32 = {{ numHeads }}u;
60
+ const HEAD_DIM: u32 = {{ headSize }}u;
61
+ const ROTARY_HALF: u32 = {{ rotaryHalf }}u;
62
+ const ROTARY_DIM: u32 = {{ rotaryDim }}u;
63
+ const Q_STRIDE: u32 = {{ packedStride if packedQkv else numHeads * headSize }}u;
64
+ const WG: u32 = {{ appendWorkgroupSize }}u;
65
+
66
+ {{ sparse_schedule() }}
67
+
68
+ {{ sparse_rotary(rotaryInterleaved) }}
69
+
70
+ @compute @workgroup_size(WG, 1, 1)
71
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>,
72
+ @builtin(num_workgroups) nwg: vec3<u32>) {
73
+ // 2D-folded flat index: gid.y carries the high bits past the
74
+ // maxComputeWorkgroupsPerDimension limit. Reduces to gid.x when nwg.y == 1.
75
+ let index = gid.x + gid.y * nwg.x * WG;
76
+ let count = params.batchSize * Q_HEADS * params.seqLen * HEAD_DIM;
77
+ if (index >= count) {
78
+ return;
79
+ }
80
+ let d = index % HEAD_DIM;
81
+ let t = (index / HEAD_DIM) % params.seqLen;
82
+ let head = (index / (HEAD_DIM * params.seqLen)) % Q_HEADS;
83
+ let batch = index / (HEAD_DIM * params.seqLen * Q_HEADS);
84
+
85
+ // A query token's rotary position is the same absolute position the attention pass
86
+ // uses for causality: the history in front of it plus its own offset.
87
+ let position = past_sequence_length(batch) + t;
88
+ // The packed query's Q block starts at offset zero, so this base serves both layouts.
89
+ let q_base = (batch * params.seqLen + t) * Q_STRIDE + head * HEAD_DIM;
90
+ if (d < ROTARY_DIM) {
91
+ let slot = rotary_slot(d);
92
+ let cs = f32(cos_cache[position * ROTARY_HALF + slot]);
93
+ let sn = f32(sin_cache[position * ROTARY_HALF + slot]);
94
+ let own = f32(query[q_base + d]);
95
+ let partner = f32(query[q_base + rotary_partner(d)]);
96
+ // Rotary Q is materialized in the operator dtype. Round to f16 here when T
97
+ // is float16, then widen only for the f32 accumulation scratch buffer.
98
+ q_rotary[index] = f32({{ scalar }}(rotary_value(own, partner, cs, sn, rotary_is_first(d))));
99
+ } else {
100
+ q_rotary[index] = f32(query[q_base + d]);
101
+ }
102
+ }
build/webgpu/test.json ADDED
@@ -0,0 +1,745 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "op": "com.microsoft.SparseAttention",
3
+ "fixtureArrays": {
4
+ "prompt_separate_two_layouts_input_blockColIndicesT": [0, 0, 1, 1, 2, 1, 2, 3, -1, 0, 0, 1, 0, 1, 2, 0, 2, 3]
5
+ },
6
+ "cases": [
7
+ {
8
+ "name": "prompt_separate_two_layouts",
9
+ "provenance": {
10
+ "notes": "The two-layout CSR example from the ONNX Runtime operator documentation, using separate Q/K/V tensors."
11
+ },
12
+ "attrs": { "num_heads": 4, "kv_num_heads": 2, "sparse_block_size": 16 },
13
+ "inputs": {
14
+ "queryT": {
15
+ "dtype": "float32",
16
+ "shape": [2, 32, 32],
17
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.5 }
18
+ },
19
+ "keyT": {
20
+ "dtype": "float32",
21
+ "shape": [2, 32, 16],
22
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31, "scale": 0.5 }
23
+ },
24
+ "valueT": {
25
+ "dtype": "float32",
26
+ "shape": [2, 32, 16],
27
+ "data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.23, "scale": 0.5 }
28
+ },
29
+ "pastKeyT": { "dtype": "float32", "shape": [2, 2, 64, 8], "data": { "kind": "constant", "value": 0.0 } },
30
+ "pastValueT": { "dtype": "float32", "shape": [2, 2, 64, 8], "data": { "kind": "constant", "value": 0.0 } },
31
+ "blockRowIndicesT": {
32
+ "dtype": "int32",
33
+ "shape": [2, 5],
34
+ "data": { "kind": "values", "values": [0, 1, 3, 5, 8, 0, 1, 3, 6, 9] }
35
+ },
36
+ "blockColIndicesT": {
37
+ "dtype": "int32",
38
+ "shape": [2, 9],
39
+ "data": {
40
+ "kind": "values",
41
+ "values": { "$ref": "#/fixtureArrays/prompt_separate_two_layouts_input_blockColIndicesT" }
42
+ }
43
+ },
44
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [32] } },
45
+ "keyTotalSequenceLengthsT": { "dtype": "int32", "shape": [2], "data": { "kind": "values", "values": [32, 32] } }
46
+ },
47
+ "outputs": {
48
+ "outputT": { "dtype": "float32", "shape": [2, 32, 32], "tolerance": 0.00002, "relTolerance": 0.0002 },
49
+ "pastKeyT": { "dtype": "float32", "shape": [2, 2, 64, 8], "tolerance": 0.00002, "relTolerance": 0.0002 },
50
+ "pastValueT": { "dtype": "float32", "shape": [2, 2, 64, 8], "tolerance": 0.00002, "relTolerance": 0.0002 }
51
+ }
52
+ },
53
+ {
54
+ "name": "prompt_packed_two_layouts",
55
+ "provenance": { "notes": "Packed [Q|K|V] form of the documented two-layout prompt case." },
56
+ "attrs": { "num_heads": 4, "kv_num_heads": 2, "sparse_block_size": 16 },
57
+ "inputs": {
58
+ "queryT": {
59
+ "dtype": "float32",
60
+ "shape": [1, 32, 64],
61
+ "data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.27, "scale": 0.5 }
62
+ },
63
+ "pastKeyT": { "dtype": "float32", "shape": [1, 2, 64, 8], "data": { "kind": "constant", "value": 0.0 } },
64
+ "pastValueT": { "dtype": "float32", "shape": [1, 2, 64, 8], "data": { "kind": "constant", "value": 0.0 } },
65
+ "blockRowIndicesT": {
66
+ "dtype": "int32",
67
+ "shape": [2, 5],
68
+ "data": { "kind": "values", "values": [0, 1, 3, 5, 8, 0, 1, 3, 6, 9] }
69
+ },
70
+ "blockColIndicesT": {
71
+ "dtype": "int32",
72
+ "shape": [2, 9],
73
+ "data": {
74
+ "kind": "values",
75
+ "values": { "$ref": "#/fixtureArrays/prompt_separate_two_layouts_input_blockColIndicesT" }
76
+ }
77
+ },
78
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [32] } },
79
+ "keyTotalSequenceLengthsT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [32] } }
80
+ },
81
+ "outputs": {
82
+ "outputT": { "dtype": "float32", "shape": [1, 32, 32], "tolerance": 0.00002, "relTolerance": 0.0002 },
83
+ "pastKeyT": { "dtype": "float32", "shape": [1, 2, 64, 8], "tolerance": 0.00002, "relTolerance": 0.0002 },
84
+ "pastValueT": { "dtype": "float32", "shape": [1, 2, 64, 8], "tolerance": 0.00002, "relTolerance": 0.0002 }
85
+ }
86
+ },
87
+ {
88
+ "name": "prompt_separate_rotary_neox",
89
+ "provenance": {
90
+ "notes": "Full-width NeoX rotary with the minimum accepted head width; rotary_interleaved values other than 1 retain the NeoX layout."
91
+ },
92
+ "attrs": { "num_heads": 4, "kv_num_heads": 2, "sparse_block_size": 16, "do_rotary": 1, "rotary_interleaved": 2 },
93
+ "inputs": {
94
+ "queryT": {
95
+ "dtype": "float32",
96
+ "shape": [1, 32, 64],
97
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.5 }
98
+ },
99
+ "keyT": {
100
+ "dtype": "float32",
101
+ "shape": [1, 32, 32],
102
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31, "scale": 0.5 }
103
+ },
104
+ "valueT": {
105
+ "dtype": "float32",
106
+ "shape": [1, 32, 32],
107
+ "data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.23, "scale": 0.5 }
108
+ },
109
+ "pastKeyT": { "dtype": "float32", "shape": [1, 2, 64, 16], "data": { "kind": "constant", "value": 0.0 } },
110
+ "pastValueT": { "dtype": "float32", "shape": [1, 2, 64, 16], "data": { "kind": "constant", "value": 0.0 } },
111
+ "blockRowIndicesT": {
112
+ "dtype": "int32",
113
+ "shape": [2, 5],
114
+ "data": { "kind": "values", "values": [0, 1, 3, 5, 8, 0, 1, 3, 6, 9] }
115
+ },
116
+ "blockColIndicesT": {
117
+ "dtype": "int32",
118
+ "shape": [2, 9],
119
+ "data": {
120
+ "kind": "values",
121
+ "values": { "$ref": "#/fixtureArrays/prompt_separate_two_layouts_input_blockColIndicesT" }
122
+ }
123
+ },
124
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [32] } },
125
+ "keyTotalSequenceLengthsT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [32] } },
126
+ "cosCacheT": { "dtype": "float32", "shape": [64, 8], "data": { "kind": "constant", "value": 0.8 } },
127
+ "sinCacheT": { "dtype": "float32", "shape": [64, 8], "data": { "kind": "constant", "value": 0.6 } }
128
+ },
129
+ "outputs": {
130
+ "outputT": { "dtype": "float32", "shape": [1, 32, 64], "tolerance": 0.00003, "relTolerance": 0.0003 },
131
+ "pastKeyT": { "dtype": "float32", "shape": [1, 2, 64, 16], "tolerance": 0.00003, "relTolerance": 0.0003 },
132
+ "pastValueT": { "dtype": "float32", "shape": [1, 2, 64, 16], "tolerance": 0.00002, "relTolerance": 0.0002 }
133
+ }
134
+ },
135
+ {
136
+ "name": "prompt_packed_partial_rotary_interleaved",
137
+ "provenance": {
138
+ "notes": "Packed QKV with interleaved rotary over the first 16 of 32 head channels; ONNX Runtime copies the remainder through."
139
+ },
140
+ "attrs": { "num_heads": 4, "kv_num_heads": 2, "sparse_block_size": 16, "do_rotary": 1, "rotary_interleaved": 1 },
141
+ "inputs": {
142
+ "queryT": {
143
+ "dtype": "float32",
144
+ "shape": [1, 32, 256],
145
+ "data": { "kind": "fillFloat32", "sinStep": 0.07, "cosStep": 0.19, "scale": 0.5 }
146
+ },
147
+ "pastKeyT": { "dtype": "float32", "shape": [1, 2, 64, 32], "data": { "kind": "constant", "value": 0.0 } },
148
+ "pastValueT": { "dtype": "float32", "shape": [1, 2, 64, 32], "data": { "kind": "constant", "value": 0.0 } },
149
+ "blockRowIndicesT": {
150
+ "dtype": "int32",
151
+ "shape": [2, 5],
152
+ "data": { "kind": "values", "values": [0, 1, 3, 5, 8, 0, 1, 3, 6, 9] }
153
+ },
154
+ "blockColIndicesT": {
155
+ "dtype": "int32",
156
+ "shape": [2, 9],
157
+ "data": {
158
+ "kind": "values",
159
+ "values": { "$ref": "#/fixtureArrays/prompt_separate_two_layouts_input_blockColIndicesT" }
160
+ }
161
+ },
162
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [32] } },
163
+ "keyTotalSequenceLengthsT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [32] } },
164
+ "cosCacheT": { "dtype": "float32", "shape": [64, 8], "data": { "kind": "constant", "value": 0.8 } },
165
+ "sinCacheT": { "dtype": "float32", "shape": [64, 8], "data": { "kind": "constant", "value": 0.6 } }
166
+ },
167
+ "outputs": {
168
+ "outputT": { "dtype": "float32", "shape": [1, 32, 128], "tolerance": 0.00003, "relTolerance": 0.0003 },
169
+ "pastKeyT": { "dtype": "float32", "shape": [1, 2, 64, 32], "tolerance": 0.00003, "relTolerance": 0.0003 },
170
+ "pastValueT": { "dtype": "float32", "shape": [1, 2, 64, 32], "tolerance": 0.00002, "relTolerance": 0.0002 }
171
+ }
172
+ },
173
+ {
174
+ "name": "decode_separate_two_layouts",
175
+ "provenance": {
176
+ "notes": "Two decode rows with different history lengths, so the scalar maximum and per-row key lengths have distinct jobs."
177
+ },
178
+ "attrs": { "num_heads": 4, "kv_num_heads": 2, "sparse_block_size": 16 },
179
+ "inputs": {
180
+ "queryT": {
181
+ "dtype": "float32",
182
+ "shape": [2, 1, 32],
183
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.5 }
184
+ },
185
+ "keyT": {
186
+ "dtype": "float32",
187
+ "shape": [2, 1, 16],
188
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31, "scale": 0.5 }
189
+ },
190
+ "valueT": {
191
+ "dtype": "float32",
192
+ "shape": [2, 1, 16],
193
+ "data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.23, "scale": 0.5 }
194
+ },
195
+ "pastKeyT": {
196
+ "dtype": "float32",
197
+ "shape": [2, 2, 64, 8],
198
+ "data": { "kind": "fillFloat32", "sinStep": 0.011, "cosStep": 0.017, "scale": 0.25 }
199
+ },
200
+ "pastValueT": {
201
+ "dtype": "float32",
202
+ "shape": [2, 2, 64, 8],
203
+ "data": { "kind": "fillFloat32", "sinStep": 0.019, "cosStep": 0.013, "scale": 0.25 }
204
+ },
205
+ "blockRowIndicesT": {
206
+ "dtype": "int32",
207
+ "shape": [2, 5],
208
+ "data": { "kind": "values", "values": [0, 1, 3, 5, 8, 0, 1, 3, 6, 9] }
209
+ },
210
+ "blockColIndicesT": {
211
+ "dtype": "int32",
212
+ "shape": [2, 9],
213
+ "data": {
214
+ "kind": "values",
215
+ "values": { "$ref": "#/fixtureArrays/prompt_separate_two_layouts_input_blockColIndicesT" }
216
+ }
217
+ },
218
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [41] } },
219
+ "keyTotalSequenceLengthsT": { "dtype": "int32", "shape": [2], "data": { "kind": "values", "values": [41, 34] } }
220
+ },
221
+ "outputs": {
222
+ "outputT": { "dtype": "float32", "shape": [2, 1, 32], "tolerance": 0.00003, "relTolerance": 0.0003 },
223
+ "pastKeyT": { "dtype": "float32", "shape": [2, 2, 64, 8], "tolerance": 0.00002, "relTolerance": 0.0002 },
224
+ "pastValueT": { "dtype": "float32", "shape": [2, 2, 64, 8], "tolerance": 0.00002, "relTolerance": 0.0002 }
225
+ }
226
+ },
227
+ {
228
+ "name": "decode_packed_rotary_neox",
229
+ "provenance": { "notes": "Packed decode with full-width NeoX rotary at each row's absolute position." },
230
+ "attrs": { "num_heads": 4, "kv_num_heads": 2, "sparse_block_size": 16, "do_rotary": 1 },
231
+ "inputs": {
232
+ "queryT": {
233
+ "dtype": "float32",
234
+ "shape": [2, 1, 128],
235
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.5 }
236
+ },
237
+ "pastKeyT": {
238
+ "dtype": "float32",
239
+ "shape": [2, 2, 64, 16],
240
+ "data": { "kind": "fillFloat32", "sinStep": 0.011, "cosStep": 0.017, "scale": 0.25 }
241
+ },
242
+ "pastValueT": {
243
+ "dtype": "float32",
244
+ "shape": [2, 2, 64, 16],
245
+ "data": { "kind": "fillFloat32", "sinStep": 0.019, "cosStep": 0.013, "scale": 0.25 }
246
+ },
247
+ "blockRowIndicesT": {
248
+ "dtype": "int32",
249
+ "shape": [2, 5],
250
+ "data": { "kind": "values", "values": [0, 1, 3, 5, 8, 0, 1, 3, 6, 9] }
251
+ },
252
+ "blockColIndicesT": {
253
+ "dtype": "int32",
254
+ "shape": [2, 9],
255
+ "data": {
256
+ "kind": "values",
257
+ "values": { "$ref": "#/fixtureArrays/prompt_separate_two_layouts_input_blockColIndicesT" }
258
+ }
259
+ },
260
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [41] } },
261
+ "keyTotalSequenceLengthsT": { "dtype": "int32", "shape": [2], "data": { "kind": "values", "values": [41, 34] } },
262
+ "cosCacheT": { "dtype": "float32", "shape": [64, 8], "data": { "kind": "constant", "value": 0.8 } },
263
+ "sinCacheT": { "dtype": "float32", "shape": [64, 8], "data": { "kind": "constant", "value": 0.6 } }
264
+ },
265
+ "outputs": {
266
+ "outputT": { "dtype": "float32", "shape": [2, 1, 64], "tolerance": 0.00003, "relTolerance": 0.0003 },
267
+ "pastKeyT": { "dtype": "float32", "shape": [2, 2, 64, 16], "tolerance": 0.00003, "relTolerance": 0.0003 },
268
+ "pastValueT": { "dtype": "float32", "shape": [2, 2, 64, 16], "tolerance": 0.00002, "relTolerance": 0.0002 }
269
+ }
270
+ },
271
+ {
272
+ "name": "chunked_separate_two_layouts",
273
+ "provenance": {
274
+ "notes": "Eight new tokens over 24 cached tokens, covering the non-prompt sequence_length > 1 path."
275
+ },
276
+ "attrs": { "num_heads": 4, "kv_num_heads": 2, "sparse_block_size": 16 },
277
+ "inputs": {
278
+ "queryT": {
279
+ "dtype": "float32",
280
+ "shape": [2, 8, 32],
281
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.5 }
282
+ },
283
+ "keyT": {
284
+ "dtype": "float32",
285
+ "shape": [2, 8, 16],
286
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31, "scale": 0.5 }
287
+ },
288
+ "valueT": {
289
+ "dtype": "float32",
290
+ "shape": [2, 8, 16],
291
+ "data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.23, "scale": 0.5 }
292
+ },
293
+ "pastKeyT": {
294
+ "dtype": "float32",
295
+ "shape": [2, 2, 64, 8],
296
+ "data": { "kind": "fillFloat32", "sinStep": 0.011, "cosStep": 0.017, "scale": 0.25 }
297
+ },
298
+ "pastValueT": {
299
+ "dtype": "float32",
300
+ "shape": [2, 2, 64, 8],
301
+ "data": { "kind": "fillFloat32", "sinStep": 0.019, "cosStep": 0.013, "scale": 0.25 }
302
+ },
303
+ "blockRowIndicesT": {
304
+ "dtype": "int32",
305
+ "shape": [2, 5],
306
+ "data": { "kind": "values", "values": [0, 1, 3, 5, 8, 0, 1, 3, 6, 9] }
307
+ },
308
+ "blockColIndicesT": {
309
+ "dtype": "int32",
310
+ "shape": [2, 9],
311
+ "data": {
312
+ "kind": "values",
313
+ "values": { "$ref": "#/fixtureArrays/prompt_separate_two_layouts_input_blockColIndicesT" }
314
+ }
315
+ },
316
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [32] } },
317
+ "keyTotalSequenceLengthsT": { "dtype": "int32", "shape": [2], "data": { "kind": "values", "values": [32, 32] } }
318
+ },
319
+ "outputs": {
320
+ "outputT": { "dtype": "float32", "shape": [2, 8, 32], "tolerance": 0.00003, "relTolerance": 0.0003 },
321
+ "pastKeyT": { "dtype": "float32", "shape": [2, 2, 64, 8], "tolerance": 0.00002, "relTolerance": 0.0002 },
322
+ "pastValueT": { "dtype": "float32", "shape": [2, 2, 64, 8], "tolerance": 0.00002, "relTolerance": 0.0002 }
323
+ }
324
+ },
325
+ {
326
+ "name": "prompt_all_rows_gqa_four_to_one",
327
+ "provenance": {
328
+ "notes": "A 64-token prompt reaches every documented block row while four query heads share each KV head."
329
+ },
330
+ "attrs": { "num_heads": 8, "kv_num_heads": 2, "sparse_block_size": 16 },
331
+ "inputs": {
332
+ "queryT": {
333
+ "dtype": "float32",
334
+ "shape": [1, 64, 64],
335
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.5 }
336
+ },
337
+ "keyT": {
338
+ "dtype": "float32",
339
+ "shape": [1, 64, 16],
340
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31, "scale": 0.5 }
341
+ },
342
+ "valueT": {
343
+ "dtype": "float32",
344
+ "shape": [1, 64, 16],
345
+ "data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.23, "scale": 0.5 }
346
+ },
347
+ "pastKeyT": { "dtype": "float32", "shape": [1, 2, 64, 8], "data": { "kind": "constant", "value": 0.0 } },
348
+ "pastValueT": { "dtype": "float32", "shape": [1, 2, 64, 8], "data": { "kind": "constant", "value": 0.0 } },
349
+ "blockRowIndicesT": {
350
+ "dtype": "int32",
351
+ "shape": [2, 5],
352
+ "data": { "kind": "values", "values": [0, 1, 3, 5, 8, 0, 1, 3, 6, 9] }
353
+ },
354
+ "blockColIndicesT": {
355
+ "dtype": "int32",
356
+ "shape": [2, 9],
357
+ "data": {
358
+ "kind": "values",
359
+ "values": { "$ref": "#/fixtureArrays/prompt_separate_two_layouts_input_blockColIndicesT" }
360
+ }
361
+ },
362
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [64] } },
363
+ "keyTotalSequenceLengthsT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [64] } }
364
+ },
365
+ "outputs": {
366
+ "outputT": { "dtype": "float32", "shape": [1, 64, 64], "tolerance": 0.00003, "relTolerance": 0.0003 },
367
+ "pastKeyT": { "dtype": "float32", "shape": [1, 2, 64, 8], "tolerance": 0.00002, "relTolerance": 0.0002 },
368
+ "pastValueT": { "dtype": "float32", "shape": [1, 2, 64, 8], "tolerance": 0.00002, "relTolerance": 0.0002 }
369
+ }
370
+ },
371
+ {
372
+ "name": "prompt_block32_head_dim16",
373
+ "provenance": {
374
+ "notes": "Block size 32 and head width 16 exercise independent key-tile and output-channel tails."
375
+ },
376
+ "attrs": { "num_heads": 4, "kv_num_heads": 2, "sparse_block_size": 32 },
377
+ "inputs": {
378
+ "queryT": {
379
+ "dtype": "float32",
380
+ "shape": [1, 64, 64],
381
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.5 }
382
+ },
383
+ "keyT": {
384
+ "dtype": "float32",
385
+ "shape": [1, 64, 32],
386
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31, "scale": 0.5 }
387
+ },
388
+ "valueT": {
389
+ "dtype": "float32",
390
+ "shape": [1, 64, 32],
391
+ "data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.23, "scale": 0.5 }
392
+ },
393
+ "pastKeyT": { "dtype": "float32", "shape": [1, 2, 128, 16], "data": { "kind": "constant", "value": 0.0 } },
394
+ "pastValueT": { "dtype": "float32", "shape": [1, 2, 128, 16], "data": { "kind": "constant", "value": 0.0 } },
395
+ "blockRowIndicesT": {
396
+ "dtype": "int32",
397
+ "shape": [2, 5],
398
+ "data": { "kind": "values", "values": [0, 1, 3, 5, 8, 0, 1, 3, 6, 9] }
399
+ },
400
+ "blockColIndicesT": {
401
+ "dtype": "int32",
402
+ "shape": [2, 9],
403
+ "data": {
404
+ "kind": "values",
405
+ "values": { "$ref": "#/fixtureArrays/prompt_separate_two_layouts_input_blockColIndicesT" }
406
+ }
407
+ },
408
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [64] } },
409
+ "keyTotalSequenceLengthsT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [64] } }
410
+ },
411
+ "outputs": {
412
+ "outputT": { "dtype": "float32", "shape": [1, 64, 64], "tolerance": 0.00003, "relTolerance": 0.0003 },
413
+ "pastKeyT": { "dtype": "float32", "shape": [1, 2, 128, 16], "tolerance": 0.00002, "relTolerance": 0.0002 },
414
+ "pastValueT": { "dtype": "float32", "shape": [1, 2, 128, 16], "tolerance": 0.00002, "relTolerance": 0.0002 }
415
+ }
416
+ },
417
+ {
418
+ "name": "prompt_explicit_scale",
419
+ "provenance": { "notes": "Explicit non-default score scale." },
420
+ "attrs": { "num_heads": 4, "kv_num_heads": 2, "sparse_block_size": 16, "scale": 0.25 },
421
+ "inputs": {
422
+ "queryT": {
423
+ "dtype": "float32",
424
+ "shape": [1, 32, 32],
425
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.5 }
426
+ },
427
+ "keyT": {
428
+ "dtype": "float32",
429
+ "shape": [1, 32, 16],
430
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31, "scale": 0.5 }
431
+ },
432
+ "valueT": {
433
+ "dtype": "float32",
434
+ "shape": [1, 32, 16],
435
+ "data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.23, "scale": 0.5 }
436
+ },
437
+ "pastKeyT": { "dtype": "float32", "shape": [1, 2, 64, 8], "data": { "kind": "constant", "value": 0.0 } },
438
+ "pastValueT": { "dtype": "float32", "shape": [1, 2, 64, 8], "data": { "kind": "constant", "value": 0.0 } },
439
+ "blockRowIndicesT": {
440
+ "dtype": "int32",
441
+ "shape": [2, 5],
442
+ "data": { "kind": "values", "values": [0, 1, 3, 5, 8, 0, 1, 3, 6, 9] }
443
+ },
444
+ "blockColIndicesT": {
445
+ "dtype": "int32",
446
+ "shape": [2, 9],
447
+ "data": {
448
+ "kind": "values",
449
+ "values": { "$ref": "#/fixtureArrays/prompt_separate_two_layouts_input_blockColIndicesT" }
450
+ }
451
+ },
452
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [32] } },
453
+ "keyTotalSequenceLengthsT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [32] } }
454
+ },
455
+ "outputs": {
456
+ "outputT": { "dtype": "float32", "shape": [1, 32, 32], "tolerance": 0.00002, "relTolerance": 0.0002 },
457
+ "pastKeyT": { "dtype": "float32", "shape": [1, 2, 64, 8], "tolerance": 0.00002, "relTolerance": 0.0002 },
458
+ "pastValueT": { "dtype": "float32", "shape": [1, 2, 64, 8], "tolerance": 0.00002, "relTolerance": 0.0002 }
459
+ }
460
+ },
461
+ {
462
+ "name": "f16_prompt_separate",
463
+ "provenance": { "notes": "Meaningful float16 prompt; scores and online-softmax accumulators remain float32." },
464
+ "attrs": { "num_heads": 4, "kv_num_heads": 2, "sparse_block_size": 16 },
465
+ "inputs": {
466
+ "queryT": {
467
+ "dtype": "float16",
468
+ "shape": [1, 32, 32],
469
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.5 }
470
+ },
471
+ "keyT": {
472
+ "dtype": "float16",
473
+ "shape": [1, 32, 16],
474
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31, "scale": 0.5 }
475
+ },
476
+ "valueT": {
477
+ "dtype": "float16",
478
+ "shape": [1, 32, 16],
479
+ "data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.23, "scale": 0.5 }
480
+ },
481
+ "pastKeyT": { "dtype": "float16", "shape": [1, 2, 64, 8], "data": { "kind": "constant", "value": 0.0 } },
482
+ "pastValueT": { "dtype": "float16", "shape": [1, 2, 64, 8], "data": { "kind": "constant", "value": 0.0 } },
483
+ "blockRowIndicesT": {
484
+ "dtype": "int32",
485
+ "shape": [2, 5],
486
+ "data": { "kind": "values", "values": [0, 1, 3, 5, 8, 0, 1, 3, 6, 9] }
487
+ },
488
+ "blockColIndicesT": {
489
+ "dtype": "int32",
490
+ "shape": [2, 9],
491
+ "data": {
492
+ "kind": "values",
493
+ "values": { "$ref": "#/fixtureArrays/prompt_separate_two_layouts_input_blockColIndicesT" }
494
+ }
495
+ },
496
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [32] } },
497
+ "keyTotalSequenceLengthsT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [32] } }
498
+ },
499
+ "outputs": {
500
+ "outputT": { "dtype": "float16", "shape": [1, 32, 32], "tolerance": 0.003, "relTolerance": 0.01 },
501
+ "pastKeyT": { "dtype": "float16", "shape": [1, 2, 64, 8], "tolerance": 0.001, "relTolerance": 0.001 },
502
+ "pastValueT": { "dtype": "float16", "shape": [1, 2, 64, 8], "tolerance": 0.001, "relTolerance": 0.001 }
503
+ }
504
+ },
505
+ {
506
+ "name": "f16_decode_packed_partial_rotary",
507
+ "provenance": { "notes": "Float16 packed decode with partial NeoX rotary and unequal per-batch history lengths." },
508
+ "attrs": { "num_heads": 4, "kv_num_heads": 2, "sparse_block_size": 16, "do_rotary": 1 },
509
+ "inputs": {
510
+ "queryT": {
511
+ "dtype": "float16",
512
+ "shape": [2, 1, 256],
513
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.5 }
514
+ },
515
+ "pastKeyT": {
516
+ "dtype": "float16",
517
+ "shape": [2, 2, 64, 32],
518
+ "data": { "kind": "fillFloat32", "sinStep": 0.011, "cosStep": 0.017, "scale": 0.25 }
519
+ },
520
+ "pastValueT": {
521
+ "dtype": "float16",
522
+ "shape": [2, 2, 64, 32],
523
+ "data": { "kind": "fillFloat32", "sinStep": 0.019, "cosStep": 0.013, "scale": 0.25 }
524
+ },
525
+ "blockRowIndicesT": {
526
+ "dtype": "int32",
527
+ "shape": [2, 5],
528
+ "data": { "kind": "values", "values": [0, 1, 3, 5, 8, 0, 1, 3, 6, 9] }
529
+ },
530
+ "blockColIndicesT": {
531
+ "dtype": "int32",
532
+ "shape": [2, 9],
533
+ "data": {
534
+ "kind": "values",
535
+ "values": { "$ref": "#/fixtureArrays/prompt_separate_two_layouts_input_blockColIndicesT" }
536
+ }
537
+ },
538
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [41] } },
539
+ "keyTotalSequenceLengthsT": { "dtype": "int32", "shape": [2], "data": { "kind": "values", "values": [41, 34] } },
540
+ "cosCacheT": { "dtype": "float16", "shape": [64, 8], "data": { "kind": "constant", "value": 0.8 } },
541
+ "sinCacheT": { "dtype": "float16", "shape": [64, 8], "data": { "kind": "constant", "value": 0.6 } }
542
+ },
543
+ "outputs": {
544
+ "outputT": { "dtype": "float16", "shape": [2, 1, 128], "tolerance": 0.003, "relTolerance": 0.01 },
545
+ "pastKeyT": { "dtype": "float16", "shape": [2, 2, 64, 32], "tolerance": 0.002, "relTolerance": 0.005 },
546
+ "pastValueT": { "dtype": "float16", "shape": [2, 2, 64, 32], "tolerance": 0.001, "relTolerance": 0.001 }
547
+ }
548
+ },
549
+ {
550
+ "name": "dense_layout_ignores_malformed_columns",
551
+ "provenance": {
552
+ "notes": "ORT classifies a layout from its final CSR count. Once dense, repeated non-prefix column contents are ignored."
553
+ },
554
+ "attrs": { "num_heads": 1, "kv_num_heads": 1, "sparse_block_size": 16 },
555
+ "inputs": {
556
+ "queryT": {
557
+ "dtype": "float32",
558
+ "shape": [1, 32, 8],
559
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.5 }
560
+ },
561
+ "keyT": {
562
+ "dtype": "float32",
563
+ "shape": [1, 32, 8],
564
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31, "scale": 0.5 }
565
+ },
566
+ "valueT": {
567
+ "dtype": "float32",
568
+ "shape": [1, 32, 8],
569
+ "data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.23, "scale": 0.5 }
570
+ },
571
+ "pastKeyT": { "dtype": "float32", "shape": [1, 1, 32, 8], "data": { "kind": "constant", "value": 0.0 } },
572
+ "pastValueT": { "dtype": "float32", "shape": [1, 1, 32, 8], "data": { "kind": "constant", "value": 0.0 } },
573
+ "blockRowIndicesT": { "dtype": "int32", "shape": [1, 3], "data": { "kind": "values", "values": [0, 1, 3] } },
574
+ "blockColIndicesT": { "dtype": "int32", "shape": [1, 3], "data": { "kind": "values", "values": [1, 1, 1] } },
575
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [32] } },
576
+ "keyTotalSequenceLengthsT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [32] } }
577
+ },
578
+ "outputs": {
579
+ "outputT": { "dtype": "float32", "shape": [1, 32, 8], "tolerance": 0.00002, "relTolerance": 0.0002 },
580
+ "pastKeyT": { "dtype": "float32", "shape": [1, 1, 32, 8], "tolerance": 0.00002, "relTolerance": 0.0002 },
581
+ "pastValueT": { "dtype": "float32", "shape": [1, 1, 32, 8], "tolerance": 0.00002, "relTolerance": 0.0002 }
582
+ }
583
+ },
584
+ {
585
+ "name": "sparse_duplicate_columns_are_idempotent",
586
+ "provenance": {
587
+ "notes": "A sparse row repeats block 0. CSR encodes a boolean mask, so the duplicate must not duplicate probability mass."
588
+ },
589
+ "attrs": { "num_heads": 1, "kv_num_heads": 1, "sparse_block_size": 16 },
590
+ "inputs": {
591
+ "queryT": {
592
+ "dtype": "float32",
593
+ "shape": [1, 48, 8],
594
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.5 }
595
+ },
596
+ "keyT": {
597
+ "dtype": "float32",
598
+ "shape": [1, 48, 8],
599
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31, "scale": 0.5 }
600
+ },
601
+ "valueT": {
602
+ "dtype": "float32",
603
+ "shape": [1, 48, 8],
604
+ "data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.23, "scale": 0.5 }
605
+ },
606
+ "pastKeyT": { "dtype": "float32", "shape": [1, 1, 48, 8], "data": { "kind": "constant", "value": 0.0 } },
607
+ "pastValueT": { "dtype": "float32", "shape": [1, 1, 48, 8], "data": { "kind": "constant", "value": 0.0 } },
608
+ "blockRowIndicesT": { "dtype": "int32", "shape": [1, 4], "data": { "kind": "values", "values": [0, 1, 3, 5] } },
609
+ "blockColIndicesT": {
610
+ "dtype": "int32",
611
+ "shape": [1, 5],
612
+ "data": { "kind": "values", "values": [0, 0, 1, 0, 0] }
613
+ },
614
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [48] } },
615
+ "keyTotalSequenceLengthsT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [48] } }
616
+ },
617
+ "outputs": {
618
+ "outputT": { "dtype": "float32", "shape": [1, 48, 8], "tolerance": 0.00002, "relTolerance": 0.0002 },
619
+ "pastKeyT": { "dtype": "float32", "shape": [1, 1, 48, 8], "tolerance": 0.00002, "relTolerance": 0.0002 },
620
+ "pastValueT": { "dtype": "float32", "shape": [1, 1, 48, 8], "tolerance": 0.00002, "relTolerance": 0.0002 }
621
+ }
622
+ },
623
+ {
624
+ "name": "empty_csr_row_uniform_scalar_total",
625
+ "provenance": {
626
+ "notes": "Pinned all-masked-row behavior from ORT's lowest-sentinel softmax: one causal key yields its value, not zero. Also covers scalar total_sequence_length and a zero-width column tensor."
627
+ },
628
+ "attrs": { "num_heads": 1, "kv_num_heads": 1, "sparse_block_size": 16 },
629
+ "inputs": {
630
+ "queryT": {
631
+ "dtype": "float32",
632
+ "shape": [1, 1, 8],
633
+ "data": { "kind": "values", "values": [0.5, -0.5, 1.0, -1.0, 2.0, -2.0, 3.0, -3.0] }
634
+ },
635
+ "keyT": {
636
+ "dtype": "float32",
637
+ "shape": [1, 1, 8],
638
+ "data": { "kind": "values", "values": [8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0] }
639
+ },
640
+ "valueT": {
641
+ "dtype": "float32",
642
+ "shape": [1, 1, 8],
643
+ "data": { "kind": "values", "values": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0] }
644
+ },
645
+ "pastKeyT": {
646
+ "dtype": "float32",
647
+ "shape": [1, 1, 16, 8],
648
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.25 }
649
+ },
650
+ "pastValueT": {
651
+ "dtype": "float32",
652
+ "shape": [1, 1, 16, 8],
653
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31, "scale": 0.25 }
654
+ },
655
+ "blockRowIndicesT": { "dtype": "int32", "shape": [1, 2], "data": { "kind": "values", "values": [0, 0] } },
656
+ "blockColIndicesT": { "dtype": "int32", "shape": [1, 0], "data": { "kind": "values", "values": [] } },
657
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [], "data": { "kind": "values", "values": [1] } },
658
+ "keyTotalSequenceLengthsT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1] } }
659
+ },
660
+ "outputs": {
661
+ "outputT": {
662
+ "dtype": "float32",
663
+ "shape": [1, 1, 8],
664
+ "data": { "kind": "values", "values": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0] },
665
+ "tolerance": 0
666
+ },
667
+ "pastKeyT": { "dtype": "float32", "shape": [1, 1, 16, 8], "tolerance": 0 },
668
+ "pastValueT": { "dtype": "float32", "shape": [1, 1, 16, 8], "tolerance": 0 }
669
+ }
670
+ },
671
+ {
672
+ "name": "optional_rotary_input_ignored_when_attribute_is_not_one",
673
+ "provenance": {
674
+ "notes": "The CPU kernel enables do_rotary only for the exact integer value 1. Otherwise optional rotary inputs are ignored and need not be paired."
675
+ },
676
+ "attrs": { "num_heads": 1, "kv_num_heads": 1, "sparse_block_size": 16, "do_rotary": 2, "rotary_interleaved": 2 },
677
+ "inputs": {
678
+ "queryT": {
679
+ "dtype": "float32",
680
+ "shape": [1, 1, 8],
681
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.5 }
682
+ },
683
+ "keyT": {
684
+ "dtype": "float32",
685
+ "shape": [1, 1, 8],
686
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31, "scale": 0.5 }
687
+ },
688
+ "valueT": {
689
+ "dtype": "float32",
690
+ "shape": [1, 1, 8],
691
+ "data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.23, "scale": 0.5 }
692
+ },
693
+ "pastKeyT": { "dtype": "float32", "shape": [1, 1, 16, 8], "data": { "kind": "constant", "value": 0.0 } },
694
+ "pastValueT": { "dtype": "float32", "shape": [1, 1, 16, 8], "data": { "kind": "constant", "value": 0.0 } },
695
+ "blockRowIndicesT": { "dtype": "int32", "shape": [1, 2], "data": { "kind": "values", "values": [0, 1] } },
696
+ "blockColIndicesT": { "dtype": "int32", "shape": [1, 1], "data": { "kind": "values", "values": [0] } },
697
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1] } },
698
+ "keyTotalSequenceLengthsT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1] } },
699
+ "cosCacheT": { "dtype": "float32", "shape": [1, 1], "data": { "kind": "constant", "value": 0.0 } }
700
+ },
701
+ "outputs": {
702
+ "outputT": { "dtype": "float32", "shape": [1, 1, 8], "tolerance": 0.000001, "relTolerance": 0.000001 },
703
+ "pastKeyT": { "dtype": "float32", "shape": [1, 1, 16, 8], "tolerance": 0.000001, "relTolerance": 0.000001 },
704
+ "pastValueT": { "dtype": "float32", "shape": [1, 1, 16, 8], "tolerance": 0.000001, "relTolerance": 0.000001 }
705
+ }
706
+ },
707
+ {
708
+ "name": "zero_width_rotary_is_a_noop",
709
+ "provenance": {
710
+ "notes": "ONNX Runtime accepts zero-width paired caches with do_rotary=1; the resulting zero-dimensional rotary transform copies Q and K through."
711
+ },
712
+ "attrs": { "num_heads": 1, "kv_num_heads": 1, "sparse_block_size": 16, "do_rotary": 1, "rotary_interleaved": 1 },
713
+ "inputs": {
714
+ "queryT": {
715
+ "dtype": "float32",
716
+ "shape": [1, 1, 16],
717
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.5 }
718
+ },
719
+ "keyT": {
720
+ "dtype": "float32",
721
+ "shape": [1, 1, 16],
722
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31, "scale": 0.5 }
723
+ },
724
+ "valueT": {
725
+ "dtype": "float32",
726
+ "shape": [1, 1, 16],
727
+ "data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.23, "scale": 0.5 }
728
+ },
729
+ "pastKeyT": { "dtype": "float32", "shape": [1, 1, 16, 16], "data": { "kind": "constant", "value": 0.0 } },
730
+ "pastValueT": { "dtype": "float32", "shape": [1, 1, 16, 16], "data": { "kind": "constant", "value": 0.0 } },
731
+ "blockRowIndicesT": { "dtype": "int32", "shape": [1, 2], "data": { "kind": "values", "values": [0, 1] } },
732
+ "blockColIndicesT": { "dtype": "int32", "shape": [1, 1], "data": { "kind": "values", "values": [0] } },
733
+ "totalSequenceLengthT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1] } },
734
+ "keyTotalSequenceLengthsT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [1] } },
735
+ "cosCacheT": { "dtype": "float32", "shape": [1, 0], "data": { "kind": "values", "values": [] } },
736
+ "sinCacheT": { "dtype": "float32", "shape": [1, 0], "data": { "kind": "values", "values": [] } }
737
+ },
738
+ "outputs": {
739
+ "outputT": { "dtype": "float32", "shape": [1, 1, 16], "tolerance": 0.000001, "relTolerance": 0.000001 },
740
+ "pastKeyT": { "dtype": "float32", "shape": [1, 1, 16, 16], "tolerance": 0.000001, "relTolerance": 0.000001 },
741
+ "pastValueT": { "dtype": "float32", "shape": [1, 1, 16, 16], "tolerance": 0.000001, "relTolerance": 0.000001 }
742
+ }
743
+ }
744
+ ]
745
+ }