Xenova HF Staff commited on
Commit
fd6745d
·
verified ·
1 Parent(s): 2da2397

sync 91d990483a17

Browse files
README.md CHANGED
@@ -18,25 +18,25 @@ See the [ONNX Runtime `PagedAttention` contrib-operator spec](https://github.com
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 | `2` | — | Packed queries of shape `(num_tokens, num_heads * head_size)`, or `(num_tokens, (num_heads + 2 * kv_num_heads) * head_size)` when `key` and `value` are absent and Q, K and V share one row. | required |
24
- | `key` | `keyT` | `T` | same as logical dtype | `2` | — | Keys of shape `(num_tokens, kv_num_heads * head_size)`. Absent means `query` carries packed `[Q\|K\|V]`. | optional |
25
- | `value` | `valueT` | `T` | same as logical dtype | `2` | — | Values of shape `(num_tokens, kv_num_heads * head_size)`. Present exactly when `key` is. | optional |
26
- | `key_cache` | `keyCacheT` | `T` | same as logical dtype | `4` | — | Block-based key cache of shape `(num_blocks, block_size, kv_num_heads, head_size)`, updated in place. | required |
27
- | `value_cache` | `valueCacheT` | `T` | same as logical dtype | `4` | — | Block-based value cache with the same shape as `key_cache`, updated in place. | required |
28
- | `cumulative_sequence_length` | `cumulativeSequenceLengthT` | `S` | `int32` | `1` | — | Exclusive prefix sums of the per-sequence token counts, shape `(batch_size + 1)`; sequence `b` owns packed tokens `[cum[b], cum[b+1])`. | required |
29
- | `past_seqlens` | `pastSeqlensT` | `S` | `int32` | `1` | — | Cached history length per sequence, shape `(batch_size)`. | required |
30
- | `block_table` | `blockTableT` | `S` | `int32` | `2` | — | Physical block index per sequence and logical block, shape `(batch_size, max_blocks_per_sequence)`. | required |
31
- | `slot_mapping` | `slotMappingT` | `S` | `int32` | `1` | — | Flat destination slot, `block_id * block_size + offset`, for each token; `-1` suppresses that token's cache write. When omitted, the slot is derived from `past_seqlens`. `block_table` remains required because it defines the read path. | optional |
32
 
33
  ## Outputs
34
 
35
- | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
36
  | --- | --- | --- | --- | --- | --- | --- |
37
- | `output` | `outputT` | `T` | `2` | derived; see description | Attention output of shape `(num_tokens, num_heads * head_size)`. | required |
38
- | `key_cache` | `keyCacheT` | `T` | `4` | same as `key_cache` | Optional return alias for the updated in-place key cache. The runtime updates both caches together even when only this alias is requested. | optional |
39
- | `value_cache` | `valueCacheT` | `T` | `4` | same as `value_cache` | Optional return alias for the updated in-place value cache. The runtime updates both caches together even when only this alias is requested. | optional |
40
 
41
  ## Attributes
42
 
@@ -44,7 +44,7 @@ Attributes and default values (overridable per request):
44
 
45
  | Attribute | Default | Description |
46
  | --- | --- | --- |
47
- | `is_causal` | `1` | Whether to apply causal masking. This package supports only value 1. Older ORT schema revisions omit this attribute and are always causal. |
48
  | `kv_num_heads` | — | Number of key/value heads. |
49
  | `num_heads` | — | Number of query heads. |
50
  | `scale` | — | Scale applied to query-key products; zero or omission selects `1 / sqrt(head_size)`. |
@@ -56,13 +56,22 @@ Attributes and default values (overridable per request):
56
  | `T` | `float16` |
57
  | `S` | `int32` |
58
 
 
 
 
 
 
 
 
 
 
59
  ## Device requirements
60
 
61
  Every implementation variant requires `shader-f16`; the package has no variant-level fallback without that capability.
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
@@ -72,10 +81,14 @@ Every implementation variant requires `shader-f16`; the package has no variant-l
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
 
 
18
 
19
  ## Inputs
20
 
21
+ | Name | Upstream name | Logical dtype | WebGPU storage | Rank | Shape | Description | Presence |
22
  | --- | --- | --- | --- | --- | --- | --- | --- |
23
+ | `queryT` | `query` | `T` | same as logical dtype | `2` | — | Packed queries of shape `(num_tokens, num_heads * head_size)`, or `(num_tokens, (num_heads + 2 * kv_num_heads) * head_size)` when `key` and `value` are absent and Q, K and V share one row. | required |
24
+ | `keyT` | `key` | `T` | same as logical dtype | `2` | — | Keys of shape `(num_tokens, kv_num_heads * head_size)`. Absent means `query` carries packed `[Q\|K\|V]`. | optional |
25
+ | `valueT` | `value` | `T` | same as logical dtype | `2` | — | Values of shape `(num_tokens, kv_num_heads * head_size)`. Present exactly when `key` is. | optional |
26
+ | `keyCacheT` | `key_cache` | `T` | same as logical dtype | `4` | — | Block-based key cache of shape `(num_blocks, block_size, kv_num_heads, head_size)`, updated in place. | required |
27
+ | `valueCacheT` | `value_cache` | `T` | same as logical dtype | `4` | — | Block-based value cache with the same shape as `key_cache`, updated in place. | required |
28
+ | `cumulativeSequenceLengthT` | `cumulative_sequence_length` | `S` | `int32` | `1` | — | Exclusive prefix sums of the per-sequence token counts, shape `(batch_size + 1)`; sequence `b` owns packed tokens `[cum[b], cum[b+1])`. | required |
29
+ | `pastSeqlensT` | `past_seqlens` | `S` | `int32` | `1` | — | Cached history length per sequence, shape `(batch_size)`. | required |
30
+ | `blockTableT` | `block_table` | `S` | `int32` | `2` | — | Physical block index per sequence and logical block, shape `(batch_size, max_blocks_per_sequence)`. | required |
31
+ | `slotMappingT` | `slot_mapping` | `S` | `int32` | `1` | — | Flat destination slot, `block_id * block_size + offset`, for each token; `-1` suppresses that token's cache write. When omitted, the slot is derived from `past_seqlens`. `block_table` remains required because it defines the read path. | optional |
32
 
33
  ## Outputs
34
 
35
+ | Name | Upstream name | Logical dtype | Rank | Shape | Description | Presence |
36
  | --- | --- | --- | --- | --- | --- | --- |
37
+ | `outputT` | `output` | `T` | `2` | derived | Attention output of shape `(num_tokens, num_heads * head_size)`. | required |
38
+ | `keyCacheT` | `key_cache` | `T` | `4` | same as `keyCacheT` | Optional return alias for the updated in-place key cache. Both caches are updated even when only this result is requested. | optional |
39
+ | `valueCacheT` | `value_cache` | `T` | `4` | same as `valueCacheT` | Optional return alias for the updated in-place value cache. Both caches are updated even when only this result is requested. | optional |
40
 
41
  ## Attributes
42
 
 
44
 
45
  | Attribute | Default | Description |
46
  | --- | --- | --- |
47
+ | `is_causal` | `1` | Whether to apply causal masking. This package supports only value 1. |
48
  | `kv_num_heads` | — | Number of key/value heads. |
49
  | `num_heads` | — | Number of query heads. |
50
  | `scale` | — | Scale applied to query-key products; zero or omission selects `1 / sqrt(head_size)`. |
 
56
  | `T` | `float16` |
57
  | `S` | `int32` |
58
 
59
+ ## Implementation variants
60
+
61
+ One implementation is selected per call from the device capabilities, the request shapes and the dtypes; these notes say what each one covers.
62
+
63
+ - `separate_derived_splitk` — Splits each token's key history into contiguous ranges, processes one range per workgroup, and merges the resulting online-softmax states. This adds parallel key ranges for grouped-query decode shapes with few `(token, KV head)` tuples.
64
+ - `separate_slot_splitk` — Splits each token's key history into contiguous ranges, processes one range per workgroup, and merges the resulting online-softmax states. This adds parallel key ranges for grouped-query decode shapes with few `(token, KV head)` tuples.
65
+ - `packed_derived_splitk` — Splits each token's key history into contiguous ranges, processes one range per workgroup, and merges the resulting online-softmax states. This adds parallel key ranges for grouped-query decode shapes with few `(token, KV head)` tuples.
66
+ - `packed_slot_splitk` — Splits each token's key history into contiguous ranges, processes one range per workgroup, and merges the resulting online-softmax states. This adds parallel key ranges for grouped-query decode shapes with few `(token, KV head)` tuples.
67
+
68
  ## Device requirements
69
 
70
  Every implementation variant requires `shader-f16`; the package has no variant-level fallback without that capability.
71
 
72
  ## Files
73
 
74
+ - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, per-variant templates, provenance)
75
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
76
  - [`test.json`](build/webgpu/test.json) — correctness cases
77
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
 
81
 
82
  ## Use with `@huggingface/kernels`
83
 
84
+ ```sh
85
+ npm install --save-exact @huggingface/kernels@0.0.1-preview.2
86
+ ```
87
+
88
+ Required output shapes and logical data types are inferred from the supplied inputs and attributes; result tensors are allocated automatically.
89
 
90
  The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
91
+ It follows the `v1` branch as fixes land. To pin exact artifact bytes, pass a 40-character commit `revision` instead of `version`.
92
 
93
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
94
 
build/webgpu/attn-flash-decode-splitk-merge.wgsl.jinja CHANGED
@@ -3,6 +3,7 @@ enable f16;
3
  {% endif %}
4
  {% if splitQueries is not defined %}{% set splitQueries = false %}{% endif %}
5
  {% if hasGate is not defined %}{% set hasGate = false %}{% endif %}
 
6
  {{ env.wgsl.resourceDeclarations }}
7
 
8
  // Split-K flash decode, pass 2 of 2. Combines the per-split un-normalized online
@@ -22,13 +23,6 @@ const NUM_SPLITS: u32 = {{ numSplits }}u;
22
  const Q_SEQ: u32 = {{ qSeq }}u;
23
  {% endif %}
24
  const Q_HIDDEN_V4: u32 = {{ qHiddenV4 }}u;
25
- {% if hasBias %}
26
- // Packed [Q; K; V] bias rows. The V bias is token-independent, so the split
27
- // pass leaves it off and it is added here once after the final normalize,
28
- // matching the non-split flash-decode semantics exactly.
29
- const HEAD_DIM: u32 = {{ headDim }}u;
30
- const Q_HIDDEN: u32 = {{ qHidden }}u;
31
- {% endif %}
32
  // FLT_MAX, not -inf, as the online (m, d) accumulator init: merges must keep
33
  // `m - m` finite so an empty lane / all--inf row contributes the exact
34
  // accumulator identity (m, d) = (-FLT_MAX, 0). Operator epilogues interpret
