Xenova HF Staff commited on
Commit
b88d0d9
·
verified ·
1 Parent(s): 3d17c9b

sync 2e7068faf55e

Browse files
README.md CHANGED
@@ -1,3 +1,102 @@
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.MatMulNBitsQkv
10
+
11
+ `com.microsoft` · ONNX Runtime contrib operator · contrib since_version 1
12
+
13
+ ## Description
14
+
15
+ Fuses RMS normalization with three block-quantized attention projections: `A_norm = RMSNorm(A + skip, norm_scale)` (or without `skip`), followed by Q, K, and V projections. The optional fourth output returns `A + skip`. Only 4-bit weights with `block_size = 32` are supported; projection biases, bfloat16, and non-default `accuracy_level` values are not implemented.
16
+
17
+ See the [ONNX Runtime `MatMulNBitsQkv` contrib-operator spec](https://github.com/microsoft/onnxruntime/blob/main/docs/ContribOperators.md#com.microsoft.MatMulNBitsQkv) for the reference semantics.
18
+
19
+ ## Inputs
20
+
21
+ | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
22
+ | --- | --- | --- | --- | --- | --- | --- |
23
+ | `A` | `aT` | `T1` | — | — | Shared activation of rank 2 `(M, K)` or rank 3 `(batch, sequence, K)`; only the last axis is the reduction axis. | required |
24
+ | `skip` | `skipT` | `T1` | — | — | Residual added to A before the normalization, with A's shape. | optional |
25
+ | `norm_scale` | `normScaleT` | `T1` | `1` | — | Simplified-layer-normalization (RMS) gain of shape `[K]`. | required |
26
+ | `q_B` | `qBT` | `T2` | `3` | — | Bit-packed uint8 Q weights of shape `(Nq, k_blocks, blob_size)`. | required |
27
+ | `q_scales` | `qScalesT` | `T1` | `2` | — | Per-block Q scales of shape `(Nq, k_blocks)`. Quantization is symmetric: there is no zero-point input, so codes are offset by the midpoint `2^(bits - 1)`. | required |
28
+ | `k_B` | `kBT` | `T2` | `3` | — | Bit-packed K weights of shape `(Nkv, k_blocks, blob_size)`. | required |
29
+ | `k_scales` | `kScalesT` | `T1` | `2` | — | Per-block K scales of shape `(Nkv, k_blocks)`. | required |
30
+ | `v_B` | `vBT` | `T2` | `3` | — | Bit-packed V weights of shape `(Nkv, k_blocks, blob_size)`. | required |
31
+ | `v_scales` | `vScalesT` | `T1` | `2` | — | Per-block V scales of shape `(Nkv, k_blocks)`. | required |
32
+
33
+ ## Outputs
34
+
35
+ | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
36
+ | --- | --- | --- | --- | --- | --- | --- |
37
+ | `Q` | `qT` | `T1` | same as `A` | derived; see description | Query projection: A's leading axes with a trailing Nq. | required |
38
+ | `K` | `kT` | `T1` | same as `A` | derived; see description | Key projection: A's leading axes with a trailing Nkv. | required |
39
+ | `V` | `vT` | `T1` | same as `A` | derived; see description | Value projection: A's leading axes with a trailing Nkv. | required |
40
+ | `input_skip_bias_sum` | `residualT` | `T1` | same as `A` | same as `A` | The residual sum A + skip, with A's shape. Requires the skip input. | optional |
41
+
42
+ ## Attributes
43
+
44
+ Attributes and default values (overridable per request):
45
+
46
+ | Attribute | Default | Description |
47
+ | --- | --- | --- |
48
+ | `accuracy_level` | `0` | Minimum internal accuracy level, following MatMulNBits semantics; this implementation supports the standard default 0. |
49
+ | `bits` | `4` | Bit width used to quantize all three weight matrices; only 4 is supported. |
50
+ | `epsilon` | `9.999999974752427e-7` | Epsilon used by the simplified layer-normalization reduction. |
51
+ | `K` | — | Input feature dimension shared by the normalized input and all projection weights. |
52
+ | `Nq` | — | Output feature dimension of the Q projection. |
53
+ | `Nkv` | — | Output feature dimension shared by the K and V projections. |
54
+ | `block_size` | — | Size of each quantization block along K; only 32 is supported. |
55
+
56
+ ## Type constraints
57
+
58
+ | Variable | Allowed dtypes |
59
+ | --- | --- |
60
+ | `T1` | `float32`, `float16` |
61
+ | `T2` | `uint8` |
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
+ - [`matmul-nbits-fused-rms-norm.wgsl.jinja`](build/webgpu/matmul-nbits-fused-rms-norm.wgsl.jinja)
70
+ - [`qkv-projection.wgsl.jinja`](build/webgpu/qkv-projection.wgsl.jinja)
71
+
72
+ ## Use with `@huggingface/kernels`
73
+
74
+ The loader derives every required output's shape and logical dtype from the manifest contract and this call.
75
+ It then allocates the result tensors automatically.
76
+
77
+ The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
78
+
79
+ Replace each `*Data` placeholder with a typed array containing the corresponding input data.
80
+
81
+ ```js
82
+ import { getKernel } from "@huggingface/kernels";
83
+
84
+ const kernel = await getKernel("webgpu-kernels/com.microsoft.MatMulNBitsQkv", { version: 1 });
85
+ const { qT, kT, vT } = await kernel({
86
+ aT: { data: aTData, shape: [2, 32] },
87
+ normScaleT: { data: normScaleTData, shape: [32] },
88
+ qBT: { data: qBTData, shape: [5, 1, 16] },
89
+ qScalesT: { data: qScalesTData, shape: [5, 1] },
90
+ kBT: { data: kBTData, shape: [3, 1, 16] },
91
+ kScalesT: { data: kScalesTData, shape: [3, 1] },
92
+ vBT: { data: vBTData, shape: [3, 1, 16] },
93
+ vScalesT: { data: vScalesTData, shape: [3, 1] },
94
+ }, {
95
+ attrs: {
96
+ K: 32,
97
+ Nq: 5,
98
+ Nkv: 3,
99
+ block_size: 32,
100
+ },
101
+ });
102
+ ```
build/webgpu/bench.json ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "op": "com.microsoft.MatMulNBitsQkv",
3
+ "tunableSpace": { "TILE_N": [4, 8, 16], "LANES": [4, 8, 16] },
4
+ "cases": [
5
+ {
6
+ "name": "qkv-q4-decode-k2048-nq2048-nkv512",
7
+ "preset": "smoke",
8
+ "vars": { "dtype": "float32" },
9
+ "attrs": { "K": 2048, "Nq": 2048, "Nkv": 512, "block_size": 32 },
10
+ "inputs": {
11
+ "aT": { "shape": [1, 2048], "dtype": "float32", "dist": "normal", "seed": 9101, "scale": 1 },
12
+ "normScaleT": { "shape": [2048], "dtype": "float32", "dist": "normal", "seed": 9102, "scale": 1 },
13
+ "qBT": { "shape": [2048, 64, 16], "dtype": "uint8", "dist": "uniform", "seed": 9103, "scale": 255 },
14
+ "qScalesT": { "shape": [2048, 64], "dtype": "float32", "dist": "normal", "seed": 9104, "scale": 0.05 },
15
+ "kBT": { "shape": [512, 64, 16], "dtype": "uint8", "dist": "uniform", "seed": 9105, "scale": 255 },
16
+ "kScalesT": { "shape": [512, 64], "dtype": "float32", "dist": "normal", "seed": 9106, "scale": 0.05 },
17
+ "vBT": { "shape": [512, 64, 16], "dtype": "uint8", "dist": "uniform", "seed": 9107, "scale": 255 },
18
+ "vScalesT": { "shape": [512, 64], "dtype": "float32", "dist": "normal", "seed": 9108, "scale": 0.05 }
19
+ },
20
+ "outputs": {
21
+ "qT": { "shape": [1, 2048], "dtype": "float32" },
22
+ "kT": { "shape": [1, 512], "dtype": "float32" },
23
+ "vT": { "shape": [1, 512], "dtype": "float32" }
24
+ },
25
+ "bench": { "metrics": [{ "type": "bandwidth", "value": "(2048 + 512 + 512) * 64 * 16 * 4" }] }
26
+ },
27
+ {
28
+ "name": "qkv-q4-llama-decode-k4096-nq4096-nkv4096",
29
+ "preset": "model",
30
+ "provenance": {
31
+ "notes": "Llama class defaults (hidden_size 4096, num_attention_heads 32, no GQA so num_key_value_heads matches) at a decode step. Bytes are GPU storage, not the packed blob: this backend gives a uint8 tensor one byte per u32 element, so a 4-bit code costs 2 bytes of traffic and the projection moves four times its on-disk size. Counting the blob understated the traffic 4x and reported this kernel at a tenth of the bandwidth rail when it runs at about half."
32
+ },
33
+ "vars": { "dtype": "float32", "tokens": 1, "K": 4096, "Nq": 4096, "Nkv": 4096 },
34
+ "attrs": { "K": 4096, "Nq": 4096, "Nkv": 4096, "block_size": 32 },
35
+ "inputs": {
36
+ "aT": { "shape": [1, 4096], "dtype": "float32", "dist": "normal", "seed": 7100, "scale": 0.5 },
37
+ "normScaleT": {
38
+ "shape": [4096],
39
+ "dtype": "float32",
40
+ "dist": "uniform",
41
+ "seed": 7101,
42
+ "scale": 0.1,
43
+ "offset": 1
44
+ },
45
+ "qBT": { "shape": [4096, 128, 16], "dtype": "uint8", "dist": "uniform", "seed": 7102, "min": 0, "max": 255 },
46
+ "qScalesT": {
47
+ "shape": [4096, 128],
48
+ "dtype": "float32",
49
+ "dist": "uniform",
50
+ "seed": 7103,
51
+ "min": 0.005,
52
+ "max": 0.05
53
+ },
54
+ "kBT": { "shape": [4096, 128, 16], "dtype": "uint8", "dist": "uniform", "seed": 7104, "min": 0, "max": 255 },
55
+ "kScalesT": {
56
+ "shape": [4096, 128],
57
+ "dtype": "float32",
58
+ "dist": "uniform",
59
+ "seed": 7105,
60
+ "min": 0.005,
61
+ "max": 0.05
62
+ },
63
+ "vBT": { "shape": [4096, 128, 16], "dtype": "uint8", "dist": "uniform", "seed": 7106, "min": 0, "max": 255 },
64
+ "vScalesT": {
65
+ "shape": [4096, 128],
66
+ "dtype": "float32",
67
+ "dist": "uniform",
68
+ "seed": 7107,
69
+ "min": 0.005,
70
+ "max": 0.05
71
+ }
72
+ },
73
+ "outputs": {
74
+ "qT": { "shape": [1, 4096], "dtype": "float32" },
75
+ "kT": { "shape": [1, 4096], "dtype": "float32" },
76
+ "vT": { "shape": [1, 4096], "dtype": "float32" }
77
+ },
78
+ "bench": { "metrics": [{ "type": "bandwidth", "value": "(args.Nq + 2 * args.Nkv) * args.K * 2" }] }
79
+ },
80
+ {
81
+ "name": "qkv-q4-mistral-decode-k4096-nq4096-nkv1024",
82
+ "preset": "model",
83
+ "provenance": {
84
+ "notes": "Mistral class defaults (hidden_size 4096, 32 query heads, 8 key/value heads, head_dim 128) -- the GQA case, where K and V projections are a quarter of Q. Bytes are GPU storage, not the packed blob: this backend gives a uint8 tensor one byte per u32 element, so a 4-bit code costs 2 bytes of traffic and the projection moves four times its on-disk size. Counting the blob understated the traffic 4x and reported this kernel at a tenth of the bandwidth rail when it runs at about half."
85
+ },
86
+ "vars": { "dtype": "float32", "tokens": 1, "K": 4096, "Nq": 4096, "Nkv": 1024 },
87
+ "attrs": { "K": 4096, "Nq": 4096, "Nkv": 1024, "block_size": 32 },
88
+ "inputs": {
89
+ "aT": { "shape": [1, 4096], "dtype": "float32", "dist": "normal", "seed": 7200, "scale": 0.5 },
90
+ "normScaleT": {
91
+ "shape": [4096],
92
+ "dtype": "float32",
93
+ "dist": "uniform",
94
+ "seed": 7201,
95
+ "scale": 0.1,
96
+ "offset": 1
97
+ },
98
+ "qBT": { "shape": [4096, 128, 16], "dtype": "uint8", "dist": "uniform", "seed": 7202, "min": 0, "max": 255 },
99
+ "qScalesT": {
100
+ "shape": [4096, 128],
101
+ "dtype": "float32",
102
+ "dist": "uniform",
103
+ "seed": 7203,
104
+ "min": 0.005,
105
+ "max": 0.05
106
+ },
107
+ "kBT": { "shape": [1024, 128, 16], "dtype": "uint8", "dist": "uniform", "seed": 7204, "min": 0, "max": 255 },
108
+ "kScalesT": {
109
+ "shape": [1024, 128],
110
+ "dtype": "float32",
111
+ "dist": "uniform",
112
+ "seed": 7205,
113
+ "min": 0.005,
114
+ "max": 0.05
115
+ },
116
+ "vBT": { "shape": [1024, 128, 16], "dtype": "uint8", "dist": "uniform", "seed": 7206, "min": 0, "max": 255 },
117
+ "vScalesT": {
118
+ "shape": [1024, 128],
119
+ "dtype": "float32",
120
+ "dist": "uniform",
121
+ "seed": 7207,
122
+ "min": 0.005,
123
+ "max": 0.05
124
+ }
125
+ },
126
+ "outputs": {
127
+ "qT": { "shape": [1, 4096], "dtype": "float32" },
128
+ "kT": { "shape": [1, 1024], "dtype": "float32" },
129
+ "vT": { "shape": [1, 1024], "dtype": "float32" }
130
+ },
131
+ "bench": { "metrics": [{ "type": "bandwidth", "value": "(args.Nq + 2 * args.Nkv) * args.K * 2" }] }
132
+ },
133
+ {
134
+ "name": "qkv-q4-gemma3-decode-k2304-nq2048-nkv1024",
135
+ "preset": "model",
136
+ "provenance": {
137
+ "notes": "Gemma3 class defaults (hidden_size 2304, 8 query heads, 4 key/value heads, head_dim 256); the projection is wider than hidden, so K does not equal Nq. Bytes are GPU storage, not the packed blob: this backend gives a uint8 tensor one byte per u32 element, so a 4-bit code costs 2 bytes of traffic and the projection moves four times its on-disk size. Counting the blob understated the traffic 4x and reported this kernel at a tenth of the bandwidth rail when it runs at about half."
138
+ },
139
+ "vars": { "dtype": "float32", "tokens": 1, "K": 2304, "Nq": 2048, "Nkv": 1024 },
140
+ "attrs": { "K": 2304, "Nq": 2048, "Nkv": 1024, "block_size": 32 },
141
+ "inputs": {
142
+ "aT": { "shape": [1, 2304], "dtype": "float32", "dist": "normal", "seed": 7300, "scale": 0.5 },
143
+ "normScaleT": {
144
+ "shape": [2304],
145
+ "dtype": "float32",
146
+ "dist": "uniform",
147
+ "seed": 7301,
148
+ "scale": 0.1,
149
+ "offset": 1
150
+ },
151
+ "qBT": { "shape": [2048, 72, 16], "dtype": "uint8", "dist": "uniform", "seed": 7302, "min": 0, "max": 255 },
152
+ "qScalesT": {
153
+ "shape": [2048, 72],
154
+ "dtype": "float32",
155
+ "dist": "uniform",
156
+ "seed": 7303,
157
+ "min": 0.005,
158
+ "max": 0.05
159
+ },
160
+ "kBT": { "shape": [1024, 72, 16], "dtype": "uint8", "dist": "uniform", "seed": 7304, "min": 0, "max": 255 },
161
+ "kScalesT": {
162
+ "shape": [1024, 72],
163
+ "dtype": "float32",
164
+ "dist": "uniform",
165
+ "seed": 7305,
166
+ "min": 0.005,
167
+ "max": 0.05
168
+ },
169
+ "vBT": { "shape": [1024, 72, 16], "dtype": "uint8", "dist": "uniform", "seed": 7306, "min": 0, "max": 255 },
170
+ "vScalesT": {
171
+ "shape": [1024, 72],
172
+ "dtype": "float32",
173
+ "dist": "uniform",
174
+ "seed": 7307,
175
+ "min": 0.005,
176
+ "max": 0.05
177
+ }
178
+ },
179
+ "outputs": {
180
+ "qT": { "shape": [1, 2048], "dtype": "float32" },
181
+ "kT": { "shape": [1, 1024], "dtype": "float32" },
182
+ "vT": { "shape": [1, 1024], "dtype": "float32" }
183
+ },
184
+ "bench": { "metrics": [{ "type": "bandwidth", "value": "(args.Nq + 2 * args.Nkv) * args.K * 2" }] }
185
+ },
186
+ {
187
+ "name": "qkv-q4-phi3-decode-k3072-nq3072-nkv3072",
188
+ "preset": "model",
189
+ "provenance": {
190
+ "notes": "Phi-3 class defaults (hidden_size 3072, num_attention_heads 32, no GQA). Bytes are GPU storage, not the packed blob: this backend gives a uint8 tensor one byte per u32 element, so a 4-bit code costs 2 bytes of traffic and the projection moves four times its on-disk size. Counting the blob understated the traffic 4x and reported this kernel at a tenth of the bandwidth rail when it runs at about half."
191
+ },
192
+ "vars": { "dtype": "float32", "tokens": 1, "K": 3072, "Nq": 3072, "Nkv": 3072 },
193
+ "attrs": { "K": 3072, "Nq": 3072, "Nkv": 3072, "block_size": 32 },
194
+ "inputs": {
195
+ "aT": { "shape": [1, 3072], "dtype": "float32", "dist": "normal", "seed": 7400, "scale": 0.5 },
196
+ "normScaleT": {
197
+ "shape": [3072],
198
+ "dtype": "float32",
199
+ "dist": "uniform",
200
+ "seed": 7401,
201
+ "scale": 0.1,
202
+ "offset": 1
203
+ },
204
+ "qBT": { "shape": [3072, 96, 16], "dtype": "uint8", "dist": "uniform", "seed": 7402, "min": 0, "max": 255 },
205
+ "qScalesT": {
206
+ "shape": [3072, 96],
207
+ "dtype": "float32",
208
+ "dist": "uniform",
209
+ "seed": 7403,
210
+ "min": 0.005,
211
+ "max": 0.05
212
+ },
213
+ "kBT": { "shape": [3072, 96, 16], "dtype": "uint8", "dist": "uniform", "seed": 7404, "min": 0, "max": 255 },
214
+ "kScalesT": {
215
+ "shape": [3072, 96],
216
+ "dtype": "float32",
217
+ "dist": "uniform",
218
+ "seed": 7405,
219
+ "min": 0.005,
220
+ "max": 0.05
221
+ },
222
+ "vBT": { "shape": [3072, 96, 16], "dtype": "uint8", "dist": "uniform", "seed": 7406, "min": 0, "max": 255 },
223
+ "vScalesT": {
224
+ "shape": [3072, 96],
225
+ "dtype": "float32",
226
+ "dist": "uniform",
227
+ "seed": 7407,
228
+ "min": 0.005,
229
+ "max": 0.05
230
+ }
231
+ },
232
+ "outputs": {
233
+ "qT": { "shape": [1, 3072], "dtype": "float32" },
234
+ "kT": { "shape": [1, 3072], "dtype": "float32" },
235
+ "vT": { "shape": [1, 3072], "dtype": "float32" }
236
+ },
237
+ "bench": { "metrics": [{ "type": "bandwidth", "value": "(args.Nq + 2 * args.Nkv) * args.K * 2" }] }
238
+ },
239
+ {
240
+ "name": "qkv-q4-llama-spec8-k4096-nq4096-nkv4096",
241
+ "preset": "model",
242
+ "provenance": {
243
+ "notes": "Llama class defaults with 8 rows, the shape speculative decoding verifies in one pass. Bytes are GPU storage, not the packed blob: this backend gives a uint8 tensor one byte per u32 element, so a 4-bit code costs 2 bytes of traffic and the projection moves four times its on-disk size. Counting the blob understated the traffic 4x and reported this kernel at a tenth of the bandwidth rail when it runs at about half."
244
+ },
245
+ "vars": { "dtype": "float32", "tokens": 8, "K": 4096, "Nq": 4096, "Nkv": 4096 },
246
+ "attrs": { "K": 4096, "Nq": 4096, "Nkv": 4096, "block_size": 32 },
247
+ "inputs": {
248
+ "aT": { "shape": [8, 4096], "dtype": "float32", "dist": "normal", "seed": 7500, "scale": 0.5 },
249
+ "normScaleT": {
250
+ "shape": [4096],
251
+ "dtype": "float32",
252
+ "dist": "uniform",
253
+ "seed": 7501,
254
+ "scale": 0.1,
255
+ "offset": 1
256
+ },
257
+ "qBT": { "shape": [4096, 128, 16], "dtype": "uint8", "dist": "uniform", "seed": 7502, "min": 0, "max": 255 },
258
+ "qScalesT": {
259
+ "shape": [4096, 128],
260
+ "dtype": "float32",
261
+ "dist": "uniform",
262
+ "seed": 7503,
263
+ "min": 0.005,
264
+ "max": 0.05
265
+ },
266
+ "kBT": { "shape": [4096, 128, 16], "dtype": "uint8", "dist": "uniform", "seed": 7504, "min": 0, "max": 255 },
267
+ "kScalesT": {
268
+ "shape": [4096, 128],
269
+ "dtype": "float32",
270
+ "dist": "uniform",
271
+ "seed": 7505,
272
+ "min": 0.005,
273
+ "max": 0.05
274
+ },
275
+ "vBT": { "shape": [4096, 128, 16], "dtype": "uint8", "dist": "uniform", "seed": 7506, "min": 0, "max": 255 },
276
+ "vScalesT": {
277
+ "shape": [4096, 128],
278
+ "dtype": "float32",
279
+ "dist": "uniform",
280
+ "seed": 7507,
281
+ "min": 0.005,
282
+ "max": 0.05
283
+ }
284
+ },
285
+ "outputs": {
286
+ "qT": { "shape": [8, 4096], "dtype": "float32" },
287
+ "kT": { "shape": [8, 4096], "dtype": "float32" },
288
+ "vT": { "shape": [8, 4096], "dtype": "float32" }
289
+ },
290
+ "bench": { "metrics": [{ "type": "bandwidth", "value": "(args.Nq + 2 * args.Nkv) * args.K * 2" }] }
291
+ }
292
+ ]
293
+ }
build/webgpu/manifest.json ADDED
@@ -0,0 +1,535 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "domain": "com.microsoft",
3
+ "name": "MatMulNBitsQkv",
4
+ "sinceVersion": 1,
5
+ "description": "Fuses RMS normalization with three block-quantized attention projections: `A_norm = RMSNorm(A + skip, norm_scale)` (or without `skip`), followed by Q, K, and V projections. The optional fourth output returns `A + skip`. Only 4-bit weights with `block_size = 32` are supported; projection biases, bfloat16, and non-default `accuracy_level` values are not implemented.",
6
+ "inputs": [
7
+ {
8
+ "role": "A",
9
+ "dtype": "T1",
10
+ "description": "Shared activation of rank 2 `(M, K)` or rank 3 `(batch, sequence, K)`; only the last axis is the reduction axis."
11
+ },
12
+ {
13
+ "role": "skip",
14
+ "dtype": "T1",
15
+ "optional": true,
16
+ "description": "Residual added to A before the normalization, with A's shape."
17
+ },
18
+ {
19
+ "role": "norm_scale",
20
+ "dtype": "T1",
21
+ "rank": 1,
22
+ "description": "Simplified-layer-normalization (RMS) gain of shape `[K]`."
23
+ },
24
+ {
25
+ "role": "q_B",
26
+ "dtype": "T2",
27
+ "rank": 3,
28
+ "description": "Bit-packed uint8 Q weights of shape `(Nq, k_blocks, blob_size)`."
29
+ },
30
+ {
31
+ "role": "q_scales",
32
+ "dtype": "T1",
33
+ "rank": 2,
34
+ "description": "Per-block Q scales of shape `(Nq, k_blocks)`. Quantization is symmetric: there is no zero-point input, so codes are offset by the midpoint `2^(bits - 1)`."
35
+ },
36
+ {
37
+ "role": "k_B",
38
+ "dtype": "T2",
39
+ "rank": 3,
40
+ "description": "Bit-packed K weights of shape `(Nkv, k_blocks, blob_size)`."
41
+ },
42
+ { "role": "k_scales", "dtype": "T1", "rank": 2, "description": "Per-block K scales of shape `(Nkv, k_blocks)`." },
43
+ {
44
+ "role": "v_B",
45
+ "dtype": "T2",
46
+ "rank": 3,
47
+ "description": "Bit-packed V weights of shape `(Nkv, k_blocks, blob_size)`."
48
+ },
49
+ { "role": "v_scales", "dtype": "T1", "rank": 2, "description": "Per-block V scales of shape `(Nkv, k_blocks)`." }
50
+ ],
51
+ "outputs": [
52
+ {
53
+ "role": "Q",
54
+ "dtype": "T1",
55
+ "rank": "ranks.aT",
56
+ "shape": "shapes.aT[:-1] + [attrs.Nq]",
57
+ "description": "Query projection: A's leading axes with a trailing Nq."
58
+ },
59
+ {
60
+ "role": "K",
61
+ "dtype": "T1",
62
+ "rank": "ranks.aT",
63
+ "shape": "shapes.aT[:-1] + [attrs.Nkv]",
64
+ "description": "Key projection: A's leading axes with a trailing Nkv."
65
+ },
66
+ {
67
+ "role": "V",
68
+ "dtype": "T1",
69
+ "rank": "ranks.aT",
70
+ "shape": "shapes.aT[:-1] + [attrs.Nkv]",
71
+ "description": "Value projection: A's leading axes with a trailing Nkv."
72
+ },
73
+ {
74
+ "role": "input_skip_bias_sum",
75
+ "dtype": "T1",
76
+ "rank": "ranks.aT",
77
+ "optional": true,
78
+ "shape": "shapes.aT",
79
+ "description": "The residual sum A + skip, with A's shape. Requires the skip input."
80
+ }
81
+ ],
82
+ "attributes": { "accuracy_level": 0, "bits": 4, "epsilon": 9.999999974752427e-7 },
83
+ "attributeDescriptions": {
84
+ "K": "Input feature dimension shared by the normalized input and all projection weights.",
85
+ "Nq": "Output feature dimension of the Q projection.",
86
+ "Nkv": "Output feature dimension shared by the K and V projections.",
87
+ "accuracy_level": "Minimum internal accuracy level, following MatMulNBits semantics; this implementation supports the standard default 0.",
88
+ "bits": "Bit width used to quantize all three weight matrices; only 4 is supported.",
89
+ "block_size": "Size of each quantization block along K; only 32 is supported.",
90
+ "epsilon": "Epsilon used by the simplified layer-normalization reduction."
91
+ },
92
+ "attributeConstraints": {
93
+ "K": { "required": true },
94
+ "Nq": { "required": true },
95
+ "Nkv": { "required": true },
96
+ "accuracy_level": { "values": [0] },
97
+ "bits": { "values": [4] },
98
+ "block_size": { "required": true, "values": [32] }
99
+ },
100
+ "typeConstraints": { "T1": ["float32", "float16"], "T2": ["uint8"] },
101
+ "args": {
102
+ "aT": { "kind": "tensor", "semantic": "A", "role": "input" },
103
+ "skipT": { "kind": "tensor", "semantic": "skip", "role": "input", "required": false },
104
+ "normScaleT": { "kind": "tensor", "semantic": "norm_scale", "role": "weights" },
105
+ "qBT": { "kind": "tensor", "semantic": "q_B", "role": "weights" },
106
+ "qScalesT": { "kind": "tensor", "semantic": "q_scales", "role": "weights" },
107
+ "kBT": { "kind": "tensor", "semantic": "k_B", "role": "weights" },
108
+ "kScalesT": { "kind": "tensor", "semantic": "k_scales", "role": "weights" },
109
+ "vBT": { "kind": "tensor", "semantic": "v_B", "role": "weights" },
110
+ "vScalesT": { "kind": "tensor", "semantic": "v_scales", "role": "weights" },
111
+ "qT": { "kind": "tensor", "semantic": "Q", "role": "output" },
112
+ "kT": { "kind": "tensor", "semantic": "K", "role": "output" },
113
+ "vT": { "kind": "tensor", "semantic": "V", "role": "output" },
114
+ "residualT": { "kind": "tensor", "semantic": "input_skip_bias_sum", "role": "output", "required": false }
115
+ },
116
+ "tunables": { "TILE_N": 8, "LANES": 8, "NORM_WORKGROUP_SIZE": 128, "ROW_TILE": 8 },
117
+ "derive": {
118
+ "aRows": "numel(shapes.aT) / max(1, attrs.K)",
119
+ "rowTile": "1 if aRows <= 1 else min(aRows, tunables.ROW_TILE)",
120
+ "rowGroups": "ceilDiv(aRows, rowTile)",
121
+ "kBlocks": "dim(shapes.qBT, 1)",
122
+ "blobSize": "dim(shapes.qBT, 2)",
123
+ "codesPerByte": "8 / attrs.bits",
124
+ "codeMask": "3 if attrs.bits == 2 else (15 if attrs.bits == 4 else 255)",
125
+ "pairSharesWord": "codesPerByte >= 2",
126
+ "epsilonValue": "attrs.epsilon",
127
+ "weightShapeOk": "ranks.qBT == 3 and ranks.kBT == 3 and ranks.vBT == 3 and dim(shapes.qBT, 0) == attrs.Nq and dim(shapes.kBT, 0) == attrs.Nkv and dim(shapes.vBT, 0) == attrs.Nkv and dim(shapes.kBT, 1) == kBlocks and dim(shapes.vBT, 1) == kBlocks and dim(shapes.kBT, 2) == blobSize and dim(shapes.vBT, 2) == blobSize and kBlocks == ceilDiv(attrs.K, attrs.block_size) and blobSize * 8 == attrs.block_size * attrs.bits",
128
+ "scaleShapeOk": "ranks.qScalesT == 2 and ranks.kScalesT == 2 and ranks.vScalesT == 2 and dim(shapes.qScalesT, 0) == attrs.Nq and dim(shapes.qScalesT, 1) == kBlocks and dim(shapes.kScalesT, 0) == attrs.Nkv and dim(shapes.kScalesT, 1) == kBlocks and dim(shapes.vScalesT, 0) == attrs.Nkv and dim(shapes.vScalesT, 1) == kBlocks",
129
+ "ioShapeOk": "(ranks.aT == 2 or ranks.aT == 3) and dim(shapes.aT, ranks.aT - 1) == attrs.K and ranks.qT == ranks.aT and ranks.kT == ranks.aT and ranks.vT == ranks.aT and dim(shapes.qT, ranks.qT - 1) == attrs.Nq and dim(shapes.kT, ranks.kT - 1) == attrs.Nkv and dim(shapes.vT, ranks.vT - 1) == attrs.Nkv and sameShape(prefix(shapes.qT, ranks.qT - 1), prefix(shapes.aT, ranks.aT - 1)) and sameShape(prefix(shapes.kT, ranks.kT - 1), prefix(shapes.aT, ranks.aT - 1)) and sameShape(prefix(shapes.vT, ranks.vT - 1), prefix(shapes.aT, ranks.aT - 1))",
130
+ "dtypeOk": "tensorDtypes.qBT == \"uint8\" and tensorDtypes.kBT == \"uint8\" and tensorDtypes.vBT == \"uint8\" and tensorDtypes.qScalesT == tensorDtypes.aT and tensorDtypes.kScalesT == tensorDtypes.aT and tensorDtypes.vScalesT == tensorDtypes.aT and tensorDtypes.qT == tensorDtypes.aT and tensorDtypes.kT == tensorDtypes.aT and tensorDtypes.vT == tensorDtypes.aT and tensorDtypes.normScaleT == tensorDtypes.aT and f16Ok(tensorDtypes.aT)",
131
+ "lanesPow2": "tunables.LANES == pow2ceil(tunables.LANES)",
132
+ "normContractOk": "ranks.normScaleT == 1 and dim(shapes.normScaleT, 0) == attrs.K and (sameShape(shapes.skipT, shapes.aT) and tensorDtypes.skipT == tensorDtypes.aT if present.skipT else true) and (sameShape(shapes.residualT, shapes.aT) and tensorDtypes.residualT == tensorDtypes.aT and present.skipT if present.residualT else true)",
133
+ "qkvShapeOk": "weightShapeOk and scaleShapeOk and ioShapeOk and dtypeOk and lanesPow2 and normContractOk and pairSharesWord and attrs.K > 0 and attrs.Nq > 0 and attrs.Nkv > 0",
134
+ "projectionTiles": "ceilDiv(attrs.Nq, tunables.TILE_N) + 2 * ceilDiv(attrs.Nkv, tunables.TILE_N)",
135
+ "dispatchFits": "projectionTiles <= device.limits.maxComputeWorkgroupsPerDimension and aRows <= device.limits.maxComputeWorkgroupsPerDimension and tunables.TILE_N * tunables.LANES <= device.limits.maxComputeInvocationsPerWorkgroup and tunables.TILE_N * tunables.LANES <= device.limits.maxComputeWorkgroupSizeX and tunables.NORM_WORKGROUP_SIZE <= device.limits.maxComputeInvocationsPerWorkgroup and tunables.NORM_WORKGROUP_SIZE <= device.limits.maxComputeWorkgroupSizeX"
136
+ },
137
+ "constants": {
138
+ "aScalar": "\"f16\" if tensorDtypes.aT == \"float16\" else \"f32\"",
139
+ "scalar": "\"f16\" if tensorDtypes.aT == \"float16\" else \"f32\"",
140
+ "usesF16": "tensorDtypes.aT == \"float16\"",
141
+ "K": "attrs.K",
142
+ "nq": "attrs.Nq",
143
+ "nkv": "attrs.Nkv",
144
+ "blockSize": "attrs.block_size",
145
+ "kBlocks": "kBlocks",
146
+ "blobSize": "blobSize",
147
+ "bits": "attrs.bits",
148
+ "codesPerByte": "codesPerByte",
149
+ "codeMask": "codeMask",
150
+ "defaultZero": "\"8.0\"",
151
+ "tileN": "tunables.TILE_N",
152
+ "lanes": "tunables.LANES",
153
+ "hidden": "attrs.K",
154
+ "workgroupSize": "tunables.NORM_WORKGROUP_SIZE",
155
+ "epsilon": "epsilonValue",
156
+ "hasSkip": "present.skipT",
157
+ "writeResidual": "present.residualT",
158
+ "K_LEN": "attrs.K",
159
+ "rowTile": "rowTile",
160
+ "rows": "aRows"
161
+ },
162
+ "bindingSets": {
163
+ "normFull": [
164
+ {
165
+ "name": "a",
166
+ "arg": "aT",
167
+ "semantic": "A",
168
+ "buffer": { "type": "read-only-storage" },
169
+ "elementType": "$aScalar"
170
+ },
171
+ {
172
+ "name": "skip",
173
+ "arg": "skipT",
174
+ "semantic": "skip",
175
+ "buffer": { "type": "read-only-storage" },
176
+ "elementType": "$aScalar"
177
+ },
178
+ {
179
+ "name": "norm_scale",
180
+ "arg": "normScaleT",
181
+ "semantic": "norm_scale",
182
+ "buffer": { "type": "read-only-storage" },
183
+ "elementType": "$aScalar",
184
+ "length": "$K_LEN"
185
+ },
186
+ { "name": "normed", "semantic": "normedA", "buffer": { "type": "storage" }, "elementType": "f32" },
187
+ {
188
+ "name": "residual",
189
+ "arg": "residualT",
190
+ "semantic": "input_skip_bias_sum",
191
+ "buffer": { "type": "storage" },
192
+ "elementType": "$aScalar"
193
+ },
194
+ {
195
+ "name": "params",
196
+ "semantic": "kernel.params",
197
+ "buffer": { "type": "uniform" },
198
+ "struct": { "name": "Params", "fields": [{ "name": "rows", "type": "u32", "value": "aRows" }] }
199
+ }
200
+ ],
201
+ "normSkip": [
202
+ {
203
+ "name": "a",
204
+ "arg": "aT",
205
+ "semantic": "A",
206
+ "buffer": { "type": "read-only-storage" },
207
+ "elementType": "$aScalar"
208
+ },
209
+ {
210
+ "name": "skip",
211
+ "arg": "skipT",
212
+ "semantic": "skip",
213
+ "buffer": { "type": "read-only-storage" },
214
+ "elementType": "$aScalar"
215
+ },
216
+ {
217
+ "name": "norm_scale",
218
+ "arg": "normScaleT",
219
+ "semantic": "norm_scale",
220
+ "buffer": { "type": "read-only-storage" },
221
+ "elementType": "$aScalar",
222
+ "length": "$K_LEN"
223
+ },
224
+ { "name": "normed", "semantic": "normedA", "buffer": { "type": "storage" }, "elementType": "f32" },
225
+ {
226
+ "name": "params",
227
+ "semantic": "kernel.params",
228
+ "buffer": { "type": "uniform" },
229
+ "struct": { "name": "Params", "fields": [{ "name": "rows", "type": "u32", "value": "aRows" }] }
230
+ }
231
+ ],
232
+ "normOnly": [
233
+ {
234
+ "name": "a",
235
+ "arg": "aT",
236
+ "semantic": "A",
237
+ "buffer": { "type": "read-only-storage" },
238
+ "elementType": "$aScalar"
239
+ },
240
+ {
241
+ "name": "norm_scale",
242
+ "arg": "normScaleT",
243
+ "semantic": "norm_scale",
244
+ "buffer": { "type": "read-only-storage" },
245
+ "elementType": "$aScalar",
246
+ "length": "$K_LEN"
247
+ },
248
+ { "name": "normed", "semantic": "normedA", "buffer": { "type": "storage" }, "elementType": "f32" },
249
+ {
250
+ "name": "params",
251
+ "semantic": "kernel.params",
252
+ "buffer": { "type": "uniform" },
253
+ "struct": { "name": "Params", "fields": [{ "name": "rows", "type": "u32", "value": "aRows" }] }
254
+ }
255
+ ],
256
+ "projection": [
257
+ { "name": "normed", "semantic": "normedA", "buffer": { "type": "read-only-storage" }, "elementType": "f32" },
258
+ {
259
+ "name": "q_b",
260
+ "arg": "qBT",
261
+ "semantic": "q_B",
262
+ "buffer": { "type": "read-only-storage" },
263
+ "elementType": "u32"
264
+ },
265
+ {
266
+ "name": "q_scales",
267
+ "arg": "qScalesT",
268
+ "semantic": "q_scales",
269
+ "buffer": { "type": "read-only-storage" },
270
+ "elementType": "$aScalar"
271
+ },
272
+ {
273
+ "name": "k_b",
274
+ "arg": "kBT",
275
+ "semantic": "k_B",
276
+ "buffer": { "type": "read-only-storage" },
277
+ "elementType": "u32"
278
+ },
279
+ {
280
+ "name": "k_scales",
281
+ "arg": "kScalesT",
282
+ "semantic": "k_scales",
283
+ "buffer": { "type": "read-only-storage" },
284
+ "elementType": "$aScalar"
285
+ },
286
+ {
287
+ "name": "v_b",
288
+ "arg": "vBT",
289
+ "semantic": "v_B",
290
+ "buffer": { "type": "read-only-storage" },
291
+ "elementType": "u32"
292
+ },
293
+ {
294
+ "name": "v_scales",
295
+ "arg": "vScalesT",
296
+ "semantic": "v_scales",
297
+ "buffer": { "type": "read-only-storage" },
298
+ "elementType": "$aScalar"
299
+ },
300
+ { "name": "q", "arg": "qT", "semantic": "Q", "buffer": { "type": "storage" }, "elementType": "$aScalar" },
301
+ { "name": "k", "arg": "kT", "semantic": "K", "buffer": { "type": "storage" }, "elementType": "$aScalar" },
302
+ { "name": "v", "arg": "vT", "semantic": "V", "buffer": { "type": "storage" }, "elementType": "$aScalar" }
303
+ ],
304
+ "projectionQ": [
305
+ { "name": "normed", "semantic": "normedA", "buffer": { "type": "read-only-storage" }, "elementType": "f32" },
306
+ {
307
+ "name": "q_b",
308
+ "arg": "qBT",
309
+ "semantic": "q_B",
310
+ "buffer": { "type": "read-only-storage" },
311
+ "elementType": "u32"
312
+ },
313
+ {
314
+ "name": "q_scales",
315
+ "arg": "qScalesT",
316
+ "semantic": "q_scales",
317
+ "buffer": { "type": "read-only-storage" },
318
+ "elementType": "$aScalar"
319
+ },
320
+ { "name": "q", "arg": "qT", "semantic": "Q", "buffer": { "type": "storage" }, "elementType": "$aScalar" }
321
+ ],
322
+ "projectionK": [
323
+ { "name": "normed", "semantic": "normedA", "buffer": { "type": "read-only-storage" }, "elementType": "f32" },
324
+ {
325
+ "name": "k_b",
326
+ "arg": "kBT",
327
+ "semantic": "k_B",
328
+ "buffer": { "type": "read-only-storage" },
329
+ "elementType": "u32"
330
+ },
331
+ {
332
+ "name": "k_scales",
333
+ "arg": "kScalesT",
334
+ "semantic": "k_scales",
335
+ "buffer": { "type": "read-only-storage" },
336
+ "elementType": "$aScalar"
337
+ },
338
+ { "name": "k", "arg": "kT", "semantic": "K", "buffer": { "type": "storage" }, "elementType": "$aScalar" }
339
+ ],
340
+ "projectionV": [
341
+ { "name": "normed", "semantic": "normedA", "buffer": { "type": "read-only-storage" }, "elementType": "f32" },
342
+ {
343
+ "name": "v_b",
344
+ "arg": "vBT",
345
+ "semantic": "v_B",
346
+ "buffer": { "type": "read-only-storage" },
347
+ "elementType": "u32"
348
+ },
349
+ {
350
+ "name": "v_scales",
351
+ "arg": "vScalesT",
352
+ "semantic": "v_scales",
353
+ "buffer": { "type": "read-only-storage" },
354
+ "elementType": "$aScalar"
355
+ },
356
+ { "name": "v", "arg": "vT", "semantic": "V", "buffer": { "type": "storage" }, "elementType": "$aScalar" }
357
+ ]
358
+ },
359
+ "variants": [
360
+ {
361
+ "id": "norm",
362
+ "priority": 20,
363
+ "when": ["qkvShapeOk", "dispatchFits", "not present.skipT", "not present.residualT"],
364
+ "intermediates": [{ "id": "normedA", "dtype": "float32", "shape": "[numel(shapes.aT)]" }],
365
+ "passes": [
366
+ {
367
+ "id": "norm",
368
+ "name": "MatMulNBitsQkv.RmsNorm",
369
+ "shader": "matmul-nbits-fused-rms-norm.wgsl.jinja",
370
+ "bindings": "normOnly",
371
+ "dispatch": { "workgroups": "aRows" }
372
+ },
373
+ {
374
+ "id": "main",
375
+ "name": "MatMulNBitsQkv.Projection",
376
+ "source": { "shader": "qkv-projection.wgsl.jinja", "inputs": { "singleProjection": "\"\"" } },
377
+ "bindings": "projection",
378
+ "dispatch": { "x": "projectionTiles", "y": "rowGroups" }
379
+ }
380
+ ]
381
+ },
382
+ {
383
+ "id": "split_norm",
384
+ "priority": 10,
385
+ "when": ["qkvShapeOk", "dispatchFits", "not present.skipT", "not present.residualT"],
386
+ "intermediates": [{ "id": "normedA", "dtype": "float32", "shape": "[numel(shapes.aT)]" }],
387
+ "passes": [
388
+ {
389
+ "id": "norm",
390
+ "name": "MatMulNBitsQkv.RmsNorm",
391
+ "shader": "matmul-nbits-fused-rms-norm.wgsl.jinja",
392
+ "bindings": "normOnly",
393
+ "dispatch": { "workgroups": "aRows" }
394
+ },
395
+ {
396
+ "id": "q",
397
+ "name": "MatMulNBitsQkv.ProjectionQ",
398
+ "source": { "shader": "qkv-projection.wgsl.jinja", "inputs": { "singleProjection": "\"q\"" } },
399
+ "bindings": "projectionQ",
400
+ "dispatch": { "x": "ceilDiv(attrs.Nq, tunables.TILE_N)", "y": "rowGroups" }
401
+ },
402
+ {
403
+ "id": "k",
404
+ "name": "MatMulNBitsQkv.ProjectionK",
405
+ "source": { "shader": "qkv-projection.wgsl.jinja", "inputs": { "singleProjection": "\"k\"" } },
406
+ "bindings": "projectionK",
407
+ "dispatch": { "x": "ceilDiv(attrs.Nkv, tunables.TILE_N)", "y": "rowGroups" }
408
+ },
409
+ {
410
+ "id": "v",
411
+ "name": "MatMulNBitsQkv.ProjectionV",
412
+ "source": { "shader": "qkv-projection.wgsl.jinja", "inputs": { "singleProjection": "\"v\"" } },
413
+ "bindings": "projectionV",
414
+ "dispatch": { "x": "ceilDiv(attrs.Nkv, tunables.TILE_N)", "y": "rowGroups" }
415
+ }
416
+ ]
417
+ },
418
+ {
419
+ "id": "skip",
420
+ "priority": 20,
421
+ "when": ["qkvShapeOk", "dispatchFits", "present.skipT", "not present.residualT"],
422
+ "intermediates": [{ "id": "normedA", "dtype": "float32", "shape": "[numel(shapes.aT)]" }],
423
+ "passes": [
424
+ {
425
+ "id": "norm",
426
+ "name": "MatMulNBitsQkv.RmsNorm",
427
+ "shader": "matmul-nbits-fused-rms-norm.wgsl.jinja",
428
+ "bindings": "normSkip",
429
+ "dispatch": { "workgroups": "aRows" }
430
+ },
431
+ {
432
+ "id": "main",
433
+ "name": "MatMulNBitsQkv.Projection",
434
+ "source": { "shader": "qkv-projection.wgsl.jinja", "inputs": { "singleProjection": "\"\"" } },
435
+ "bindings": "projection",
436
+ "dispatch": { "x": "projectionTiles", "y": "rowGroups" }
437
+ }
438
+ ]
439
+ },
440
+ {
441
+ "id": "split_skip",
442
+ "priority": 10,
443
+ "when": ["qkvShapeOk", "dispatchFits", "present.skipT", "not present.residualT"],
444
+ "intermediates": [{ "id": "normedA", "dtype": "float32", "shape": "[numel(shapes.aT)]" }],
445
+ "passes": [
446
+ {
447
+ "id": "norm",
448
+ "name": "MatMulNBitsQkv.RmsNorm",
449
+ "shader": "matmul-nbits-fused-rms-norm.wgsl.jinja",
450
+ "bindings": "normSkip",
451
+ "dispatch": { "workgroups": "aRows" }
452
+ },
453
+ {
454
+ "id": "q",
455
+ "name": "MatMulNBitsQkv.ProjectionQ",
456
+ "source": { "shader": "qkv-projection.wgsl.jinja", "inputs": { "singleProjection": "\"q\"" } },
457
+ "bindings": "projectionQ",
458
+ "dispatch": { "x": "ceilDiv(attrs.Nq, tunables.TILE_N)", "y": "rowGroups" }
459
+ },
460
+ {
461
+ "id": "k",
462
+ "name": "MatMulNBitsQkv.ProjectionK",
463
+ "source": { "shader": "qkv-projection.wgsl.jinja", "inputs": { "singleProjection": "\"k\"" } },
464
+ "bindings": "projectionK",
465
+ "dispatch": { "x": "ceilDiv(attrs.Nkv, tunables.TILE_N)", "y": "rowGroups" }
466
+ },
467
+ {
468
+ "id": "v",
469
+ "name": "MatMulNBitsQkv.ProjectionV",
470
+ "source": { "shader": "qkv-projection.wgsl.jinja", "inputs": { "singleProjection": "\"v\"" } },
471
+ "bindings": "projectionV",
472
+ "dispatch": { "x": "ceilDiv(attrs.Nkv, tunables.TILE_N)", "y": "rowGroups" }
473
+ }
474
+ ]
475
+ },
476
+ {
477
+ "id": "skipsum",
478
+ "priority": 20,
479
+ "when": ["qkvShapeOk", "dispatchFits", "present.skipT", "present.residualT"],
480
+ "intermediates": [{ "id": "normedA", "dtype": "float32", "shape": "[numel(shapes.aT)]" }],
481
+ "passes": [
482
+ {
483
+ "id": "norm",
484
+ "name": "MatMulNBitsQkv.RmsNorm",
485
+ "shader": "matmul-nbits-fused-rms-norm.wgsl.jinja",
486
+ "bindings": "normFull",
487
+ "dispatch": { "workgroups": "aRows" }
488
+ },
489
+ {
490
+ "id": "main",
491
+ "name": "MatMulNBitsQkv.Projection",
492
+ "source": { "shader": "qkv-projection.wgsl.jinja", "inputs": { "singleProjection": "\"\"" } },
493
+ "bindings": "projection",
494
+ "dispatch": { "x": "projectionTiles", "y": "rowGroups" }
495
+ }
496
+ ]
497
+ },
498
+ {
499
+ "id": "split_skipsum",
500
+ "priority": 10,
501
+ "when": ["qkvShapeOk", "dispatchFits", "present.skipT", "present.residualT"],
502
+ "intermediates": [{ "id": "normedA", "dtype": "float32", "shape": "[numel(shapes.aT)]" }],
503
+ "passes": [
504
+ {
505
+ "id": "norm",
506
+ "name": "MatMulNBitsQkv.RmsNorm",
507
+ "shader": "matmul-nbits-fused-rms-norm.wgsl.jinja",
508
+ "bindings": "normFull",
509
+ "dispatch": { "workgroups": "aRows" }
510
+ },
511
+ {
512
+ "id": "q",
513
+ "name": "MatMulNBitsQkv.ProjectionQ",
514
+ "source": { "shader": "qkv-projection.wgsl.jinja", "inputs": { "singleProjection": "\"q\"" } },
515
+ "bindings": "projectionQ",
516
+ "dispatch": { "x": "ceilDiv(attrs.Nq, tunables.TILE_N)", "y": "rowGroups" }
517
+ },
518
+ {
519
+ "id": "k",
520
+ "name": "MatMulNBitsQkv.ProjectionK",
521
+ "source": { "shader": "qkv-projection.wgsl.jinja", "inputs": { "singleProjection": "\"k\"" } },
522
+ "bindings": "projectionK",
523
+ "dispatch": { "x": "ceilDiv(attrs.Nkv, tunables.TILE_N)", "y": "rowGroups" }
524
+ },
525
+ {
526
+ "id": "v",
527
+ "name": "MatMulNBitsQkv.ProjectionV",
528
+ "source": { "shader": "qkv-projection.wgsl.jinja", "inputs": { "singleProjection": "\"v\"" } },
529
+ "bindings": "projectionV",
530
+ "dispatch": { "x": "ceilDiv(attrs.Nkv, tunables.TILE_N)", "y": "rowGroups" }
531
+ }
532
+ ]
533
+ }
534
+ ]
535
+ }
build/webgpu/matmul-nbits-fused-rms-norm.wgsl.jinja ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% if usesF16 %}
2
+ enable f16;
3
+ {% endif %}
4
+ {{ env.wgsl.resourceDeclarations }}
5
+
6
+ // Shared MatMulNBits MLP/QKV normalization pass.
7
+ // normed[row, d] = (A + skip)[row, d] * inverseSqrt(mean_d((A + skip)^2) + eps) * norm_scale[d]
8
+ // One workgroup owns one row. Every intermediate stays in f32 and `normed` is an
9
+ // f32 scratch tensor, so the projection pass reads exactly what the fused
10
+ // single-dispatch kernel keeps in registers -- the two paths agree for float16
11
+ // inputs instead of differing by one narrowing.
12
+ const HIDDEN: u32 = {{ hidden }}u;
13
+ const WG: u32 = {{ workgroupSize }}u;
14
+ const EPSILON: f32 = {{ epsilon }};
15
+
16
+ var<workgroup> partial: array<f32, WG>;
17
+
18
+ {% macro wgsl_tree_fold_stmt(a, op, idx, svar) %}
19
+ {% if op == "max" %}
20
+ {{ a }}[{{ idx }}] = max({{ a }}[{{ idx }}], {{ a }}[{{ idx }} + {{ svar }}]);
21
+ {%- else %}
22
+ {{ a }}[{{ idx }}] = {{ a }}[{{ idx }}] + {{ a }}[{{ idx }} + {{ svar }}];
23
+ {%- endif %}
24
+ {% endmacro %}
25
+ {% macro wgsl_tree_fold(arrays, op="add", idx="lid", wg="WORKGROUP_SIZE", svar="stride", typed=false, form="tail", breakInline=false, bodyInline=false, barrierFirst=false) %}
26
+ var {{ svar }}{{ ": u32 " if typed else " " }}= {{ wg }} / 2u;
27
+ loop {
28
+ {% if form == "head" %}
29
+ {% if breakInline %}
30
+ if ({{ svar }} == 0u) { break; }
31
+ {% else %}
32
+ if ({{ svar }} == 0u) {
33
+ break;
34
+ }
35
+ {% endif %}
36
+ {% endif %}
37
+ {% if bodyInline %}
38
+ if ({{ idx }} < {{ svar }}) { {{ wgsl_tree_fold_stmt(arrays[0], op, idx, svar) }} }
39
+ {% else %}
40
+ if ({{ idx }} < {{ svar }}) {
41
+ {% for a in arrays %}
42
+ {{ wgsl_tree_fold_stmt(a, op, idx, svar) }}
43
+ {% endfor %}
44
+ }
45
+ {% endif %}
46
+ {% if form == "head" %}
47
+ {% if barrierFirst %}
48
+ workgroupBarrier();
49
+ {{ svar }} = {{ svar }} / 2u;
50
+ {% else %}
51
+ {{ svar }} = {{ svar }} / 2u;
52
+ workgroupBarrier();
53
+ {% endif %}
54
+ {% else %}
55
+ workgroupBarrier();
56
+ if ({{ svar }} == 1u) {
57
+ break;
58
+ }
59
+ {{ svar }} = {{ svar }} / 2u;
60
+ {% endif %}
61
+ }
62
+ {%- endmacro %}
63
+
64
+ // Reusing partial after this reduction requires a barrier between the read of
65
+ // partial[0] and the next write, or the next round can race the prior readers.
66
+ {% set trailingBarrier = trailingBarrier is defined and trailingBarrier %}
67
+ fn reduce_sum(value: f32, tid: u32) -> f32 {
68
+ partial[tid] = value;
69
+ workgroupBarrier();
70
+ {{ wgsl_tree_fold(["partial"], idx="tid", wg="WG", form="head") }}
71
+ {% if trailingBarrier %}
72
+ let total = partial[0];
73
+ workgroupBarrier();
74
+ return total;
75
+ {% else %}
76
+ return partial[0];
77
+ {% endif %}
78
+ }
79
+
80
+
81
+ fn row_value(index: u32) -> f32 {
82
+ {% if hasSkip %}
83
+ return f32(a[index]) + f32(skip[index]);
84
+ {% else %}
85
+ return f32(a[index]);
86
+ {% endif %}
87
+ }
88
+
89
+ @compute @workgroup_size(WG, 1, 1)
90
+ fn main(@builtin(workgroup_id) wg: vec3<u32>,
91
+ @builtin(num_workgroups) nwg: vec3<u32>,
92
+ @builtin(local_invocation_id) lid: vec3<u32>) {
93
+ // 2D-folded row index: wg.y carries the high bits past the
94
+ // maxComputeWorkgroupsPerDimension dispatch limit. Reduces to wg.x when nwg.y == 1.
95
+ let row = wg.x + wg.y * nwg.x;
96
+ if (row >= params.rows) {
97
+ return;
98
+ }
99
+ let tid = lid.x;
100
+ let base = row * HIDDEN;
101
+
102
+ var local_sq = 0.0;
103
+ for (var d = tid; d < HIDDEN; d = d + WG) {
104
+ let value = row_value(base + d);
105
+ local_sq = local_sq + value * value;
106
+ }
107
+ let inv = inverseSqrt(reduce_sum(local_sq, tid) / f32(HIDDEN) + EPSILON);
108
+
109
+ for (var d = tid; d < HIDDEN; d = d + WG) {
110
+ let index = base + d;
111
+ let value = row_value(index);
112
+ {% if writeResidual %}
113
+ residual[index] = {{ scalar }}(value);
114
+ {% endif %}
115
+ normed[index] = value * inv * f32(norm_scale[d]);
116
+ }
117
+ }
build/webgpu/metadata.json ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "com.microsoft.MatMulNBitsQkv",
3
+ "id": "_com_microsoft_matmulnbitsqkv_webgpu_07222b0",
4
+ "version": 1,
5
+ "license": "Apache-2.0",
6
+ "backend": { "type": "webgpu" },
7
+ "digest": {
8
+ "algorithm": "sha256",
9
+ "files": {
10
+ "bench.json": "TMTSE3iJebIWYjPDDVFyPwWC3YdOekG83a3gOGp3Xao=",
11
+ "manifest.json": "xLf6qGf+IK/6nqf+JAlvzmyVSbm5jUarc5DL/IVW3w0=",
12
+ "matmul-nbits-fused-rms-norm.wgsl.jinja": "4TI9Mc/RZWVgMl6NVG2iwe5OpvqQmswhe6xtGvctNmI=",
13
+ "qkv-projection.wgsl.jinja": "sINPOc7UfVRs5YFgICljS7B6QxhlPuL6IVEEatlIaMY=",
14
+ "test.json": "Ttc9zF84a34u+oxrvXeKL3Wah8leZYmRrkN0Ddw+P0E="
15
+ }
16
+ },
17
+ "provenance": { "kernel": { "sha": "2e7068faf55e7f43df740015f6d1ee49391a41c5", "dirty": false } },
18
+ "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/com.microsoft.MatMulNBitsQkv" }
19
+ }
build/webgpu/qkv-projection.wgsl.jinja ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% macro matmul_nbits_packed_code(fn="packed_weight", buffer="b", kBlocks="params.kBlocks", blobSize="params.blobSize", bits=4) %}
2
+ fn {{ fn }}(n: u32, block: u32, offset: u32) -> u32 {
3
+ {% if bits == 2 %}
4
+ let byte_index = offset / 4u;
5
+ let shift = (offset % 4u) * 2u;
6
+ let packed_index = (n * {{ kBlocks }} + block) * {{ blobSize }} + byte_index;
7
+ return ({{ buffer }}[packed_index] >> shift) & 3u;
8
+ {% elif bits == 4 %}
9
+ let byte_index = offset / 2u;
10
+ let shift = (offset % 2u) * 4u;
11
+ let packed_index = (n * {{ kBlocks }} + block) * {{ blobSize }} + byte_index;
12
+ return ({{ buffer }}[packed_index] >> shift) & 15u;
13
+ {% else %}
14
+ let packed_index = (n * {{ kBlocks }} + block) * {{ blobSize }} + offset;
15
+ return {{ buffer }}[packed_index] & 255u;
16
+ {% endif %}
17
+ }
18
+ {%- endmacro %}
19
+
20
+ {% if usesF16 %}
21
+ enable f16;
22
+ {% endif %}
23
+ {{ env.wgsl.resourceDeclarations }}
24
+
25
+ // com.microsoft.MatMulNBitsQkv, projection pass.
26
+ // Q[row, n] = dot(A_norm[row], q_weight[n]), and likewise K and V.
27
+ {% if source.singleProjection %}
28
+ // This specialization binds and computes one projection.
29
+ {% else %}
30
+ // All three projections read the same normalized row, so one dispatch covers
31
+ // them: the column-tile grid is laid out as Q's tiles, then K's, then V's, and
32
+ // a workgroup's tile index alone decides which projection it serves. That keeps
33
+ // the branch workgroup-uniform -- no lane in a workgroup ever takes a different
34
+ // arm -- while still reading each activation row once per tile instead of once
35
+ // per projection.
36
+ {% endif %}
37
+ // Within a tile, LANES threads cooperate on one output column: thread
38
+ // (column, lane) strides the reduction axis by LANES and the partials are
39
+ // folded inside the column's own lane group. Codes dequantize as
40
+ // (code - ZERO) * block_scale with the scale hoisted out of the inner loop;
41
+ // this operator has no zero-point input, so ZERO is the symmetric midpoint.
42
+ // A workgroup also covers ROW_TILE activation rows, reusing each unpacked code
43
+ // across their accumulators.
44
+ const K: u32 = {{ K }}u;
45
+ {% if source.singleProjection == "q" %}
46
+ const N: u32 = {{ nq }}u;
47
+ {% elif source.singleProjection %}
48
+ const N: u32 = {{ nkv }}u;
49
+ {% else %}
50
+ const NQ: u32 = {{ nq }}u;
51
+ const NKV: u32 = {{ nkv }}u;
52
+ {% endif %}
53
+ const BLOCK_SIZE: u32 = {{ blockSize }}u;
54
+ const KBLOCKS: u32 = {{ kBlocks }}u;
55
+ const BLOB_SIZE: u32 = {{ blobSize }}u;
56
+ const TILE_N: u32 = {{ tileN }}u;
57
+ const LANES: u32 = {{ lanes }}u;
58
+ const ROW_TILE: u32 = {{ rowTile }}u;
59
+ const ROWS: u32 = {{ rows }}u;
60
+ const WG: u32 = TILE_N * LANES;
61
+ const ZERO: f32 = {{ defaultZero }};
62
+ {% if not source.singleProjection %}
63
+ const Q_TILES: u32 = (NQ + TILE_N - 1u) / TILE_N;
64
+ const KV_TILES: u32 = (NKV + TILE_N - 1u) / TILE_N;
65
+ {% endif %}
66
+
67
+ const BITS: u32 = {{ bits }}u;
68
+ // Codes per logical byte and the mask for one code. Physical uint8 storage uses
69
+ // a u32 slot, so packing increases the number of codes returned by each load.
70
+ const CODES_PER_BYTE: u32 = {{ codesPerByte }}u;
71
+ const CODE_MASK: u32 = {{ codeMask }}u;
72
+
73
+ {% for stream in (["q", "k", "v"] if not source.singleProjection else [source.singleProjection]) %}
74
+ {{ matmul_nbits_packed_code(fn=stream ~ "_code", buffer=stream ~ "_b", kBlocks="KBLOCKS", blobSize="BLOB_SIZE", bits=bits) }}
75
+ // Decode two consecutive reduction-axis codes from one stored word. An odd
76
+ // offset would straddle words, so callers advance by two from an even start.
77
+ fn {{ stream }}_code_pair(n: u32, block: u32, offset: u32) -> vec2<u32> {
78
+ let word = {{ stream }}_b[(n * KBLOCKS + block) * BLOB_SIZE + offset / CODES_PER_BYTE];
79
+ let shift = (offset % CODES_PER_BYTE) * BITS;
80
+ return vec2<u32>((word >> shift) & CODE_MASK, (word >> (shift + BITS)) & CODE_MASK);
81
+ }
82
+ {% endfor %}
83
+
84
+ var<workgroup> reduction: array<f32, WG * ROW_TILE>;
85
+
86
+ {% macro walk_block(codeFn, guarded) %}
87
+ for (var offset = lane * 2u; offset + 1u < BLOCK_SIZE; offset = offset + LANES * 2u) {
88
+ let k = k_base + offset;
89
+ {% if guarded %}
90
+ if (k + 1u < K) {
91
+ {% endif %}
92
+ let codes = {{ codeFn }}_pair(n, block, offset);
93
+ let code_lo = f32(codes.x) - ZERO;
94
+ let code_hi = f32(codes.y) - ZERO;
95
+ {% for r in range(rowTile) %}
96
+ block_acc_{{ r }} = block_acc_{{ r }} + normed[base_{{ r }} + k] * code_lo
97
+ + normed[base_{{ r }} + k + 1u] * code_hi;
98
+ {% endfor %}
99
+ {% if guarded %}
100
+ } else if (k < K) {
101
+ let code = f32({{ codeFn }}(n, block, offset)) - ZERO;
102
+ {% for r in range(rowTile) %}
103
+ block_acc_{{ r }} = block_acc_{{ r }} + normed[base_{{ r }} + k] * code;
104
+ {% endfor %}
105
+ }
106
+ {% endif %}
107
+ }
108
+ {%- endmacro %}
109
+
110
+ {% macro project(codeFn, scalesBuffer) %}
111
+ for (var block = 0u; block < KBLOCKS; block = block + 1u) {
112
+ let scale = f32({{ scalesBuffer }}[n * KBLOCKS + block]);
113
+ let k_base = block * BLOCK_SIZE;
114
+ {% for r in range(rowTile) %}
115
+ var block_acc_{{ r }} = 0.0;
116
+ {% endfor %}
117
+ // Each trip handles two codes. BLOCK_SIZE is even for every admitted
118
+ // packing, so paired trips cover a full block. Only a final partial block
119
+ // needs bounds checks; the branch is workgroup-uniform.
120
+ if (k_base + BLOCK_SIZE <= K) {
121
+ {{ walk_block(codeFn, false) }}
122
+ } else {
123
+ {{ walk_block(codeFn, true) }}
124
+ }
125
+ {% for r in range(rowTile) %}
126
+ acc_{{ r }} = acc_{{ r }} + block_acc_{{ r }} * scale;
127
+ {% endfor %}
128
+ }
129
+ {%- endmacro %}
130
+
131
+ @compute @workgroup_size(WG, 1, 1)
132
+ fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>) {
133
+ let row0 = wg.y * ROW_TILE;
134
+ let tid = lid.x;
135
+ let column = tid / LANES;
136
+ let lane = tid % LANES;
137
+ {% for r in range(rowTile) %}
138
+ // Rows past the end of the batch clamp onto the last real row; their
139
+ // accumulators are finite and the store guard drops them.
140
+ let base_{{ r }} = min(row0 + {{ r }}u, ROWS - 1u) * K;
141
+ {% endfor %}
142
+
143
+ {% if source.singleProjection %}
144
+ let n = wg.x * TILE_N + column;
145
+
146
+ {% for r in range(rowTile) %}
147
+ var acc_{{ r }} = 0.0;
148
+ {% endfor %}
149
+ if (n < N) {
150
+ {{ project(source.singleProjection ~ "_code", source.singleProjection ~ "_scales") }}
151
+ }
152
+ {% else %}
153
+ // Tile index selects the projection, so every thread in this workgroup takes
154
+ // the same arm and the reduction below stays uniform.
155
+ let tile = wg.x;
156
+ var projection = 2u;
157
+ var local_tile = tile - Q_TILES - KV_TILES;
158
+ if (tile < Q_TILES) {
159
+ projection = 0u;
160
+ local_tile = tile;
161
+ } else if (tile < Q_TILES + KV_TILES) {
162
+ projection = 1u;
163
+ local_tile = tile - Q_TILES;
164
+ }
165
+ let n = local_tile * TILE_N + column;
166
+ let limit = select(NKV, NQ, projection == 0u);
167
+
168
+ {% for r in range(rowTile) %}
169
+ var acc_{{ r }} = 0.0;
170
+ {% endfor %}
171
+ if (n < limit) {
172
+ if (projection == 0u) {
173
+ {{ project("q_code", "q_scales") }}
174
+ } else if (projection == 1u) {
175
+ {{ project("k_code", "k_scales") }}
176
+ } else {
177
+ {{ project("v_code", "v_scales") }}
178
+ }
179
+ }
180
+ {% endif %}
181
+
182
+ {% for r in range(rowTile) %}
183
+ reduction[{{ r }}u * WG + tid] = acc_{{ r }};
184
+ {% endfor %}
185
+ workgroupBarrier();
186
+ // Fold within each column's own LANES-wide group, one group per staged row.
187
+ // LANES is a power of two, so lane + stride never leaves the group.
188
+ for (var stride = LANES / 2u; stride > 0u; stride = stride / 2u) {
189
+ if (lane < stride) {
190
+ {% for r in range(rowTile) %}
191
+ reduction[{{ r }}u * WG + tid] = reduction[{{ r }}u * WG + tid] + reduction[{{ r }}u * WG + tid + stride];
192
+ {% endfor %}
193
+ }
194
+ workgroupBarrier();
195
+ }
196
+
197
+ {% if source.singleProjection %}
198
+ if (lane == 0u && n < N) {
199
+ {% for r in range(rowTile) %}
200
+ if (row0 + {{ r }}u < ROWS) {
201
+ {{ source.singleProjection }}[(row0 + {{ r }}u) * N + n] = {{ scalar }}(reduction[{{ r }}u * WG + tid]);
202
+ }
203
+ {% endfor %}
204
+ }
205
+ {% else %}
206
+ if (lane == 0u && n < limit) {
207
+ {% for r in range(rowTile) %}
208
+ if (row0 + {{ r }}u < ROWS) {
209
+ let value_{{ r }} = {{ scalar }}(reduction[{{ r }}u * WG + tid]);
210
+ if (projection == 0u) {
211
+ q[(row0 + {{ r }}u) * NQ + n] = value_{{ r }};
212
+ } else if (projection == 1u) {
213
+ k[(row0 + {{ r }}u) * NKV + n] = value_{{ r }};
214
+ } else {
215
+ v[(row0 + {{ r }}u) * NKV + n] = value_{{ r }};
216
+ }
217
+ }
218
+ {% endfor %}
219
+ }
220
+ {% endif %}
221
+ }
build/webgpu/test.json ADDED
@@ -0,0 +1,663 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "op": "com.microsoft.MatMulNBitsQkv",
3
+ "fixtureArrays": {
4
+ "norm_gqa_input_vBT": [90, 17, 203, 156, 64, 241, 112, 38, 175, 229, 83, 11, 198, 147, 52, 220, 105]
5
+ },
6
+ "cases": [
7
+ {
8
+ "name": "norm_gqa",
9
+ "provenance": {
10
+ "notes": "Grouped-query shape: Nq is twice Nkv, so the three projections have two different column counts."
11
+ },
12
+ "attrs": { "K": 32, "Nq": 8, "Nkv": 4, "block_size": 32 },
13
+ "inputs": {
14
+ "aT": {
15
+ "dtype": "float32",
16
+ "shape": [3, 32],
17
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.8 }
18
+ },
19
+ "normScaleT": {
20
+ "dtype": "float32",
21
+ "shape": [32],
22
+ "data": { "kind": "fillFloat32", "sinStep": 0.23, "cosStep": 0.41, "scale": 0.4, "offset": 1.0 }
23
+ },
24
+ "qBT": {
25
+ "dtype": "uint8",
26
+ "shape": [8, 1, 16],
27
+ "data": { "kind": "cycle", "values": [27, 180, 75, 226, 33, 150, 201, 108, 57, 246, 129, 66, 195] }
28
+ },
29
+ "qScalesT": {
30
+ "dtype": "float32",
31
+ "shape": [8, 1],
32
+ "data": { "kind": "cycle", "values": [0.04, 0.055, 0.05, 0.065, 0.06, 0.075, 0.07] }
33
+ },
34
+ "kBT": {
35
+ "dtype": "uint8",
36
+ "shape": [4, 1, 16],
37
+ "data": { "kind": "cycle", "values": [211, 44, 137, 98, 165, 20, 233, 121, 78, 190, 15, 252, 87, 143, 61] }
38
+ },
39
+ "kScalesT": {
40
+ "dtype": "float32",
41
+ "shape": [4, 1],
42
+ "data": { "kind": "cycle", "values": [0.045, 0.03, 0.07, 0.05, 0.08, 0.035] }
43
+ },
44
+ "vBT": {
45
+ "dtype": "uint8",
46
+ "shape": [4, 1, 16],
47
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/norm_gqa_input_vBT" } }
48
+ },
49
+ "vScalesT": {
50
+ "dtype": "float32",
51
+ "shape": [4, 1],
52
+ "data": { "kind": "cycle", "values": [0.052, 0.038, 0.061, 0.029, 0.073] }
53
+ }
54
+ },
55
+ "outputs": {
56
+ "qT": { "dtype": "float32", "shape": [3, 8], "tolerance": 0.0001, "relTolerance": 0.0001 },
57
+ "kT": { "dtype": "float32", "shape": [3, 4], "tolerance": 0.0001, "relTolerance": 0.0001 },
58
+ "vT": { "dtype": "float32", "shape": [3, 4], "tolerance": 0.0001, "relTolerance": 0.0001 }
59
+ }
60
+ },
61
+ {
62
+ "name": "norm_rows_past_one_tile",
63
+ "provenance": {
64
+ "notes": "Ten activation rows against an eight-row projection tile: two row groups where the second carries only two real rows, so the store guard has to drop the six that clamped onto the last row."
65
+ },
66
+ "attrs": { "K": 32, "Nq": 8, "Nkv": 4, "block_size": 32 },
67
+ "inputs": {
68
+ "aT": {
69
+ "dtype": "float32",
70
+ "shape": [10, 32],
71
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31, "scale": 0.8 }
72
+ },
73
+ "normScaleT": {
74
+ "dtype": "float32",
75
+ "shape": [32],
76
+ "data": { "kind": "fillFloat32", "sinStep": 0.23, "cosStep": 0.41, "scale": 0.4, "offset": 1.0 }
77
+ },
78
+ "qBT": {
79
+ "dtype": "uint8",
80
+ "shape": [8, 1, 16],
81
+ "data": { "kind": "cycle", "values": [27, 180, 75, 226, 33, 150, 201, 108, 57, 246, 129, 66, 195] }
82
+ },
83
+ "qScalesT": {
84
+ "dtype": "float32",
85
+ "shape": [8, 1],
86
+ "data": { "kind": "cycle", "values": [0.04, 0.055, 0.05, 0.065, 0.06, 0.075, 0.07] }
87
+ },
88
+ "kBT": {
89
+ "dtype": "uint8",
90
+ "shape": [4, 1, 16],
91
+ "data": { "kind": "cycle", "values": [211, 44, 137, 98, 165, 20, 233, 121, 78, 190, 15, 252, 87, 143, 61] }
92
+ },
93
+ "kScalesT": {
94
+ "dtype": "float32",
95
+ "shape": [4, 1],
96
+ "data": { "kind": "cycle", "values": [0.045, 0.03, 0.07, 0.05, 0.08, 0.035] }
97
+ },
98
+ "vBT": {
99
+ "dtype": "uint8",
100
+ "shape": [4, 1, 16],
101
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/norm_gqa_input_vBT" } }
102
+ },
103
+ "vScalesT": {
104
+ "dtype": "float32",
105
+ "shape": [4, 1],
106
+ "data": { "kind": "cycle", "values": [0.052, 0.038, 0.061, 0.029, 0.073] }
107
+ }
108
+ },
109
+ "outputs": {
110
+ "qT": { "dtype": "float32", "shape": [10, 8], "tolerance": 0.0001, "relTolerance": 0.0001 },
111
+ "kT": { "dtype": "float32", "shape": [10, 4], "tolerance": 0.0001, "relTolerance": 0.0001 },
112
+ "vT": { "dtype": "float32", "shape": [10, 4], "tolerance": 0.0001, "relTolerance": 0.0001 }
113
+ }
114
+ },
115
+ {
116
+ "name": "skip_gqa",
117
+ "attrs": { "K": 32, "Nq": 8, "Nkv": 4, "block_size": 32, "epsilon": 0.00001 },
118
+ "inputs": {
119
+ "aT": {
120
+ "dtype": "float32",
121
+ "shape": [3, 32],
122
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.8 }
123
+ },
124
+ "skipT": {
125
+ "dtype": "float32",
126
+ "shape": [3, 32],
127
+ "data": { "kind": "fillFloat32", "sinStep": 0.37, "cosStep": 0.11, "scale": 0.5 }
128
+ },
129
+ "normScaleT": {
130
+ "dtype": "float32",
131
+ "shape": [32],
132
+ "data": { "kind": "fillFloat32", "sinStep": 0.23, "cosStep": 0.41, "scale": 0.4, "offset": 1.0 }
133
+ },
134
+ "qBT": {
135
+ "dtype": "uint8",
136
+ "shape": [8, 1, 16],
137
+ "data": { "kind": "cycle", "values": [27, 180, 75, 226, 33, 150, 201, 108, 57, 246, 129, 66, 195] }
138
+ },
139
+ "qScalesT": {
140
+ "dtype": "float32",
141
+ "shape": [8, 1],
142
+ "data": { "kind": "cycle", "values": [0.04, 0.055, 0.05, 0.065, 0.06, 0.075, 0.07] }
143
+ },
144
+ "kBT": {
145
+ "dtype": "uint8",
146
+ "shape": [4, 1, 16],
147
+ "data": { "kind": "cycle", "values": [211, 44, 137, 98, 165, 20, 233, 121, 78, 190, 15, 252, 87, 143, 61] }
148
+ },
149
+ "kScalesT": {
150
+ "dtype": "float32",
151
+ "shape": [4, 1],
152
+ "data": { "kind": "cycle", "values": [0.045, 0.03, 0.07, 0.05, 0.08, 0.035] }
153
+ },
154
+ "vBT": {
155
+ "dtype": "uint8",
156
+ "shape": [4, 1, 16],
157
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/norm_gqa_input_vBT" } }
158
+ },
159
+ "vScalesT": {
160
+ "dtype": "float32",
161
+ "shape": [4, 1],
162
+ "data": { "kind": "cycle", "values": [0.052, 0.038, 0.061, 0.029, 0.073] }
163
+ }
164
+ },
165
+ "outputs": {
166
+ "qT": { "dtype": "float32", "shape": [3, 8], "tolerance": 0.0001, "relTolerance": 0.0001 },
167
+ "kT": { "dtype": "float32", "shape": [3, 4], "tolerance": 0.0001, "relTolerance": 0.0001 },
168
+ "vT": { "dtype": "float32", "shape": [3, 4], "tolerance": 0.0001, "relTolerance": 0.0001 }
169
+ }
170
+ },
171
+ {
172
+ "name": "skipsum_gqa",
173
+ "attrs": { "K": 32, "Nq": 8, "Nkv": 4, "block_size": 32 },
174
+ "inputs": {
175
+ "aT": {
176
+ "dtype": "float32",
177
+ "shape": [3, 32],
178
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.8 }
179
+ },
180
+ "skipT": {
181
+ "dtype": "float32",
182
+ "shape": [3, 32],
183
+ "data": { "kind": "fillFloat32", "sinStep": 0.37, "cosStep": 0.11, "scale": 0.5 }
184
+ },
185
+ "normScaleT": {
186
+ "dtype": "float32",
187
+ "shape": [32],
188
+ "data": { "kind": "fillFloat32", "sinStep": 0.23, "cosStep": 0.41, "scale": 0.4, "offset": 1.0 }
189
+ },
190
+ "qBT": {
191
+ "dtype": "uint8",
192
+ "shape": [8, 1, 16],
193
+ "data": { "kind": "cycle", "values": [27, 180, 75, 226, 33, 150, 201, 108, 57, 246, 129, 66, 195] }
194
+ },
195
+ "qScalesT": {
196
+ "dtype": "float32",
197
+ "shape": [8, 1],
198
+ "data": { "kind": "cycle", "values": [0.04, 0.055, 0.05, 0.065, 0.06, 0.075, 0.07] }
199
+ },
200
+ "kBT": {
201
+ "dtype": "uint8",
202
+ "shape": [4, 1, 16],
203
+ "data": { "kind": "cycle", "values": [211, 44, 137, 98, 165, 20, 233, 121, 78, 190, 15, 252, 87, 143, 61] }
204
+ },
205
+ "kScalesT": {
206
+ "dtype": "float32",
207
+ "shape": [4, 1],
208
+ "data": { "kind": "cycle", "values": [0.045, 0.03, 0.07, 0.05, 0.08, 0.035] }
209
+ },
210
+ "vBT": {
211
+ "dtype": "uint8",
212
+ "shape": [4, 1, 16],
213
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/norm_gqa_input_vBT" } }
214
+ },
215
+ "vScalesT": {
216
+ "dtype": "float32",
217
+ "shape": [4, 1],
218
+ "data": { "kind": "cycle", "values": [0.052, 0.038, 0.061, 0.029, 0.073] }
219
+ }
220
+ },
221
+ "outputs": {
222
+ "qT": { "dtype": "float32", "shape": [3, 8], "tolerance": 0.0001, "relTolerance": 0.0001 },
223
+ "kT": { "dtype": "float32", "shape": [3, 4], "tolerance": 0.0001, "relTolerance": 0.0001 },
224
+ "vT": { "dtype": "float32", "shape": [3, 4], "tolerance": 0.0001, "relTolerance": 0.0001 },
225
+ "residualT": { "dtype": "float32", "shape": [3, 32], "tolerance": 0.000001, "relTolerance": 0.000001 }
226
+ }
227
+ },
228
+ {
229
+ "name": "decode_single_row",
230
+ "attrs": { "K": 32, "Nq": 8, "Nkv": 4, "block_size": 32 },
231
+ "inputs": {
232
+ "aT": {
233
+ "dtype": "float32",
234
+ "shape": [1, 32],
235
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.8 }
236
+ },
237
+ "skipT": {
238
+ "dtype": "float32",
239
+ "shape": [1, 32],
240
+ "data": { "kind": "fillFloat32", "sinStep": 0.37, "cosStep": 0.11, "scale": 0.5 }
241
+ },
242
+ "normScaleT": {
243
+ "dtype": "float32",
244
+ "shape": [32],
245
+ "data": { "kind": "fillFloat32", "sinStep": 0.23, "cosStep": 0.41, "scale": 0.4, "offset": 1.0 }
246
+ },
247
+ "qBT": {
248
+ "dtype": "uint8",
249
+ "shape": [8, 1, 16],
250
+ "data": { "kind": "cycle", "values": [27, 180, 75, 226, 33, 150, 201, 108, 57, 246, 129, 66, 195] }
251
+ },
252
+ "qScalesT": {
253
+ "dtype": "float32",
254
+ "shape": [8, 1],
255
+ "data": { "kind": "cycle", "values": [0.04, 0.055, 0.05, 0.065, 0.06, 0.075, 0.07] }
256
+ },
257
+ "kBT": {
258
+ "dtype": "uint8",
259
+ "shape": [4, 1, 16],
260
+ "data": { "kind": "cycle", "values": [211, 44, 137, 98, 165, 20, 233, 121, 78, 190, 15, 252, 87, 143, 61] }
261
+ },
262
+ "kScalesT": {
263
+ "dtype": "float32",
264
+ "shape": [4, 1],
265
+ "data": { "kind": "cycle", "values": [0.045, 0.03, 0.07, 0.05, 0.08, 0.035] }
266
+ },
267
+ "vBT": {
268
+ "dtype": "uint8",
269
+ "shape": [4, 1, 16],
270
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/norm_gqa_input_vBT" } }
271
+ },
272
+ "vScalesT": {
273
+ "dtype": "float32",
274
+ "shape": [4, 1],
275
+ "data": { "kind": "cycle", "values": [0.052, 0.038, 0.061, 0.029, 0.073] }
276
+ }
277
+ },
278
+ "outputs": {
279
+ "qT": { "dtype": "float32", "shape": [1, 8], "tolerance": 0.0001, "relTolerance": 0.0001 },
280
+ "kT": { "dtype": "float32", "shape": [1, 4], "tolerance": 0.0001, "relTolerance": 0.0001 },
281
+ "vT": { "dtype": "float32", "shape": [1, 4], "tolerance": 0.0001, "relTolerance": 0.0001 },
282
+ "residualT": { "dtype": "float32", "shape": [1, 32], "tolerance": 0.000001, "relTolerance": 0.000001 }
283
+ }
284
+ },
285
+ {
286
+ "name": "equal_nq_nkv",
287
+ "provenance": {
288
+ "notes": "Multi-head shape where Nq equals Nkv; the tile grid still lays Q, K and V out in sequence."
289
+ },
290
+ "attrs": { "K": 32, "Nq": 8, "Nkv": 8, "block_size": 32 },
291
+ "inputs": {
292
+ "aT": {
293
+ "dtype": "float32",
294
+ "shape": [2, 32],
295
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.8 }
296
+ },
297
+ "normScaleT": {
298
+ "dtype": "float32",
299
+ "shape": [32],
300
+ "data": { "kind": "fillFloat32", "sinStep": 0.23, "cosStep": 0.41, "scale": 0.4, "offset": 1.0 }
301
+ },
302
+ "qBT": {
303
+ "dtype": "uint8",
304
+ "shape": [8, 1, 16],
305
+ "data": { "kind": "cycle", "values": [27, 180, 75, 226, 33, 150, 201, 108, 57, 246, 129, 66, 195] }
306
+ },
307
+ "qScalesT": {
308
+ "dtype": "float32",
309
+ "shape": [8, 1],
310
+ "data": { "kind": "cycle", "values": [0.04, 0.055, 0.05, 0.065, 0.06, 0.075, 0.07] }
311
+ },
312
+ "kBT": {
313
+ "dtype": "uint8",
314
+ "shape": [8, 1, 16],
315
+ "data": { "kind": "cycle", "values": [211, 44, 137, 98, 165, 20, 233, 121, 78, 190, 15, 252, 87, 143, 61] }
316
+ },
317
+ "kScalesT": {
318
+ "dtype": "float32",
319
+ "shape": [8, 1],
320
+ "data": { "kind": "cycle", "values": [0.045, 0.03, 0.07, 0.05, 0.08, 0.035] }
321
+ },
322
+ "vBT": {
323
+ "dtype": "uint8",
324
+ "shape": [8, 1, 16],
325
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/norm_gqa_input_vBT" } }
326
+ },
327
+ "vScalesT": {
328
+ "dtype": "float32",
329
+ "shape": [8, 1],
330
+ "data": { "kind": "cycle", "values": [0.052, 0.038, 0.061, 0.029, 0.073] }
331
+ }
332
+ },
333
+ "outputs": {
334
+ "qT": { "dtype": "float32", "shape": [2, 8], "tolerance": 0.0001, "relTolerance": 0.0001 },
335
+ "kT": { "dtype": "float32", "shape": [2, 8], "tolerance": 0.0001, "relTolerance": 0.0001 },
336
+ "vT": { "dtype": "float32", "shape": [2, 8], "tolerance": 0.0001, "relTolerance": 0.0001 }
337
+ }
338
+ },
339
+ {
340
+ "name": "tiles_not_aligned",
341
+ "provenance": {
342
+ "notes": "Neither column count is a multiple of the column tile, so both projections have a partial last tile."
343
+ },
344
+ "attrs": { "K": 32, "Nq": 5, "Nkv": 3, "block_size": 32 },
345
+ "inputs": {
346
+ "aT": {
347
+ "dtype": "float32",
348
+ "shape": [2, 32],
349
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.8 }
350
+ },
351
+ "normScaleT": {
352
+ "dtype": "float32",
353
+ "shape": [32],
354
+ "data": { "kind": "fillFloat32", "sinStep": 0.23, "cosStep": 0.41, "scale": 0.4, "offset": 1.0 }
355
+ },
356
+ "qBT": {
357
+ "dtype": "uint8",
358
+ "shape": [5, 1, 16],
359
+ "data": { "kind": "cycle", "values": [27, 180, 75, 226, 33, 150, 201, 108, 57, 246, 129, 66, 195] }
360
+ },
361
+ "qScalesT": {
362
+ "dtype": "float32",
363
+ "shape": [5, 1],
364
+ "data": { "kind": "cycle", "values": [0.04, 0.055, 0.05, 0.065, 0.06, 0.075, 0.07] }
365
+ },
366
+ "kBT": {
367
+ "dtype": "uint8",
368
+ "shape": [3, 1, 16],
369
+ "data": { "kind": "cycle", "values": [211, 44, 137, 98, 165, 20, 233, 121, 78, 190, 15, 252, 87, 143, 61] }
370
+ },
371
+ "kScalesT": {
372
+ "dtype": "float32",
373
+ "shape": [3, 1],
374
+ "data": { "kind": "cycle", "values": [0.045, 0.03, 0.07, 0.05, 0.08, 0.035] }
375
+ },
376
+ "vBT": {
377
+ "dtype": "uint8",
378
+ "shape": [3, 1, 16],
379
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/norm_gqa_input_vBT" } }
380
+ },
381
+ "vScalesT": {
382
+ "dtype": "float32",
383
+ "shape": [3, 1],
384
+ "data": { "kind": "cycle", "values": [0.052, 0.038, 0.061, 0.029, 0.073] }
385
+ }
386
+ },
387
+ "outputs": {
388
+ "qT": { "dtype": "float32", "shape": [2, 5], "tolerance": 0.0001, "relTolerance": 0.0001 },
389
+ "kT": { "dtype": "float32", "shape": [2, 3], "tolerance": 0.0001, "relTolerance": 0.0001 },
390
+ "vT": { "dtype": "float32", "shape": [2, 3], "tolerance": 0.0001, "relTolerance": 0.0001 }
391
+ }
392
+ },
393
+ {
394
+ "name": "tail_block_partial_k",
395
+ "provenance": { "notes": "K is not a multiple of block_size, so the final block is partly out of range." },
396
+ "attrs": { "K": 20, "Nq": 8, "Nkv": 4, "block_size": 32 },
397
+ "inputs": {
398
+ "aT": {
399
+ "dtype": "float32",
400
+ "shape": [2, 20],
401
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.8 }
402
+ },
403
+ "normScaleT": {
404
+ "dtype": "float32",
405
+ "shape": [20],
406
+ "data": { "kind": "fillFloat32", "sinStep": 0.23, "cosStep": 0.41, "scale": 0.4, "offset": 1.0 }
407
+ },
408
+ "qBT": {
409
+ "dtype": "uint8",
410
+ "shape": [8, 1, 16],
411
+ "data": { "kind": "cycle", "values": [27, 180, 75, 226, 33, 150, 201, 108, 57, 246, 129, 66, 195] }
412
+ },
413
+ "qScalesT": {
414
+ "dtype": "float32",
415
+ "shape": [8, 1],
416
+ "data": { "kind": "cycle", "values": [0.04, 0.055, 0.05, 0.065, 0.06, 0.075, 0.07] }
417
+ },
418
+ "kBT": {
419
+ "dtype": "uint8",
420
+ "shape": [4, 1, 16],
421
+ "data": { "kind": "cycle", "values": [211, 44, 137, 98, 165, 20, 233, 121, 78, 190, 15, 252, 87, 143, 61] }
422
+ },
423
+ "kScalesT": {
424
+ "dtype": "float32",
425
+ "shape": [4, 1],
426
+ "data": { "kind": "cycle", "values": [0.045, 0.03, 0.07, 0.05, 0.08, 0.035] }
427
+ },
428
+ "vBT": {
429
+ "dtype": "uint8",
430
+ "shape": [4, 1, 16],
431
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/norm_gqa_input_vBT" } }
432
+ },
433
+ "vScalesT": {
434
+ "dtype": "float32",
435
+ "shape": [4, 1],
436
+ "data": { "kind": "cycle", "values": [0.052, 0.038, 0.061, 0.029, 0.073] }
437
+ }
438
+ },
439
+ "outputs": {
440
+ "qT": { "dtype": "float32", "shape": [2, 8], "tolerance": 0.0001, "relTolerance": 0.0001 },
441
+ "kT": { "dtype": "float32", "shape": [2, 4], "tolerance": 0.0001, "relTolerance": 0.0001 },
442
+ "vT": { "dtype": "float32", "shape": [2, 4], "tolerance": 0.0001, "relTolerance": 0.0001 }
443
+ }
444
+ },
445
+ {
446
+ "name": "rank3_activation",
447
+ "attrs": { "K": 32, "Nq": 8, "Nkv": 4, "block_size": 32 },
448
+ "inputs": {
449
+ "aT": {
450
+ "dtype": "float32",
451
+ "shape": [1, 4, 32],
452
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.8 }
453
+ },
454
+ "normScaleT": {
455
+ "dtype": "float32",
456
+ "shape": [32],
457
+ "data": { "kind": "fillFloat32", "sinStep": 0.23, "cosStep": 0.41, "scale": 0.4, "offset": 1.0 }
458
+ },
459
+ "qBT": {
460
+ "dtype": "uint8",
461
+ "shape": [8, 1, 16],
462
+ "data": { "kind": "cycle", "values": [27, 180, 75, 226, 33, 150, 201, 108, 57, 246, 129, 66, 195] }
463
+ },
464
+ "qScalesT": {
465
+ "dtype": "float32",
466
+ "shape": [8, 1],
467
+ "data": { "kind": "cycle", "values": [0.04, 0.055, 0.05, 0.065, 0.06, 0.075, 0.07] }
468
+ },
469
+ "kBT": {
470
+ "dtype": "uint8",
471
+ "shape": [4, 1, 16],
472
+ "data": { "kind": "cycle", "values": [211, 44, 137, 98, 165, 20, 233, 121, 78, 190, 15, 252, 87, 143, 61] }
473
+ },
474
+ "kScalesT": {
475
+ "dtype": "float32",
476
+ "shape": [4, 1],
477
+ "data": { "kind": "cycle", "values": [0.045, 0.03, 0.07, 0.05, 0.08, 0.035] }
478
+ },
479
+ "vBT": {
480
+ "dtype": "uint8",
481
+ "shape": [4, 1, 16],
482
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/norm_gqa_input_vBT" } }
483
+ },
484
+ "vScalesT": {
485
+ "dtype": "float32",
486
+ "shape": [4, 1],
487
+ "data": { "kind": "cycle", "values": [0.052, 0.038, 0.061, 0.029, 0.073] }
488
+ }
489
+ },
490
+ "outputs": {
491
+ "qT": { "dtype": "float32", "shape": [1, 4, 8], "tolerance": 0.0001, "relTolerance": 0.0001 },
492
+ "kT": { "dtype": "float32", "shape": [1, 4, 4], "tolerance": 0.0001, "relTolerance": 0.0001 },
493
+ "vT": { "dtype": "float32", "shape": [1, 4, 4], "tolerance": 0.0001, "relTolerance": 0.0001 }
494
+ }
495
+ },
496
+ {
497
+ "name": "two_quant_blocks",
498
+ "provenance": { "notes": "K=64 covers two of the provider's supported 32-element quantization blocks." },
499
+ "attrs": { "K": 64, "Nq": 8, "Nkv": 4, "block_size": 32 },
500
+ "inputs": {
501
+ "aT": {
502
+ "dtype": "float32",
503
+ "shape": [2, 64],
504
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.8 }
505
+ },
506
+ "skipT": {
507
+ "dtype": "float32",
508
+ "shape": [2, 64],
509
+ "data": { "kind": "fillFloat32", "sinStep": 0.37, "cosStep": 0.11, "scale": 0.5 }
510
+ },
511
+ "normScaleT": {
512
+ "dtype": "float32",
513
+ "shape": [64],
514
+ "data": { "kind": "fillFloat32", "sinStep": 0.23, "cosStep": 0.41, "scale": 0.4, "offset": 1.0 }
515
+ },
516
+ "qBT": {
517
+ "dtype": "uint8",
518
+ "shape": [8, 2, 16],
519
+ "data": { "kind": "cycle", "values": [27, 180, 75, 226, 33, 150, 201, 108, 57, 246, 129, 66, 195] }
520
+ },
521
+ "qScalesT": {
522
+ "dtype": "float32",
523
+ "shape": [8, 2],
524
+ "data": { "kind": "cycle", "values": [0.04, 0.055, 0.05, 0.065, 0.06, 0.075, 0.07] }
525
+ },
526
+ "kBT": {
527
+ "dtype": "uint8",
528
+ "shape": [4, 2, 16],
529
+ "data": { "kind": "cycle", "values": [211, 44, 137, 98, 165, 20, 233, 121, 78, 190, 15, 252, 87, 143, 61] }
530
+ },
531
+ "kScalesT": {
532
+ "dtype": "float32",
533
+ "shape": [4, 2],
534
+ "data": { "kind": "cycle", "values": [0.045, 0.03, 0.07, 0.05, 0.08, 0.035] }
535
+ },
536
+ "vBT": {
537
+ "dtype": "uint8",
538
+ "shape": [4, 2, 16],
539
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/norm_gqa_input_vBT" } }
540
+ },
541
+ "vScalesT": {
542
+ "dtype": "float32",
543
+ "shape": [4, 2],
544
+ "data": { "kind": "cycle", "values": [0.052, 0.038, 0.061, 0.029, 0.073] }
545
+ }
546
+ },
547
+ "outputs": {
548
+ "qT": { "dtype": "float32", "shape": [2, 8], "tolerance": 0.0001, "relTolerance": 0.0001 },
549
+ "kT": { "dtype": "float32", "shape": [2, 4], "tolerance": 0.0001, "relTolerance": 0.0001 },
550
+ "vT": { "dtype": "float32", "shape": [2, 4], "tolerance": 0.0001, "relTolerance": 0.0001 },
551
+ "residualT": { "dtype": "float32", "shape": [2, 64], "tolerance": 0.000001, "relTolerance": 0.000001 }
552
+ }
553
+ },
554
+ {
555
+ "name": "f16_skipsum",
556
+ "attrs": { "K": 32, "Nq": 8, "Nkv": 4, "block_size": 32 },
557
+ "inputs": {
558
+ "aT": {
559
+ "dtype": "float16",
560
+ "shape": [2, 32],
561
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.8 }
562
+ },
563
+ "skipT": {
564
+ "dtype": "float16",
565
+ "shape": [2, 32],
566
+ "data": { "kind": "fillFloat32", "sinStep": 0.37, "cosStep": 0.11, "scale": 0.5 }
567
+ },
568
+ "normScaleT": {
569
+ "dtype": "float16",
570
+ "shape": [32],
571
+ "data": { "kind": "fillFloat32", "sinStep": 0.23, "cosStep": 0.41, "scale": 0.4, "offset": 1.0 }
572
+ },
573
+ "qBT": {
574
+ "dtype": "uint8",
575
+ "shape": [8, 1, 16],
576
+ "data": { "kind": "cycle", "values": [27, 180, 75, 226, 33, 150, 201, 108, 57, 246, 129, 66, 195] }
577
+ },
578
+ "qScalesT": {
579
+ "dtype": "float16",
580
+ "shape": [8, 1],
581
+ "data": { "kind": "cycle", "values": [0.04, 0.055, 0.05, 0.065, 0.06, 0.075, 0.07] }
582
+ },
583
+ "kBT": {
584
+ "dtype": "uint8",
585
+ "shape": [4, 1, 16],
586
+ "data": { "kind": "cycle", "values": [211, 44, 137, 98, 165, 20, 233, 121, 78, 190, 15, 252, 87, 143, 61] }
587
+ },
588
+ "kScalesT": {
589
+ "dtype": "float16",
590
+ "shape": [4, 1],
591
+ "data": { "kind": "cycle", "values": [0.045, 0.03, 0.07, 0.05, 0.08, 0.035] }
592
+ },
593
+ "vBT": {
594
+ "dtype": "uint8",
595
+ "shape": [4, 1, 16],
596
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/norm_gqa_input_vBT" } }
597
+ },
598
+ "vScalesT": {
599
+ "dtype": "float16",
600
+ "shape": [4, 1],
601
+ "data": { "kind": "cycle", "values": [0.052, 0.038, 0.061, 0.029, 0.073] }
602
+ }
603
+ },
604
+ "outputs": {
605
+ "qT": { "dtype": "float16", "shape": [2, 8], "tolerance": 0.002, "relTolerance": 0.01 },
606
+ "kT": { "dtype": "float16", "shape": [2, 4], "tolerance": 0.002, "relTolerance": 0.01 },
607
+ "vT": { "dtype": "float16", "shape": [2, 4], "tolerance": 0.002, "relTolerance": 0.01 },
608
+ "residualT": { "dtype": "float16", "shape": [2, 32], "tolerance": 0.002, "relTolerance": 0.002 }
609
+ }
610
+ },
611
+ {
612
+ "name": "f16_norm",
613
+ "attrs": { "K": 32, "Nq": 8, "Nkv": 4, "block_size": 32 },
614
+ "inputs": {
615
+ "aT": {
616
+ "dtype": "float16",
617
+ "shape": [3, 32],
618
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.8 }
619
+ },
620
+ "normScaleT": {
621
+ "dtype": "float16",
622
+ "shape": [32],
623
+ "data": { "kind": "fillFloat32", "sinStep": 0.23, "cosStep": 0.41, "scale": 0.4, "offset": 1.0 }
624
+ },
625
+ "qBT": {
626
+ "dtype": "uint8",
627
+ "shape": [8, 1, 16],
628
+ "data": { "kind": "cycle", "values": [27, 180, 75, 226, 33, 150, 201, 108, 57, 246, 129, 66, 195] }
629
+ },
630
+ "qScalesT": {
631
+ "dtype": "float16",
632
+ "shape": [8, 1],
633
+ "data": { "kind": "cycle", "values": [0.04, 0.055, 0.05, 0.065, 0.06, 0.075, 0.07] }
634
+ },
635
+ "kBT": {
636
+ "dtype": "uint8",
637
+ "shape": [4, 1, 16],
638
+ "data": { "kind": "cycle", "values": [211, 44, 137, 98, 165, 20, 233, 121, 78, 190, 15, 252, 87, 143, 61] }
639
+ },
640
+ "kScalesT": {
641
+ "dtype": "float16",
642
+ "shape": [4, 1],
643
+ "data": { "kind": "cycle", "values": [0.045, 0.03, 0.07, 0.05, 0.08, 0.035] }
644
+ },
645
+ "vBT": {
646
+ "dtype": "uint8",
647
+ "shape": [4, 1, 16],
648
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/norm_gqa_input_vBT" } }
649
+ },
650
+ "vScalesT": {
651
+ "dtype": "float16",
652
+ "shape": [4, 1],
653
+ "data": { "kind": "cycle", "values": [0.052, 0.038, 0.061, 0.029, 0.073] }
654
+ }
655
+ },
656
+ "outputs": {
657
+ "qT": { "dtype": "float16", "shape": [3, 8], "tolerance": 0.002, "relTolerance": 0.01 },
658
+ "kT": { "dtype": "float16", "shape": [3, 4], "tolerance": 0.002, "relTolerance": 0.01 },
659
+ "vT": { "dtype": "float16", "shape": [3, 4], "tolerance": 0.002, "relTolerance": 0.01 }
660
+ }
661
+ }
662
+ ]
663
+ }