@@ -99,13 +93,8 @@ fn main(
99
  // and globalDenom lands on exactly 0. Emit 0 rather than dividing so an all-empty
100
  // row has a defined zero result.
101
  var outValue = select(vec4<f32>(0.0), outV / globalDenom, globalDenom > 0.0);
102
- {% if hasBias %}
103
- // V bias row base: 2 * qHidden (skip the packed Q and K bias blocks) + this head.
104
- let vBiasBase = 2u * Q_HIDDEN + h * HEAD_DIM + d4 * 4u;
105
- outValue = outValue + vec4<f32>(bias[vBiasBase], bias[vBiasBase + 1u], bias[vBiasBase + 2u], bias[vBiasBase + 3u]);
106
- {% endif %}
107
  {% if hasGate %}
108
- // Qwen3.5 full attention gates the normalized attention output elementwise.
109
  let gateV = vec4<f32>(gate[qBaseV4 + d4]);
110
  outValue = outValue * (vec4<f32>(1.0) / (vec4<f32>(1.0) + exp(-gateV)));
111
  {% endif %}
 
3
  {% endif %}
4
  {% if splitQueries is not defined %}{% set splitQueries = false %}{% endif %}
5
  {% if hasGate is not defined %}{% set hasGate = false %}{% endif %}
6
+ {% set qSeq = qSeq | default(0) %}
7
  {{ env.wgsl.resourceDeclarations }}
8
 
9
  // Split-K flash decode, pass 2 of 2. Combines the per-split un-normalized online
 
23
  const Q_SEQ: u32 = {{ qSeq }}u;
24
  {% endif %}
25
  const Q_HIDDEN_V4: u32 = {{ qHiddenV4 }}u;
 
 
 
 
 
 
 
26
  // FLT_MAX, not -inf, as the online (m, d) accumulator init: merges must keep
27
  // `m - m` finite so an empty lane / all--inf row contributes the exact
28
  // accumulator identity (m, d) = (-FLT_MAX, 0). Operator epilogues interpret
 
93
  // and globalDenom lands on exactly 0. Emit 0 rather than dividing so an all-empty
94
  // row has a defined zero result.
95
  var outValue = select(vec4<f32>(0.0), outV / globalDenom, globalDenom > 0.0);
 
 
 
 
 
96
  {% if hasGate %}
97
+ // The gated route multiplies the normalized attention output elementwise by its gate.
98
  let gateV = vec4<f32>(gate[qBaseV4 + d4]);
99
  outValue = outValue * (vec4<f32>(1.0) / (vec4<f32>(1.0) + exp(-gateV)));
100
  {% endif %}
build/webgpu/bench.json CHANGED
@@ -1,5 +1,4 @@
1
  {
2
- "op": "com.microsoft.PagedAttention",
3
  "tunableSpace": {
4
  "WORKGROUP_SIZE": [32, 64, 128],
5
  "SCATTER_WORKGROUP_SIZE": [64, 128, 256],
@@ -32,7 +31,7 @@
32
  "metrics": [{ "type": "bandwidth", "value": "2 * args.batch * args.kvHeads * args.past * args.headDim * 2" }]
33
  },
34
  "provenance": {
35
- "notes": "The KV bytes are counted off named vars like the model cases, not hardcoded: the literal this replaced used the QUERY head count for a grouped-query shape and four bytes for a float16 cache, so it over-stated the traffic eightfold and the case reported an impossible rate."
36
  }
37
  },
38
  {
 
1
  {
 
2
  "tunableSpace": {
3
  "WORKGROUP_SIZE": [32, 64, 128],
4
  "SCATTER_WORKGROUP_SIZE": [64, 128, 256],
 
31
  "metrics": [{ "type": "bandwidth", "value": "2 * args.batch * args.kvHeads * args.past * args.headDim * 2" }]
32
  },
33
  "provenance": {
34
+ "notes": "The bandwidth metric derives KV traffic from the key/value head count and the two-byte float16 cache element size."
35
  }
36
  },
37
  {
build/webgpu/manifest.json CHANGED
@@ -2,133 +2,40 @@
2
  "domain": "com.microsoft",
3
  "name": "PagedAttention",
4
  "sinceVersion": 1,
5
- "description": "Attention over a block-based (paged) KV cache: `cumulative_sequence_length` marks the sequence boundaries and `block_table` maps a sequence's history onto scattered blocks. This step's K/V are scattered into the cache, then attended with that history. Grouped-query heads, `scale`, packed `[Q|K|V]`, `slot_mapping`, and float16 cache storage are supported; the cache outputs alias the input caches and are updated in place. Rotary embeddings, softcap, local windows, LATENT layout, narrower value heads, quantized KV, head sinks, q/k normalization, scales, and attention metadata are not implemented.",
6
- "inputs": [
7
- {
8
- "role": "query",
9
- "dtype": "T",
10
- "rank": 2,
11
- "description": "Packed queries of shape `(num_tokens, num_heads * head_size)`, or `(num_tokens, (num_heads + 2 * kv_num_heads) * head_size)` when `key` and `value` are absent and Q, K and V share one row."
12
- },
13
- {
14
- "role": "key",
15
- "dtype": "T",
16
- "rank": 2,
17
- "optional": true,
18
- "description": "Keys of shape `(num_tokens, kv_num_heads * head_size)`. Absent means `query` carries packed `[Q|K|V]`."
19
- },
20
- {
21
- "role": "value",
22
- "dtype": "T",
23
- "rank": 2,
24
- "optional": true,
25
- "description": "Values of shape `(num_tokens, kv_num_heads * head_size)`. Present exactly when `key` is."
26
- },
27
- {
28
- "role": "key_cache",
29
- "dtype": "T",
30
- "rank": 4,
31
- "description": "Block-based key cache of shape `(num_blocks, block_size, kv_num_heads, head_size)`, updated in place."
32
- },
33
- {
34
- "role": "value_cache",
35
- "dtype": "T",
36
- "rank": 4,
37
- "description": "Block-based value cache with the same shape as `key_cache`, updated in place."
38
- },
39
- {
40
- "role": "cumulative_sequence_length",
41
- "dtype": "S",
42
- "rank": 1,
43
- "description": "Exclusive prefix sums of the per-sequence token counts, shape `(batch_size + 1)`; sequence `b` owns packed tokens `[cum[b], cum[b+1])`."
44
- },
45
- {
46
- "role": "past_seqlens",
47
- "dtype": "S",
48
- "rank": 1,
49
- "description": "Cached history length per sequence, shape `(batch_size)`."
50
- },
51
- {
52
- "role": "block_table",
53
- "dtype": "S",
54
- "rank": 2,
55
- "description": "Physical block index per sequence and logical block, shape `(batch_size, max_blocks_per_sequence)`."
56
- },
57
- {
58
- "role": "slot_mapping",
59
- "dtype": "S",
60
- "rank": 1,
61
- "optional": true,
62
- "description": "Flat destination slot, `block_id * block_size + offset`, for each token; `-1` suppresses that token's cache write. When omitted, the slot is derived from `past_seqlens`. `block_table` remains required because it defines the read path."
63
- }
64
- ],
65
- "outputs": [
66
- {
67
- "role": "output",
68
  "dtype": "T",
69
  "rank": 2,
70
- "shape": "[dim(shapes.queryT, 0), attrs.num_heads * headSize]",
71
- "description": "Attention output of shape `(num_tokens, num_heads * head_size)`."
72
  },
73
- {
74
- "role": "key_cache",
75
- "dtype": "T",
76
- "rank": 4,
77
- "shape": "shapes.keyCacheT",
78
- "optional": true,
79
- "description": "Optional return alias for the updated in-place key cache. The runtime updates both caches together even when only this alias is requested."
80
- },
81
- {
82
- "role": "value_cache",
83
- "dtype": "T",
84
- "rank": 4,
85
- "shape": "shapes.valueCacheT",
86
- "optional": true,
87
- "description": "Optional return alias for the updated in-place value cache. The runtime updates both caches together even when only this alias is requested."
88
- }
89
- ],
90
- "attributes": { "is_causal": 1 },
91
- "attributeDescriptions": {
92
- "is_causal": "Whether to apply causal masking. This package supports only value 1. Older ORT schema revisions omit this attribute and are always causal.",
93
- "kv_num_heads": "Number of key/value heads.",
94
- "num_heads": "Number of query heads.",
95
- "scale": "Scale applied to query-key products; zero or omission selects `1 / sqrt(head_size)`."
96
  },
 
97
  "attributeConstraints": {
98
  "is_causal": { "values": [1] },
99
  "kv_num_heads": { "required": true },
100
  "num_heads": { "required": true }
101
  },
102
  "typeConstraints": { "T": ["float16"], "S": ["int32"] },
103
- "args": {
104
- "queryT": { "kind": "tensor", "semantic": "query", "role": "input" },
105
- "keyT": { "kind": "tensor", "semantic": "key", "role": "input", "required": false },
106
- "valueT": { "kind": "tensor", "semantic": "value", "role": "input", "required": false },
107
- "keyCacheT": { "kind": "tensor", "semantic": "key_cache", "role": "inout" },
108
- "valueCacheT": { "kind": "tensor", "semantic": "value_cache", "role": "inout" },
109
- "cumulativeSequenceLengthT": {
110
- "kind": "tensor",
111
- "semantic": "cumulative_sequence_length",
112
- "role": "input",
113
- "dtype": "int32"
114
- },
115
- "pastSeqlensT": { "kind": "tensor", "semantic": "past_seqlens", "role": "input", "dtype": "int32" },
116
- "blockTableT": { "kind": "tensor", "semantic": "block_table", "role": "input", "dtype": "int32" },
117
- "slotMappingT": {
118
- "kind": "tensor",
119
- "semantic": "slot_mapping",
120
- "role": "input",
121
- "dtype": "int32",
122
- "required": false
123
- },
124
- "outputT": { "kind": "tensor", "semantic": "output", "role": "output" }
125
- },
126
  "tunables": {
127
- "WORKGROUP_SIZE": 64,
128
- "SCATTER_WORKGROUP_SIZE": 64,
129
- "MAX_SPLITS": 16,
130
- "SPLIT_MIN_KEYS": 128,
131
- "SPLIT_TARGET_WORKGROUPS": 1024
132
  },
133
  "derive": {
134
  "tokenCount": "dim(shapes.queryT, 0)",
@@ -150,35 +57,31 @@
150
  "packedKv": "not present.keyT and not present.valueT and dim(shapes.queryT, 1) == packedStride",
151
  "slotShapeOk": "dim(shapes.slotMappingT, 0) == tokenCount and ranks.slotMappingT == 1 if present.slotMappingT else true",
152
  "pagedContractOk": "cacheShapeOk and headLayoutOk and scheduleShapeOk and ioShapeOk and slotShapeOk",
153
- "dispatchFits": "tokenCount <= device.limits.maxComputeWorkgroupsPerDimension and attrs.num_heads <= device.limits.maxComputeWorkgroupsPerDimension and tunables.WORKGROUP_SIZE <= device.limits.maxComputeInvocationsPerWorkgroup and tunables.SCATTER_WORKGROUP_SIZE <= device.limits.maxComputeInvocationsPerWorkgroup",
154
  "pagedBaseWorkgroups": "tokenCount * attrs.kv_num_heads",
 
 
 
 
 
 
155
  "pagedKeyCeiling": "maxBlocks * blockSize",
156
  "pagedSplitWg": "min(256, max(32, pow2ceil(headVec)))",
 
 
157
  "pagedSplitCap": "max(1, min(tunables.MAX_SPLITS, pagedKeyCeiling / tunables.SPLIT_MIN_KEYS))",
158
  "numSplits": "max(1, min(pagedSplitCap, ceilDiv(tunables.SPLIT_TARGET_WORKGROUPS, max(1, pagedBaseWorkgroups))))",
159
  "pagedSplitScratchBytes": "tokenCount * attrs.num_heads * numSplits * headSize * 4",
160
  "pagedSplitStatsBytes": "2 * tokenCount * attrs.num_heads * numSplits * 4",
161
- "pagedSplitFits": "numSplits >= 2 and cacheVec4Ok and headVec >= 1 and pagedSplitScratchBytes <= device.limits.maxStorageBufferBindingSize and pagedSplitScratchBytes <= device.limits.maxBufferSize and pagedSplitStatsBytes <= device.limits.maxStorageBufferBindingSize and pagedSplitStatsBytes <= device.limits.maxBufferSize and numSplits <= device.limits.maxComputeWorkgroupsPerDimension and headVec <= device.limits.maxComputeInvocationsPerWorkgroup and headVec <= device.limits.maxComputeWorkgroupSizeX and pagedSplitWg <= device.limits.maxComputeInvocationsPerWorkgroup and pagedSplitWg <= device.limits.maxComputeWorkgroupSizeX"
162
- },
163
- "constants": {
164
  "aScalar": "dtypes.T",
165
  "scalar": "dtypes.T",
166
  "numHeads": "attrs.num_heads",
167
  "kvNumHeads": "attrs.kv_num_heads",
168
- "headSize": "headSize",
169
- "blockSize": "blockSize",
170
- "maxBlocks": "maxBlocks",
171
- "qHidden": "qHidden",
172
- "kvHidden": "kvHidden",
173
- "packedStride": "packedStride",
174
  "packedQkv": "not present.keyT",
175
  "hasSlotMapping": "present.slotMappingT",
176
- "qPerKv": "qPerKv",
177
- "attnWorkgroup": "tunables.WORKGROUP_SIZE",
178
- "headVec": "headVec",
179
  "cacheVec": "(\"vec4<f16>\" if dtypes.T == \"f16\" else \"vec4<f32>\") if cacheVec4Ok else dtypes.T",
180
- "cacheVec4Ok": "cacheVec4Ok",
181
- "numSplits": "numSplits",
182
  "headDimV4": "headVec",
183
  "headDim": "headSize",
184
  "qNumHeads": "attrs.num_heads",
@@ -186,367 +89,61 @@
186
  "hasBias": false,
187
  "outVec": "\"vec4<f16>\" if dtypes.T == \"f16\" else \"vec4<f32>\""
188
  },
189
- "bindingSets": {
190
- "scatterSeparateDerived": [
191
- {
192
- "name": "key",
193
- "arg": "keyT",
194
- "semantic": "key",
195
- "buffer": { "type": "read-only-storage" },
196
- "elementType": "$aScalar"
197
- },
198
- {
199
- "name": "value",
200
- "arg": "valueT",
201
- "semantic": "value",
202
- "buffer": { "type": "read-only-storage" },
203
- "elementType": "$aScalar"
204
- },
205
- {
206
- "name": "key_cache",
207
- "arg": "keyCacheT",
208
- "semantic": "key_cache",
209
- "buffer": { "type": "storage" },
210
- "elementType": "$aScalar"
211
- },
212
- {
213
- "name": "value_cache",
214
- "arg": "valueCacheT",
215
- "semantic": "value_cache",
216
- "buffer": { "type": "storage" },
217
- "elementType": "$aScalar"
218
- },
219
- {
220
- "name": "cumulative_sequence_length",
221
- "arg": "cumulativeSequenceLengthT",
222
- "semantic": "cumulative_sequence_length",
223
- "buffer": { "type": "read-only-storage" },
224
- "elementType": "i32"
225
- },
226
- {
227
- "name": "past_seqlens",
228
- "arg": "pastSeqlensT",
229
- "semantic": "past_seqlens",
230
- "buffer": { "type": "read-only-storage" },
231
- "elementType": "i32"
232
- },
233
- {
234
- "name": "block_table",
235
- "arg": "blockTableT",
236
- "semantic": "block_table",
237
- "buffer": { "type": "read-only-storage" },
238
- "elementType": "i32"
239
- },
240
- {
241
- "name": "params",
242
- "semantic": "kernel.params",
243
- "buffer": { "type": "uniform" },
244
- "struct": {
245
- "name": "Params",
246
- "fields": [
247
- { "name": "batchSize", "type": "u32", "value": "batchSize" },
248
- { "name": "scatterCount", "type": "u32", "value": "tokenCount * attrs.kv_num_heads * headSize" }
249
- ]
250
- }
251
- }
252
- ],
253
- "scatterSeparateSlot": [
254
- {
255
- "name": "key",
256
- "arg": "keyT",
257
- "semantic": "key",
258
- "buffer": { "type": "read-only-storage" },
259
- "elementType": "$aScalar"
260
- },
261
- {
262
- "name": "value",
263
- "arg": "valueT",
264
- "semantic": "value",
265
- "buffer": { "type": "read-only-storage" },
266
- "elementType": "$aScalar"
267
- },
268
- {
269
- "name": "key_cache",
270
- "arg": "keyCacheT",
271
- "semantic": "key_cache",
272
- "buffer": { "type": "storage" },
273
- "elementType": "$aScalar"
274
- },
275
- {
276
- "name": "value_cache",
277
- "arg": "valueCacheT",
278
- "semantic": "value_cache",
279
- "buffer": { "type": "storage" },
280
- "elementType": "$aScalar"
281
- },
282
- {
283
- "name": "slot_mapping",
284
- "arg": "slotMappingT",
285
- "semantic": "slot_mapping",
286
- "buffer": { "type": "read-only-storage" },
287
- "elementType": "i32"
288
- },
289
- {
290
- "name": "params",
291
- "semantic": "kernel.params",
292
- "buffer": { "type": "uniform" },
293
- "struct": {
294
- "name": "Params",
295
- "fields": [{ "name": "scatterCount", "type": "u32", "value": "tokenCount * attrs.kv_num_heads * headSize" }]
296
- }
297
- }
298
- ],
299
- "scatterPackedDerived": [
300
- {
301
- "name": "query",
302
- "arg": "queryT",
303
- "semantic": "query",
304
- "buffer": { "type": "read-only-storage" },
305
- "elementType": "$aScalar"
306
- },
307
- {
308
- "name": "key_cache",
309
- "arg": "keyCacheT",
310
- "semantic": "key_cache",
311
- "buffer": { "type": "storage" },
312
- "elementType": "$aScalar"
313
- },
314
- {
315
- "name": "value_cache",
316
- "arg": "valueCacheT",
317
- "semantic": "value_cache",
318
- "buffer": { "type": "storage" },
319
- "elementType": "$aScalar"
320
- },
321
- {
322
- "name": "cumulative_sequence_length",
323
- "arg": "cumulativeSequenceLengthT",
324
- "semantic": "cumulative_sequence_length",
325
- "buffer": { "type": "read-only-storage" },
326
- "elementType": "i32"
327
- },
328
- {
329
- "name": "past_seqlens",
330
- "arg": "pastSeqlensT",
331
- "semantic": "past_seqlens",
332
- "buffer": { "type": "read-only-storage" },
333
- "elementType": "i32"
334
- },
335
- {
336
- "name": "block_table",
337
- "arg": "blockTableT",
338
- "semantic": "block_table",
339
- "buffer": { "type": "read-only-storage" },
340
- "elementType": "i32"
341
- },
342
- {
343
- "name": "params",
344
- "semantic": "kernel.params",
345
- "buffer": { "type": "uniform" },
346
- "struct": {
347
- "name": "Params",
348
- "fields": [
349
- { "name": "batchSize", "type": "u32", "value": "batchSize" },
350
- { "name": "scatterCount", "type": "u32", "value": "tokenCount * attrs.kv_num_heads * headSize" }
351
- ]
352
- }
353
- }
354
- ],
355
- "scatterPackedSlot": [
356
- {
357
- "name": "query",
358
- "arg": "queryT",
359
- "semantic": "query",
360
- "buffer": { "type": "read-only-storage" },
361
- "elementType": "$aScalar"
362
- },
363
- {
364
- "name": "key_cache",
365
- "arg": "keyCacheT",
366
- "semantic": "key_cache",
367
- "buffer": { "type": "storage" },
368
- "elementType": "$aScalar"
369
- },
370
- {
371
- "name": "value_cache",
372
- "arg": "valueCacheT",
373
- "semantic": "value_cache",
374
- "buffer": { "type": "storage" },
375
- "elementType": "$aScalar"
376
- },
377
- {
378
- "name": "slot_mapping",
379
- "arg": "slotMappingT",
380
- "semantic": "slot_mapping",
381
- "buffer": { "type": "read-only-storage" },
382
- "elementType": "i32"
383
- },
384
- {
385
- "name": "params",
386
- "semantic": "kernel.params",
387
- "buffer": { "type": "uniform" },
388
- "struct": {
389
- "name": "Params",
390
- "fields": [{ "name": "scatterCount", "type": "u32", "value": "tokenCount * attrs.kv_num_heads * headSize" }]
391
- }
392
- }
393
- ],
394
- "attention": [
395
- {
396
- "name": "query",
397
- "arg": "queryT",
398
- "semantic": "query",
399
- "buffer": { "type": "read-only-storage" },
400
- "elementType": "$aScalar"
401
- },
402
- {
403
- "name": "key_cache",
404
- "arg": "keyCacheT",
405
- "semantic": "key_cache",
406
- "buffer": { "type": "read-only-storage" },
407
- "elementType": "$cacheVec"
408
- },
409
- {
410
- "name": "value_cache",
411
- "arg": "valueCacheT",
412
- "semantic": "value_cache",
413
- "buffer": { "type": "read-only-storage" },
414
- "elementType": "$cacheVec"
415
- },
416
- {
417
- "name": "cumulative_sequence_length",
418
- "arg": "cumulativeSequenceLengthT",
419
- "semantic": "cumulative_sequence_length",
420
- "buffer": { "type": "read-only-storage" },
421
- "elementType": "i32"
422
- },
423
- {
424
- "name": "past_seqlens",
425
- "arg": "pastSeqlensT",
426
- "semantic": "past_seqlens",
427
- "buffer": { "type": "read-only-storage" },
428
- "elementType": "i32"
429
- },
430
- {
431
- "name": "block_table",
432
- "arg": "blockTableT",
433
- "semantic": "block_table",
434
- "buffer": { "type": "read-only-storage" },
435
- "elementType": "i32"
436
- },
437
- {
438
- "name": "output",
439
- "arg": "outputT",
440
- "semantic": "output",
441
- "buffer": { "type": "storage" },
442
- "elementType": "$aScalar"
443
- },
444
- {
445
- "name": "params",
446
- "semantic": "kernel.params",
447
- "buffer": { "type": "uniform" },
448
- "struct": {
449
- "name": "Params",
450
- "fields": [
451
- { "name": "batchSize", "type": "u32", "value": "batchSize" },
452
- { "name": "scale", "type": "f32", "value": "attrs.scale if has(attrs, \"scale\") else 0" }
453
- ]
454
- }
455
- }
456
- ],
457
- "splitAttention": [
458
- {
459
- "name": "query",
460
- "arg": "queryT",
461
- "semantic": "query",
462
- "buffer": { "type": "read-only-storage" },
463
- "elementType": "$aScalar"
464
- },
465
- {
466
- "name": "key_cache",
467
- "arg": "keyCacheT",
468
- "semantic": "key_cache",
469
- "buffer": { "type": "read-only-storage" },
470
- "elementType": "$cacheVec"
471
- },
472
- {
473
- "name": "value_cache",
474
- "arg": "valueCacheT",
475
- "semantic": "value_cache",
476
- "buffer": { "type": "read-only-storage" },
477
- "elementType": "$cacheVec"
478
- },
479
- {
480
- "name": "cumulative_sequence_length",
481
- "arg": "cumulativeSequenceLengthT",
482
- "semantic": "cumulative_sequence_length",
483
- "buffer": { "type": "read-only-storage" },
484
- "elementType": "i32"
485
- },
486
- {
487
- "name": "past_seqlens",
488
- "arg": "pastSeqlensT",
489
- "semantic": "past_seqlens",
490
- "buffer": { "type": "read-only-storage" },
491
- "elementType": "i32"
492
- },
493
- {
494
- "name": "block_table",
495
- "arg": "blockTableT",
496
- "semantic": "block_table",
497
- "buffer": { "type": "read-only-storage" },
498
- "elementType": "i32"
499
- },
500
- { "name": "partial_out", "semantic": "partialOut", "buffer": { "type": "storage" }, "elementType": "vec4<f32>" },
501
- {
502
- "name": "partial_stats",
503
- "semantic": "partialStats",
504
- "buffer": { "type": "storage" },
505
- "elementType": "vec2<f32>"
506
- },
507
- {
508
- "name": "params",
509
- "semantic": "kernel.params",
510
- "buffer": { "type": "uniform" },
511
- "struct": {
512
- "name": "Params",
513
- "fields": [
514
- { "name": "batchSize", "type": "u32", "value": "batchSize" },
515
- { "name": "scale", "type": "f32", "value": "attrs.scale if has(attrs, \"scale\") else 0" }
516
- ]
517
- }
518
- }
519
- ],
520
- "splitMerge": [
521
- {
522
- "name": "partial_out",
523
- "semantic": "partialOut",
524
- "buffer": { "type": "read-only-storage" },
525
- "elementType": "vec4<f32>"
526
- },
527
- {
528
- "name": "partial_stats",
529
- "semantic": "partialStats",
530
- "buffer": { "type": "read-only-storage" },
531
- "elementType": "vec2<f32>"
532
- },
533
- {
534
- "name": "output",
535
- "arg": "outputT",
536
- "semantic": "output",
537
- "buffer": { "type": "storage" },
538
- "elementType": "$outVec"
539
- }
540
- ]
541
  },
542
  "variants": [
543
  {
544
  "id": "separate_derived_splitk",
545
- "description": "Splits each token's key history into contiguous ranges, one workgroup per range, and merges the per-range online-softmax states. It serves grouped-query decode shapes, where one workgroup per `(token, KV head)` leaves too few workgroups to fill the device.",
546
  "priority": 10,
 
547
  "requires": { "features": ["shader-f16"] },
548
- "when": ["pagedContractOk", "dispatchFits", "pagedSplitFits", "separateKv", "not present.slotMappingT"],
549
- "constants": { "attnWorkgroup": "pagedSplitWg" },
550
  "intermediates": [
551
  { "id": "partialOut", "dtype": "float32", "shape": "[tokenCount * attrs.num_heads * numSplits * headSize]" },
552
  { "id": "partialStats", "dtype": "float32", "shape": "[2 * tokenCount * attrs.num_heads * numSplits]" }
@@ -556,59 +153,95 @@
556
  "id": "scatter",
557
  "name": "PagedAttention.ScatterKV",
558
  "shader": "paged-scatter-kv.wgsl.jinja",
559
- "bindings": "scatterSeparateDerived",
560
  "dispatch": {
561
- "threads": "tokenCount * attrs.kv_num_heads * headSize",
562
- "workgroupSize": "tunables.SCATTER_WORKGROUP_SIZE"
 
563
  }
564
  },
565
  {
566
  "id": "split_attention",
567
  "name": "PagedAttention.AttendSplitK",
568
- "source": { "shader": "paged-attention.wgsl.jinja", "inputs": { "splitK": true } },
569
- "bindings": "splitAttention",
 
 
 
 
 
 
 
 
 
 
 
570
  "dispatch": { "x": "tokenCount", "y": "attrs.kv_num_heads", "z": "numSplits" }
571
  },
572
  {
573
  "id": "merge",
574
  "name": "PagedAttention.AttendSplitKMerge",
575
- "source": { "shader": "attn-flash-decode-splitk-merge.wgsl.jinja", "inputs": { "layout": "\"bsh\"" } },
576
- "bindings": "splitMerge",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
577
  "dispatch": { "x": 1, "y": "attrs.num_heads", "z": "tokenCount" }
578
  }
579
  ]
580
  },
581
  {
582
  "id": "separate_derived",
 
583
  "requires": { "features": ["shader-f16"] },
584
- "when": ["pagedContractOk", "dispatchFits", "separateKv", "not present.slotMappingT"],
585
  "passes": [
586
  {
587
  "id": "scatter",
588
  "name": "PagedAttention.ScatterKV",
589
  "shader": "paged-scatter-kv.wgsl.jinja",
590
- "bindings": "scatterSeparateDerived",
591
  "dispatch": {
592
- "threads": "tokenCount * attrs.kv_num_heads * headSize",
593
- "workgroupSize": "tunables.SCATTER_WORKGROUP_SIZE"
 
594
  }
595
  },
596
  {
597
  "id": "main",
598
  "name": "PagedAttention.Attend",
599
  "shader": "paged-attention.wgsl.jinja",
600
- "bindings": "attention",
 
 
 
 
 
 
 
 
 
601
  "dispatch": { "x": "tokenCount", "y": "attrs.kv_num_heads" }
602
  }
603
  ]
604
  },
605
  {
606
  "id": "separate_slot_splitk",
607
- "description": "Splits each token's key history into contiguous ranges, one workgroup per range, and merges the per-range online-softmax states. It serves grouped-query decode shapes, where one workgroup per `(token, KV head)` leaves too few workgroups to fill the device.",
608
  "priority": 10,
 
609
  "requires": { "features": ["shader-f16"] },
610
- "when": ["pagedContractOk", "dispatchFits", "pagedSplitFits", "separateKv", "present.slotMappingT"],
611
- "constants": { "attnWorkgroup": "pagedSplitWg" },
612
  "intermediates": [
613
  { "id": "partialOut", "dtype": "float32", "shape": "[tokenCount * attrs.num_heads * numSplits * headSize]" },
614
  { "id": "partialStats", "dtype": "float32", "shape": "[2 * tokenCount * attrs.num_heads * numSplits]" }
@@ -618,59 +251,95 @@
618
  "id": "scatter",
619
  "name": "PagedAttention.ScatterKV",
620
  "shader": "paged-scatter-kv.wgsl.jinja",
621
- "bindings": "scatterSeparateSlot",
622
  "dispatch": {
623
- "threads": "tokenCount * attrs.kv_num_heads * headSize",
624
- "workgroupSize": "tunables.SCATTER_WORKGROUP_SIZE"
 
625
  }
626
  },
627
  {
628
  "id": "split_attention",
629
  "name": "PagedAttention.AttendSplitK",
630
- "source": { "shader": "paged-attention.wgsl.jinja", "inputs": { "splitK": true } },
631
- "bindings": "splitAttention",
 
 
 
 
 
 
 
 
 
 
 
632
  "dispatch": { "x": "tokenCount", "y": "attrs.kv_num_heads", "z": "numSplits" }
633
  },
634
  {
635
  "id": "merge",
636
  "name": "PagedAttention.AttendSplitKMerge",
637
- "source": { "shader": "attn-flash-decode-splitk-merge.wgsl.jinja", "inputs": { "layout": "\"bsh\"" } },
638
- "bindings": "splitMerge",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
639
  "dispatch": { "x": 1, "y": "attrs.num_heads", "z": "tokenCount" }
640
  }
641
  ]
642
  },
643
  {
644
  "id": "separate_slot",
 
645
  "requires": { "features": ["shader-f16"] },
646
- "when": ["pagedContractOk", "dispatchFits", "separateKv", "present.slotMappingT"],
647
  "passes": [
648
  {
649
  "id": "scatter",
650
  "name": "PagedAttention.ScatterKV",
651
  "shader": "paged-scatter-kv.wgsl.jinja",
652
- "bindings": "scatterSeparateSlot",
653
  "dispatch": {
654
- "threads": "tokenCount * attrs.kv_num_heads * headSize",
655
- "workgroupSize": "tunables.SCATTER_WORKGROUP_SIZE"
 
656
  }
657
  },
658
  {
659
  "id": "main",
660
  "name": "PagedAttention.Attend",
661
  "shader": "paged-attention.wgsl.jinja",
662
- "bindings": "attention",
 
 
 
 
 
 
 
 
 
663
  "dispatch": { "x": "tokenCount", "y": "attrs.kv_num_heads" }
664
  }
665
  ]
666
  },
667
  {
668
  "id": "packed_derived_splitk",
669
- "description": "Splits each token's key history into contiguous ranges, one workgroup per range, and merges the per-range online-softmax states. It serves grouped-query decode shapes, where one workgroup per `(token, KV head)` leaves too few workgroups to fill the device.",
670
  "priority": 10,
 
671
  "requires": { "features": ["shader-f16"] },
672
- "when": ["pagedContractOk", "dispatchFits", "pagedSplitFits", "packedKv", "not present.slotMappingT"],
673
- "constants": { "attnWorkgroup": "pagedSplitWg" },
674
  "intermediates": [
675
  { "id": "partialOut", "dtype": "float32", "shape": "[tokenCount * attrs.num_heads * numSplits * headSize]" },
676
  { "id": "partialStats", "dtype": "float32", "shape": "[2 * tokenCount * attrs.num_heads * numSplits]" }
@@ -680,59 +349,95 @@
680
  "id": "scatter",
681
  "name": "PagedAttention.ScatterKV",
682
  "shader": "paged-scatter-kv.wgsl.jinja",
683
- "bindings": "scatterPackedDerived",
684
  "dispatch": {
685
- "threads": "tokenCount * attrs.kv_num_heads * headSize",
686
- "workgroupSize": "tunables.SCATTER_WORKGROUP_SIZE"
 
687
  }
688
  },
689
  {
690
  "id": "split_attention",
691
  "name": "PagedAttention.AttendSplitK",
692
- "source": { "shader": "paged-attention.wgsl.jinja", "inputs": { "splitK": true } },
693
- "bindings": "splitAttention",
 
 
 
 
 
 
 
 
 
 
 
694
  "dispatch": { "x": "tokenCount", "y": "attrs.kv_num_heads", "z": "numSplits" }
695
  },
696
  {
697
  "id": "merge",
698
  "name": "PagedAttention.AttendSplitKMerge",
699
- "source": { "shader": "attn-flash-decode-splitk-merge.wgsl.jinja", "inputs": { "layout": "\"bsh\"" } },
700
- "bindings": "splitMerge",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
701
  "dispatch": { "x": 1, "y": "attrs.num_heads", "z": "tokenCount" }
702
  }
703
  ]
704
  },
705
  {
706
  "id": "packed_derived",
 
707
  "requires": { "features": ["shader-f16"] },
708
- "when": ["pagedContractOk", "dispatchFits", "packedKv", "not present.slotMappingT"],
709
  "passes": [
710
  {
711
  "id": "scatter",
712
  "name": "PagedAttention.ScatterKV",
713
  "shader": "paged-scatter-kv.wgsl.jinja",
714
- "bindings": "scatterPackedDerived",
715
  "dispatch": {
716
- "threads": "tokenCount * attrs.kv_num_heads * headSize",
717
- "workgroupSize": "tunables.SCATTER_WORKGROUP_SIZE"
 
718
  }
719
  },
720
  {
721
  "id": "main",
722
  "name": "PagedAttention.Attend",
723
  "shader": "paged-attention.wgsl.jinja",
724
- "bindings": "attention",
 
 
 
 
 
 
 
 
 
725
  "dispatch": { "x": "tokenCount", "y": "attrs.kv_num_heads" }
726
  }
727
  ]
728
  },
729
  {
730
  "id": "packed_slot_splitk",
731
- "description": "Splits each token's key history into contiguous ranges, one workgroup per range, and merges the per-range online-softmax states. It serves grouped-query decode shapes, where one workgroup per `(token, KV head)` leaves too few workgroups to fill the device.",
732
  "priority": 10,
 
733
  "requires": { "features": ["shader-f16"] },
734
- "when": ["pagedContractOk", "dispatchFits", "pagedSplitFits", "packedKv", "present.slotMappingT"],
735
- "constants": { "attnWorkgroup": "pagedSplitWg" },
736
  "intermediates": [
737
  { "id": "partialOut", "dtype": "float32", "shape": "[tokenCount * attrs.num_heads * numSplits * headSize]" },
738
  { "id": "partialStats", "dtype": "float32", "shape": "[2 * tokenCount * attrs.num_heads * numSplits]" }
@@ -742,48 +447,85 @@
742
  "id": "scatter",
743
  "name": "PagedAttention.ScatterKV",
744
  "shader": "paged-scatter-kv.wgsl.jinja",
745
- "bindings": "scatterPackedSlot",
746
  "dispatch": {
747
- "threads": "tokenCount * attrs.kv_num_heads * headSize",
748
- "workgroupSize": "tunables.SCATTER_WORKGROUP_SIZE"
 
749
  }
750
  },
751
  {
752
  "id": "split_attention",
753
  "name": "PagedAttention.AttendSplitK",
754
- "source": { "shader": "paged-attention.wgsl.jinja", "inputs": { "splitK": true } },
755
- "bindings": "splitAttention",
 
 
 
 
 
 
 
 
 
 
 
756
  "dispatch": { "x": "tokenCount", "y": "attrs.kv_num_heads", "z": "numSplits" }
757
  },
758
  {
759
  "id": "merge",
760
  "name": "PagedAttention.AttendSplitKMerge",
761
- "source": { "shader": "attn-flash-decode-splitk-merge.wgsl.jinja", "inputs": { "layout": "\"bsh\"" } },
762
- "bindings": "splitMerge",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
763
  "dispatch": { "x": 1, "y": "attrs.num_heads", "z": "tokenCount" }
764
  }
765
  ]
766
  },
767
  {
768
  "id": "packed_slot",
 
769
  "requires": { "features": ["shader-f16"] },
770
- "when": ["pagedContractOk", "dispatchFits", "packedKv", "present.slotMappingT"],
771
  "passes": [
772
  {
773
  "id": "scatter",
774
  "name": "PagedAttention.ScatterKV",
775
  "shader": "paged-scatter-kv.wgsl.jinja",
776
- "bindings": "scatterPackedSlot",
777
  "dispatch": {
778
- "threads": "tokenCount * attrs.kv_num_heads * headSize",
779
- "workgroupSize": "tunables.SCATTER_WORKGROUP_SIZE"
 
780
  }
781
  },
782
  {
783
  "id": "main",
784
  "name": "PagedAttention.Attend",
785
  "shader": "paged-attention.wgsl.jinja",
786
- "bindings": "attention",
 
 
 
 
 
 
 
 
 
787
  "dispatch": { "x": "tokenCount", "y": "attrs.kv_num_heads" }
788
  }
789
  ]
 
2
  "domain": "com.microsoft",
3
  "name": "PagedAttention",
4
  "sinceVersion": 1,
5
+ "inputs": {
6
+ "queryT": { "onnx": "query", "dtype": "T", "rank": 2 },
7
+ "keyT": { "onnx": "key", "dtype": "T", "rank": 2, "optional": true },
8
+ "valueT": { "onnx": "value", "dtype": "T", "rank": 2, "optional": true },
9
+ "keyCacheT": { "onnx": "key_cache", "dtype": "T", "rank": 4 },
10
+ "valueCacheT": { "onnx": "value_cache", "dtype": "T", "rank": 4 },
11
+ "cumulativeSequenceLengthT": { "onnx": "cumulative_sequence_length", "dtype": "S", "rank": 1, "storage": "int32" },
12
+ "pastSeqlensT": { "onnx": "past_seqlens", "dtype": "S", "rank": 1, "storage": "int32" },
13
+ "blockTableT": { "onnx": "block_table", "dtype": "S", "rank": 2, "storage": "int32" },
14
+ "slotMappingT": { "onnx": "slot_mapping", "dtype": "S", "rank": 1, "optional": true, "storage": "int32" }
15
+ },
16
+ "outputs": {
17
+ "outputT": {
18
+ "onnx": "output",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  "dtype": "T",
20
  "rank": 2,
21
+ "shape": "[dim(shapes.queryT, 0), attrs.num_heads * headSize]"
 
22
  },
23
+ "keyCacheT": { "onnx": "key_cache", "dtype": "T", "rank": 4, "optional": true, "shape": "shapes.keyCacheT" },
24
+ "valueCacheT": { "onnx": "value_cache", "dtype": "T", "rank": 4, "optional": true, "shape": "shapes.valueCacheT" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  },
26
+ "attributes": { "is_causal": { "default": 1 }, "kv_num_heads": {}, "num_heads": {}, "scale": {} },
27
  "attributeConstraints": {
28
  "is_causal": { "values": [1] },
29
  "kv_num_heads": { "required": true },
30
  "num_heads": { "required": true }
31
  },
32
  "typeConstraints": { "T": ["float16"], "S": ["int32"] },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  "tunables": {
34
+ "WORKGROUP_SIZE": { "default": 64 },
35
+ "SCATTER_WORKGROUP_SIZE": { "default": 64 },
36
+ "MAX_SPLITS": { "default": 16 },
37
+ "SPLIT_MIN_KEYS": { "default": 128 },
38
+ "SPLIT_TARGET_WORKGROUPS": { "default": 1024 }
39
  },
40
  "derive": {
41
  "tokenCount": "dim(shapes.queryT, 0)",
 
57
  "packedKv": "not present.keyT and not present.valueT and dim(shapes.queryT, 1) == packedStride",
58
  "slotShapeOk": "dim(shapes.slotMappingT, 0) == tokenCount and ranks.slotMappingT == 1 if present.slotMappingT else true",
59
  "pagedContractOk": "cacheShapeOk and headLayoutOk and scheduleShapeOk and ioShapeOk and slotShapeOk",
60
+ "dispatchFits": "tokenCount <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535) and attrs.num_heads <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535) and tunables.WORKGROUP_SIZE <= device.limits.maxComputeInvocationsPerWorkgroup and tunables.SCATTER_WORKGROUP_SIZE <= device.limits.maxComputeInvocationsPerWorkgroup",
61
  "pagedBaseWorkgroups": "tokenCount * attrs.kv_num_heads",
62
+ "pagedAttnQueryFloats": "2 * headSize * qPerKv",
63
+ "pagedAttnLaneFloats": "3 * qPerKv + 1",
64
+ "pagedAttnWgBudget": "floor(device.limits.maxComputeWorkgroupStorageSize / 4 - pagedAttnQueryFloats) / pagedAttnLaneFloats",
65
+ "pagedAttnWg": "max(32, min(tunables.WORKGROUP_SIZE, pow2ceil(max(1, floor(pagedAttnWgBudget)) + 1) / 2))",
66
+ "pagedAttnStorageBytes": "(pagedAttnQueryFloats + pagedAttnLaneFloats * pagedAttnWg) * 4",
67
+ "pagedAttnStorageFits": "pagedAttnStorageBytes <= device.limits.maxComputeWorkgroupStorageSize and pagedAttnWg <= device.limits.maxComputeInvocationsPerWorkgroup and pagedAttnWg <= device.limits.maxComputeWorkgroupSizeX",
68
  "pagedKeyCeiling": "maxBlocks * blockSize",
69
  "pagedSplitWg": "min(256, max(32, pow2ceil(headVec)))",
70
+ "pagedSplitStorageBytes": "(2 * headSize * qPerKv + (3 * qPerKv + 1) * pagedSplitWg) * 4",
71
+ "pagedSplitStorageFits": "pagedSplitStorageBytes <= device.limits.maxComputeWorkgroupStorageSize",
72
  "pagedSplitCap": "max(1, min(tunables.MAX_SPLITS, pagedKeyCeiling / tunables.SPLIT_MIN_KEYS))",
73
  "numSplits": "max(1, min(pagedSplitCap, ceilDiv(tunables.SPLIT_TARGET_WORKGROUPS, max(1, pagedBaseWorkgroups))))",
74
  "pagedSplitScratchBytes": "tokenCount * attrs.num_heads * numSplits * headSize * 4",
75
  "pagedSplitStatsBytes": "2 * tokenCount * attrs.num_heads * numSplits * 4",
76
+ "pagedSplitFits": "numSplits >= 2 and cacheVec4Ok and headVec >= 1 and pagedSplitScratchBytes <= device.limits.maxStorageBufferBindingSize and pagedSplitScratchBytes <= device.limits.maxBufferSize and pagedSplitStatsBytes <= device.limits.maxStorageBufferBindingSize and pagedSplitStatsBytes <= device.limits.maxBufferSize and numSplits <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535) and headVec <= device.limits.maxComputeInvocationsPerWorkgroup and headVec <= device.limits.maxComputeWorkgroupSizeX and pagedSplitWg <= device.limits.maxComputeInvocationsPerWorkgroup and pagedSplitWg <= device.limits.maxComputeWorkgroupSizeX",
 
 
77
  "aScalar": "dtypes.T",
78
  "scalar": "dtypes.T",
79
  "numHeads": "attrs.num_heads",
80
  "kvNumHeads": "attrs.kv_num_heads",
 
 
 
 
 
 
81
  "packedQkv": "not present.keyT",
82
  "hasSlotMapping": "present.slotMappingT",
83
+ "attnWorkgroup": "pagedAttnWg",
 
 
84
  "cacheVec": "(\"vec4<f16>\" if dtypes.T == \"f16\" else \"vec4<f32>\") if cacheVec4Ok else dtypes.T",
 
 
85
  "headDimV4": "headVec",
86
  "headDim": "headSize",
87
  "qNumHeads": "attrs.num_heads",
 
89
  "hasBias": false,
90
  "outVec": "\"vec4<f16>\" if dtypes.T == \"f16\" else \"vec4<f32>\""
91
  },
92
+ "when": ["pagedContractOk", "dispatchFits"],
93
+ "bindings": {
94
+ "key": { "arg": "keyT", "buffer": "read-only-storage", "elementType": "$aScalar" },
95
+ "value": { "arg": "valueT", "buffer": "read-only-storage", "elementType": "$aScalar" },
96
+ "key_cache": { "arg": "keyCacheT", "buffer": "storage", "elementType": "$aScalar" },
97
+ "value_cache": { "arg": "valueCacheT", "buffer": "storage", "elementType": "$aScalar" },
98
+ "cumulative_sequence_length": {
99
+ "arg": "cumulativeSequenceLengthT",
100
+ "buffer": "read-only-storage",
101
+ "elementType": "i32"
102
+ },
103
+ "past_seqlens": { "arg": "pastSeqlensT", "buffer": "read-only-storage", "elementType": "i32" },
104
+ "block_table": { "arg": "blockTableT", "buffer": "read-only-storage", "elementType": "i32" },
105
+ "params": {
106
+ "buffer": "uniform",
107
+ "struct": [
108
+ { "name": "batchSize", "type": "u32", "value": "batchSize" },
109
+ { "name": "scatterCount", "type": "u32", "value": "tokenCount * attrs.kv_num_heads * headSize" }
110
+ ]
111
+ },
112
+ "slot_mapping": { "arg": "slotMappingT", "buffer": "read-only-storage", "elementType": "i32" },
113
+ "params_2": {
114
+ "name": "params",
115
+ "buffer": "uniform",
116
+ "struct": [{ "name": "scatterCount", "type": "u32", "value": "tokenCount * attrs.kv_num_heads * headSize" }]
117
+ },
118
+ "query": { "arg": "queryT", "buffer": "read-only-storage", "elementType": "$aScalar" },
119
+ "key_cache_2": {
120
+ "arg": "keyCacheT",
121
+ "name": "key_cache",
122
+ "buffer": "read-only-storage",
123
+ "elementType": "$cacheVec"
124
+ },
125
+ "value_cache_2": {
126
+ "arg": "valueCacheT",
127
+ "name": "value_cache",
128
+ "buffer": "read-only-storage",
129
+ "elementType": "$cacheVec"
130
+ },
131
+ "params_3": {
132
+ "name": "params",
133
+ "buffer": "uniform",
134
+ "struct": [
135
+ { "name": "batchSize", "type": "u32", "value": "batchSize" },
136
+ { "name": "scale", "type": "f32", "value": "attrs.scale if has(attrs, \"scale\") else 0" }
137
+ ]
138
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  },
140
  "variants": [
141
  {
142
  "id": "separate_derived_splitk",
 
143
  "priority": 10,
144
+ "when": ["pagedSplitFits", "pagedSplitStorageFits", "separateKv", "not present.slotMappingT"],
145
  "requires": { "features": ["shader-f16"] },
146
+ "derive": { "attnWorkgroup": "pagedSplitWg" },
 
147
  "intermediates": [
148
  { "id": "partialOut", "dtype": "float32", "shape": "[tokenCount * attrs.num_heads * numSplits * headSize]" },
149
  { "id": "partialStats", "dtype": "float32", "shape": "[2 * tokenCount * attrs.num_heads * numSplits]" }
 
153
  "id": "scatter",
154
  "name": "PagedAttention.ScatterKV",
155
  "shader": "paged-scatter-kv.wgsl.jinja",
156
+ "bindings": ["key", "value", "key_cache", "value_cache", "cumulative_sequence_length", "past_seqlens", "block_table", "params"],
157
  "dispatch": {
158
+ "x": "min(ceilDiv((tokenCount * attrs.kv_num_heads * headSize), (tunables.SCATTER_WORKGROUP_SIZE)), 65535)",
159
+ "y": "ceilDiv(ceilDiv((tokenCount * attrs.kv_num_heads * headSize), (tunables.SCATTER_WORKGROUP_SIZE)), 65535)",
160
+ "z": 1
161
  }
162
  },
163
  {
164
  "id": "split_attention",
165
  "name": "PagedAttention.AttendSplitK",
166
+ "shader": "paged-attention.wgsl.jinja",
167
+ "derive": { "splitK": true },
168
+ "bindings": [
169
+ "query",
170
+ "key_cache_2",
171
+ "value_cache_2",
172
+ "cumulative_sequence_length",
173
+ "past_seqlens",
174
+ "block_table",
175
+ { "scratch": "partialOut", "name": "partial_out", "elementType": "vec4<f32>" },
176
+ { "scratch": "partialStats", "name": "partial_stats", "elementType": "vec2<f32>" },
177
+ "params_3"
178
+ ],
179
  "dispatch": { "x": "tokenCount", "y": "attrs.kv_num_heads", "z": "numSplits" }
180
  },
181
  {
182
  "id": "merge",
183
  "name": "PagedAttention.AttendSplitKMerge",
184
+ "shader": "attn-flash-decode-splitk-merge.wgsl.jinja",
185
+ "derive": { "layout": "\"bsh\"" },
186
+ "bindings": [
187
+ {
188
+ "scratch": "partialOut",
189
+ "name": "partial_out",
190
+ "buffer": "read-only-storage",
191
+ "elementType": "vec4<f32>"
192
+ },
193
+ {
194
+ "scratch": "partialStats",
195
+ "name": "partial_stats",
196
+ "buffer": "read-only-storage",
197
+ "elementType": "vec2<f32>"
198
+ },
199
+ { "arg": "outputT", "name": "output", "elementType": "$outVec" }
200
+ ],
201
  "dispatch": { "x": 1, "y": "attrs.num_heads", "z": "tokenCount" }
202
  }
203
  ]
204
  },
205
  {
206
  "id": "separate_derived",
207
+ "when": ["pagedAttnStorageFits", "separateKv", "not present.slotMappingT"],
208
  "requires": { "features": ["shader-f16"] },
 
209
  "passes": [
210
  {
211
  "id": "scatter",
212
  "name": "PagedAttention.ScatterKV",
213
  "shader": "paged-scatter-kv.wgsl.jinja",
214
+ "bindings": ["key", "value", "key_cache", "value_cache", "cumulative_sequence_length", "past_seqlens", "block_table", "params"],
215
  "dispatch": {
216
+ "x": "min(ceilDiv((tokenCount * attrs.kv_num_heads * headSize), (tunables.SCATTER_WORKGROUP_SIZE)), 65535)",
217
+ "y": "ceilDiv(ceilDiv((tokenCount * attrs.kv_num_heads * headSize), (tunables.SCATTER_WORKGROUP_SIZE)), 65535)",
218
+ "z": 1
219
  }
220
  },
221
  {
222
  "id": "main",
223
  "name": "PagedAttention.Attend",
224
  "shader": "paged-attention.wgsl.jinja",
225
+ "bindings": [
226
+ "query",
227
+ "key_cache_2",
228
+ "value_cache_2",
229
+ "cumulative_sequence_length",
230
+ "past_seqlens",
231
+ "block_table",
232
+ { "arg": "outputT", "name": "output", "elementType": "$aScalar" },
233
+ "params_3"
234
+ ],
235
  "dispatch": { "x": "tokenCount", "y": "attrs.kv_num_heads" }
236
  }
237
  ]
238
  },
239
  {
240
  "id": "separate_slot_splitk",
 
241
  "priority": 10,
242
+ "when": ["pagedSplitFits", "pagedSplitStorageFits", "separateKv", "present.slotMappingT"],
243
  "requires": { "features": ["shader-f16"] },
244
+ "derive": { "attnWorkgroup": "pagedSplitWg" },
 
245
  "intermediates": [
246
  { "id": "partialOut", "dtype": "float32", "shape": "[tokenCount * attrs.num_heads * numSplits * headSize]" },
247
  { "id": "partialStats", "dtype": "float32", "shape": "[2 * tokenCount * attrs.num_heads * numSplits]" }
 
251
  "id": "scatter",
252
  "name": "PagedAttention.ScatterKV",
253
  "shader": "paged-scatter-kv.wgsl.jinja",
254
+ "bindings": ["key", "value", "key_cache", "value_cache", "slot_mapping", "params_2"],
255
  "dispatch": {
256
+ "x": "min(ceilDiv((tokenCount * attrs.kv_num_heads * headSize), (tunables.SCATTER_WORKGROUP_SIZE)), 65535)",
257
+ "y": "ceilDiv(ceilDiv((tokenCount * attrs.kv_num_heads * headSize), (tunables.SCATTER_WORKGROUP_SIZE)), 65535)",
258
+ "z": 1
259
  }
260
  },
261
  {
262
  "id": "split_attention",
263
  "name": "PagedAttention.AttendSplitK",
264
+ "shader": "paged-attention.wgsl.jinja",
265
+ "derive": { "splitK": true },
266
+ "bindings": [
267
+ "query",
268
+ "key_cache_2",
269
+ "value_cache_2",
270
+ "cumulative_sequence_length",
271
+ "past_seqlens",
272
+ "block_table",
273
+ { "scratch": "partialOut", "name": "partial_out", "elementType": "vec4<f32>" },
274
+ { "scratch": "partialStats", "name": "partial_stats", "elementType": "vec2<f32>" },
275
+ "params_3"
276
+ ],
277
  "dispatch": { "x": "tokenCount", "y": "attrs.kv_num_heads", "z": "numSplits" }
278
  },
279
  {
280
  "id": "merge",
281
  "name": "PagedAttention.AttendSplitKMerge",
282
+ "shader": "attn-flash-decode-splitk-merge.wgsl.jinja",
283
+ "derive": { "layout": "\"bsh\"" },
284
+ "bindings": [
285
+ {
286
+ "scratch": "partialOut",
287
+ "name": "partial_out",
288
+ "buffer": "read-only-storage",
289
+ "elementType": "vec4<f32>"
290
+ },
291
+ {
292
+ "scratch": "partialStats",
293
+ "name": "partial_stats",
294
+ "buffer": "read-only-storage",
295
+ "elementType": "vec2<f32>"
296
+ },
297
+ { "arg": "outputT", "name": "output", "elementType": "$outVec" }
298
+ ],
299
  "dispatch": { "x": 1, "y": "attrs.num_heads", "z": "tokenCount" }
300
  }
301
  ]
302
  },
303
  {
304
  "id": "separate_slot",
305
+ "when": ["pagedAttnStorageFits", "separateKv", "present.slotMappingT"],
306
  "requires": { "features": ["shader-f16"] },
 
307
  "passes": [
308
  {
309
  "id": "scatter",
310
  "name": "PagedAttention.ScatterKV",
311
  "shader": "paged-scatter-kv.wgsl.jinja",
312
+ "bindings": ["key", "value", "key_cache", "value_cache", "slot_mapping", "params_2"],
313
  "dispatch": {
314
+ "x": "min(ceilDiv((tokenCount * attrs.kv_num_heads * headSize), (tunables.SCATTER_WORKGROUP_SIZE)), 65535)",
315
+ "y": "ceilDiv(ceilDiv((tokenCount * attrs.kv_num_heads * headSize), (tunables.SCATTER_WORKGROUP_SIZE)), 65535)",
316
+ "z": 1
317
  }
318
  },
319
  {
320
  "id": "main",
321
  "name": "PagedAttention.Attend",
322
  "shader": "paged-attention.wgsl.jinja",
323
+ "bindings": [
324
+ "query",
325
+ "key_cache_2",
326
+ "value_cache_2",
327
+ "cumulative_sequence_length",
328
+ "past_seqlens",
329
+ "block_table",
330
+ { "arg": "outputT", "name": "output", "elementType": "$aScalar" },
331
+ "params_3"
332
+ ],
333
  "dispatch": { "x": "tokenCount", "y": "attrs.kv_num_heads" }
334
  }
335
  ]
336
  },
337
  {
338
  "id": "packed_derived_splitk",
 
339
  "priority": 10,
340
+ "when": ["pagedSplitFits", "pagedSplitStorageFits", "packedKv", "not present.slotMappingT"],
341
  "requires": { "features": ["shader-f16"] },
342
+ "derive": { "attnWorkgroup": "pagedSplitWg" },
 
343
  "intermediates": [
344
  { "id": "partialOut", "dtype": "float32", "shape": "[tokenCount * attrs.num_heads * numSplits * headSize]" },
345
  { "id": "partialStats", "dtype": "float32", "shape": "[2 * tokenCount * attrs.num_heads * numSplits]" }
 
349
  "id": "scatter",
350
  "name": "PagedAttention.ScatterKV",
351
  "shader": "paged-scatter-kv.wgsl.jinja",
352
+ "bindings": ["query", "key_cache", "value_cache", "cumulative_sequence_length", "past_seqlens", "block_table", "params"],
353
  "dispatch": {
354
+ "x": "min(ceilDiv((tokenCount * attrs.kv_num_heads * headSize), (tunables.SCATTER_WORKGROUP_SIZE)), 65535)",
355
+ "y": "ceilDiv(ceilDiv((tokenCount * attrs.kv_num_heads * headSize), (tunables.SCATTER_WORKGROUP_SIZE)), 65535)",
356
+ "z": 1
357
  }
358
  },
359
  {
360
  "id": "split_attention",
361
  "name": "PagedAttention.AttendSplitK",
362
+ "shader": "paged-attention.wgsl.jinja",
363
+ "derive": { "splitK": true },
364
+ "bindings": [
365
+ "query",
366
+ "key_cache_2",
367
+ "value_cache_2",
368
+ "cumulative_sequence_length",
369
+ "past_seqlens",
370
+ "block_table",
371
+ { "scratch": "partialOut", "name": "partial_out", "elementType": "vec4<f32>" },
372
+ { "scratch": "partialStats", "name": "partial_stats", "elementType": "vec2<f32>" },
373
+ "params_3"
374
+ ],
375
  "dispatch": { "x": "tokenCount", "y": "attrs.kv_num_heads", "z": "numSplits" }
376
  },
377
  {
378
  "id": "merge",
379
  "name": "PagedAttention.AttendSplitKMerge",
380
+ "shader": "attn-flash-decode-splitk-merge.wgsl.jinja",
381
+ "derive": { "layout": "\"bsh\"" },
382
+ "bindings": [
383
+ {
384
+ "scratch": "partialOut",
385
+ "name": "partial_out",
386
+ "buffer": "read-only-storage",
387
+ "elementType": "vec4<f32>"
388
+ },
389
+ {
390
+ "scratch": "partialStats",
391
+ "name": "partial_stats",
392
+ "buffer": "read-only-storage",
393
+ "elementType": "vec2<f32>"
394
+ },
395
+ { "arg": "outputT", "name": "output", "elementType": "$outVec" }
396
+ ],
397
  "dispatch": { "x": 1, "y": "attrs.num_heads", "z": "tokenCount" }
398
  }
399
  ]
400
  },
401
  {
402
  "id": "packed_derived",
403
+ "when": ["pagedAttnStorageFits", "packedKv", "not present.slotMappingT"],
404
  "requires": { "features": ["shader-f16"] },
 
405
  "passes": [
406
  {
407
  "id": "scatter",
408
  "name": "PagedAttention.ScatterKV",
409
  "shader": "paged-scatter-kv.wgsl.jinja",
410
+ "bindings": ["query", "key_cache", "value_cache", "cumulative_sequence_length", "past_seqlens", "block_table", "params"],
411
  "dispatch": {
412
+ "x": "min(ceilDiv((tokenCount * attrs.kv_num_heads * headSize), (tunables.SCATTER_WORKGROUP_SIZE)), 65535)",
413
+ "y": "ceilDiv(ceilDiv((tokenCount * attrs.kv_num_heads * headSize), (tunables.SCATTER_WORKGROUP_SIZE)), 65535)",
414
+ "z": 1
415
  }
416
  },
417
  {
418
  "id": "main",
419
  "name": "PagedAttention.Attend",
420
  "shader": "paged-attention.wgsl.jinja",
421
+ "bindings": [
422
+ "query",
423
+ "key_cache_2",
424
+ "value_cache_2",
425
+ "cumulative_sequence_length",
426
+ "past_seqlens",
427
+ "block_table",
428
+ { "arg": "outputT", "name": "output", "elementType": "$aScalar" },
429
+ "params_3"
430
+ ],
431
  "dispatch": { "x": "tokenCount", "y": "attrs.kv_num_heads" }
432
  }
433
  ]
434
  },
435
  {
436
  "id": "packed_slot_splitk",
 
437
  "priority": 10,
438
+ "when": ["pagedSplitFits", "pagedSplitStorageFits", "packedKv", "present.slotMappingT"],
439
  "requires": { "features": ["shader-f16"] },
440
+ "derive": { "attnWorkgroup": "pagedSplitWg" },
 
441
  "intermediates": [
442
  { "id": "partialOut", "dtype": "float32", "shape": "[tokenCount * attrs.num_heads * numSplits * headSize]" },
443
  { "id": "partialStats", "dtype": "float32", "shape": "[2 * tokenCount * attrs.num_heads * numSplits]" }
 
447
  "id": "scatter",
448
  "name": "PagedAttention.ScatterKV",
449
  "shader": "paged-scatter-kv.wgsl.jinja",
450
+ "bindings": ["query", "key_cache", "value_cache", "slot_mapping", "params_2"],
451
  "dispatch": {
452
+ "x": "min(ceilDiv((tokenCount * attrs.kv_num_heads * headSize), (tunables.SCATTER_WORKGROUP_SIZE)), 65535)",
453
+ "y": "ceilDiv(ceilDiv((tokenCount * attrs.kv_num_heads * headSize), (tunables.SCATTER_WORKGROUP_SIZE)), 65535)",
454
+ "z": 1
455
  }
456
  },
457
  {
458
  "id": "split_attention",
459
  "name": "PagedAttention.AttendSplitK",
460
+ "shader": "paged-attention.wgsl.jinja",
461
+ "derive": { "splitK": true },
462
+ "bindings": [
463
+ "query",
464
+ "key_cache_2",
465
+ "value_cache_2",
466
+ "cumulative_sequence_length",
467
+ "past_seqlens",
468
+ "block_table",
469
+ { "scratch": "partialOut", "name": "partial_out", "elementType": "vec4<f32>" },
470
+ { "scratch": "partialStats", "name": "partial_stats", "elementType": "vec2<f32>" },
471
+ "params_3"
472
+ ],
473
  "dispatch": { "x": "tokenCount", "y": "attrs.kv_num_heads", "z": "numSplits" }
474
  },
475
  {
476
  "id": "merge",
477
  "name": "PagedAttention.AttendSplitKMerge",
478
+ "shader": "attn-flash-decode-splitk-merge.wgsl.jinja",
479
+ "derive": { "layout": "\"bsh\"" },
480
+ "bindings": [
481
+ {
482
+ "scratch": "partialOut",
483
+ "name": "partial_out",
484
+ "buffer": "read-only-storage",
485
+ "elementType": "vec4<f32>"
486
+ },
487
+ {
488
+ "scratch": "partialStats",
489
+ "name": "partial_stats",
490
+ "buffer": "read-only-storage",
491
+ "elementType": "vec2<f32>"
492
+ },
493
+ { "arg": "outputT", "name": "output", "elementType": "$outVec" }
494
+ ],
495
  "dispatch": { "x": 1, "y": "attrs.num_heads", "z": "tokenCount" }
496
  }
497
  ]
498
  },
499
  {
500
  "id": "packed_slot",
501
+ "when": ["pagedAttnStorageFits", "packedKv", "present.slotMappingT"],
502
  "requires": { "features": ["shader-f16"] },
 
503
  "passes": [
504
  {
505
  "id": "scatter",
506
  "name": "PagedAttention.ScatterKV",
507
  "shader": "paged-scatter-kv.wgsl.jinja",
508
+ "bindings": ["query", "key_cache", "value_cache", "slot_mapping", "params_2"],
509
  "dispatch": {
510
+ "x": "min(ceilDiv((tokenCount * attrs.kv_num_heads * headSize), (tunables.SCATTER_WORKGROUP_SIZE)), 65535)",
511
+ "y": "ceilDiv(ceilDiv((tokenCount * attrs.kv_num_heads * headSize), (tunables.SCATTER_WORKGROUP_SIZE)), 65535)",
512
+ "z": 1
513
  }
514
  },
515
  {
516
  "id": "main",
517
  "name": "PagedAttention.Attend",
518
  "shader": "paged-attention.wgsl.jinja",
519
+ "bindings": [
520
+ "query",
521
+ "key_cache_2",
522
+ "value_cache_2",
523
+ "cumulative_sequence_length",
524
+ "past_seqlens",
525
+ "block_table",
526
+ { "arg": "outputT", "name": "output", "elementType": "$aScalar" },
527
+ "params_3"
528
+ ],
529
  "dispatch": { "x": "tokenCount", "y": "attrs.kv_num_heads" }
530
  }
531
  ]
build/webgpu/metadata.json CHANGED
@@ -1,20 +1,32 @@
1
  {
2
  "name": "com.microsoft.PagedAttention",
3
- "id": "_com_microsoft_pagedattention_webgpu_da91370",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
- "attn-flash-decode-splitk-merge.wgsl.jinja": "8RI/yP6118n/rLRo0OG9NJ+kZ2xdmJyj093Tw8C07nc=",
11
- "bench.json": "UwS/SwZvyxZNimhsfEODD8GUpSN1saYbJFO+2AJpBFE=",
12
- "manifest.json": "4OMoAQRJcGm7t8TLViM098p15V0i788gvT/vpkfbt1c=",
13
- "paged-attention.wgsl.jinja": "LcAWM3XNSdLIc67tQSUJ6v9Sa4QcRIetF3cnNwkdu9c=",
14
- "paged-scatter-kv.wgsl.jinja": "WiuV+h3AOSDU0ENfvZ5iequjvL9TFR3h1gCQgGswPRA=",
15
- "test.json": "AZ89NKbsfzyghJ0lIz57f3tm0r9H62tEPhY1uP5yWEM="
16
  }
17
  },
18
- "provenance": { "kernel": { "sha": "2e7068faf55e7f43df740015f6d1ee49391a41c5", "dirty": false } },
19
- "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/com.microsoft.PagedAttention" }
 
 
 
 
 
 
 
 
 
 
 
 
20
  }
 
1
  {
2
  "name": "com.microsoft.PagedAttention",
3
+ "id": "_com_microsoft_pagedattention_webgpu_a50bcf6",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
+ "attn-flash-decode-splitk-merge.wgsl.jinja": "uOgPgm5HioxH0ha2Z7Q0b/8qESsCmwKAS/198ogYX3I=",
11
+ "bench.json": "n1VW3Kl/whbn6CsIdfs0MHNrvUTskejhRkd5jqAAGmk=",
12
+ "manifest.json": "T7kLnpUD+K08/2E51GlZEK8v9YsGXliBIVnku1LRgRM=",
13
+ "paged-attention.wgsl.jinja": "NS4JduPGQ8x2YxgIap9fKuggGtyqse0u503A7rvzSM4=",
14
+ "paged-scatter-kv.wgsl.jinja": "EEb8eRJICdQOiQPsThu7rGm97W8iYFFH5zL3vArnQ8Q=",
15
+ "test.json": "UtJ6CP4M4fFlZm0lZdIrcX+8AxB/5gIC88fXZb6Iw5Q="
16
  }
17
  },
18
+ "provenance": { "kernel": { "sha": "91d990483a174128daf7673f3f37a7c890493ae1", "dirty": false } },
19
+ "webgpu": {
20
+ "manifestSpec": "2.0",
21
+ "variants": {
22
+ "separate_derived_splitk": ["attn-flash-decode-splitk-merge.wgsl.jinja", "paged-attention.wgsl.jinja", "paged-scatter-kv.wgsl.jinja"],
23
+ "separate_derived": ["paged-attention.wgsl.jinja", "paged-scatter-kv.wgsl.jinja"],
24
+ "separate_slot_splitk": ["attn-flash-decode-splitk-merge.wgsl.jinja", "paged-attention.wgsl.jinja", "paged-scatter-kv.wgsl.jinja"],
25
+ "separate_slot": ["paged-attention.wgsl.jinja", "paged-scatter-kv.wgsl.jinja"],
26
+ "packed_derived_splitk": ["attn-flash-decode-splitk-merge.wgsl.jinja", "paged-attention.wgsl.jinja", "paged-scatter-kv.wgsl.jinja"],
27
+ "packed_derived": ["paged-attention.wgsl.jinja", "paged-scatter-kv.wgsl.jinja"],
28
+ "packed_slot_splitk": ["attn-flash-decode-splitk-merge.wgsl.jinja", "paged-attention.wgsl.jinja", "paged-scatter-kv.wgsl.jinja"],
29
+ "packed_slot": ["paged-attention.wgsl.jinja", "paged-scatter-kv.wgsl.jinja"]
30
+ }
31
+ }
32
  }
build/webgpu/paged-attention.wgsl.jinja CHANGED
@@ -1,24 +1,17 @@
1
- {% set splitK = source.splitK is defined and source.splitK %}
2
  {% set keyLo = "splitStart" if splitK else "0u" %}
3
  {% set keyHi = "splitEnd" if splitK else "key_bound" %}
4
  enable f16;
5
  {{ env.wgsl.resourceDeclarations }}
6
 
7
  // com.microsoft.PagedAttention, attention pass.
8
- // One workgroup per (packed token, KV head). The workgroup sweeps that token's
9
- // KV history in tiles of WG keys with one key per thread, merging each tile into
10
- // a running online softmax, so the score row is never materialized.
11
- // A workgroup serves every query head that shares its KV head, reusing each
12
- // cache read across the group. Q_PER_KV == 1 reduces to ordinary attention.
13
- // The history is not contiguous: key j of sequence b lives in physical block
14
- // block_table[b, j / BLOCK_SIZE] at offset j % BLOCK_SIZE, which is the whole
15
- // point of a paged cache. Each tile resolves that indirection once per key and
16
- // parks the resulting row bases in workgroup memory, so the value accumulation
17
- // below re-reads a base instead of re-walking the block table for every head
18
- // dimension.
19
- // Masking is causal by construction rather than by comparison: a token's key
20
- // bound is its own absolute position plus one, so keys beyond it are never
21
- // visited.
22
  {% if splitK %}
23
  // Split-K partitions the key axis into NUM_SPLITS contiguous ranges. Each range
24
  // writes its unnormalized online state (max, denominator, and weighted V sum),
@@ -87,20 +80,22 @@ var<workgroup> key_rows: array<u32, WG>;
87
  // calls before their shared partial storage is reused.
88
  {% set combineSubgroups = combineSubgroups is defined and combineSubgroups %}
89
  {% if combineSubgroups %}
90
- // Per-subgroup partials are published into a deterministic slot: the subgroup's
91
- // ordinal index within the workgroup (lidx / sgSize). The online (m, d) merge
92
- // is not float-associative, so thread 0 must fold partials in a fixed order.
93
- // Subgroups partition a workgroup into contiguous ordinal ranges on supported
94
- // backends, so the ordinal slot is unique per subgroup and every slot in
95
- // [0, subgroupCount) is written (each subgroup elects one leader).
96
- // Sized for the worst case of one partial per invocation.
 
97
  var<workgroup> partialM: array<f32, WG>;
98
  var<workgroup> partialD: array<f32, WG>;
 
99
  var<workgroup> combinedMD: vec2<f32>;
100
 
101
  // When the whole workgroup is one subgroup the subgroup reduce already covers
102
- // it (no barriers, no shared state); otherwise subgroup leaders publish
103
- // partials through shared memory and thread 0 folds them in ordinal order.
104
  fn combine_partials(m: f32, d: f32, lidx: u32, sgSize: u32) -> vec2<f32> {
105
  let sgM = subgroupMax(m);
106
  // A lane with no elements contributes d == 0 (exact identity). A +inf
@@ -111,31 +106,24 @@ fn combine_partials(m: f32, d: f32, lidx: u32, sgSize: u32) -> vec2<f32> {
111
  if (sgSize == WG) {
112
  return vec2<f32>(sgM, sgD);
113
  }
114
- let subgroupCount = (WG + sgSize - 1u) / sgSize;
115
- // Pre-seed every fold slot with the (max, denom) identity. The fold below reads a
116
- // fixed subgroupCount slots in ordinal order (for determinism), but a slot whose
117
- // subgroup elects no leader this call — e.g. a fully out-of-window key tile in the
118
- // flash-attention loop that re-uses this shared memory each iteration — would
119
- // otherwise read stale shared memory. Identity makes such a slot a no-op.
120
- // (max identity = -FLT_MAX, denom identity = 0.)
121
- if (lidx < subgroupCount) {
122
- partialM[lidx] = -FLT_MAX;
123
- partialD[lidx] = 0.0;
124
- }
125
- workgroupBarrier();
126
  if (subgroupElect()) {
127
- let slot = lidx / sgSize;
128
- partialM[slot] = sgM;
129
- partialD[slot] = sgD;
130
  }
131
  workgroupBarrier();
132
  if (lidx == 0u) {
133
  var accM = -FLT_MAX;
134
  var accD = 0.0;
135
- for (var i = 0u; i < subgroupCount; i = i + 1u) {
136
- let mNew = max(accM, partialM[i]);
137
- accD = accD * exp_shift(accM, mNew) + partialD[i] * exp_shift(partialM[i], mNew);
138
- accM = mNew;
 
 
 
 
 
139
  }
140
  combinedMD = vec2<f32>(accM, accD);
141
  }
@@ -150,9 +138,8 @@ var<workgroup> partialM: array<f32, {{ mdExtent }}>;
150
  var<workgroup> partialD: array<f32, {{ mdExtent }}>;
151
  {% if mdStreamed %}
152
 
153
- // In-place fold of {{ mdStreams }} streams. The caller stores its per-thread
154
- // partials into partialM/partialD first and reads the merged pair of stream s
155
- // from slot s * WG afterwards.
156
  fn combine_partials_streams(lidx: u32) {
157
  workgroupBarrier();
158
  var stride = WG / 2u;
 
1
+ {% set splitK = splitK is defined and splitK %}
2
  {% set keyLo = "splitStart" if splitK else "0u" %}
3
  {% set keyHi = "splitEnd" if splitK else "key_bound" %}
4
  enable f16;
5
  {{ env.wgsl.resourceDeclarations }}
6
 
7
  // com.microsoft.PagedAttention, attention pass.
8
+ // One workgroup per packed token and KV head sweeps the token's history in
9
+ // WG-key tiles and merges them into an online softmax without materializing the
10
+ // score row. Query heads sharing that KV head reuse each resolved cache row.
11
+ // Key j of sequence b resides in block_table[b, j / BLOCK_SIZE] at offset
12
+ // j % BLOCK_SIZE; each tile resolves this mapping once per key and stores the
13
+ // row bases in workgroup memory. The exclusive key bound is the token's absolute
14
+ // position plus one, so later keys are never visited.
 
 
 
 
 
 
 
15
  {% if splitK %}
16
  // Split-K partitions the key axis into NUM_SPLITS contiguous ranges. Each range
17
  // writes its unnormalized online state (max, denominator, and weighted V sum),
 
80
  // calls before their shared partial storage is reused.
81
  {% set combineSubgroups = combineSubgroups is defined and combineSubgroups %}
82
  {% if combineSubgroups %}
83
+ // Cross-subgroup merge that assumes nothing about which invocations share a
84
+ // subgroup or how many subgroups there are: each subgroup's elected lane
85
+ // publishes the subgroup pair in the slot at its OWN invocation index and sets
86
+ // that index's bit in a workgroup bitmask; thread 0 then folds exactly the
87
+ // published slots, in ascending index order (the online (m, d) merge is not
88
+ // float-associative, so the order is fixed), and clears the mask for the next
89
+ // call as it reads it. Workgroup memory starts zeroed, so the mask needs no
90
+ // setup. Same three collectives as a single-subgroup reduce, two barriers.
91
  var<workgroup> partialM: array<f32, WG>;
92
  var<workgroup> partialD: array<f32, WG>;
93
+ var<workgroup> leaderMask: array<atomic<u32>, (WG + 31u) / 32u>;
94
  var<workgroup> combinedMD: vec2<f32>;
95
 
96
  // When the whole workgroup is one subgroup the subgroup reduce already covers
97
+ // it (no barriers, no shared state). `subgroup_size` is the size of the current
98
+ // subgroup and uniform, so the test is exact and may guard the barriers below.
99
  fn combine_partials(m: f32, d: f32, lidx: u32, sgSize: u32) -> vec2<f32> {
100
  let sgM = subgroupMax(m);
101
  // A lane with no elements contributes d == 0 (exact identity). A +inf
 
106
  if (sgSize == WG) {
107
  return vec2<f32>(sgM, sgD);
108
  }
 
 
 
 
 
 
 
 
 
 
 
 
109
  if (subgroupElect()) {
110
+ partialM[lidx] = sgM;
111
+ partialD[lidx] = sgD;
112
+ atomicOr(&leaderMask[lidx / 32u], 1u << (lidx % 32u));
113
  }
114
  workgroupBarrier();
115
  if (lidx == 0u) {
116
  var accM = -FLT_MAX;
117
  var accD = 0.0;
118
+ for (var w = 0u; w < (WG + 31u) / 32u; w = w + 1u) {
119
+ var bits = atomicExchange(&leaderMask[w], 0u);
120
+ while (bits != 0u) {
121
+ let slot = w * 32u + firstTrailingBit(bits);
122
+ bits = bits & (bits - 1u);
123
+ let mNew = max(accM, partialM[slot]);
124
+ accD = accD * exp_shift(accM, mNew) + partialD[slot] * exp_shift(partialM[slot], mNew);
125
+ accM = mNew;
126
+ }
127
  }
128
  combinedMD = vec2<f32>(accM, accD);
129
  }
 
138
  var<workgroup> partialD: array<f32, {{ mdExtent }}>;
139
  {% if mdStreamed %}
140
 
141
+ // In-place fold of {{ mdStreams }} streams. Input partials occupy
142
+ // partialM/partialD; stream s returns its merged pair in slot s * WG.
 
143
  fn combine_partials_streams(lidx: u32) {
144
  workgroupBarrier();
145
  var stride = WG / 2u;
build/webgpu/paged-scatter-kv.wgsl.jinja CHANGED
@@ -1,30 +1,30 @@
1
  {% macro flat_index_2d(name="i", bound="params.count", guardInline=false, note="dispatch-limit") %}
2
  {% if note == "dispatch-limit" %}
3
- // 2D-folded flat index: gid.y carries the high bits past the
4
- // maxComputeWorkgroupsPerDimension dispatch limit (outputs > 16.7M elements).
5
  {% elif note == "limit" %}
6
- // 2D-folded flat index: gid.y carries the high bits past the
7
- // maxComputeWorkgroupsPerDimension limit.
8
  {% elif note == "device-axis" %}
9
- // The flat dispatch is folded across x/y at the device's per-axis workgroup
10
- // limit; gid.y carries the high portion of the output index.
11
  {% elif note == "vec4-limit" %}
12
- // 2D-folded flat vec4 index: gid.y carries the high bits past the
13
- // maxComputeWorkgroupsPerDimension limit (the dispatch caps x and spills into y).
14
  {% elif note == "element-limit" %}
15
  // 2D-folded flat element index: gid.y carries the high bits past the
16
- // maxComputeWorkgroupsPerDimension limit.
17
  {% elif note == "dispatch" %}
18
- // 2D-folded flat index: gid.y carries the high bits past the
19
- // maxComputeWorkgroupsPerDimension dispatch limit.
20
  {% endif %}
21
  {% if bound == "" %}
22
- let {{ name }} = gid.x + gid.y * nwg.x * {{ tunables.WORKGROUP_SIZE }}u;
23
  {%- elif guardInline %}
24
- let {{ name }} = gid.x + gid.y * nwg.x * {{ tunables.WORKGROUP_SIZE }}u;
25
  if ({{ name }} >= {{ bound }}) { return; }
26
  {%- else %}
27
- let {{ name }} = gid.x + gid.y * nwg.x * {{ tunables.WORKGROUP_SIZE }}u;
28
  if ({{ name }} >= {{ bound }}) {
29
  return;
30
  }
@@ -80,16 +80,15 @@ fn paged_row_base(block_id: u32, slot_in_block: u32, kv_head: u32) -> u32 {
80
  {{ paged_sequence_index(withLookup=not hasSlotMapping) }}
81
 
82
  @compute @workgroup_size({{ tunables.SCATTER_WORKGROUP_SIZE }})
83
- fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) nwg: vec3<u32>) {
84
  {{ flat_index_2d(bound="params.scatterCount", note="element-limit") }}
85
  let dim = i % HEAD_DIM;
86
  let kv_head = (i / HEAD_DIM) % KV_HEADS;
87
  let token = i / (KV_HEADS * HEAD_DIM);
88
 
89
  {% if hasSlotMapping %}
90
- // slot_mapping gives the flat cache slot directly, and -1 means "do not store
91
- // this token" -- the scheduler uses it to suppress writes for a prefix-cache
92
- // hit or a rejected speculative token. block_table still defines the read path.
93
  let slot = slot_mapping[token];
94
  if (slot < 0) {
95
  return;
 
1
  {% macro flat_index_2d(name="i", bound="params.count", guardInline=false, note="dispatch-limit") %}
2
  {% if note == "dispatch-limit" %}
3
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
4
+ // per-axis workgroup fold width (outputs > 16.7M elements).
5
  {% elif note == "limit" %}
6
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
7
+ // per-axis workgroup fold width.
8
  {% elif note == "device-axis" %}
9
+ // The flat dispatch is folded across x/y at a fixed per-axis workgroup
10
+ // width; gid.y carries the high portion of the output index.
11
  {% elif note == "vec4-limit" %}
12
+ // 2D-folded flat vec4 index: gid.y carries the high bits past the dispatch's
13
+ // per-axis workgroup fold width (the dispatch caps x and spills into y).
14
  {% elif note == "element-limit" %}
15
  // 2D-folded flat element index: gid.y carries the high bits past the
16
+ // dispatch's per-axis workgroup fold width.
17
  {% elif note == "dispatch" %}
18
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
19
+ // per-axis workgroup fold width.
20
  {% endif %}
21
  {% if bound == "" %}
22
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ tunables.WORKGROUP_SIZE }}u;
23
  {%- elif guardInline %}
24
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ tunables.WORKGROUP_SIZE }}u;
25
  if ({{ name }} >= {{ bound }}) { return; }
26
  {%- else %}
27
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ tunables.WORKGROUP_SIZE }}u;
28
  if ({{ name }} >= {{ bound }}) {
29
  return;
30
  }
 
80
  {{ paged_sequence_index(withLookup=not hasSlotMapping) }}
81
 
82
  @compute @workgroup_size({{ tunables.SCATTER_WORKGROUP_SIZE }})
83
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
84
  {{ flat_index_2d(bound="params.scatterCount", note="element-limit") }}
85
  let dim = i % HEAD_DIM;
86
  let kv_head = (i / HEAD_DIM) % KV_HEADS;
87
  let token = i / (KV_HEADS * HEAD_DIM);
88
 
89
  {% if hasSlotMapping %}
90
+ // `slot_mapping` gives the flat cache slot directly. A negative slot suppresses
91
+ // this token's cache write; `block_table` still defines the read path.
 
92
  let slot = slot_mapping[token];
93
  if (slot < 0) {
94
  return;
build/webgpu/test.json CHANGED
@@ -1,5 +1,4 @@
1
  {
2
- "op": "com.microsoft.PagedAttention",
3
  "cases": [
4
  {
5
  "name": "two_sequences_gqa",
@@ -45,7 +44,9 @@
45
  },
46
  {
47
  "name": "two_sequences_packed_qkv",
48
- "provenance": { "notes": "Same schedule with Q, K and V packed on one query row." },
 
 
49
  "attrs": { "num_heads": 4, "kv_num_heads": 2 },
50
  "inputs": {
51
  "queryT": {
@@ -76,7 +77,7 @@
76
  {
77
  "name": "two_sequences_key_cache_output",
78
  "provenance": {
79
- "notes": "Requests only the updated key-cache alias; the paired schema value-cache output remains internal."
80
  },
81
  "attrs": { "num_heads": 4, "kv_num_heads": 2 },
82
  "inputs": {
@@ -206,7 +207,7 @@
206
  {
207
  "name": "slot_mapping_skips_token",
208
  "provenance": {
209
- "notes": "A slot of -1 suppresses that token's cache write, which a scheduler uses for a prefix-cache hit or a rejected speculative token. The cache slot keeps its prior contents and attention still reads it."
210
  },
211
  "attrs": { "num_heads": 4, "kv_num_heads": 2 },
212
  "inputs": {
@@ -520,7 +521,7 @@
520
  {
521
  "name": "pinned_paged_history",
522
  "provenance": {
523
- "notes": "Expected values computed by an independent implementation written from the ONNX Runtime schema text alone, with every tensor value rounded to its declared float16 dtype before evaluation, so this case checks the trusted reference as well as the kernels. The block table [2, 0, 1] is deliberately not the identity, so a kernel that ignored it would read the wrong blocks; the two new tokens land in blocks 0 and 1 respectively."
524
  },
525
  "attrs": { "num_heads": 2, "kv_num_heads": 1 },
526
  "inputs": {
@@ -685,7 +686,7 @@
685
  {
686
  "name": "splitk_slot_mapping",
687
  "provenance": {
688
- "notes": "The split-K schedule with an explicit slot mapping instead of a derived write slot: same 201-key history over a 256-key block table, so the two split ranges both run and the scatter pass takes the slot-mapping arm."
689
  },
690
  "attrs": { "num_heads": 2, "kv_num_heads": 1 },
691
  "inputs": {
@@ -766,7 +767,7 @@
766
  {
767
  "name": "splitk_packed_qkv_slot_mapping",
768
  "provenance": {
769
- "notes": "The split-K schedule with both a packed query row and an explicit slot mapping, the last of the four scatter/query combinations the family covers."
770
  },
771
  "attrs": { "num_heads": 2, "kv_num_heads": 1 },
772
  "inputs": {
@@ -798,6 +799,49 @@
798
  "slotMappingT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [208] } }
799
  },
800
  "outputs": { "outputT": { "dtype": "float16", "shape": [1, 16], "tolerance": 0.00001, "relTolerance": 0.0001 } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
801
  }
802
  ]
803
  }
 
1
  {
 
2
  "cases": [
3
  {
4
  "name": "two_sequences_gqa",
 
44
  },
45
  {
46
  "name": "two_sequences_packed_qkv",
47
+ "provenance": {
48
+ "notes": "Q, K, and V are packed into one query row while preserving the two-sequence paged schedule."
49
+ },
50
  "attrs": { "num_heads": 4, "kv_num_heads": 2 },
51
  "inputs": {
52
  "queryT": {
 
77
  {
78
  "name": "two_sequences_key_cache_output",
79
  "provenance": {
80
+ "notes": "Only the updated key-cache alias is requested; the optional value-cache result is omitted."
81
  },
82
  "attrs": { "num_heads": 4, "kv_num_heads": 2 },
83
  "inputs": {
 
207
  {
208
  "name": "slot_mapping_skips_token",
209
  "provenance": {
210
+ "notes": "A negative slot suppresses that token's cache write. The cache slot retains its prior contents and remains available to attention."
211
  },
212
  "attrs": { "num_heads": 4, "kv_num_heads": 2 },
213
  "inputs": {
 
521
  {
522
  "name": "pinned_paged_history",
523
  "provenance": {
524
+ "notes": "Expected values are independently derived from the ONNX Runtime schema after rounding every tensor to its declared float16 dtype. A nonidentity block table [2, 0, 1] places the two new tokens in physical blocks 0 and 1."
525
  },
526
  "attrs": { "num_heads": 2, "kv_num_heads": 1 },
527
  "inputs": {
 
686
  {
687
  "name": "splitk_slot_mapping",
688
  "provenance": {
689
+ "notes": "A 201-key history over a 256-key table creates two split-K ranges, while explicit slot mapping selects the corresponding cache-scatter path."
690
  },
691
  "attrs": { "num_heads": 2, "kv_num_heads": 1 },
692
  "inputs": {
 
767
  {
768
  "name": "splitk_packed_qkv_slot_mapping",
769
  "provenance": {
770
+ "notes": "A packed query row and explicit slot mapping exercise the split-K scatter path with both optional layout modes active."
771
  },
772
  "attrs": { "num_heads": 2, "kv_num_heads": 1 },
773
  "inputs": {
 
799
  "slotMappingT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [208] } }
800
  },
801
  "outputs": { "outputT": { "dtype": "float16", "shape": [1, 16], "tolerance": 0.00001, "relTolerance": 0.0001 } }
802
+ },
803
+ {
804
+ "name": "mqa_32_query_heads_per_kv_head_workgroup_storage",
805
+ "provenance": {
806
+ "notes": "A 32:1 multi-query decode with head size 64 stages 24,832 bytes of query, output, and probability data per workgroup. The case therefore declares a 32 KiB workgroup-storage requirement and must be rejected as inapplicable at the WebGPU 16 KiB minimum."
807
+ },
808
+ "requires": { "limits": { "maxComputeWorkgroupStorageSize": 32768 } },
809
+ "attrs": { "num_heads": 32, "kv_num_heads": 1 },
810
+ "inputs": {
811
+ "queryT": {
812
+ "dtype": "float16",
813
+ "shape": [1, 2048],
814
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31, "scale": 0.9 }
815
+ },
816
+ "keyT": {
817
+ "dtype": "float16",
818
+ "shape": [1, 64],
819
+ "data": { "kind": "fillFloat32", "sinStep": 0.23, "cosStep": 0.13, "scale": 0.8 }
820
+ },
821
+ "valueT": {
822
+ "dtype": "float16",
823
+ "shape": [1, 64],
824
+ "data": { "kind": "fillFloat32", "sinStep": 0.41, "cosStep": 0.07, "scale": 0.7 }
825
+ },
826
+ "keyCacheT": {
827
+ "dtype": "float16",
828
+ "shape": [8, 16, 1, 64],
829
+ "data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.37, "scale": 0.6 }
830
+ },
831
+ "valueCacheT": {
832
+ "dtype": "float16",
833
+ "shape": [8, 16, 1, 64],
834
+ "data": { "kind": "fillFloat32", "sinStep": 0.29, "cosStep": 0.19, "scale": 0.5 }
835
+ },
836
+ "cumulativeSequenceLengthT": { "dtype": "int32", "shape": [2], "data": { "kind": "values", "values": [0, 1] } },
837
+ "pastSeqlensT": { "dtype": "int32", "shape": [1], "data": { "kind": "values", "values": [40] } },
838
+ "blockTableT": {
839
+ "dtype": "int32",
840
+ "shape": [1, 8],
841
+ "data": { "kind": "values", "values": [0, 1, 2, 3, 4, 5, 6, 7] }
842
+ }
843
+ },
844
+ "outputs": { "outputT": { "dtype": "float16", "shape": [1, 2048], "tolerance": 0.00001, "relTolerance": 0.0001 } }
845
  }
846
  ]
847
  }